JVM深入浅出(5)--- 垃圾收集器
自己在学习《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) (华章原创精品) - 周志明》时的一些思考和总结
1. 经典垃圾收集器
首先必须上这张图

1.1 Serial收集器
最简单最基础的垃圾收集器,作用于新生代,gc时需要暂停所有的用户线程,且是单线程垃圾回收,。
serial单线程收集的特点,注定了它在垃圾回收时没有线程交互的开销,简单高效。
垃圾收集时使用的是标记-复制算法

1.2 ParNew收集器
是serial收集器的多线程版本,只能和CMS搭配工作,没有开启Parnew的jvm指令,当使用CMS垃圾收集器时,新生代收集器默认使用ParNew收集器。


1.3 Parallel Scavenge收集器
Parallel Scavenge收集器和ParNew收集器在很多地方都有相似之处,不同的是Parallel scavenge更关注的是吞吐量。
什么是吞吐量
良好的响应速度能提升用户体验,而高吞吐量则可以最高效利用处理器资源,尽快完成程序的运算任务。就好比延迟关注的是用户等待垃圾收集中STW的时间,而吞吐量关注的是跑完整个程序花费的时间
Parallel scavenge 控制吞吐量的参数
- 控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis
- 直接设置吞吐量大小的-XX:GCTimeRatio参数
除此之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得我们关注,开启这个参数后,虚拟机会根据实际运行情况,调节新生代大小,Eden/survivor 比例,晋升老年代对象大小这些参数来提供最合适的停顿时间和吞吐量。
1.4 Serial Old收集器
Serial old 是serial收集器在老年代上的版本,使用的是标记-整理算法。这个收集器主要意义也是供客户端模式下的HotSpot虚拟机使用。
使用场景
-
在JDK5 之前和parallel scavenge搭配,
-
作为CMS发生失败时的备选,在并发收集发生Concurrent Mode Failure时使用。

1.5 Parallel Old收集器
是parallel scavenge收集器的在老年代版本。是JDK6之后推出的, 在此之前 Parallel scavenge 只能和serial old搭配,而这种搭配,老年代是单线程,新生代是多线程,当老年代内存较大时候,总吞吐量可能还不如ParNew + CMS。
在Parallel Old 推出后,"吞吐量优先“ 收集器有了合适的组合,在注重吞吐量的情况下可以使用parallel scavenge + Parallel Old

1.6 CMS收集器
CMS是老年代的垃圾收集器,目标是低延迟,采用的是标记-清除算法实现的(之前提到过,由于标记清除不需要移动对象,所以延迟是低了,但是多了碎片内存访问的消耗,所以吞吐量就低了)
流程:
- 初始标记(STW)
- 初始标记仅仅是标记一下GCroots直接访问到的对象,速度很快(得益于Oopmap的加成),会暂停所有用户线程
- 并发标记
- 并发标记时不会暂停其他用户线程,是通过GCroots遍历能访问到的引用对象。
- 重新标记(STW)
- 会暂停其他用户线程,重新标记是纠正一下并发标记期间的标记记录,采用的是增量更新算法(增量更新是解决黑色节点指向白色节点的情况,是通过记录增加引用的黑色节点,在并发标记后将这些黑色节点变成灰色,重新访问它的引用对象)。
- 并发清除
- 不暂停用户线程

