目录
HotSpot的算法细节实现
根节点枚举
安全点
安全区域
记忆集与卡表
记忆集
作用
卡表(Card Table)
卡页(Card Page)
元素变脏(Dirty)
写屏障
写屏障
写前屏障(Pre-Write Barrier)
写后屏障(Post-Write Barrier)
伪共享
伪共享解决方案
并发的可达性分析
为什么必须在一个能保障一致性的快照上才能进行对象图的遍历?
以下两个条件同时满足时,会产生“对象消失”的问题:(即原本应该是黑色的对象被误标为白色)
对象消失问题解决方案:
经典垃圾收集器
HotSpot虚拟机经典垃圾收集器的关系
并行与并发
并行
并发
Serial收集器
特点
优点
缺点
使用方式
ParNew收集器
特点
优点
缺点
使用方式
JVM参数设置
Parallel Scavenge收集器
特点
吞吐量
优点
缺点
适用场景
JVM参数设置
Serial Old收集器
特点
优点
缺点
应用场景
JVM参数设置
注意事项
Parallel Old收集器
特点
优点
缺点
应用场景
JVM参数设置
CMS收集器
特点
原理步骤
优点
缺点
G1收集器(Garbage First收集器)
特点
Region中的特殊区域
Region区域
G1 GC过程
G1常用参数
所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,因此毫无疑问根节点枚举与之前提及的整理内存碎片一样会面临相似的“Stop The World”的困扰。现在可达性分析算法耗时最长的查找引用连的过程已经可以做到与用户线程一起并发,但根节点枚举始终还是必须子一个能保障一致性的快照中才得到以进行——这里“一致性”的意思是整个枚举期间执行子系统看起来就像被冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化的情况,若这点不能满足的话,分析结果准确性也就无法保证。这是导致垃圾收集过程必须停顿所有用户线程的其中一个重要原因,即使是号称停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、ZGC等收集器,枚举根节点时也是必须要停顿的。
由于目前主流Java虚拟机使用的都是准确式垃圾收集,所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。
在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找。
在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法忍受的高昂。
实际上HotSpot也的确没有为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置被称为安全点(Sagepoint)。
安全的设定,决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。因此,安全点的选定即不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时间的内存负荷。
安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,因为每条指令只能怪的时间都非常短暂,程序不太可能因为指令流长度太长这样的原因而长时间执行,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。
使用安全点的设计似乎已经完美解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题了,但实际情况却并不一定。安全点机制保证了程序执行时,在不太长时间内就会遇到可进入垃圾收集过程的安全点。
程序“不执行”的时候呢? 所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,迅即也显然不可能持续等待线程重新被激活分配处理器时间。对于这种情况,就必须引入安全区域(Safe Region)来解决了。
安全区域是指能够确保在某一段代码之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。可以把安全区域看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,知道收到可以离开安全区域的信号为止。
在分代收集理论的时候,提到为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的具体实现。
用以避免把整个老年代加进GC Roots扫描范围。
事实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题.
记录精度:
卡精度:指的是用一种称为“卡表”(Card Table)的方式去实现记忆集,也是目前最常用的一种记忆集实现形式。
记忆集是一种“抽象”的数据结构,抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的集体实现。
是记忆集的一种具体实现,定义了记忆集的记录精度、与堆内存的映射关系等。
字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块。
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1。没有则标识为0.
如何变脏,如果在对象赋值的那一刻去更新维护卡表呢?
在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。
可以看作在虚拟机层面对“引用类型字段赋值”整个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。
在赋值前的部分的写屏障叫作写前屏障。
在赋值后的部分的写屏障叫作写后屏障。
HotSpot虚拟机的许多收集器中都有使用到写屏障,但直至G1收集器出现之前,其他收集器都只用到了写后屏障。
卡表在高并发场景下还面临着“伪共享”问题。
是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。
不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏。
HotSpot虚拟机新参数:-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。
可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析,这意味着必须全程冻结用户线程的运行。
在根节点枚举这个步骤中,由于GC Roots相比起整个Java堆中全部的对象毕竟还算是极少数,且在各种优化技巧(OopMap)的加持下,它带来的停顿已经是非常短暂且相对固定(不随堆容量而增长)的了。可从GC Roots再继续往下遍历对象图,这一步骤的停顿时间就必定会与Java堆容量直接成正比例关系了:堆越大,存储的对象越多,对象图结构越复杂,要标记更多对象而产生的停顿时间自然就更长。
三色标记(Tri-color Marking)作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下颜色:
只需破坏这两个条件的任意一个即可。
以上无论时对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。在HotSpot虚拟机中,增量更新和原始快照这两种解决方案都有实际应用。
譬如,CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。
图注:七种不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用;
收集器所处的区域,则表示它是属于新生代收集器或者是老年代收集器。
并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。
举个栗子:
“你妈妈在给你打扫房间的时候,肯定也会让你老老实实地在椅子上或者房间外待着,如果她一边打扫,你一边乱扔纸屑,这房间还能打扫完?”
所以就会把你限制住,暂停你一切行动,直到打扫完。哈哈哈哈,这栗子挺形象,画面感满满。
-XX:+UseSerialGC
-XX:+UseParNewGC
是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
-XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。
不要认为这个参数设置的更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
-XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。
譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
是一 个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区 的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数 了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时 间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)。
即以上特点。
未知,(知道的猿友们可基于补充,评论即可)
主要用于Client模式
使用方式:-XX:+UseSerialGC
需要说明一下, Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集, 并非 直接调用 Serial Old收集器, 但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样的, 所以在官方的许多资料中都是 直接以Serial Old代替PS MarkSweep进行讲解.
未知,(知道的猿友们可基于补充,评论即可)
-XX:+UseParallelOldGC:指定使用Parallel Old收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务得响应速度,希望系统停顿时间尽可能短,以给用户带来良好得交互体验。CMS收集器就非常符合这类应用的需求。
1.初始标记(CMS initial mark)
初始标记仍然需要“Stop The World”.
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
2.并发标记(CMS concurrent mark)
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
3.重新标记(CMS remark)
重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
4.并发清除(CMS concurrent sweep)
并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路喝基于Region的内存布局形式。早在JDK7刚刚确立项目目标、Oracle公司制定的JDK7RoadMap里面,G1收集器就被视作JDK7中HotSpot虚拟机的一项重要进化特征。从JDK6 UPdate 14开始就有Early Access版本的G1收集器供开发人员实验和试用,但由此开始G1收集器的“实验状态”(Experimental)持续了数年时间,直至JDK7 Update4,Oracle才认为它达到足够成熟的商用程度,移除了"Experimental"的标识;到了JDK 8 Update40的时候,G1提供并发的类卸载的支持,补全了其计划功能的最后一块拼图。这个版本以后的G1收集器才被Oracle官方称为“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。
Region中的特殊区域Humongous(巨大的),专门用来存储大对象。
判定条件:G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。
超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分进行看待。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间。将整个堆空间细分为若干个小的区域。
G1提供了两种GC模式,Young GC和Mixed GC,两种均是完全Stop The World的。
在G1 GC垃圾回收的过程一个有四个阶段:
G1 Young GC
Young GC执行前
堆分为大约2000个区域。最小大小为1Mb,最大大小为32Mb。蓝色区域保存老年代对象,绿色区域保存年轻对象。
执行Young GC
将存活的对象(即复制或移动)到一个或多个幸存者区域。如果满足老化阈值,则某些对象将被提升到老年代区域。
G1的年轻GC结束
最近升级的对象以深蓝色显示。幸存者区域为绿色。
总而言之,关于G1的年轻一代,以下几点:
G1 Mix GC
初始标记阶段(initial mark,STW)
存活的对象的初始标记背负在年轻的垃圾收集器上。在日志中,此标记为 GC pause(young)(inital-mark)。
并发标记阶段(Concurrent Marking)
如果找到空白区域(如“X”所示),则在Remark阶段将其立即删除。另外,计算确定活跃度的信息。
最终标记阶段(Remark,STW)
空区域将被删除并回收。现在可以计算机所有区域的区域活跃度。
最终标记阶段(Remark,STW)
空区域将被删除并回收。现在可以计算所有区域的区域活跃度。
筛选回收阶段/复制清理阶段(Cleanup,STW)
G1选择“活跃度”最低的区域,这些区域可以被最快地收集。然后与年轻的GC同时收集这些区域。这在日志中表示为[GC pause (mixed)]。因此,年轻代和老年代都是同时收集的。
筛选回收阶段-(复制/清理)阶段之后
选定的区域已被收集并压缩为图中所示的深蓝色区域和深绿色区域。
总结
参数/默认值 | 含义 |
---|---|
-XX:+UseG1GC | 使用 G1 垃圾收集器 |
-XX:MaxGCPauseMillis=200 | 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保 证达到) |
-XX:InitiatingHeapOccupancyPercent=45 | mixed gc中也有一个阈值参数 ,当老年代大小占整个堆大小百分 比达到该阈值时,会触发一次mixed gc. 默认值为 45 |
-XX:NewRatio=n | 新生代与老生代(new/old generation)的大小比例(Ratio). 默认值为 2. |
-XX:SurvivorRatio=n | eden/survivor 空间大小的比例(Ratio). 默认值为 8. |
-XX:MaxTenuringThreshold=n | 提升年老代的最大临界值(tenuring threshold). 默认值为 15. |
-XX:ParallelGCThreads=n | 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平 台不同而不同 |
-XX:ConcGCThreads=n | 并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而 不同. |
-XX:G1ReservePercent=n | 设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认 值是 10. |
-XX:G1HeapRegionSize=n | 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指 定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值 为 1Mb, 最大值为 32Mb. |
作者:筱白爱学习!!
欢迎关注转发评论点赞沟通,您的支持是筱白的动力!