垃圾收集器(下)
本文将介绍CMS和G1两款虚拟机
1. CMS虚拟机
1.1 基本描述:
CMS(Concurrent Mark Sweep)
收集器是一种以获取最短回收停顿时间为目标的收集器
特点: 老年代、并发收集、低停顿
1.2 运行过程:
从图可以看出需要经历四个过程:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrenr mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
1.3 过程详细
-
其中初始标记、重新标记这两个步骤依然需要停顿其他用户线程(
Stop The World
)。 -
初始标记: 仅仅只是标记出
GC ROOTS
能直接关联到的对象,速度很快 -
并发标记: 阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。
-
而重新标记: 阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,但比并发标记阶段要短。
小结描述:
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作;
所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的 ;
HotSpot在JDK1.5
推出的第一款真正意义上的并发(Concurrent)收集器;
第一次实现了让垃圾收集线程与用户线程(基本上)同时工作
1.4 缺点描述:
CMS存在明显的缺点:
缺点 | 描述 |
---|---|
对CPU资源非常敏感 | 并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。CMS的默认收集线程数量是=(CPU数量+3)/4 |
无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生 | 并发清理是用户线程还在运行,这个阶段产生的垃圾无法在这次被清理,只能等待下次被处理 |
标记-清除 算法收集后,会产生大量碎片 | 这是Mark-Sweep算法的弊端 |
1.5 缺点描述详细:
一、对CPU资源非常敏感:
-
当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响可能很大。
-
针对这种情况,曾出现了
"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS)
;
类似使用抢占式
来模拟多任务机制
的思想,让收集线程和用户线程交替运行,减少收集线程运行时间; -
这样垃圾收集过程更长,但是对用户程序影响小一些。实际上i-CMS效果很一般,目前已经被声明为**“deprecated”**
二、“浮动垃圾”
由于CMS并发清理阶段用户线程还在运行,
伴随程序的运行会有新的垃圾不断产生,
这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,
只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”
三、标记清理算法介绍:
-
空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。由于在垃圾收集阶段用户线程还需要运行, 即需要预留足够的内存空间给用户线程使用,
-
因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,
需要预留一部分内存空间提供并发收集时的程序运作使用。 -
在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数
-XX:CMSInitiatingOccupancyFraction
的值来提高触发百分比,以降低内存回收次数提高性能。 -
JDK1.6中,CMS收集器的启动阈值已经提升到92%。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现
“Concurrent Mode Failure”
失败, -
这时候虚拟机将启动后备预案:临时启用
Serial Old
收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。 -
所以说参数
-XX:CMSInitiatingOccupancyFraction
设置的过高将会很容易导致“Concurrent Mode Failure”
失败,性能反而降低。 -
-XX:+UseCMSCompactAtFullCollection
(默认开启) 使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;但合并整理过程无法并发,虽然控件碎片没有了,但停顿时间会变长; -
-XX:+CMSFullGCsBeforeCompaction
设置执行多少次不压缩的Full GC后,来一次压缩整理;默认为0,也就是说每次都执行Full GC,不会进行压缩整理; -
由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用
"碰撞指针"
分配内存消耗大
参数介绍:
-XX:+UseConcMarkSweepGC 指定使用CMS收集器
总结:
详细项 | 仔细描述 |
---|---|
特点 | 老年代、基于标记清理(Mark-Sweep)算法,注重最短回收停顿时间,并发收集、低停顿 |
应用场景 | 注重服务器响应时间,系统停顿时间短,以此给带来友好体验 |
2.G1 收集器
2.1 基本描述:
G1(Garbage First)收集器是JDK1.7提供的一个新的面向服务端应用的垃圾收集器,其目标就是替换掉JDK1.5发布的CMS收集器
2.2 特点:
-
并发与并行:
- 能充分利用多CPU、多核环境下的硬件优势;
- 使用多个CPU(CPU或CPU核心)来缩短停顿(Stop The World)时间;
- 同时可以并发让垃圾收集与用户程序同时进行
-
分代收集:
- 分代概念保留;同时能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
- 虽然是独立,但他能够采用不同方式去处理新建对象和已经存活了一段时间、熬过多次GC的老年代对象以获取更好收集效果。
-
空间整合:
- 从整体来看是基于“标记-整理”算法实现;
- 从局部(两个Region之间)来看是基于
复制
算法实现的; - 但是都意味着G1运行期间不会产生内存碎片空间,收集后,内存更规整,遇到大对象时,不会因为没有连续空间而进行下一次GC,甚至一次Full GC。
-
可预测的停顿:
- 降低停顿是G1和CMS共同关注点,
- 但G1除了追求低停顿,还能建立可预测的停顿模型, 即可以明确地指定在一个长度为M的时间片内,消耗在垃圾收集的时间不超过N毫秒
-
堆内存布局:
- G1 将内堆划分为多个大小相等独立区域(Region),虽然保留新生代和老年代的概念,
- 但是新生代和老年代不再是物流隔离,都是一部分Region的集合。
2.3 设置参数
"-XX:+UseG1GC":指定使用G1收集器;
"-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
"-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒;
"-XX:G1HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;
2.4 为什么G1收集器可以实现可预测的停顿
-
可以有计划地避免在Java堆的进行
全区域
的垃圾收集; -
G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;
-
每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);
-
这就保证了在有限的时间内可以获取尽可能高的收集效率;
2.5 一个对象被不同区域引用的问题
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,
判断对象存活时,是否需要扫描整个Java堆才能保证准确?在其他的分代收集器,也存在这样的问题(而G1更突出):回收新生代也不得不同时扫描老年代?这样的话会降低Minor GC的效率;
2.6 解决方法:
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
- 每个Region都有一个对应的
Remembered Set
; - 每次
Reference
类型数据写操作时,都会产生一个Write Barrier
暂时中断写操作; - 然后检查将要写入的引用指向的对象是否和该
Reference类型数据在不同的Region
(在分代中:检查老年代对象是否引用了新生代对象); - 如果不同,通过
CardTable
把相关引用信息记录到引用指向对象的所在Region
对应的Remembered Set
中; - 当进行垃圾收集时,在GC根节点的枚举范围加入
Remembered Set
; - 就可以保证不进行全局扫描,也不会有遗漏。
2.7 G1收集器运作过程
不计算维护Remembered Set的操作,可以分为4个步骤。
1. 初始标记(Initial Marking)
-
仅标记一下GC Roots能直接关联到的对象;
-
且修改
TAMS(Next Top at Mark Start)
,让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象; -
需要
Stop The World
,但速度很快;
2. 并发标记(Concurrent Marking)
-
进行GC Roots Tracing的过程;
-
刚才产生的集合中标记出存活对象;
-
耗时较长,用户程序并发执行;
-
并不能保证可以标记出所有的存活对象(还有最终标记);
3. 最终标记(Final Marking)
-
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
-
上一阶段(并发标记阶段)对象的变化记录在线程的
Remembered Set Log
; -
这个阶段把
Remembered Set Log
合并到Remembered Set
中; -
需要
Stop The World
,且停顿时间比初始标记稍长,但远比并发标记短; 虽然停顿,但是能并行执行最终标记过程 -
采用多线程并行执行来提升效率;
4. 筛选回收(Live Data Counting and Evacuation)
-
首先 排序各个Region的回收价值和成本;
-
根据用户期望的GC停顿时间来制定回收计划;
参考
《深入理解Java虚拟机》–周志明