CMS收集器的三个缺点:
- 对处理器资源敏感,毕竟是并发设计的。因为线程占用会导致应用程序变慢,降低了吞吐量(我之前提到的标记清除算法的缺点也是其中之一)
- CMS收集器无法处理浮动垃圾(在并发标记过程中,白色标记成黑色,导致这些垃圾需要在之后的gc中回收),有可能出现
Con-current Mode Failure(并发失败),进而导致完全STW的full gc。当发生并发失败时候,就不得不把CMS换成Serial Old来使用。- 因为CMS必须预留空间给线程进行分配,所以通常在老年代内存还没使用满就得进行gc(JDK5的默认设置是68%)
- 标记清除算法会产生大量的碎片化空间,进而导致full gc
1.7 Garbage First收集器
G1收集器是为了取代ParNew + CMS 的组合,与之前的收集器不同,G1收集器的作用范围是整个堆。G1是专门针对服务端应用程序的垃圾收集器
- 什么是G1收集器的Mixed GC模式?
在G1收集器中,虽然还有新生代,老年代的概念,但是却没有实际新生代,老年代的内存,而是一个又一个的Region(意思是有些区属于eden区,survivior区,有些是老年代),而GC回收会优先回收那些回收价值高(哪个区回收内存最多)的区,这就是G1的Mixed GC模式。
- G1的humongous区
除了新生代,老年代,G1还有一片特殊的区,专门用来存储大对象的,叫做humongous区,当对象超过G1中region的内存的一半时,会被放进humongous区中。
- G1的回收思路
G1中的最小回收单元是region,G1中新生代,老年代不再是固定的了,而是一系列区的集合。G1收集器会去跟踪每个region的回收价值(也就是回收所获得空间以及回收花费时间的经验值),在后台中维护一个优先级列表,在回收时,优先选择价值高的区进行回收。
- G1中如何解决跨代引用问题
和其他垃圾收集器一样,跨代引用问题也是通过记忆集来解决的。但是在G1中不太一样,G1中每个region都会有自己的记忆集,这些记忆集会记录下其他region到自己的指针,并记录下这些指针在哪些卡页中。G1的记忆集本质上是哈希表,key是region的地址,value是一个集合,里面的存储元素是卡表的索引号(也就是双向的结构,“我指向谁,谁指向我”),由于region数量比传统收集器分代要多,所以G1的内存占用也要比传统收集器更高
- G1收集器中并发标记阶段处理措施
在CMS 中采用的是增量更新算法来纠正并发标记中用户线程的影响,而G1收集器采用的是原始快照算法(SATB)来做的
原始快照算法就是处理灰色节点引用被删除,导致有些黑色节点最终变成白色节点被回收。做法是通过记录删除的引用,在结束并发标记后,将删除的对象置灰,重新访问这些对象的引用
- G1收集器在并发过程中遇到用户线程对象分配的情况
G1为每个region设置了两个名为TAMS(Top at Mark Start)的指针,把指针中这一块内存用于用户线程分配对象,G1收集器会标记这上面的对象是默认存活的,不加入垃圾回收的范围。
- G1收集器怎样建立起可靠的停顿预测模型?
用户通过-XX:MaxGCPauseMillis参数指定的停顿时间 只意味着垃圾收集发生之前的期望值。G1收集器会记录每个region记忆集中脏卡数量,回收耗时,等花费成本,分析出平均值,标准方差,置信度等指标,来预测回收哪些region能做到回收价值最高,且停顿时间小于预期值。
-
G1算法详细步骤:G1除了并发标记外,其他都要暂停用户线程的
-
初始标记
- 标记GCroots直接关联到的对象,修改TAMS指针的值。耗时短,有STW
-
并发标记
- 不暂停用户线程,并发标记整个引用链,同时处理SATB对象
-
最终标记
- 主要是原始快照算法,处理并发标记剩下的中的SATB记录,会暂停所有用户线程
-
筛选回收
- 对region回收成本和价值排序,根据用户期待时间指定回收计划。必须暂停用户线程
-

