《Java 虚拟机内幕:从垃圾回收到类加载的深度解析》
目录
HotSpot的算法细节实现
枚举根节点
安全点
安全区域
记忆集合卡表
并发的可达性分析
垃圾收集器
CMS收集器
G1收集器
内存分配与回收策略
类加载机制
类加载时机
HotSpot的算法细节实现
枚举根节点
安全点
安全区域
记忆集合卡表
并发的可达性分析
垃圾收集器
CMS收集器
G1收集器
内存分配与回收策略
类加载机制
类加载时机
类的加载过程
加载
验证
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
解析
初始化
类加载器
双亲委派模型
启动类加载器
扩展类加载器
系统类加载器
破坏双亲委派
类的加载过程
加载
验证
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
解析
初始化
类加载器
双亲委派模型
启动类加载器
扩展类加载器
系统类加载器
破坏双亲委派
HotSpot的算法细节实现
枚举根节点
必须要暂停用户线程
当前主流java虚拟机都是准确式垃圾收集,停顿之后不需要一个不差的检查完所有的位置,而是知道哪些位置存放对象引用.Hopsot中使用了一个OopMap的数据结构来完成.
安全点
不能每条指令都生成OopMap
在特地位置记录OopMap的信息,这些地方就是安全点
如何在垃圾收集中让所有的线程都跑到最近的安全点
方法:抢先式中断
方法:主动式中断
安全区域
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。
记忆集合卡表
为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
卡表就是记忆集的一种具体实现
卡表最简单的形式可以只是一个字节数组[
并发的可达性分析
可达性分析理论上需要在一致性快照下进行,这就说明需要全部暂停用户线程.
三色标记
如果垃圾收集和用户线程同时进行会产生错误
一种是把原本消亡的对象错误标记为存活,这不是好事,但其实是可以容忍的,只不过产生了一点逃过本次收集的浮动垃圾而已,下次收集清理掉就好。
另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了,程序肯定会因此发生错误.
当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:·赋值器插入了一条或多条从黑色对象到白色对象的新引用;·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)。
增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
CMS-->增量更新
G1-->原始快照
垃圾收集器
CMS收集器
1)初始标记(CMS initial mark)2)并发标记(CMS concurrent mark)3)重新标记(CMS remark)4)并发清除(CMS concurrent sweep)
初始标记、重新标记是要 stop the world
并发标记就是要用到增量更新
整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
缺点: 处理器资源比较敏感
无法处理浮动垃圾
空间碎片的产生
G1收集器
收集器面向局部收集的设计思路和基于Region的内存布局形式,连续的Java堆划分为多个大小相等的独立区域(Region)
G1是一款主要面向服务端应用的垃圾收集器,优先处理回收价值收益最大的那些Region.它是一个可预测停顿时间的低延迟的收集器。
初始标记(Initial Marking)·并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。·最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。·筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划.必须暂停
优点:G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,
缺点:负载比较高,内存占用大
内存分配与回收策略
对象优先在新生代Eden区
大对象直接进老年代 (新生代需要复制)
长期存活的进入到老年代里
动态年龄判定:
当前年龄已经占了一半了,那么就把当前年龄以及大于当前年龄的放到老年代,避免来回复制
空间分配担保:
每次新生代进行垃圾收集 必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
如果不成立,则虚拟机会先查看XX:HandlePromotionFailure参数的设置值是否允许担保失败(HandlePromotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XXHandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。
类加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制
类加载时机
HotSpot的算法细节实现
枚举根节点
必须要暂停用户线程
当前主流java虚拟机都是准确式垃圾收集,停顿之后不需要一个不差的检查完所有的位置,而是知道哪些位置存放对象引用.Hopsot中使用了一个OopMap的数据结构来完成.
安全点
不能每条指令都生成OopMap
在特地位置记录OopMap的信息,这些地方就是安全点
如何在垃圾收集中让所有的线程都跑到最近的安全点
方法:抢先式中断
方法:主动式中断
安全区域
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。
记忆集合卡表
为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
卡表就是记忆集的一种具体实现
卡表最简单的形式可以只是一个字节数组[
并发的可达性分析
可达性分析理论上需要在一致性快照下进行,这就说明需要全部暂停用户线程.
三色标记
如果垃圾收集和用户线程同时进行会产生错误
一种是把原本消亡的对象错误标记为存活,这不是好事,但其实是可以容忍的,只不过产生了一点逃过本次收集的浮动垃圾而已,下次收集清理掉就好。
另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了,程序肯定会因此发生错误.
当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:·赋值器插入了一条或多条从黑色对象到白色对象的新引用;·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)。
增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
CMS-->增量更新
G1-->原始快照
垃圾收集器
CMS收集器
1)初始标记(CMS initial mark)2)并发标记(CMS concurrent mark)3)重新标记(CMS remark)4)并发清除(CMS concurrent sweep)
初始标记、重新标记是要 stop the world
并发标记就是要用到增量更新
整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
缺点: 处理器资源比较敏感
无法处理浮动垃圾
空间碎片的产生
G1收集器
收集器面向局部收集的设计思路和基于Region的内存布局形式,连续的Java堆划分为多个大小相等的独立区域(Region)
G1是一款主要面向服务端应用的垃圾收集器,优先处理回收价值收益最大的那些Region.它是一个可预测停顿时间的低延迟的收集器。
初始标记(Initial Marking)·并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。·最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。·筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划.必须暂停
优点:G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,
缺点:负载比较高,内存占用大
内存分配与回收策略
对象优先在新生代Eden区
大对象直接进老年代 (新生代需要复制)
长期存活的进入到老年代里
动态年龄判定:
当前年龄已经占了一半了,那么就把当前年龄以及大于当前年龄的放到老年代,避免来回复制
空间分配担保:
每次新生代进行垃圾收集 必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
如果不成立,则虚拟机会先查看XX:HandlePromotionFailure参数的设置值是否允许担保失败(HandlePromotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XXHandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。
类加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制
类加载时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称 为连接(Linking)。
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段
类的加载过程
加载
1)通过一个类的全限定名来获取定义此类的二进制字节流。2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
public static int value = 123
准备阶段给0
如果是final 那么准备阶段就直接进行赋值
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
初始化
到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码将主导权移交给应用程序。
public static int value = 123
初始化就是赋值123的过程
类加载器
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。
双亲委派模型
启动类加载器
固定路径下的,固定jar包
扩展类加载器
对java语言的扩展
系统类加载器
加载的是我们写的程序
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载
破坏双亲委派
OGSi 热部署
线程上下文加载器
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段
类的加载过程
加载
1)通过一个类的全限定名来获取定义此类的二进制字节流。2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
public static int value = 123
准备阶段给0
如果是final 那么准备阶段就直接进行赋值
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
初始化
到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码将主导权移交给应用程序。
public static int value = 123
初始化就是赋值123的过程
类加载器
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。
双亲委派模型
启动类加载器
固定路径下的,固定jar包
扩展类加载器
对java语言的扩展
系统类加载器
加载的是我们写的程序
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载
破坏双亲委派
OGSi 热部署
线程上下文加载器