JVM知识点
2021-08-16 18:53:15 6 举报
AI智能生成
jvm
作者其他创作
大纲/内容
JVM与Java体系结构
Java虚拟机整体架构祥图
截图
Java代码执行过程详图
截图
汇编语言、机器语言、高级语言关系
截图
JVM的架构模型
基于栈式
优点
设计和实现简单,适用于资源受限的系统
避开了寄存器的分配难题:使用零地址指令方式分配
指令流中大部分都是零地址指令,执行过程依赖操作栈,指令集更小,编译器容易实现
8位字节码,所以说指令集更小,但是完成一项操作花费的指令相对多。
不需要硬件支持,可移植性更好,更好实现跨平台
缺点
性能下降,实现同样的功能需要更多的指令,毕竟还要入栈出栈等操作
指令
地址、操作数
零地址只有操作数
基于栈式的,因为是操作栈顶的元素,所以不需要地址
一地址有一个地址,一个操作数
二地址有两个地址,一个操作数
基于寄存器式
优点
性能优秀,执行更高效
花费更少的指令去完成一项操作
缺点
指令集架构完全依赖硬件,可移植性差
典型应用是X86的二进制指令集,比如传统的PC以及安卓的Davlik虚拟机
16位字节码
大部分情况下,指令集往往以一地址指令,二地址指令和三地址指令为主。
javap 查看字节码
-v输出附加信息
-l输出行号和本地变量表
-p显示所有类和成员
-c对代码进行反汇编
JVM的生命周期
虚拟机的启动
通过引导类加载器bootstrap class loader创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的。
虚拟机的执行
执行一个所谓的Java程序的时候,真正执行的是一个叫Java虚拟机的进程
虚拟机的退出
程序正常执行结束
执行过程遇到异常或错误而异常终止
操作系统错误导致Java虚拟机进程终止
Runtime类或System类的exit方法、runtime类的halt方法,并且Java安全管理器允许这次exit或halt操作
halt停止、停下、阻止
exit方法源码:static native void halt0(int status)
JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机退出的情况
JVM发展历程
sun Classic VM
世界第一款商用Java虚拟机
JDK1.4时被淘汰
只提供了解释器
如果使用JIT编译器,就需要外挂,但是JIT和解释器不能配合工作
Exact VM
为了解决上一个虚拟机问题,JDK1.2时,sun提供了此虚拟机
Exact Memory Management:准确式内存管理
虚拟机知道内存中某个位置的数据是什么类型
具有现代高性能虚拟机的雏形
热点探测
编译器与解释器混合工作模式
只在Solaris平台短暂使用,其他平台还是Classic vm
英雄气短,被hotspot虚拟机替换
hotspot虚拟机
最初由Longview Technologies的小公司设计,1997年被sun公司收购,2009年sun公司被甲骨文收购
JDK1.3时,HotSpot VM成为默认虚拟机
绝对市场地位,称霸武林
JDK6,8等均默认
HotSpot就是他的热点代码探测技术
通过计数器找到最具编译价值代码,触发即时编译或栈上替换
通过编译器与解释器协同工作,在优化响应时间和最佳执行性能中取得平衡
走路去目的地,
JRockit
BEA公司
专注服务器端应用
不太关注程序启动速度,引起JRockit内部不包括解析器实现,全部代码靠即时编译器编译后执行
世界上最快的JVM
全面的Java运行时解决方案组合
JRockit Real Time提供毫秒或微秒级的JVM响应时间,适合财务、军事指挥,电信网络的需要
MissionControl服务套件,极低的开销,来监控、管理和分析生成环境中的应用程序的工具
2008年BEA被oracle收购
JDK8中,在HOTSPOT的基础上,移植JRockit的优秀特性
IBM J9
全称:IBM Technology for java Virtual Machine 简称IT4J,内部代号J9
市场定位与HotSpot接近,服务器端、桌面应用,嵌入式等多用途VM
广泛应用于IBM的各种Java产品
IBM产品结合使用性能最好
有影响力的三大商用虚拟机之一
2017开源,OPEN J9
KVM和CDC/CLDC HotSpot
JavaME产品线产品
智能控制器、传感器、老人手机等
Azul VM
与特定硬件平台绑定、软硬件配合的专有虚拟机
运行于Azul Systems公司的专有硬件Vega系统上的虚拟机
每个实例可以管理至少数十个CPU和数百GB内存的硬件资源,并提供在巨大内存范围内实现可控的GC时间的垃圾收集器,专有硬件优化的线程调度等优秀特性
2010年,发布自己的Zing JVM,可以在通用X86平台上提供接近于vega系统的特性
Liquid VM
BEA公司开发的,运行在自家Hypervisor系统上
不需要操作系统支持,本身实现了一个专用操作系统的必要功能,如线程调度、文件系统、网络支持等
随着JRockit虚拟机终止开发,Liquid VM项目也停止了
Apache Harmony
JDK1.5,1.6兼容
IBM和Intel联合开发的开源JVM,2011年退役
Java类库代码吸纳进了Android SDK
Microsoft JVM
只能在Windows平台运行,xp系统中不用了。
TaobaoJVM
基于OpenJDK开发了自己的定制版本AlibabaJDK
深度定制且开源的高性能服务器版JAVA虚拟机
GCIH:GC invisible heap,将生命周期较长的Java对象从heap中已到heap之外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率
GCIH中的对象还能够在多个JAVA虚拟机进程中实现共享
使用crc32指令实现JVM intrinsic降低JNI的调用开销
针对大数据场景的ZenGC
在阿里产品上性能高,硬件严重依赖intel的CPU,损失了兼容性,但是提高了性能
淘宝、天猫上线,把oracle官方JVM版本全部替换
Dalvik VM
谷歌开发,应用于Android系统,安卓2.2提供了JIT,发展迅猛
只能称作虚拟机,不能称作Java虚拟机,没有遵循Java虚拟机规范
不能直接执行Java的Class文件
基于寄存器架构,不是jvm的栈架构
执行的是编译后的dex文件,执行效率比较高
安卓5.0使用支持提前编译AOT的ART VM替换Dalvik VM
Grall VM
2018年4月,Oracle labs公开了GraalVM。
跨语言全栈虚拟机,可以作为任何语言的运行平台使用
运行时数据区概述
不同的JVM对于内存的划分方式和管理机制存在部分差异,后续针对HotSpot虚拟机进行介绍
截图
程序计数器(PC寄存器)
运行时数据区中唯一不会出现OOM的区域,没有垃圾回收
当前线程所执行的字节码的行号指示器
为了线程切换后能恢复到正确的位置
每个线程有一个独立的程序计数器,线程之间互不影响。
如果线程执行的Java方法,则计数器记录正在执行的虚拟机字节码的指令的地址
如果正在执行的本地方法,这个计数器值则应为空。(undefined)
虚拟机栈
内存中的栈与堆
栈是运行时的单位,而堆是存储的单位,栈解决程序如何执行,如何处理数据。堆解决的是数据存储问题,即数据怎么放,放在哪里。
基本内容
Java虚拟机栈,早起也叫Java栈,每个线程创建时都会创建一个虚拟机栈,内部保存一个个栈帧,对应着一次次的Java方法调用
生命周期和线程的一致
主管Java程序的运行,保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。
局部变量 vs 成员变量
基本数据类型 VS 引用类型变量(类,数组,接口)
优点
快速有效的存储方式,访问速度仅次于程序计数器
JVM直接对JAVA栈的操作只有两个
每个方法执行,伴随着进栈(入栈,压栈)
执行结束的出栈
栈不存在垃圾回收,但是存在OOM
Java栈大小是动态或者固定不变的。如果是动态扩展,无法申请到足够内存OOM,如果是固定,线程请求的栈容量超过固定值,则StackOverflowError
使用-Xss (记忆:站着做一个小手术,栈Xss),设置线程的最大栈空间
截图
栈的存储单位
每个线程都有自己的栈,栈中的数据以栈帧格式存储
线程上正在执行的每个方法都各自对应一个栈帧
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各个数据信息
先进后出,后进先出
一条活动的线程中,一个时间点上,只会有一个活动的栈帧。只有当前正在执行的方法的栈顶栈帧是有效的,这个称为当前栈帧,对应方法是当前方法,对应类是当前类
执行引擎运行的所有字节码指令只针对当前栈帧进行操作
如果方法中调用了其他方法,对应的新的栈帧会被创建出来,放在顶端,成为新的当前帧
栈运行原理
不同线程中包含的栈帧不允许存在相互引用。
当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为新的栈帧。
Java方法有两种返回方式
一种是正常的函数返回,使用return指令
另外一种是抛出异常,不管哪种方式,都会导致栈帧被弹出
栈的内部结构
局部变量表
定义为一个数字数组,主要用于存储方法参数,定义在方法体内部的局部变量,数据类型包括各类基本数据类型,对象引用,以及return address类型
局部变量表建立在线程的栈上,是线程私有的,因此不存在数据安全问题
局部变量表容量大小是在编译期确定下来的
局部变量表存放编译期可知的各种基本数据类型(8种),引用类型(reference),return address 类型
最基本的存储单元是slot
32位占用一个slot,64位类型(long和double)占用两个slot
局部变量表中的变量只有在当前方法调用中有效,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。
方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
关于Slot的理解
JVM虚拟机会为局部变量表中的每个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this,会存放在index为0的slot处,其余的参数表顺序继续排列
截图
栈帧中的局部变量表中的槽位是可以重复的,如果一个局部变量过了其作用域,那么其作用域之后申明的新的局部变量就有可能会复用过期局部变量的槽位,从而达到节省资源的目的
静态变量与局部变量对比及小结
变量的分类
按照数据类型分
基本数据类型
引用数据类型
按照声明的位置
成员变量,在使用前经历过初始化过程
类变量
链接的准备阶段给类变量默认赋值,初始化阶段显示赋值,即静态代码块赋值
实例变量
随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
局部变量
在使用前,必须进显式赋值,否则编译不通过
补充:
在栈帧中,与性能调优关系最密切的部分,就是局部变量表,方法执行时,虚拟机使用局部变量表完成方法的传递
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
操作数栈
在方法执行的过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈
截图
如果被调用方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新程序计数器中下一条需要执行的字节码指令
Java虚拟机的解释引擎是基于栈的执行引擎,其中栈就是操作数栈
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
每一个操作数栈会拥有一个明确的栈深度,用于存储数值,最大深度在编译期就定义好
栈中,32bit类型占用一个栈单位深度,64bit类型占用两个栈单位深度
操作数栈并非采用访问索引方式进行数据访问,而是只能通过标准的入栈、出栈操作完成一次数据访问
栈顶缓存技术
由于操作数是存储在内存中,频繁的进行内存读写操作影响执行速度,将栈顶元素全部缓存到物理CPU的寄存器中,依此降低对内存的读写次数,提升执行引擎的执行效率
动态链接
指向常量池的方法 引用
每一个栈帧内部都包含一个指向运行时常量池中,该帧所属方法的引用
目的是为了支持当前方法的代码能够实现动态链接,比如invokedynamic指令
在java源文件被编译成字节码文件中时,所有的变量、方法引用都作为符号引用,保存在class文件的常量池中。
描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的。
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
原创整理不易,还请扫码JVM运行时数据区详图,谢谢支持
常量池、运行时常量池
常量池在字节码文件中,运行时常量池,在运行时的方法区中
方法返回地址
存放调用该方法的pc寄存器的值
方法的结束
正常执行完成
出现未处理异常,非正常退出
无论哪种方式退出,方法退出后,都会返回该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
异常退出的,返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息
执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口
返回指令包括
ireturn返回值是boolean,byte,char,short,和int类型时使用
lreturn
dreturn
areturn
引用类型
还有一个return指供声明为 void的方法、实例初始化方法、类和接口的初始化方法使用
本质上,方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
一些附加信息
允许携带与Java虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息。不确定有,可选情况
方法的调用
静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行时期间保持不变,这种情况下降调用方的符号引用转为直接引用的过程称为静态链接
动态链接
如果被调用的方法无法再编译期被确定下来,只能在运行期将调用的方法的符号引用转为直接引用,这种引用转换过程具备动态性,因此被称为动态链接
方法的绑定
绑定是一个字段、方法、或者类在符号引用被替换为直接引用的过程。仅仅发生一次。
早期绑定
被调用的目标方法如果再编译期可知,且运行期保持不变
晚期绑定
被调用的方法在编译期无法被确定,只能够在程序运行期根据实际的类型绑定相关的方法。
Java中任何一个普通方法都具备虚函数的特征(运行期确认,具备晚期绑定的特点),C++中则使用关键字virtual来显式定义
如果在java程序中,不希望某个方法拥有虚函数的特征,则可以使用关键字final来标记这个方法
虚方法和非虚方法
非虚方法
如果方法在编译期就确定了具体的调用版本,则这个版本在运行时是不可变的。这样的方法称为非虚方法
静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法
其他方法称为虚方法
方法调用指令
普通调用指令
invokestatic
调用静态方法,解析阶段确定唯一方法版本
invokespecial
调用<init>方法,私有及父类方法,解析阶段确定唯一方法版本
invokevirtual
调用所有虚方法
invokeinterface
调用接口方法
其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法
动态调用指令JDK1.7新增
invokedynamic
动态解析出需要调用的方法,然后执行
直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式
截图
静态语言和动态语言
区别在于对类型的检查是编译器还是运行期,满足编译期就是静态类型语言,反之就是动态类型语言。
Java是静态类型语言,动态调用指令增加了动态语言的特性
方法重写的本质
找到操作数栈顶的第一个元素所执行的对象的实际类型,记做C
如果在类型C中找到与常量池中描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,如果不通过,则返回java.lang.IllegalAccessError异常
否则,按照继承关系从下往上依次对C的各个父类进行上一步的搜索和验证过程。
如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
虚方法表
面向对象的编程中,会很频繁的使用动态分配,如果每次动态分配的过程都要重新在类的方法元数据中搜索合适的目标的话,就可能影响到执行效率,因此为了提高性能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找
每个类都有一个虚方法表,表中存放着各个方法的实际入口
虚方法表会在类加载的链接阶段被创建,并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法也初始化完毕
截图
本地方法接口
什么是本地方法
简单讲,就是一个Java调用非Java代码的接口
为什么使用native method
与Java环境外交互
例如与操作系统底层或硬件交换信息时的情况
例如启动一个线程
本地方法栈
Java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法的调用
本地方法栈,也是线程私有的。
允许被实现成固定或者是可动态扩展的内存大小。
内存溢出情况和Java虚拟机栈相同
使用C语言实现
具体做法是Native Method Stack 中登记native方法,在Execution Engine执行时加载到本地方法库
当某个线程调用一个本地方法时,就会进入一个全新,不受虚拟机限制的世界,它和虚拟机拥有同样的权限。
并不是所有的JVM都支持本地方法,因为Java虚拟机规范并没有明确要求本地方法栈的使用语言,具体实现方式,数据结构等
Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一
堆
堆的核心概述
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
Java堆区在JVM启动的时候即被创建,其空间大小也就确认了。堆内存的大小是可调节的
Java虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(TLAB)
“几乎”所有的对象实例都在这里分配内存
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,引用指向对象或者数组在堆中的位置
方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
堆是GC执行垃圾回收的重点区域
堆空间细分为:
Java7及之前
内存逻辑上分为:
新生区
Eden区
Survivor区
from
to
谁空谁是to
养老区
永久区
Java8及之后
内存逻辑上分为:
新生区
Eden区
Survivor区
from
to
谁空谁是to
养老区
元空间
约定
新生区==新生代==年轻代
养老区==老年区==老年代
永久区==永久代
jvisualvm工具
安装插件后可查看
子主题
-XX:+PrintGCDetails
可开启打印查看方法区实现
设置堆内存的大小与OOM
-Xms :小秘书表示堆空间的起始内存。
-Xmx:小明星表示堆空间的最大内存
超过最大内存将抛出OOM
通常将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾会后清理完堆区后,不需要重新分隔计算堆区的大小,从而提高性能
默认情况下
初始内存大小
物理电脑内存大小/64
截图
最大内存大小
物理电脑内存/4
jps命令
查看当前程序运行的进程
jstat
查看JVM在gc时的统计信息
jstat -gc 进程号
年轻代与老年代
Java对象划分为两类:生命周期短和长的。
新生代与老年代空间默认比例1:2
-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
ratio:比率比例的意思
jinfo -flag NewRatio 进程号,查看参数设定值
在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是:8:1:1
-XX:SurvivorRatio调整这个空间比例
Eden与Survivor区的比例
实际是6:1:1,因为有自适应机制
-XX:-UseAdaptiveSizePolicy:-表示关闭自适应,实际没有用。直接用Ratio分配即可
几乎所有的Java对象都是在Eden区被new出来的。
Eden放不了的大对象,直接进入老年代了。
IBM研究表明,新生代80%的对象都是朝生夕死
-Xmn:洗面奶,设置新生代最大内存大小,如果同时设置了新生代比例与此参数冲突,则以此参数为准。
图解对象分配一般过程
1、new的对象先放在Eden区,此区有大小限制
2、当创建新对象,Eden空间填满时,会触发Minor GC,将Eden不再被其他对象引用的对象进行销毁。再加载新的对象放到Eden区
3、将Eden中剩余的对象移到幸存者0区
4、再次触发垃圾回收,此时上次幸存者下来的,放在幸存者0区的,如果没有回收,就会放到幸存者1区
5、再次经历垃圾回收,又会将幸存者重新放回幸存者0区,依次类推
6、可以设置一个次数,默认是15次,超过15次,则会将幸存者区幸存下来的转去老年区
-XX:MaxTenuringThreshold=N进行设置
子主题
总结:
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间搜集
对象分配特殊过程
截图
触发YGC,幸存者区就会进行回收,不会主动进行回收
超大对象eden放不下,就要看Old区大小是否可以放下
old区也放不下,需要FullGC(MajorGC),这两GC概念还是有区别的。下面详解
常用调优工具
JDK命令行
Eclipse:Memory Analyzer Tool
Jconsole
VisualVM
Jprofiler
Java Flight Recorder
MinorGC,MajorGC,FullGC
针对HotSpotVM的实现
GC按照内存回收区域分为
部分收集
新生代收集
MinorGC (YoungGC)
老年代收集
MajorGC/old
GC
GC
目前只有CMS GC会单独收集老年代的行为
很多时候MajorGC与FullGC混淆使用,具体分辨是老年代回收还是整堆回收
混合收集
收集整个新生代以及部分老年代的垃圾收集
目前只有G1 GC会有这种行为
整堆收集
收集整个Java堆和方法区的垃圾收集
MajorGC
FullGC
MinorGC的触发条件
当年轻代空间不足时,就会触发MinorGC,这里的年轻代指的是Eden代满,Survivor满不会触发GC。每次MinorGC会清理年轻代的内存
因为Java对象大多朝生夕灭,所以MinorGC非常频繁
Minor翻译,较小的,未成年的
MinorGC会引发STW
后面详解
老年代GC(MajorGC/FullGC)触发条件
指发生在老年代的GC,对象从老年代消失,我们说“MajorGC”“FullGC”发生了
出现了MajorGC,经常会伴随至少一次MinorGC
非绝对,在Parallel Scavenge收集器的收集策略里就直接进行MajorGC的策略选择过程
也就是老年代空间不足,会先尝试触发MinorGC,如果之后空间还不足,则触发MajorGC
MajorGC的速度比MinorGC慢10倍以上,STW的时间更长
如果MajorGC后,内存还不足,就报OOM了
FullGC的触发机制
1、调用System.gc()时,系统建议执行FullGC,但是不必然执行
2、老年代空间不足
3、方法区空间不足
4、通过MinorGC后进入老年代的平均大小,大于老年代的可用内存
5、由Eden区,Survivor 0区向Survivor 1区复制时,对象的大小大于ToSpace可用内存,则把改对象转存到老年代,且老年代的可用内存小于该对象的大小
FullGC是开发或调优中尽量要避免的,这样暂停时间会短一些。
堆空间分代思想
其实不分代也可以,分代的理由是优化GC性能
内存分配策略
如果对象再Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor区容纳,则被移动到Survivor空间中,并将对象年龄设置为1,对象再Survivor区每熬过一次MinorGC,年龄就+1,当年龄增加到一定程度(默认为15,不同Jvm,GC都所有不同)时,就会被晋升到老年代中
-XX:MaxTenuringThreshold
优先分配到Eden
大对象直接分配到老年代
尽量避免程序中出现过多的大对象
长期存活的对象分配到老年代
动态对象年龄分配
如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄
空间分配担保
-XX:HandlePromotionFailure
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间,是否大于新生代所有对象的总空间
如果大于,则此次MinorGC是安全的
如果小于,则查看-XX:HandlePromotionFailure设置是否允许担保失败
true
会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
大于,则尝试进行一次MinorGC,但是这次MinorGC依然是有风险的
小于,则改为进行一次FullGC
false
则改为进行一次FullGC
jdk6update24之后,这个参数不会再影响到虚拟机的空间分配担保策略。
规则改为只要老年代的连续空间大于新生代对象总大小,或者历次晋升的平均大小,就会进行MinorGC
否则进行FullGC
子主题
为对象分配内存TLAB
Thread Local Allocation Buffer
堆区是线程共享区域,任何线程都可以访问到堆区的共享数据
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
TLAB
从内存模型而不是垃圾收集的角度,对Eden区域进行划分,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间中
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们将这种内存分配方式成为快速分配策略
openjdk衍生出来的JVM都提供了TLAB的设计
截图
补充
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但是JVM确实是将TLAB作为内存分配的首选
开发人员通过-XX:UseTLAB设置是否开启TLAB空间
默认情况下,TLAB空间内存非常小,仅占有整个Eden空间的1%,通过-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小
一旦对象在TLAB空间分配内存失败,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存
小结堆空间的参数设置
学会怎么在官网查找Java语言规范,java虚拟机规范等
单击打开博客
截图
堆是分配对象的唯一选择吗
随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术,将会导致一些微秒变化,所有对象分配到堆上渐渐变得不那么绝对了。
有一种特殊情况,如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样无需堆上分配,也不需要垃圾回收了,也是最常见的堆外存储技术
TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现了off-heap,实现了将生命周期较长的Java对象从heap中移动heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的
逃逸分析概述
逃逸分析的基本行为就是分析对象动态作用域
当一个对象在方法中定义后,对象只在方法内部使用,则认为没有发生逃逸
当一个对象在方法中被定义后,它被外部方法引用,则认为发生逃逸,例如作为调用参数传递到其他地方中
截图
栈上分配
将堆分配转为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
同步策略
如果一个对象被发现只能从一个线程被访问到,对于这个对象的操作可以不考虑同步
JIT编译器可以借助逃逸分析来判断同步块所使用的的锁对象,是否只能够被一个线程访问,而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候,就会取消对这部分代码的同步。这样就大大提高并发性和性能,这个取消同步的过程就叫同步省略,也叫锁消除
截图
分离对象或标量替换
有的对象可能不需要作为一个连续的内存结构存在,也可以被访问到,那么对象的部分(或全部)可以不存储在内存。而是存储在CPU寄存器中
子主题
标量是指一个无法再分解的更小的数据的数据。Java中原始数据类型就是标量
可以分解的数据叫聚合量,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量
截图
子主题
标量替换参数:-XX:EliminateAllocations,默认打开
方法区
栈、堆、方法区交互关系
子主题
子主题
方法区的理解
Java虚拟机规范中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但是一些简单的实现,可能不会选择去进行垃圾收集或者进行压缩。对于HotSpot而言,方法区还有一个别名叫Non-Heap(非堆),目的就是要和堆分开
所以方法区看作是一块独立于Java堆的内存空间
方法区和Java堆一样,是各个线程共享的内存区域
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样,都是可以不连续的
方法区的大小和堆空间一样,可以选择固定大小或者可扩展
方法区的大小决定了系统可以保存多少个类,如果定义太多类,加载大量的第三方的Jar包,Tomcat部署过多工程,导致方法区溢出,虚拟机同样会抛出内存溢出OOM:PermGen space或者Metaspace
关闭JVM就会释放这个区域的内存
HotSpot中方法区的演进
在jdk7及以前,习惯上把方法区,称为永久代,jdk8开始,使用元空间取代了永久代
元空间的本质和永久带类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存
根据Jvm规范,如果方法区无法满足新的内存分配需求,将抛出OOM异常
本质上,方法区和永久代并不等价,仅是对HostSpot而言的。
Java虚拟机规范,对如何实现方法区,不做统一要求,例如BEA JRockit/IBM J9中不存在永久代的概念
现在来看,当年使用永久代,不是好的点子,导致Java程序更容易OOM
-XX:MaxPermSize
设置方法区大小与OOM
方法区大小不是固定的,jvm可以根据应用动态调整
JDK7及以前
通过-XX:PermSize 来设置永久代初始分配空间,默认值是20.75M
-XX:MaxPermSize来设定永久代最大可分配空间。
32位机器默认是64M
64位机器默认是82M
如果JVM加载的类信息容量超过了这个值,会报OOM:PermGenspace
JDK8及以后
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
默认值依赖平台
windows下初始为21M,最大是-1即没有限制
如果不指定大小,虚拟机耗用所有可用系统内存,元数据区发生溢出,一样OOM:Metaspace
对于一个64位服务端JVM来说,默认的初始元数据区空间为21M,这就是初始的高水位线。一旦触及这个水位线,FULLGC会触发并卸载没有用的类,然后高水位线会被重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放空间不足,在不超过最大设定值时,适当提高该值。如果释放空间过多,则适当降低该值。
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,fullGC多次调用。为了避免频繁FullGC,建议将-XX:MetaspaceSize设置为一个相对较高的值
如何解决OOM
要解决OOM或heap space异常,一般的手段是通过内存映像分析工具,对dump出来的堆转存储快照进行分析,重点确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄露,还是内存溢出
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,于是就能找到内存泄露对象时通过怎样的路径与GC Roots相关联,导致垃圾收集器无法自动回收他们。根据引用链信息,可以较准确的定位出泄露代码的位置
如果不存在内存泄露,或者说内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与物理机器内存对比是否还可以调大,从代码检查是否某些对象生命周期过长,持有状态时间过长,尝试减少程序运行时的内存耗用
方法区的内部结构
方法区存储什么
它用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存
类型信息
对于每个加载的类型(类Class,接口Interface,枚举Enum,注解annotation)
JVM必须在方法区中存储以下类型信息
这个类的完整有效名称(全名=包名.类名)
这个类型直接父类的完整有效名,对于interface或Object没有父类
这个类型的修饰符,public,abstract,final
这个类型直接接口的一个有序列表
域信息
JVM必须在方法区中保存类型的所有域的相关信息,以及域的声明顺序
域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序
方法名称
方法的返回类型
或void
方法参数的数量和类型
按顺序
方法的修饰符
public,private,protected,static,final,synchronized,native,abstract的一个子集
方法的字节码bytecodes,操作数栈,局部变量表及大小
异常表
abstract和native方法除外
每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引。
non-final的类变量
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
类变量被类的所有实例共享,即使没有类实例时,你也可以访问他
全局常量
static final
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
常量池
方法区,内部包含了运行时常量池
字节码文件,内部包含了常量池
运行时将常量池加载到方法区,就是运行时常量池
要弄清楚方法区,需要理解清楚ClassFile,因为加载类的信息都在方法区
要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池
一个有效的字节码文件中除了包含的类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用
为什么要用常量池?
一个java源文件中的类、接口、编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大,以至于不能直接存到字节码里。换一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接会用到运行时常量池。
常量池有什么?
数量值
字符串值
类引用
字段引用
方法引用
常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
运行时常量池
运行时常量池是方法区的一部分
常量池表是class文件的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
在加载类和接口到虚拟机后,就会创建对应的运行时常量池
JVM为每个已加载的类型都维护一个常量池,池中的数据像数组项一样,通过索引访问
运行时常量池包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后,才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里转换为真实地址。
运行时常量池,相对于class文件常量池的另一个重要特征是:具备动态性
例如:String.intern可以将字符串也放入运行时常量池
当创建类或接口的运行时常量池,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值。则JVM会抛出OOM异常
这里注意,常量池数量为N,则索引为1到N-1,
方法区使用举例
方法区的演进细节
首先明确,只有HotSpot才有永久代
HotSpot中方法区的变化
jdk1.6及之前
有永久代,静态变量存放在永久代上
子主题
jdk1.7
有永久代,但已经逐步去永久代,字符串常量池,静态变量移除,保存在堆中
子主题
jdk1.8及之后
无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆
子主题
永久代为什么被元空间替换
随着 JAVA8的到来,HotSpotVM中再也见不到永久代了,但是并不意味着类的元数据信息也消失了,这些数据被转移到了一个与堆不相连的本地内存区域,这个区域叫做元空间MetaSpace
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统的可用内存空间
为永久代设置空间大小很难确定,在某些场景下,如果动态加载类过多,就容易产生OOM
而元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制
对永久代进行调优是很困难的
方法区的垃圾回收
有些人认为方法区是没有垃圾收集行为的,其实不然。Java虚拟机规范对方法区的约束非常宽松,提到过可以不要求虚拟机在方法区实现垃圾收集。事实上,也确实有未实现或未能完整实现方法区类型卸载的收集器,如JDK11 ZGc
方法区的垃圾收集主要回收两部分内容
常量池中废弃的常量
HotSpot对常量池的回收策略很明确,只要常量池中的常量没有被任何地方引用,就可以被回收
回收废弃常量与回收Java堆中对象非常类似
不再使用的类型
需要同时满足三个条件
该类所有的实例已经被回收
java堆中不存在该类及其任何派生子类的实例
加载该类的类加载器已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法
满足以上三个条件后,并不是和对象一样立即被回收,仅仅是允许。
HotSpot虚拟机提供了-Xnoclassgc参数进行控制
在大量使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OSGI这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力
方法区内常量池中主要存放的两大类常量:
字面量
比较接近Java语言层次的常量概念,如文本字符串,被声明为final的常量值等
符号引用
属于编译原理方面的概念
类和接口的全限定名
字段的方法和描述符
方法的名称和描述符
HotSpot虚拟机对象探秘
对象的实例化
创建对象的方式
new
最常见的方式
变形:Xxx的静态方法
XxxBuilder/XxxFactory的静态方法
Class的newInstance
JDK9标记过时,反射的方式,只能调用空参的构造器,权限必须是public
Constructor的newInstance
反射的方式,可以调用空参,带参的构造器,权限没有要求。
使用clone
不调用任何构造器,当前类需要实现Cloneable接口,实现clone方法
使用反序列化
从文件、网络等获取一个对象的二进制流
第三方库Objenesis
创建对象的步骤
1、判断对象对应的类是否加载、链接、初始化
当虚拟机遇到一条字节码new指令时。首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载解析初始化过。如果没有,在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为key值进行查找对应的.class文件,如果没有找到文件,则抛出ClassNotFoundException异常
2、为对象分配内存
首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小
如果Java堆内存中不规则,虚拟机就必须维护一个列表,记录哪些内存可用,哪些不可用。分配的时候在列表中找一个足够大的空间分配,然后更新列表。这种分配方式叫空闲列表(Free List)。
选择哪种由Java堆是否规整决定,Java堆是否规整由所采用的的垃圾收集器是否带有空间压缩整理(Compact)的能力决定
当使用Serial,ParNew等带有压缩整理过程的收集器,指针碰撞简单高效;
当使用CMS基于清除(Sweep)算法收集器时,只能采用空闲列表来分配内存;(CMS为了能在多数情况下分配内存更快,设计了一个Linear Allocatioin Buffer的分配缓冲区,通过空闲列表拿到一大块分配缓冲区后,在它里面仍可使用指针碰撞方式分配)
假设Java 堆中内存时绝对规整的,所有被使用过的内存放在一边,空闲的内存放在另一边,中间放一个指针作为分界点指示器。那么内存分配就是指针指向空闲的方向,挪动一段与对象大小相等的举例。这种分配方式成为指针碰撞(Bump The Pointer)。
3、处理并发安全问题
对象创建是非常频繁的行为,还需要考虑并发情况下,仅仅修改一个指针所指向的位置也是不安全的,例如正在给对象A分配内存,指针还未修改,对象B又使用原来的指针分配内存。解决问题有两种可选方案:
a、对分配内存空间的动作进行同步处理。实际上虚拟机采取CAS配上失败重试的方式保证更新操作的原子性。
b、把内存分配的动作按照线程划分到不同的空间中进行,每个线程在Java堆中,预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
虚拟机是否使用TLAB,可以通过-XX: +/-UseTLAB参数来设定。
4、初始化分配到的空间
内存分配完成后,虚拟机将分配到的内存空间(不包括对象头)都初始化为零值。如果使用了TLAB,这个工作可以提前到TLAB分配时进行。
这步操作保证对象的实例字段在Java代码中,可以不赋初始值就直接使用,程序可以访问到字段对应数据类型所对应的零值。
这步操作保证对象的实例字段在Java代码中,可以不赋初始值就直接使用,程序可以访问到字段对应数据类型所对应的零值。
5、设置对象的对象头
接下来Java虚拟机还要对对象进行必要的设置,例如对象时哪个类的实例、如何才能找到类的元数据信息,对象的哈希码(实际上对象的HashCode会延后真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放到对象的对象头(Object Header)
6、执行init方法进行初始化
上面工作完成后,从虚拟机角度来说,一个新的对象已经产生了,但是从Java程序的视角来说,对象创建才刚刚开始,对象的构造方法(Class文件中init()方法)还未执行,所有字段都是默认的零值。new指令之后接着执行init方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全构造出来
对象的内存布局
对象头
包含两部分
这部分数据的长度在32位和64位的虚拟机(未开启指针压缩中)分别是32bit和64bit,官方称为【Mark Word】运行时元数据
哈希值
GC分代年龄
锁状态标志
线程持有的锁
偏向线程ID
偏向时间戳
对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,根据对象状态的不同,Markword可以复用自己的空间。
类型指针
即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确认该对象属于哪个类的实例
说明:如果是数组,还需要记录数组的长度
实例数据
对象的实例数据部分,是对象的真正存储的有效信息,即我们在程序代码中定义的各种类型的字段内容,无论是父类继承下来,还是子类中定义的字段都要鸡柳下来。
1、这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
2、分配策略参数-XX:FieldsAllocationStyle
3、HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)
4、从默认的分配策略中可以看出,相同宽度的字段总被分配到一起存放。
5、在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
6、如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认也是true),那么子类中较窄的变量也允许插入父类变量的空隙之间,以节省一点点空间。
2、分配策略参数-XX:FieldsAllocationStyle
3、HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)
4、从默认的分配策略中可以看出,相同宽度的字段总被分配到一起存放。
5、在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
6、如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认也是true),那么子类中较窄的变量也允许插入父类变量的空隙之间,以节省一点点空间。
对齐填充
对其填充,这并不是必然存在,没有特别的意义,它仅仅起着占位符的作用。
因为HotSpot虚拟机自动内存管理系统,要对对象的起始地址必须是8字节的整数倍,换句话就是任何对象的大小都必须是8字节的整数倍。
对象头已经精心设计为8字节的整数倍,1倍或者2倍。
对象实例数据部分如果没有对齐的话,就需要通过对其填充来补全。
对象头已经精心设计为8字节的整数倍,1倍或者2倍。
对象实例数据部分如果没有对齐的话,就需要通过对其填充来补全。
对象的访问定位
【使用句柄】
使用句柄,Java堆中将划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄包含对象实例数据与类型数据各自的具体信息。
截图
【直接指针】
使用指针,reference中存储的直接就是对象地址,如果访问对象本身,不需要多一次的间接访问的开销。
截图
两种方式各有优势:
使用句柄最大好处是reference中存放的是稳定句柄地址,在对象被移动(垃圾搜集时会产生)时只改变句柄中实例数据指针,reference本身不用改变。
使用指针最大好处就是速度快,节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,所以积少成多也是一项可观的执行成本。
HotSpot主要是用指针,进行对象访问(例外情况,如果使用Shenandoah收集器的话,也会有一次额外的转发)。
直接内存
不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域
直接内存是在java堆外的,直接向系统申请的内存区间
来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
通常,访问直接内存的速度会优于Java堆,即读写性能高
因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存
Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
也可能导致OOM异常
直接内存在堆外,所以大小不受限于-Xmx指定的最大堆大小
但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存
缺点
分配回收成本较高
不受JVM内存回收管理
直接内存大小可以通过MaxDirectMemorySize设置
如果不指定,默认与堆的最大值-Xmx参数值一致
执行引擎
执行引擎概述
执行引擎是Java虚拟机核心的组成部分之一
虚拟机的执行引擎由软件自行实现,物理机的执行引擎是操作系统层面上
能够执行不被硬件直接支持的指令格式
执行引擎的工作过程
1、执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
2、每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址
3、当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息
Java代码编译和执行过程
大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过上图中的各个步骤
为什么说Java是半编译半解释型语言
JVM在执行Java代码的时候,通常会将解释执行与编译执行二者结合起来进行
机器码,指令,汇编语言
机器码
各种采用二进制编码方式表示的指令,叫做机器指令码。机器语言。机器指令与CPU紧密相关,不同种类的CPU所对应的机器指令也就不同
指令
由于机器码由01组成,可读性太差。于是人们发明了指令
指令就是把机器码特定的0和1序列,简化成对应的指令,一般为英文编写如mov,inc等,可读性稍好
由于不同的硬件平台,执行同一个操作,对应的机器码可能不同。所以不同的硬件平台的同一种指令,对应的机器码也可能不同
指令集
不同硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集
x86指令集,对应的x86架构的平台
ARM指令集,对应的是ARM架构的平台
汇编
由于指令的可读性太差,于是又有了汇编语言
汇编语言用助记符代替机器指令的操作码,用地址符号或标号,代替指令或操作数的地址。
汇编语言要翻译成机器指令码,计算机才能识别和执行
解释器
当Java虚拟机启动时,会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为赌赢平台的本地机器指令执行
解析器真正意义上所承担的角色就是一个运行时翻译者,将字节码文件中的内容翻译为对应的平台的本地机器指令执行
当一条字节码指令被解释执行完成后,接着在根据PC寄存器中的记录下一条需要被执行的字节码执行解释执行
古老的字节码解释器
现在普遍使用的模板解释器
模板解释器将每一条字节码和一个模板函数相关联,模板函数直接产生这条字节码执行时的机器码,提高解释器的性能
HotSpot中
Interpreter模块
实现了解释器的核心功能
Code模块
用于管理HotSpot在运行时生成的本地机器指令
JIT编译器
就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
JVM平台支持一种叫做即时编译的技术,目的是避免解释执行,而是将整个函数体编译成机器码,每次函数执行时,只执行编译后的机器码即可。使执行效率大幅提升
为什么两条腿走路?
首先程序启动后,解释器可以马上发挥作用,省去编译时间,立即执行
编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后执行效率更高
对于服务端应用,启动时间并非关注重点,但是对于看重启动时间的应用场景,就需要找到一个平衡点。
当Java虚拟机启动时,解释器可以首先发挥作用,而不是等待即时编译器全部编译完成后再执行,这样可以省去很多不必要的编译时间,随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率
概念解释
前端编译器
把.java文件转换为.class文件的过程
sun的Javac,
后端运行期编译器
把字节码转为机器码的过程
JIT编译器:hotSpot的C1,C2编译器
静态提前编译器
Ahead of Time Compliler AOT,直接把.java文件编译器本地机器代码的过程
GNU Compiler for the Java(GCJ)
什么时候选择JIT
热点代码及探测方式
需要根据代码被调用执行的频率而定,需要被编译为本地代码的字节码,也称之为热点代码。
JIT编译器会在运行时针对频繁调用的热点代码做出深度优化,将其直接编译为对应平台的本地机器指令。以此提升Java程序的执行性能
一个被多次调用的方法,后者一个方法体内部循环次数较多的循环体,都可以被称之为热点代码
因此可以通过JIT编译器编译为本地机器指令,由于这种编译方法发生在方法的执行过程中,因此也被称之为栈上替换,OSR On Statck Replacement
一个方法调用都少次才能达到标准?这个依靠热点探测功能
hotspot采用的基于计数器的热点探测
方法调用计数器
统计方法调用次数
默认阈值,Client模式下是1500次,Server模式下是10000次
-XX:CompileThreshold
回边计数器
统计循环体执行的循环次数
截图
当一个方法被调用时,如果不存在已被编译过的版本,则将此方法的调用计数器+1,然后判断方法调用计数器与回边计数器之和,是否超过方法调用计数器的阈值。如果已经超过,会向即时编译器提交一个该方法的代码编译请求。
截图
热度衰减
当超过一定的时间限度,如果方法调用次数仍然不足以提交即时编译器编译,那么这个方法的调用计数器就会被减少一半。
-XX:UseCounterHalfLifeTime参数设置半衰周期的时间,单位是秒
hotspot可以设置程序执行的方式
-Xint:完全采用解释器模式执行
-Xcomp完全采用即时编译器模式执行,如果即时编译器出现问题,解释器会介入执行
-Xmixed采用解释器+即时编译器的混合模式共同执行
hotspot中JIT分类
内嵌两个JIT编译器
client
server
大多情况下简称C1,C2
-client:指定Java虚拟机在Client模式下,并使用C1编译器
C1编译器会对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度
方法内联
将引用的函数代码编译到引用点处,减少栈帧的生成,减少参数传递以及跳转过程
去虚拟化
对唯一的实现类进行内联
冗余消除
在运行期把一些不会执行的代码折叠掉
-server:指定虚拟机在server模式下,并使用C2编译器
C2进行耗时较长的优化,以及激进优化,单优化后的代码执行效率更高
逃逸分析是优化的基础,基于逃逸分析在C2上有几种优化
标量替换
用标量值代替聚合对象的属性值
栈上分配
对于未逃逸的对象分配在栈而不是堆
同步消除
清除同步操作,通常指synchronized
最后
jdk10起,hotspot又引入了个全新的即时编译器Graal编译器
JDK9引入了AOT编译器
0 条评论
下一页