G1 VS CMS
- G1优点
- G1采用标记-整理和标记-复制,而CMS用的是标记-清除,这一点G1不会产生碎片化空间,适合长时间运行。
- G1支持用户设定gc期望执行时间,G1会优先回收某些region,尽量追求在延迟和吞吐量中寻找一个平衡
- G1缺点
- 从内存角度来说,G1需要为每个region都维护其记忆集,这就导致G1占用内存更大,而CMS的记忆集就相对简单多了。
- CMS是通过写后屏障来维护卡表,而G1不单单是通过写后屏障维护卡表,为了实现SATB算法,还要通过写前屏障来记录引用的变化。
2.低延迟垃圾收集器
衡量垃圾收集器的三项指标,三者是不可能同时实现的三角
- 延迟
- 吞吐量
- 内存占用
2.1 Shenandoah收集器
Shenandoah收集器和G1收集器高度类似,同样是基于Region的堆内存布局,同样有humongous区,回收策略也是优先回收价值最大的。
与G1的不同之处
- G1回收的时候是要暂停用户线程的,但是shenandoah是并发回收的
- shenandoah默认不使用分代收集,不会有新生代和老年代。
- shenandoah不使用G1中的记忆集,而是改名用“连接矩阵”的全局数据结构来记录跨代引用。
shenandoah中并发回收阶段,是将存活的对象复制到未被使用的region中,如果用户线程暂停是很简单的,在并发的场景下,shenandoah用到了Brooks pointer转发指针来实现。这通常是在原先对象的内存上设置保护陷阱,当线程访问到原先对象的内存,就把访问转发到新对象上。
2.2 ZGC收集器
ZGC 和shenandoah目标类似,都是希望在不影响吞吐量的情况下尽量去减小延迟。
ZGC是基于region内存分区的,不设分代,使用了读屏障,染色指针,和内存多重映射的技术来实现可并发的标记-整理算法的,来降低延迟的一款垃圾收集器。
3. 如何选择合适的垃圾收集器
3.1 收集器的权衡
-
应用程序的关注点:
-
数据分析、科学计算类的任务,目标是能尽快算出结果,关注点就是吞吐量
-
SLA应用,关注的就是延迟
-
客户端应用或者嵌入式应用,关注的就是内存占用
-
-
·运行应用的基础设施
- 系统架构,操作系统这些
-
JDK发行商,版本号
3.2 虚拟机及垃圾收集器日志
HotSpot所有功能的日志都收归到了“-Xlog”参数上

- 最关键的参数:
- selector,由标签(Tag)和日志级别(Level)共同组成,指的是某个模块的名字,希望虚拟机打哪个模块的日志,如gc的则是-Xlog:gc
- 日志级别从低到高,共有Trace,Debug,Info,Warning,Error,Off六种级别
- 修饰器(Decorator)来要求每行日志输出都附加上额外的内容,支持附加 在日志行上的信息包括:
- time:当前日期和时间。
- uptime:虚拟机启动到现在经过的时间,以秒为单位。
- timemillis:当前时间的毫秒数,相当于System.currentTimeMillis()的输出。
- uptimemillis:虚拟机启动到现在经过的毫秒数。
- timenanos:当前时间的纳秒数,相当于System.nanoTime()的输出。
- uptimenanos:虚拟机启动到现在经过的纳秒数。
- pid:进程ID。
- tid:线程ID。
- level:日志级别
- tags:日志输出的标签集。
4. 内存分配与回收策略
- 对象优先在Eden分配
大多数情况下,对象会先在新生代的eden区进行分配,当Eden空间不够时,则发起一次minor gc
- 大对象直接进入老年代
HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配。目的是为了避免在Eden区和两个survivor区中来回复制,产生大量内存复制操作。
- 长期存活的对象将进入老年代
对象的对象头中,设置了对象年龄计数器,每经历过一次minor gc的对象,这个年龄会+1,通过MaxTenuringThreshold设置进入老年代的对象年龄,当对象年龄大于这个值,将会进入老年代。
- 动态对象年龄判定
当survivor区中,相同年龄的对象大小总和大于survivor空间的一半,年龄大于或者等于该年龄的对象就会进入老年代,无需等到gc年龄。
- 空间分配担保
- 在发生minor gc前 先检查老年代连续空间大小是否大于所有新生代对象,如果大于,可以确保minor gc安全,如果不大于,则检测handlepromotionfailure是否允许担保失败,如果允许,则取之前每一次回收晋升到老年代对象的内存平均值,看最大可用连续空间是否大于这个值,如果大于就尝试minor gc,如果小于或者不允许担保,则进行full gc

浙公网安备 33010602011771号