Java虚拟机知识体系
2022-06-14 23:06:59 0 举报
AI智能生成
Java虚拟机知识体系
作者其他创作
大纲/内容
参考资料
https://www.jianshu.com/p/c0769aafa436
https://www.processon.com/view/5f814c8c5653bb06eff357ee?fromnew=1
https://www.processon.com/view/5ec5d7c60791290fe0768668?fromnew=1
JVM线程分类
用户线程
Java程序创建的线程
后台线程
虚拟机线程
这个线程等待 JVM 到达安全点操作出现
这些操作必须要在独立的线程里执行,因为当
堆修改无法进行时,线程都需要 JVM 位于安全点。
堆修改无法进行时,线程都需要 JVM 位于安全点。
这类线程有
stop-theworld 垃圾回收
线程栈 dump
线程暂停
周期性任务线程
GC 线程
编译器线程
信号分发线程
JVM整体结构
图解
类加载子系统
加载
链接
初始化
运行时数据区
线程共享
方法区/元空间
堆内存
线程专属
虚拟机栈
程序计数器
本地方法栈
执行引擎
解释器
即时编译器
垃圾回收器
JVM规范包括
Class文件格式
图解
版本信息
常量池信息
访问标识
类型信息
本类类型
父类类型
接口信息
字段信息
方法信息
数据类型
类库
反射
加载和创建类或接口,如classLoader
安全
多线程
弱引用
ASM应用
作用
可以直接生产二进制class文件
可以在类被
编程模型
core api
tree api
运行时数据区
线程专有
程序计数器(PC寄存器)
运行时数据区中唯一不会出现OOM的区域,没有垃圾回收
当前线程所执行的字节码的行号指示器,为了线程切换后能恢复到正确的位置
每个线程有一个独立的程序计数器,线程之间互不影响。
如果线程执行的Java方法,则计数器记录正在执行的虚拟机字节码的指令的地址
如果正在执行的本地方法,这个计数器值则应为空。(undefined)
虚拟机栈
概述
Java虚拟机栈,早起也叫Java栈,每个线程创建时都会创建一个虚拟机栈,
内部保存一个个栈帧,对应着一次次的Java方法调用
内部保存一个个栈帧,对应着一次次的Java方法调用
生命周期和线程的一致
主管Java程序的运行,保存方法的局部变量(8种基本数据类型,对象的引用地址),
部分结果,并参与方法的调用和返回。
部分结果,并参与方法的调用和返回。
栈的存储单位
每个线程都有自己的栈,栈中的数据以栈帧格式存储
线程上正在执行的每个方法都各自对应一个栈帧
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各个数据信息
先进后出,后进先出
一条活动的线程中,一个时间点上,只会有一个活动的栈帧。
只有当前正在执行的方法的栈顶栈帧是有效的,这个称为当前栈帧,对应方法是当前方法,对应类是当前类
只有当前正在执行的方法的栈顶栈帧是有效的,这个称为当前栈帧,对应方法是当前方法,对应类是当前类
执行引擎运行的所有字节码指令只针对当前栈帧进行操作
如果方法中调用了其他方法,对应的新的栈帧会被创建出来,放在顶端,成为新的当前帧
栈运行原理
不同线程中包含的栈帧不允许存在相互引用。
当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,
接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为新的栈帧。
接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为新的栈帧。
Java方法有两种返回方式
一种是正常的函数返回,使用return指令
另外一种是抛出异常,不管哪种方式,都会导致栈帧被弹出
栈的内部结构
局部变量表
定义为一个数字数组,主要用于存储方法参数,定义在方法体内部的局部变量,
数据类型包括各类基本数据类型,对象引用,以及return address类型
数据类型包括各类基本数据类型,对象引用,以及return address类型
局部变量表建立在线程的栈上,是线程私有的,因此不存在数据安全问题
局部变量表容量大小是在编译期确定下来的
局部变量表存放编译期可知的各种基本数据类型(8种),引用类型(reference),return address 类型
最基本的存储单元是slot
32位占用一个slot,64位类型(long和double)占用两个slot
局部变量表中的变量只有在当前方法调用中有效,
虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。
虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。
方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
关于Slot的理解
JVM虚拟机会为局部变量表中的每个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this,会存放在index为0的slot处,其余的参数表顺序继续排列
截图
this截图
栈帧中的局部变量表中的槽位是可以重复的,如果一个局部变量过了其作用域,那么其作用域之后申明的新的局部变量就有可能会复用过期局部变量的槽位,从而达到节省资源的目的
静态变量与局部变量对比及小结
变量的分类
按照数据类型分
基本数据类型
引用数据类型
按照声明的位置
成员变量,在使用前经历过初始化过程
类变量
链接的准备阶段给类变量默认赋值,初始化阶段显示赋值,即静态代码块赋值
实例变量
随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
局部变量
在使用前,必须进显式赋值,否则编译不通过
补充:
在栈帧中,与性能调优关系最密切的部分,就是局部变量表,方法执行时,虚拟机使用局部变量表完成方法的传递
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
操作数栈
在方法执行的过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈
截图
如果被调用方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新程序计数器中下一条需要执行的字节码指令
Java虚拟机的解释引擎是基于栈的执行引擎,其中栈就是操作数栈
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
每一个操作数栈会拥有一个明确的栈深度,用于存储数值,最大深度在编译期就定义好
栈中,32bit类型占用一个栈单位深度,64bit类型占用两个栈单位深度
操作数栈并非采用访问索引方式进行数据访问,而是只能通过标准的入栈、出栈操作完成一次数据访问
栈顶缓存技术
由于操作数是存储在内存中,频繁的进行内存读写操作影响执行速度,将栈顶元素全部缓存到物理CPU的寄存器中,依此降低对内存的读写次数,提升执行引擎的执行效率
动态链接
指向常量池的方法 引用
每一个栈帧内部都包含一个指向运行时常量池中该帧所属方法的引用
目的是为了支持当前方法的代码能够实现动态链接,比如invokedynamic指令
在java源文件被编译成字节码文件中时,所有的变量、方法引用都作为符号引用,保存在class文件的常量池中。
描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的。
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
常量池、运行时常量池
常量池在字节码文件中,运行时常量池,在运行时的方法区中
方法的调用
静态链接
当一个字节码文件被装载进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会把该类的方法也初始化完毕
截图
方法返回地址
存放调用该方法的pc寄存器的值
方法的结束
正常执行完成
出现未处理异常,非正常退出
无论哪种方式退出,方法退出后,都会返回该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
异常退出的,返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息
执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口
返回指令包括
ireturn返回值是boolean,byte,char,short,和int类型时使用
lreturn
dreturn
areturn
引用类型
还有一个return指供声明为 void的方法、实例初始化方法、类和接口的初始化方法使用
本质上,方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
一些附加信息
允许携带与Java虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息。不确定有,可选情况
本地方法栈
线程共享
堆
什么Jvm的堆
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
Java堆区在JVM启动的时候即被创建,其空间大小也就确认了。堆内存的大小是可调节的
Java虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(TLAB)
“几乎”所有的对象实例都在这里分配内存
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,引用指向对象或者数组在堆中的位置
方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
堆是GC执行垃圾回收的重点区域
堆的内存结构
Java7及之前
新生区
Eden区
Survivor区
from
to
谁空谁是to
老年区
永久区
Java8及之后
新生区
Eden区
Survivor区
from
to
谁空谁是to
老年区
元空间
堆中对象分配流程
图解
方法区
方法区的理解
Java虚拟机规范中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,
但是一些简单的实现,可能不会选择去进行垃圾收集或者进行压缩。
对于HotSpot而言,方法区还有一个别名叫Non-Heap(非堆),目的就是要和堆分开
但是一些简单的实现,可能不会选择去进行垃圾收集或者进行压缩。
对于HotSpot而言,方法区还有一个别名叫Non-Heap(非堆),目的就是要和堆分开
所以方法区看作是一块独立于Java堆的内存空间
方法区和Java堆一样,是各个线程共享的内存区域
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样,都是可以不连续的
方法区的大小和堆空间一样,可以选择固定大小或者可扩展
方法区的大小决定了系统可以保存多少个类,如果定义太多类,加载大量的第三方的Jar包,
Tomcat部署过多工程,导致方法区溢出,虚拟机同样会抛出内存溢出OOM:PermGen space或者Metaspace
Tomcat部署过多工程,导致方法区溢出,虚拟机同样会抛出内存溢出OOM:PermGen space或者Metaspace
关闭JVM就会释放这个区域的内存
HotSpot中方法区的演进
在jdk7及以前,习惯上把方法区,称为永久代,
jdk8开始,使用元空间取代了永久代
jdk8开始,使用元空间取代了永久代
元空间的本质和永久带类似,都是对JVM规范中方法区的实现。
不过元空间与永久代最大的区别在于:
元空间不在虚拟机设置的内存中,而是使用本地内存
不过元空间与永久代最大的区别在于:
元空间不在虚拟机设置的内存中,而是使用本地内存
根据Jvm规范,如果方法区无法满足新的内存分配需求,将抛出OOM异常
本质上,方法区和永久代并不等价,仅是对HostSpot而言的。
Java虚拟机规范,对如何实现方法区,不做统一要求,例如BEA JRockit/IBM J9中不存在永久代的概念
现在来看,当年使用永久代,不是好的点子,导致Java程序更容易OOM
-XX:MaxPermSize
永久代为什么被元空间替换
随着 JAVA8的到来,HotSpotVM中再也见不到永久代了,但是并不意味着类的元数据信息也消失了,
这些数据被转移到了一个与堆不相连的本地内存区域,这个区域叫做元空间MetaSpace
这些数据被转移到了一个与堆不相连的本地内存区域,这个区域叫做元空间MetaSpace
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统的可用内存空间
为永久代设置空间大小很难确定,在某些场景下,如果动态加载类过多,就容易产生OOM
而元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制
对永久代进行调优是很困难的
设置方法区大小与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会触发并卸载没有用的类,然后高水位线会被重置。新的高水位线的值取决于GC后释放了多少元空间。
如果释放空间不足,在不超过最大设定值时,适当提高该值。如果释放空间过多,则适当降低该值。
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,fullGC多次调用。
为了避免频繁FullGC,建议将-XX:MetaspaceSize设置为一个相对较高的值
为了避免频繁FullGC,建议将-XX:MetaspaceSize设置为一个相对较高的值
方法区存储什么
它用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存
类型信息
对于每个加载的类型(类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,
方法区的垃圾回收
有些人认为方法区是没有垃圾收集行为的,其实不然。Java虚拟机规范对方法区的约束非常宽松,提到过可以不要求虚拟机在方法区实现垃圾收集。事实上,也确实有未实现或未能完整实现方法区类型卸载的收集器,如JDK11 ZGc
方法区的垃圾收集主要回收两部分内容
常量池中废弃的常量
HotSpot对常量池的回收策略很明确,只要常量池中的常量没有被任何地方引用,就可以被回收
回收废弃常量与回收Java堆中对象非常类似
不再使用的类型
需要同时满足三个条件
该类所有的实例已经被回收
java堆中不存在该类及其任何派生子类的实例
加载该类的类加载器已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法
满足以上三个条件后,并不是和对象一样立即被回收,仅仅是允许。
HotSpot虚拟机提供了-Xnoclassgc参数进行控制
在大量使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OSGI这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力
方法区内常量池中主要存放的两大类常量:
字面量
比较接近Java语言层次的常量概念,如文本字符串,被声明为final的常量值等
符号引用
属于编译原理方面的概念
类和接口的全限定名
字段的方法和描述符
方法的名称和描述符
堆与,栈、方法区的关系
从线程共享的角度来看
从堆、栈、方法区交互关系来看
垃圾回收
垃圾回收相关概念
System.gc()的理解
System.gc或Runtime.getRuntime().gc()的调用,会显示触发FullGC,同时会对老年代和新生代进行回收,尝试释放被丢对象占用的内存
然后System.gc调用无法保证对垃圾收集器的调用
一些特殊情况下,比如编写性能基准,我们可以在运行之间调用System.gc
内存溢出与内存泄露
OOM
java 虚拟机的堆内存设置不够
代码创建大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
内存泄露
只有对象不再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄露
实际情况有一些疏忽导致对象的生命周期变的很长甚至OOM,宽泛意义上的内存泄露
举例
单例的生命周期和程序是一样长,如果单例程序中,持有对外部对象的引用的话,那么这个外部对象是不能被回收的,导致内存泄露
一些提供close的资源未关闭导致内存泄露,如数据库链接,网络链接,和IO
StopTheWorld
垃圾回收的并行与并发
并发
同一时间段内,几个程序都在同一个处理器上运行
CPU切换
并行
一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互相不抢占资源,可以同时进行,我们称之为并行
并行因素取决于CPU的核心数量
并发的多个任务之间抢占资源
并行多个任务之间不互相抢占资源
垃圾回收的并发与并行
并行
多条垃圾收集器并行工作,用户线程处于等待状态
串行
单线程执行。
安全点与安全区域
安全点
程序执行并非在所有地方都能停顿下来开始GC,只有特定的位置才能停顿下来开始GC,这些位置称为安全点
如果太少,导致GC等待时间长,如果太多导致运行时性能问题,大部分指令执行都比较短,
通常会根据是否具有让程序长时间执行的特征为标准选择一些执行时间较长的指令作为安全点,
比如方法调用,循环跳转和异常跳转等
通常会根据是否具有让程序长时间执行的特征为标准选择一些执行时间较长的指令作为安全点,
比如方法调用,循环跳转和异常跳转等
抢先式中断
中断所有线程,如果还有线程不在安全点,就恢复线程,让线程跑到安全点
没有虚拟机采用
主动式中断
设置一个中断标志,各个线程运行到安全点的时候,主动轮询这个标志,如果标志为真,则将自己进行中断挂起
安全区域
如果线程处于sleep或者blocked状态,这时候线程无法响应jvm中断请求,走到安全点去中断挂起。对于这种情况,就需要安全区域来解决
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何位置开始GC都是安全的。
当线程运行到安全区域代码时,首先标志已经进入了安全区域,如果GC,JVM会忽略标识为安全区域状态的线程
当线程即将离开安全区域时,会检查JVM是否已经完成GC,如果完成了,则继续运行。否则线程必须等待直到收到可以安全离开安全区域的信号为止
强引用
最传统的引用定义,程序代码中普遍存在的引用赋值,类似new Object这种引用关系,
无论任何情况下,强引用存在,垃圾收集器永远不会回收掉被引用的对象
无论任何情况下,强引用存在,垃圾收集器永远不会回收掉被引用的对象
强引用是造成java内存泄露的主要原因之一
强引用可以直接访问目标对象
软引用
软引用需要用 SoftReference 类来实现
系统将要发生内存溢出之前,会将这些对象列入回收范围之中进行第二次回收,
如果这些回收后还没有足够内存,才会抛出内存溢出异常
如果这些回收后还没有足够内存,才会抛出内存溢出异常
软引用通常用来实现内存敏感的缓存,高速缓存就有用到软引用
垃圾回收器在某个时间决定回收软可达的对象的时候,
会清理软引用,并可选的把引用存放到一个引用队列
会清理软引用,并可选的把引用存放到一个引用队列
弱引用
弱引用需要用 WeakReference 类来实现
它比软引用的生存期更短,对于只有弱引用的对象
来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
一个对象是否有虚引用存在,完全不会对其生存时间构成影响。
唯一目的就是在这个对象被收集器回收时收到一个系统通知
唯一目的就是在这个对象被收集器回收时收到一个系统通知
他不能单独使用,也无法通过虚引用获取被引用的对象。
终结器引用
用以实现对象的finalize方法,所以被称为终结器引用
无需手动编码,其内部配合引用队列使用
GC时,终结器引用入队,
由finalize线程通过终结器引用找到被引用对象并调用他的finalize方法,
第二次GC时才能回收被引用对象
由finalize线程通过终结器引用找到被引用对象并调用他的finalize方法,
第二次GC时才能回收被引用对象
垃圾回收相关算法
标记阶段算法(找到垃圾)
标记阶段:引用计数算法
ReferenceCounting
ReferenceCounting
对每个对象保存一个整型的引用计数器属性,用于记录被对象引用的情况
被对象引用了就+1,引用失效就-1,0表示不可能再被使用,可进行回收
优点:实现简单,垃圾便于辨识,判断效率高,回收没有延迟性
缺点
需要单独的字段存储计数器,增加了存储空间的开销
每次赋值需要更新计数器,伴随加减法操作,增加了时间开销
无法处理循环引用的情况,致命缺陷,导致JAVA的垃圾回收器中没有使用这类算法
小结
引用计数算法,是很多语言的资源回收选择,例如python,它更是同时支持引用计数和垃圾回收机制
Python如何解决循环引用
手动解除
使用弱引用,weakref,python提供的标准库,旨在解决循环引用
标记阶段:可达性分析算法
GC Root tracing
根搜索算法
GC Root tracing
根搜索算法
java c#选择的
基本思路
是以根对象(GCRoots)为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达
使用可达性分析算法后,内存中存活的对象都被被根对象集合直接或间接连接着,搜索所走过的路径称为引用链
如果目标对象没有任何引用链相连,则是不可达的,意味着该对象已经死亡,可以标记为垃圾对象
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活的对象
GC Roots包括
虚拟机栈中引用的对象
比如各个线程被调用的方法中使用到的参数、局部变量
本地方法栈内JNI,引用的对象
方法区中静态属性引用的对象
比如:java类的引用类型静态变量
方法区中常量引用的对象
比如字符串常量池里的引用
所有被同步锁synchronized持有的对象
Java虚拟机内部的引用
基本数据类型对应的class对象,一些常驻的异常对象,如nullpointerException,OOMerror,系统类加载器
反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等
除了固定的GC Roots集合之外,根据用户选择的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时性的加入,共同构成完整GCRoots集合,比如分代收集和局部回收
如果只针对Java堆中某一块内存区域进行垃圾回收,必须要考虑这个区域的对象可能被其他区域对象所引用,这是需要一并将关联的区域对象加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
小技巧
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那么它就是一个Root
如果需要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话,分析结果的准确性就无法保证。
这也是GC进行时必须STW的一个重要原因,即使是号称几乎不会发生停顿的CMS收集器中,枚举根节点也是必须要停顿的。
清除阶段算法(怎么回收垃圾)
清除阶段:标记-清除算法
Mark-sweep
Mark-sweep
执行流程
从引用根节点开始遍历,标记所有被引用的对象,
一般是在对象Header中记录为可达对象
一般是在对象Header中记录为可达对象
注意标记引用对象,不是垃圾对象
对堆内存从头到尾进行线性的遍历,
如果发现某个对象在其Header中没有标记为可达对象,则将其回收
如果发现某个对象在其Header中没有标记为可达对象,则将其回收
所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,
下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就存放。
下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就存放。
缺点
效率不算高
在GC的时候,需要停止整个应用程序,导致用户体验差。
这种方式清理出来的空闲内存不连续,
产生内存碎片,需要维护一个空闲列表
产生内存碎片,需要维护一个空闲列表
清除阶段:复制算法
执行流程
将内存空间分为两块,每次使用其中一块。在垃圾回收时,
将正在使用的内存中的存活的对象复制到未被使用的内存块中,
之后清除正在使用的内存块中的所有的对象,交换两个内存的角色,
最后完成垃圾回收
将正在使用的内存中的存活的对象复制到未被使用的内存块中,
之后清除正在使用的内存块中的所有的对象,交换两个内存的角色,
最后完成垃圾回收
优点
没有标记和清除的过程,实现简单高效
复制过去以后的保证空间的连续性,不会出现碎片的问题
缺点
需要两倍的内存空间
对于G1这种拆分为大量region的GC,复制而不是移动,
意味着GC需要维护region之间的引用关系,不管是内存占用或者时间开销也不小。
意味着GC需要维护region之间的引用关系,不管是内存占用或者时间开销也不小。
如果系统中的垃圾对象很多,需要复制的存活对象数量并不会太大,或者非常低才行
清除阶段:标记-压缩算法
也叫标记整理算法
也叫标记整理算法
执行过程
第一个阶段和标记清除算法一样,从根节点开始标记所有被引用的对象
第二阶段将所有的存活对象压缩在内存的一端,按照顺序排放之后清理边界外所有的空间
说明
最终效果等同于标记清除算法执行完成后,再进行一次内存碎片整理。
与标记清除算法本质区别,标记清除算法是非移动式的算法,标记压缩是移动式的
是否移动回收后的存活对象是一项优缺点并存的风险决策
优点
消除了标记清除算法内存区域分散的缺点,
消除了复制算法中,内存减半代价
缺点
从效率上来讲,标记整理算法要低于复制算法
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
移动的过程中,需要全程暂停用户应用程序,即STW
分代收集算法
不同生命周期的对象可以采取不同额收集方式,以便提高回收效率
几乎所有的GC都采用分代收集算法执行垃圾回收的
HotSpot中
年轻代
生命周期短,存活率低,回收频繁
老年代
区域较大,生命周期长,存活率高,回收不及年轻代频繁
增量收集算法、分区算法
增量收集算法思想
每次垃圾收集线程只收集一小片区域的内存空间,
接着切换到应用程序线程,依次反复,直到垃圾收集完成
接着切换到应用程序线程,依次反复,直到垃圾收集完成
通过对线程间冲突的妥善管理,
允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
缺点
线程和上下文切换导致系统吞吐量的下降
分区算法
为了控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,
每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的时间
每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的时间
分代算法是将对象按照生命周期长短划分为两个部分,
分区算法是将整个堆划分为连续的不同的小区间
分区算法是将整个堆划分为连续的不同的小区间
每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个小区间
比较
对象的finalization机制
Java语言提供了对象终止finaliztion机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
当垃圾回收器发现没有引用指向一个对象,即垃圾回收此对象之前,总会先调用这个对象的finalize()方法
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放,通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件,套接字和数据库链接等
定义虚拟机的对象可能的三种状态
可触及的
从根节点开始,可以到达这个对象
可复活的
对象的所有引用都被释放了,但是对象有可能在finalize()中复活
不可触及的
对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次
只有对象再不可触及时才可以被回收
具体过程
判断一个对象ObjA是否可以被回收,至少需要经历两次标记过程
1、如果对象到GCRoots没有引用链,则进行第一次标记
2、进行筛选,判断此对象是否有必要执行finalize()方法
如果对象A没有重写finalize方法,或者finalize方法已经被虚拟机调用过,则虚拟机视为没有必要执行,对象A被判定为不可触及的
如果对象A重写finalize()方法,且还未执行过,那么A会被插入到F-queue队列中,有一个虚拟机自动创建的,低优先级的Finalizer线程触发其finalize()方法执行
finalize方法是对象逃脱死亡的最后机会,稍后GC会对F-queue队列中的对象进行第二次标记,如果A在finalize方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,A会被移除即将回收集合。之后,对象会再次出现没有引用存在的情况下,finalize方法不会再被调用,对象直接变为不可触及状态
MAT与JProfiler的GC Roots溯源
MAT是Memory Analyzer的简称,是一款功能强大的Java堆内存分析器。
用于查找内存泄露以及查看内存消耗情况,基于Eclipse开发的一款免费性能分析工具
用于查找内存泄露以及查看内存消耗情况,基于Eclipse开发的一款免费性能分析工具
垃圾回收器
GC分类与性能指标
垃圾回收器分类
按垃圾回收线程数
串行垃圾回收器
串行回收指同一个时间段内,只允许一个CPU用于执行垃圾回收操作,
此时工作线程被暂停,直到垃圾收集工作结束
此时工作线程被暂停,直到垃圾收集工作结束
在单CPU处理器或者较小应用内存等硬件平台不是特别优越的场合,
串行回收器的性能表现可以超过并行回收器和并发回收器。
所以串行回收默认被应用在客户端的client模式下的JVM中
串行回收器的性能表现可以超过并行回收器和并发回收器。
所以串行回收默认被应用在客户端的client模式下的JVM中
在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器
并行垃圾回收器
和串行相反,并行收集可以运用在多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,
不过并行回收仍然与串行回收一样,采用独占式,使用了STW机制
不过并行回收仍然与串行回收一样,采用独占式,使用了STW机制
按照工作模式分
并发式
垃圾回收器与应用程序交替工作,以尽可能减少应用程序的停顿时间
独占式
一旦运行,就停止应用程序中所有的用户线程,直到垃圾回收过程完全结束
按照碎片处理方式
压缩式
非压缩式
按个工作内存区间分
年轻代
老年代
性能指标
吞吐量
运行用户代码的时间占总运行时间的比例
总运行时间:程序的运行时间+内存回收的时间
吞吐量优先,意味着单位时间内,STW的时间最短
暂停时间
执行垃圾收集时,程序的工作线程被暂停的时间
暂停时间优先,意味着单次STW的时间最短,但是频率可能增加
内存占用
Java堆区所占的内存大小
其他
垃圾收集开销
吞吐量的补数,垃圾收集所占用的时间与总运行时间的比例
收集频率
相对于应用程序的执行,收集操作发生的频率
快速
一个对象从诞生到被回收经历的时间
不可能三角
简单来说抓住两点,吞吐量和暂停时间
高吞吐量与低暂停时间,是一对互相竞争的。因为如果高吞吐量优先,必然需要降低内存回收的执行频率,
导致GC需要更长的暂停时间来执行内存回收。
导致GC需要更长的暂停时间来执行内存回收。
如果选择低延迟优先为原则,也只能频繁的执行内存回收,引起程序吞吐量的下降
现在的标准,在最大吞吐量优先的情况下,降低停顿时间
不同的垃圾回收器概述
垃圾回收器的发展迭代史
Serial GC
1999年jdk1.3.1
第一款GC
ParNew
是SerialGC收集器的多线程版本
Parallel GC和Concurrent Mark SweepGC
jdk1.4.2
2002年2月26日
ParallelGC在JDK1.6之后称为HotSpot默认GC
G1
2012年
jdk1.7u4
2017年JDK9中G1变成默认的垃圾收集器,以替代CMS
2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性改善最坏情况下的延迟
Epsilon 垃圾回收器、ZGC,可伸缩低延迟垃圾回收器
2018年9月JDK11
Shenandoah GC:低停顿时间的GC,实验版
2019年3月JDK12
增强ZGC
2019年9月JDK13
删除CMS垃圾回收器,扩展ZGC在macOS和Windows上的应用
2020年3月JDK14
7款经典垃圾收集器和垃圾分代之间的关系
截图
垃圾收集器的组合关系
截图
jdk8之前,可以用虚线参考关系
CMS下面的实线,是CMS回收失败的后备方案
JDK8中取消了红线的组合,标记为废弃的。如果要用也可以用。
JDK9中将红线做了remove
jdk14中弃用了绿线组合
jdk14中删除了CMSGC
JDK9默认G1
JDK8默认Parallel Scavenge 和Parallel old Gc
新生代用了Parallel Scavenge 则老年代自动触发用Parallel old
Parallel底层与ParNew底层不同,所以不能和CMS组合
如何查看默认的垃圾收集器
-XX:+PrintCommandLineFlags
jinfo -flag 相关垃圾回收器参数 进程ID
各种垃圾回收器原理
Serial回收器:串行回收(单线程、 复制算法)
Serial收集器采用复制算法,串行回收和STW机制的方式执行内存回收
除了年轻代,还有用于执行老年代的Serial old收集器,
同样采取了串行回收,但是用标记压缩算法
同样采取了串行回收,但是用标记压缩算法
使用一个CPU或者一条收集线程去完成垃圾收集工作,在进行垃圾收集时,必须暂停其他所有工作线程
优势
简单而高效,对于限定单个CPU的环境来说,
由于没有线程交互的开销,可以获取最高的单线程收集效率
由于没有线程交互的开销,可以获取最高的单线程收集效率
是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器
HotSpot虚拟机中,使用-XX:+UseSerialGC指定年轻代和老年代使用串行收集器
对于交互强的应用而言,不会采取串行垃圾收集器
ParNew回收器:并行回收( Serial+多线程)
除了采用并行回收,其他方面和Serial之间几乎没有任何区别
-XX:UseParNewGC手工指定ParNew收集器执行内存回收任务,它表示年轻代使用,不影响老年代
-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数
很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
Parallel回收器:吞吐量优先(多线程复制算法、高效)
也是并行回收,在垃圾收集过程中都需要暂停所有的工作线程。
和ParNew不同,它的目标是达到一个可控制的吞吐量
自适应调节策略也是Parallel 与ParNew的一个重要区别
适合后台运算不需要太多交互的任务,例如执行批量处理,订单处理,工资支付,科学计算的应用程序
Parallel old采取标记压缩算法,同样基于并行回收和STW机制
参数配置
-XX:+UseParallelGC
手动指定年轻代使用此收集器执行内存回收任务
-XX:+UseParallelOldGC
手工指定老年代使用并行回收收集器,分别适用于新生代和老年代,默认jdk8是开启的
与上面这两个参数关联,开启一个,默认开启另一个。
-XX:ParallelGCThreads
设置年轻代并行收集器的线程数,一般与CPU数量相同,如果CPU数量大于8个,则值=3+(5*N/8)
-XX:MaxGCPauseMillis
设置收集器最大停顿时间,单位毫秒
改参数谨慎使用
-XX:GCTimeRatio
垃圾收集占总时间比,用于衡量吞吐量大小
默认99,取值范围0-100,也就是垃圾回收时间不超过1%
与上一个参数矛盾,暂停时间越长,Ratio参数就容易超过设定比例
-XX:+UseAdaptiveSizePolicy
开启自适应调节策略
这种模式下,年轻代大小,Eden和Survivor的比例,晋升老年底对象年龄参数都会被自动调整
为了达到堆大小,吞吐量和停顿时间之间的平衡点
在手动调优比较困难的场景下,可以直接用自适应方式,仅指定虚拟机最大堆,目标吞吐量和停顿时间,让虚拟机自己完成调优工作
CMS回收器:低延迟(多线程标记清除算法)
执行流程
初始标记:STW,仅仅只是标记处GC Roots能直接关联的对象,
一旦标记完成后就会恢复之前被暂停的所有应用线程,
由于直接关联对象比较小,所以这里速度非常快
一旦标记完成后就会恢复之前被暂停的所有应用线程,
由于直接关联对象比较小,所以这里速度非常快
并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,
这个过程耗时较长,但是不需要停顿用户线程。可以与垃圾收集线程一起并发运行
这个过程耗时较长,但是不需要停顿用户线程。可以与垃圾收集线程一起并发运行
重新标记:为了修正并发标记期间,因用户程序继续运作导致标记产生变动的那一部分对象的标记记录
并发清除:清理删除标记阶段判断的已经死亡的对象,释放内存空间。
由于不需要移动存活对象,所以这个阶段也可以与用户线程同时并发
由于不需要移动存活对象,所以这个阶段也可以与用户线程同时并发
CMS说明
初始标记和重新标记阶段仍然需要STW机制
由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。
因此CMS收集器不能像其他收集器那样等到老年代几乎填满再进行回收,而是当堆内存使用率达到某一阈值时,便开始进行回收。
因此CMS收集器不能像其他收集器那样等到老年代几乎填满再进行回收,而是当堆内存使用率达到某一阈值时,便开始进行回收。
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure失败,
这时虚拟机启用备用方案,临时启用Serial old 收集器来重新进行老年代的垃圾收集,这样停顿时间就长了。
这时虚拟机启用备用方案,临时启用Serial old 收集器来重新进行老年代的垃圾收集,这样停顿时间就长了。
CMS采取标记清除算法,会产生内存碎片,只能够选择空闲列表执行内存分配
为什么不采取标记压缩呢?
因为并发清除时,如果用压缩整理内存,原来的用户线程使用的内存就无法使用了。标记压缩更适合STW场景下使用
jdk1.5推出Concurrent Mark Sweep 并发的标记清除,
第一次实现了让垃圾收集线程与用户线程同时工作
优点
并发收集
低延迟
缺点
会产生内存碎片
对CPU资源非常敏感
在并发阶段会占用一部分线程导致应用程序变慢
无法处理浮动垃圾
并发标记阶段是与工作线程同时运行,如果并发阶段产生垃圾对象,CMS无法进行标记,
导致新产生的垃圾对象没有被及时回收,只能在下一次执行GC时释放空间
导致新产生的垃圾对象没有被及时回收,只能在下一次执行GC时释放空间
参数
-XX:+UseConcMarkSweepGC
手工指定CMS收集器执行内存回收任务
开启后,自动将-XX:UseParNewGC打开,即ParNew(Young区)+CMS(old区)+Serial GC组合
-XX:CMSlnitiatingOccupanyFraction
设置堆内存使用率的阈值
一旦达到该阈值,则开始进行回收
jdk5及之前默认68,即老年代的空间使用率达到68%时会执行一次CMS回收
JDK6及以上默认值为92%
如果内存增长缓慢,可以设置一个稍大的值,有效降低CMS的触发频率,减少老年代回收的次数
如果应用程序内存使用率增加很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。
-XX:+UseCMSCompactAtFullCollection
用于执行完Full GC后对内存空间进行压缩整理
不过内存压缩无法并发执行,会带来停顿时间更长的问题
-XX:CMSFullGCsBeforeCompaction
设置执行多少次FullGC后对内存空间进行压缩整理
-XX:ParallelCMSThreads
设置CMS的线程数量
默认启动的线程数是(ParallelGCThreads+3)/4
ParallelGCThreads是年轻代并行收集器的线程数
小结
如果想要最小化使用内存和并行开销,选择Serial GC
如果最大化应用程序的吞吐量,选择ParallelGC
如果想要最小化的GC的中断或停顿时间,选择CMS GC
jdk9标记为废弃的,jdk14已经删除了
G1回收器:区域化分代式
G1说明
官方给G1设定的目标
就是在延迟可控的情况下,获得尽可能高的吞吐量,
所以才担当起全功能收集器的重任和期望
所以才担当起全功能收集器的重任和期望
主要目标是获取最短垃圾回收停顿时间
Garbage First
G1是一个并行回收器,他把堆内存分割为很多不相关的区域(Region)(物理上不连续)
使用不同的region表示Eden,s0,s1,老年代等
G1跟踪各个region里面垃圾堆积的价值大小,在后台维护一个优先列表,
每次根据允许的收集时间,优先回收价值最大的Region
每次根据允许的收集时间,优先回收价值最大的Region
region
所有region大小相同,且在JVM生命周期内不会改变
region可以充当多个角色
JDK1.7版本正式启用,jdk9以后默认垃圾回收器
JDK8还不是默认的,需要用-XX:+UseG1GC来启用
记忆集
每个region对应一个记忆集
通过记忆集避免全局扫描
每次引用类型数据写操作时,会产生一个写屏障暂时中断操作
然后检查将要希尔的引用指向的对象是否和该引用对象类型数据在不同的region,如果不同就通过CardTable把相关的引用信息记录到引用指向对象所在的Region对应的记忆集中
当进行垃圾收集时,在GC根节点枚举范围加入记忆集,就可以保证不进行全局扫描,也不会有遗漏
优点
并行与并发
分代收集
同时兼顾年轻代与老年代
空间整合
region之间用复制算法,整体可以看做是标记压缩算法。
两种算法都避免内存碎片,有利于程序长时间运行,分配大对象不会因为无法找到连续空间提前触发下一次GC,
尤其当Java堆非常大的时候,G1优势更加明显
尤其当Java堆非常大的时候,G1优势更加明显
可预测的停顿时间模型
能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不能超过N毫秒
缺点
相较于CMS,G1不具备全方位,压倒性优势。比如用户程序运行中,
G1无论是为了垃圾收集产生的内存占用,还是程序运行时的额外执行负载都要比CMS要高
G1无论是为了垃圾收集产生的内存占用,还是程序运行时的额外执行负载都要比CMS要高
经验上来说,小内存应用CMS表现大概率优于G1,在大内存上G1优势发挥更多,平衡点再6-8GB
参数设置
-XX:+UseG1GC
-XX:G1HeapRegionSize
设置每个Region大小,值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆划分出约2048个区域,默认是堆内存的1/2000
-XX:MaxGCPauseMillis
设置期望达到的最大GC停顿时间指标,JVM尽力但不保证,默认200ms
-XX:ParallelGCThread
设置STW工作线程数的值,最多设置8
-XX:ConcGCThreads
设置并发标记的线程数,将N设置为并行垃圾回收线程数(parallelGCThreads)的1/4左右
-XX:InitiatingHeapOccupancyPercent
设置触发并发GC周期的Java堆占用率阈值,超过此值就触发GC,默认是45
常见调优
第一步开启G1垃圾收集器
第二步,设置堆的最大内存
第三步,设置最大的停顿时间
G1提供了三种垃圾回收模式在不同的条件下触发
YoungGC
MixedGC
FullGC
适用场景
面向服务器端应用,针对具有大内存,多处理器的机器
最主要应用是需要低GC延迟
如:在堆大小约6GB或更大,可预测的暂停时间可以低于0.5s,
G1每次清理一部分region来保证每次GC停顿时间不会过长
G1每次清理一部分region来保证每次GC停顿时间不会过长
用来替换1.5中的CMS
超过50%的Java堆被活动数据占用
对象分配频率或年代提升频率变化很大
GC停顿时间过长,长于0.5~1秒
垃圾回收过程
年轻代GC
当年轻代eden区用尽时
并行独占式收集器
老年代并发标记过程
当堆内存使用到一定值,默认45%
混合回收
标记完成马上开始混合回收
G1老年代回收器不需要整个老年底都被回收,一次只需要扫描回收一小部分老年代的region就可以了。
同时这个老年代回收是和年轻代一起被回收的。
有可能fullGC
G1回收过程一,年轻代GC
1、扫描根
根是指static变量指向的对象,正在执行的方法调用链上的局部变量等。根引用连同Rset记录的外部引用作为扫描存活对象的入口
2、更新Rset
处理dirty card queue中的card,更新Rset,此阶段完成后,Rset可以准确的反应老年代所在的内存分段中对象的引用
3、处理Rset
识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象
4、复制对象
对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor去中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,会加一,达到阈值会被复制到old区中空的内存分段,如果Survivor区空间不够,Eden空间的部分数据会直接晋升到老年代空间
5、处理引用
处理强软弱虚,终结器引用,本地方法接口引用等,最红eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
G1回收过程二、并发标记过程
初始标记阶段STW
标记从根节点直接可达的对象,并且触发一次年轻代GC
根区域扫描阶段
扫描Survivor区直接可达老年代区域对象,并标记被引用的对象,这个过程在youngGC之前完成
并发标记
和应用程序并发执行,并发标记阶段若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。
并发标记过程中,会计算每个区域的对象活性,存活对象的比例
再次标记
由于应用程序持续进行,需要修正上次标记结果,STW,G1采取比CMS更快的初始快照算法
独占清理
计算各个区域存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下个阶段做铺垫,STW,
这个阶段并不会实际上去做垃圾的收集
并发清理阶段
识别并清理完全空闲的区域
G1回收过程三:混合回收
当越来越多的对象晋升到老年代old region时,为了避免内存被耗尽,虚拟机会触发一次混合的垃圾收集器,该算法除了回收整个young region,还会回收一部分的old region。也要注意Mixed gc并不是fullgc
并发标记结束后,老年代中百分百为垃圾的内存分段被回收了。部分为垃圾的内存分段被计算出来了,默认情况下,这些老年代的内存分段会分8次被回收-XX:G1MixedGCCountTarget设置
混合回收的回收集包括八分之一的老年代,Eden区内存分段,Survivor区内存分段。
由于老年代中内存分段默认分8次回收,G1会优先回收垃圾多的内存分段,并且有一个阈值会决定内存分段是否被回收。-XX:G1MixedGCLiveThresholdPercent,默认为65%。意思是垃圾占比达到65%才会被回收。如果垃圾占比比较低,意味存活对象较高,复制的时候花更多时间。
混合回收不一定要进行8次,有一个阈值:-XX:G1HeapWastePercent
默认值是10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存比例低于10%,则不再进行混合回收,因为GC花费更多的时间,但是回收到的内存却很少。
G1可选过程四:fullGC
G1初衷就是要避免FULLGC,如果上述方式不能正常工作,G1会停止应用程序的执行。使用单线程的内存回收算法进行垃圾回收,性能非常差。应用程序停顿时间长
比如堆太小,当G1复制存活对象的时候没有空的内存分段可用,则会回退到FullGC
导致FullGC原因可能有两个
回收阶段的时候没有足够的to-space存放晋升的对象
并发处理过程完成之前空间耗尽了。
优化建议
避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小
固定的年轻代大小会覆盖暂停时间目标
暂停时间目标不要太苛刻,太苛刻会影响吞吐量
垃圾回收器总结
截图
GC日志分析
截图
截图
截图
截图
截图
截图
GC EASY
垃圾回收器的新发展
截图
Shenandoah GC
强项
低延迟时间
弱项
高运行负担下的吞吐量下降
ZGC
在尽可能堆吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾回收的停顿时间限制在10毫秒以内的低延迟
并发标记,并发预备重分配,并发重分配,并发重映射
除了初始标记是STW,其他地方几乎都是并发执行的
类加载子系统
作用
负责从文件系统或者网络中加载Class文件,Class文件开头有特定标识,魔术,咖啡杯壁
Classloader只负责class文件的加载,至于是否可运行,则由执行引擎决定
加载的类信息存放于称为方法区的内存空间,除了类信息,方法区还会存放运行时常量池信息,还可能包括字符串字面量和数字常量
常量池运行时加载到内存中,即运行时常量池
角色
截图
类的加载过程
加载
加载刚好是加载过程的一个阶段,二者意思不能混淆
通过一个类的全限定名获取定义此类的二进制字节流
本地系统获取
网络获取,Web Applet
zip压缩包获取,jar,war
运行时计算生成,动态代理
有其他文件生成,jsp
专有数据库提取.class文件,比较少见
加密文件中获取,防止Class文件被反编译的保护措施
将这个字节流所代表的的静态存储结果转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
链接
验证
目的
确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
四种验证
文件格式验证
CA FE BA BE(魔数,Java虚拟机识别)
主次版本号
常量池的常量中是否有不被支持的常量类型
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
元数据验证
对字节码描述的信息进行语义分析,保证描述符合Java规范
类是否有父类,除了Object之外,所有的类都应该有父类
类的父类是否继承了不允许被继承的类(被final修饰的类)
如果这个类不是 抽象类,是否实现了其父类或接口中要求实现的所有方法。
类的字段,方法是否与父类的产生矛盾。例如方法参数都一样,返回值不同
字节码验证
通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。
对类的方法体,进行校验分析,保证在运行时不会做出危害虚拟机的行为
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,不会出现类似于在操作数栈放了一个int类型的数据,使用时却按照long类型加载到本地变量表中的情况。
保障任何跳转指令都不会跳转到方法体之外的字节码指令上。
符号引用验证
通过字符串描述的全限定名是否能找到对应的类
符号引用中的类、字段、方法的可访问性是否可被当前类访问
准备
为类变量分配内存,并且设置该类变量的初始值,即零值
零值
不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化
不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象一起分配到Java堆中
解析
将常量池内的符号引用转换为直接引用的过程
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
符号引用就是一组符号来描述引用的目标。符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中
直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄
解析动作主要针对类,或接口,字段,类方法,接口方法,方法类型等。
对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info
对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info
初始化
初始化阶段是执行类构造器方法<clinit>()的过程
此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
非法的前向引用问题
如果没有类变量和静态代码块,也不会有clinit
构造器方法中指令按照语句在源文中出现的顺序执行
<clinit>()不同于类的构造器(关联:构造器是虚拟机视角下的<init>())
若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁
使用
卸载
补充说明:
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。
解析阶段不一定,在某些情况下可以在初始化阶段之后再开始,为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)
Java虚拟机规范严格规定了,有且只有六种情况,必须立即对类进行初始化
1、遇到new,getstatic,putstatic或invokestatic这四条字节码指令时。
使用new关键字实例化对象
读取或设置一个类型的静态字段(final修饰已在编译期将结果放入常量池的静态字段除外)
调用一个类型的静态方法的时候
2、对类型进行反射调用,如果类型没有经过初始化,则需要触发初始化
3、初始化类的时候,发现父类没有初始化,则先触发父类初始化
4、虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会初始化这个主类
5、只用JDK7中新加入的动态语言支持,如果一个java.lang.invoke.MethodHandler实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且这个方法对应的类没有进行初始化,则先触发其初始化
6、当一个接口中定了JDK8新加入的默认方法时,如果这个接口的实现类发生了初始化,要先将接口进行初始化
除了以上几种情况,其他使用类的方式被看做是对类的被动使用,都不会导致类的初始化
类加载器分类
说明
引导类加载器和自定义加载器
概念上,将所有派生于抽象类ClassLoader的类加载器都划分为自定义加载器
获取类加载器
截图
对于用户来说定义器来说,默认使用系统类加载器进行加载
Java的核心类库,使用引导类加载器进行加载
关于ClassLoader
是一个抽象类,除了启动类加载器,其他类加载器都继承自他
启动类加载器
C/C++语言实现,嵌套JVM内部
用来加载Java核心类库,rt.jar,resources.jar,sun.boot.class.path路径下的内容
代码获取加载路径
并不继承java.lang.ClassLoader,没有父加载器
加载扩展类和应用程序类加载器,并指定为他们的父类加载器
出于安全考虑,Bootstrap启动类加载器只加载包名为java\javax\sun等开头的类
扩展类加载器
Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
派生于ClassLoader类
父类加载器为启动类加载器
从java.ext.dirs系统属性所指定的目录中加载类库,或从jre/lib/ext子目录下加载类库
代码
应用程序类加载器(系统类加载器)
Java语言编写,由sun.misc.Launcher$AppClassLoader实现
派生于ClassLoader类
父类加载器为扩展类加载器
负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
通过ClassLoader#getSystemClassLoader()方法可以后去到改类加载器
用户自定义类加载器
为什么要用自定义类加载器
隔离加载类
例如使中间件的Jar包与应用程序Jar包不冲突
修改类加载的方式
启动类加载器必须使用,其他可以根据需要自定义加载
扩展加载源
防止源码泄露
对字节码进行加密,自定义类加载器实现解密
实现步骤
继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器
1.2之前,继承并重写loadClass方法,
1.2之后,建议把自定义的类加载逻辑写在findClass()方法中
如果没有太过复杂的需求,可以直接继承URLClassLoader类,可以避免自己编写findClass()方法,及其获取字节码流的方式,使自定义类加载器编写更加简洁
双亲委派
原理
Java虚拟机对Class文件采用的是按需加载,而且加载class文件时,
Java虚拟机使用的是双亲委派模式,即把请求交由父类处理,它是异种任务委派模式
Java虚拟机使用的是双亲委派模式,即把请求交由父类处理,它是异种任务委派模式
截图
1、如果一个类加载器收到了类加载请求,它并不会自己先去加载。
而是把这个请求委托给父类的加载器去执行
而是把这个请求委托给父类的加载器去执行
2、如果父类加载器还存在其父类加载器,则进一步向上委托,
依次递归,请求最终将达到顶层的启动类加载器
依次递归,请求最终将达到顶层的启动类加载器
3、如果父类的加载器可以完成类加载任务,就成功返回,
倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
优势
避免类的重复加载
保护程序安全,防止核心API被篡改
沙箱安全机制
保证对Java核心源代码的保护
执行流程
补充
在JVM中表示两个class对象,是否为同一个类存在两个必要条件
类的完整类名必须一致,包括包名
加载这个类的ClassLoader必须相同
JVM必须知道一个类型是由启动类加载器加载的,还是由用户类加载器加载的。
如果是用户类加载器加载的,JVM会将这个类加载器的一个引用作为类型信息的一部分,保存到方法区中。
如果是用户类加载器加载的,JVM会将这个类加载器的一个引用作为类型信息的一部分,保存到方法区中。
执行引擎
执行引擎概述
执行引擎是Java虚拟机核心的组成部分之一
虚拟机的执行引擎由软件自行实现,物理机的执行引擎是操作系统层面上
能够执行不被硬件直接支持的指令格式
执行引擎的工作过程
1、执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
2、每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址
3、当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,
以及通过对象头中的元数据指针定位到目标对象的类型信息
以及通过对象头中的元数据指针定位到目标对象的类型信息
Java代码编译和执行过程
大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过上图中的各个步骤
为什么说Java是半编译半解释型语言
JVM在执行Java代码的时候,通常会将解释执行与编译执行二者结合起来进行
概念解释
前端编译器
把.java文件转换为.class文件的过程
sun的Javac,
后端运行期编译器
把字节码转为机器码的过程
JIT编译器:hotSpot的C1,C2编译器
静态提前编译器
Ahead of Time Compliler AOT,直接把.java文件编译器本地机器代码的过程
GNU Compiler for the Java(GCJ)
机器码,指令,汇编语言
机器码
各种采用二进制编码方式表示的指令,叫做机器指令码。机器语言。
机器指令与CPU紧密相关,不同种类的CPU所对应的机器指令也就不同
机器指令与CPU紧密相关,不同种类的CPU所对应的机器指令也就不同
指令
由于机器码由01组成,可读性太差。于是人们发明了指令
指令就是把机器码特定的0和1序列,简化成对应的指令,一般为英文编写如mov,inc等,可读性稍好
由于不同的硬件平台,执行同一个操作,对应的机器码可能不同。
所以不同的硬件平台的同一种指令,对应的机器码也可能不同
所以不同的硬件平台的同一种指令,对应的机器码也可能不同
指令集
不同硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集
x86指令集,对应的x86架构的平台
ARM指令集,对应的是ARM架构的平台
汇编
由于指令的可读性太差,于是又有了汇编语言
汇编语言用助记符代替机器指令的操作码,用地址符号或标号,代替指令或操作数的地址。
汇编语言要翻译成机器指令码,计算机才能识别和执行
JIT编译器
什么是JIT编译器
就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
JVM平台支持一种叫做即时编译的技术,目的是避免解释执行,而是将整个函数体编译成机器码,
每次函数执行时,只执行编译后的机器码即可。使执行效率大幅提升
每次函数执行时,只执行编译后的机器码即可。使执行效率大幅提升
什么时候选择JIT
热点代码及探测方式
需要根据代码被调用执行的频率而定,需要被编译为本地代码的字节码,也称之为热点代码。
JIT编译器会在运行时针对频繁调用的热点代码做出深度优化,将其直接编译为对应平台的本地机器指令。以此提升Java程序的执行性能
一个被多次调用的方法,后者一个方法体内部循环次数较多的循环体,都可以被称之为热点代码
因此可以通过JIT编译器编译为本地机器指令,由于这种编译方法发生在方法的执行过程中,因此也被称之为栈上替换,OSR On Statck Replacement
一个方法调用都少次才能达到标准?这个依靠热点探测功能
hotspot采用的基于计数器的热点探测
方法调用计数器
统计方法调用次数
默认阈值,Client模式下是1500次,Server模式下是10000次
-XX:CompileThreshold
回边计数器
统计循环体执行的循环次数
截图
当一个方法被调用时,如果不存在已被编译过的版本,则将此方法的调用计数器+1,然后判断方法调用计数器与回边计数器之和,是否超过方法调用计数器的阈值。如果已经超过,会向即时编译器提交一个该方法的代码编译请求。
截图
热度衰减
当超过一定的时间限度,如果方法调用次数仍然不足以提交即时编译器编译,那么这个方法的调用计数器就会被减少一半。
-XX:UseCounterHalfLifeTime参数设置半衰周期的时间,单位是秒
hotspot中JIT分类
内嵌两个JIT编译器
client
server
大多情况下简称C1,C2
-client:指定Java虚拟机在Client模式下,并使用C1编译器
C1编译器会对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度
方法内联
将引用的函数代码编译到引用点处,减少栈帧的生成,减少参数传递以及跳转过程
去虚拟化
对唯一的实现类进行内联
冗余消除
在运行期把一些不会执行的代码折叠掉
-server:指定虚拟机在server模式下,并使用C2编译器
C2进行耗时较长的优化,以及激进优化,单优化后的代码执行效率更高
逃逸分析是优化的基础,基于逃逸分析在C2上有几种优化
标量替换
用标量值代替聚合对象的属性值
栈上分配
对于未逃逸的对象分配在栈而不是堆
同步消除
清除同步操作,通常指synchronized
hotspot可以设置程序执行的方式
-Xint:完全采用解释器模式执行
-Xcomp完全采用即时编译器模式执行,如果即时编译器出现问题,解释器会介入执行
-Xmixed采用解释器+即时编译器的混合模式共同执行
为什么两条腿走路?
首先程序启动后,解释器可以马上发挥作用,省去编译时间,立即执行
编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后执行效率更高
对于服务端应用,启动时间并非关注重点,但是对于看重启动时间的应用场景,就需要找到一个平衡点。
当Java虚拟机启动时,解释器可以首先发挥作用,而不是等待即时编译器全部编译完成后再执行,
这样可以省去很多不必要的编译时间,随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率
这样可以省去很多不必要的编译时间,随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率
jdk10起,hotspot又引入了个全新的即时编译器Graal编译器
JDK9引入了AOT编译器
解释器
当Java虚拟机启动时,会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为赌赢平台的本地机器指令执行
解析器真正意义上所承担的角色就是一个运行时翻译者,将字节码文件中的内容翻译为对应的平台的本地机器指令执行
当一条字节码指令被解释执行完成后,接着在根据PC寄存器中的记录下一条需要被执行的字节码执行解释执行
古老的字节码解释器
现在普遍使用的模板解释器
模板解释器将每一条字节码和一个模板函数相关联,模板函数直接产生这条字节码执行时的机器码,提高解释器的性能
HotSpot中
Interpreter模块
实现了解释器的核心功能
Code模块
用于管理HotSpot在运行时生成的本地机器指令
直接内存
不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域
直接内存是在java堆外的,直接向系统申请的内存区间
来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
通常,访问直接内存的速度会优于Java堆,即读写性能高
因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存
Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
也可能导致OOM异常
直接内存在堆外,所以大小不受限于-Xmx指定的最大堆大小
但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存
缺点
分配回收成本较高
不受JVM内存回收管理
直接内存大小可以通过MaxDirectMemorySize设置
如果不指定,默认与堆的最大值-Xmx参数值一致
监控工具
作用
jps
jinfo
jstack
jmap
jstat
jstated
子主题
jcmd
子主题
jmc
飞行记录,热点方法
较长
visualVm
VVM
JRF
突击
JVM调优
内存方面
总内存大小
各块内存分配,新生代,老年代,存活区
选择合适的垃圾回收算法,控制GC停顿时间和次数
辅助优化
内存泄漏
热点内存
线程方面
死锁检查
辅助代码优化
Dump线程详细信息
CPU热点
如何调优
GC
GC时间要足够小
GC次数要足够小
尽量让对象少进入老年代
减少 fullGc
发生 fullGc的间隔长
最少1分钟
策略
减少对象创建对象数量
减少使用全局变量和大对象
调整新生代,老年代的大小到最合适
选择合适的GC收集器,并设置合适的参数
冷思考
不考虑优化
0 条评论
下一页