开始学习!!!!
上篇介绍了基本的垃圾回收机制,本篇来记录一下垃圾回收算法和市面上的常用的垃圾回收器。
目录
垃圾回收算法
复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制 到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。
缺点:浪费的内存空间。
标记清除算法
-
先根据可达性算法标记出相应的可回收对象(图中黄色部分)
-
对可回收的对象进行回收
缺点:会造成内存碎片!
标记整理法
前面两步和标记清除法一样,不同的是它在标记清除法的基础上添加了一个整理的过程 ,即将所有的存活对象都往一端移动,紧邻排列,再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。
缺点:每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下。
分代收集算法
分代收集算法集合了上面所有算法的优点,最大程度避免了上述算法的缺点,是目前市面上现代虚拟机采用的首选算法。
分代收集算法根据对象存活周期的不同将堆分成新生代和老生代(Java8以前还有个永久代),默认比例为 1 : 2,新生代又分为 Eden 区, from Survivor 区(简称S0),to Survivor 区(简称 S1),三者的比例为 8: 1 : 1,这样就可以根据新老生代的特点选择最合适的垃圾回收算法,我们把新生代发生的 GC 称为 Young GC(也叫 Minor GC),老年代发生的 GC 称为 Old GC(也称为 Full GC)。
- 对象在新生代被分配与回收
- 大部分对象在很短的时间内会被回收,一般分配在 Eden 区,当 Eden 区将满时,触发 Minor GC;
- 经过 Minor GC 后只有少部分对象会存活,它们会被移到 S0 区(这就是为啥空间大小 Eden: S0: S1 = 8:1:1, Eden 区远大于 S0,S1 的原因,因为在 Eden 区触发的 Minor GC 把大部对象(接近98%)都回收了,只留下少量存活的对象。此时s1是空的
- 下一次
Eden
区满时,再执行一次垃圾回收。此次会将Eden
和S0
区中所有垃圾对象清除,并将存活对象复制到S1
,此时S0
变为空。 - 如此反复在
S0
和S1
之间切换。(可以看出,这里其实就是复制算法,因为在 Eden 区分配的对象大部分在 Minor GC 后都消亡了,只剩下极少部分存活对象(这也是为啥 Eden:S0:S1 默认为 8:1:1 的原因),S0,S1 区域也比较小,所以最大限度地降低了复制算法造成的对象频繁拷贝带来的开销。)
- 晋升老年代:使用
-XX:PretenureSizeThreshold
来控制直接升入老年代的对象大小。- 当对象的年龄达到了阈值(默认15),则会从S0(或S1)晋升到老年代。
- 对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。
- S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代。
- 空间分配担保
- 在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行 Minor GC,否则可能进行一次 Full GC。
- Stop The World
- 老年代满了,就会触发full gc,full gc 会同时回收新生代和老年代(即对整个堆进行GC),它会导致 Stop The World(简称 STW),造成挺大的性能开销。(STW是指在GC期间,只有gc线程在执行,其他线程被挂起。
- Full GC
- Full GC 会清理整个堆中的不可用对象,一般要花较长的时间。
- 如果只有Eden区,就会使在Eden区短暂存活的对象过早的进入老年代,从而很快占满老年代从而触发Full GC。
- 有了 S0,S1的缓冲,只有少数的对象会进入老年代,老年代大小也就不会这么快地增长,也就避免了过早地触发 Full GC。
- Safe Point
- 什么时候Full GC 才比较好,Safe Point就是指开始进行Full GC的时间点。
-
Safe Point 主要指的是以下特定位置:
-
循环的末尾
-
方法返回前
-
调用方法的 call 之后
-
抛出异常的位置
-
垃圾回收器
图中连线表示可以互相配合使用。
Serial:单线程的,会使用一个 CPU 或一个收集线程来完成垃圾回收。
ParNew:Serial 收集器的多线程版本,其他像收集算法,STW,对象分配规则,回收策略与 Serial 收集器完成一样。只有它能与 CMS 收集器配合工作。
CMS:正意义上的并发收集器,它第一次实现了垃圾收集线程与用户线程(基本上)同时工作,它采用的是传统的 GC 收集器代码框架,与 Serial,ParNew 共用一套代码框架,所以能与这两者一起配合工作。以实现最短 STW 时间为目标的收集器,采用的是标记清除法。
三色标记工作步骤:
- 初次标记:标记的是GC Root对象,过程会造成STW。
- 并发标记:标记所有的old对象。
- 重新标记:因为第2步是并发操作,所以可能存在一些在第一步结束后才不用的对象,导致没被标记,所以需要进行修正,此过程也是STW。
- 并发清理:标记清理算法。
缺点:1. CMS 收集器对 CPU 资源非常敏感。
2. 无法处理浮动垃圾。
3. 标记清除法,会产生大量的内存碎片。
Parallel Scavenge:使用复制算法,多线程,工作于新生代的垃圾收集器。
CMS和Parallel Scavenge的不同:
- CMS 等垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),也就是说 CMS 等垃圾收集器更适合用到与用户交互的程序,因为停顿时间越短,用户体验越好,而 Parallel Scavenge 收集器关注的是吞吐量,所以更适合做后台运算等不需要太多用户交互的任务。
- 自适应策略:Parallel Scavenge 收集器提供参数 -XX:UseAdaptiveSizePolicy,这个参数不需要手工指定新生代大小,Eden 与 Survivor 比例(SurvivorRatio)等细节,只需要设置好基本的堆大小(-Xmx 设置最大堆),以及最大垃圾收集时间与吞吐量大小,虚拟机就会根据当前系统运行情况收集监控信息,动态调整这些参数以尽可能地达到我们设定的最大垃圾收集时间或吞吐量大小这两个指标。
Serial Old :Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法。
G1:面向服务端的垃圾收集器,被称为驾驭一切的垃圾回收器。
G1 各代的存储地址不是连续的,每一代都使用了 n 个不连续的大小相同的 Region,每个Region占有一块连续的虚拟内存地址,H表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象,这样超大对象就直接分配到了老年代,防止了反复拷贝移动。
每个region都有一个RSet空间,用来记录region中对像是否被其他region中对象引用。使得垃圾收集器不需要扫描整个堆来找到谁引用了当前分区中的对象,只需要扫描RSet即可。
传统的收集器如果发生 Full GC 是对整个堆进行全区域的垃圾收集,而分配成各个 Region 的话,方便 G1 跟踪各个 Region 里垃圾堆积的价值大小(回收所获得的空间大小及回收所需经验值),这样根据价值大小维护一个优先列表,根据允许的收集时间,优先收集回收价值最大的 Region,也就避免了整个老年代的回收,也就减少了 STW 造成的停顿时间。同时由于只收集部分 Region,可就做到了 STW 时间的可控。
与 CMS 相比,它在以下两个方面表现更出色
-
运作期间不会产生内存碎片,G1 从整体上看采用的是标记-整理法,局部(两个 Region)上看是基于复制算法实现的,两个算法都不会产生内存碎片,收集后提供规整的可用内存,这样有利于程序的长时间运行。
-
在 STW 上建立了可预测的停顿时间模型,用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内。
-
G1采用SATB算法,将消失的用于记录下来,下次在GC扫描的时候检查记录。可以避免漏标。
GMS和G1垃圾回收器都有使用三色标记算法(详细的三色标记算法参考文档:http://t.csdnimg.cn/LlvZK),针对三色标记算法中出现的因为并发问题导致对象漏标(遗漏/忘记标记,应该存活的对象,被回收了,概括为忘记让对象存活了)的情况,2者的处理方式不同:CMS采用的是增量更新方案,G1则采用的是原始快照的方案。
漏标问题的发生必须满足两个充要条件:
1,至少有一个黑色对象新增了对白色对象的引用
2,所有灰色对象指向该白色对象的引用都断开了
增量更新:如果有黑色对象在自己标记后,又重新指向了白色对象。那么我就把这个黑色对象的引用记录下来,在后续「重新标记」阶段再以这个黑色对象为根,对其引用进行重新扫描。通过这种方式,被黑色对象引用的白色对象就会变成灰色,从而变为存活状态。
原始快照:如果灰色对象在扫描完成前删除了对白色对象的引用,那么我们就在灰色对象取消引用之前,先将灰色对象引用的白色对象记录下来。在后续「重新标记」阶段再以这些白色对象为根,对它的引用进行扫描,从而避免了漏标的问题。通过这种方式,原本漏标的对象就会被重新扫描变成灰色,从而变为存活状态。但是这种方式可能会把本来真的要取消引用的对象给错误的复活了,从而产生浮动垃圾。但是就像前面说的,多标的问题是可以忽略的。
总结:
生产环境中我们要根据不同的场景来选择垃圾收集器组合
1. 行在桌面环境处于 Client 模式:Serial + Serial Old 。
2. 需要响应时间快,用户体验好的:ParNew + CMS。
3. G1根据吞吐量决定是否使用。
参考:
1. 看完这篇垃圾回收,和面试官扯皮没问题了(现在点外卖是天价!)