垃圾收集算法
分代收集理论
目前 jvm 的垃圾收集器都采用分代收集理论,根据对象的存活周期分为老年代和年轻代,这样就可以根据每个年代的特点采用的合适的垃圾收集算法。
标记-复制算法
复制算法将内存分为大小相同的两块,每次只使用其中一块,当这一块的内存用完时,就将这块区域内还存活的对象复制到另外一块区域去,再把这一块的区域清空。
优点:效率高。
缺点:只能使用一半的内存,利用率不高。
标记-清除算法
标记-清除分为两个阶段:标志 和 清除。标记还存活的对象,回收未被标记的对象,反之亦可。
优点:
相较于复制算法内存利用率较高。
缺点:
- 会产生大量不连续的内存碎片。
- 如果需要标记的对象较多,那么效率较低。
标记-整理算法
标记阶段于 标记-清除算法 一样,区别在于后续步骤不是回收对象,而是让还存活的对象往内存的一端移动,然后清理掉边界以外的内存。
对于年轻代,每次收集都会有大量的对象死去,适合使用复制算法,仅需要付出少量的复制代价就能完成垃圾收集。
对于老年代,大多数对象都是长时间存活的,且没有空间进行分配担保,必须采用 标记-清除 或者 标记-整理算法。
标记-清除 或者 标记-整理算法会比复制算法慢10倍以上。
垃圾收集器
1. Serial (串行) 收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)
Serial 是最基本、最古老的垃圾收集器,串行的意思是指它是单线程工作的。单线程不仅仅是指它垃圾收集时只会使用一个线程,还指的是它在进行垃圾收集的期间会停掉所有的工作线程(Stop the world),直到收集结束。
新生代采用的是复制算法,年老代采用的标记-整理算法。
但是 Serial 也有它的优点,那就是简单高效。因为没有了多线程资源竞争的开销,自然效率相比于其他收集器而言会高很多。
Serial old 是 Serial 收集器的老年代版本,同样也是单线程。
2. Parallel (并行) Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))
Parallel Scavenge 是 Serial 多线程版本,除了垃圾回收时采用了多线程以外,其他行为(控制参数,回收策略,收集算法)与 Serial 类似。默认的垃圾收集线程数与 cpu 的核心数一致,当然也可以通过 (- XX:ParallelGCThreads) 指定。
Parallel Scavenge 更关注的是 cpu 的吞吐率,吞吐率指的是 cpu运行用户代码的时间与cpu总消耗时间的比值,而 CMS 更在意的是用户程序的停顿时间(提升用户的使用体验)。
新生代采用复制算法,老年代采用标记-整理算法。
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,采用标记-整理算法。需要注意的是, Parallel Scavenge(新生代)+ Parallel Old(老年代)是 JDK1.8 的默认垃圾收集器组合。
3. ParNew 收集器(-XX:+UseParNewGC)
ParNew 与 Parallel 收集器很类似,主要的区别在于它可以跟 CMS 收集器配合使用。
新生代采用复制算法,老年代采用标记-整理算法。
ParNew 是很多运行在 Server 模式下的虚拟机的首选收集器,除了 serial 以外,只有它能与 CMS 配合。
4.CMS收集器(-XX:+UseConcMarkSweepGC(old))
CMS 全程 Concurrent Mark Sweet,是一种以最短停顿时间为目标的垃圾收集器。它是 HotSpot 虚拟机第一个真正意义上的并行垃圾收集器,非常适合想要提高用户体验的应用,它可以让垃圾回收线程与用户线程同时工作。
顾名思义,CMS 采用标志-清除算法,运行过程比前面的收集器都要复杂,整个过程可分为四个步骤:
- 初始标记: 暂停其他的所有线程(STW),记录下能通过 gc roots 直接引用的对象,只有一层遍历,速度很快。
- 并发标记: 并发标记指的是从第1步中记录的 gc roots 直接引用的对象遍历到整个内存的对象的过程,这个过程耗时较长,但可以与用户线程并发运行。但因为用户线程也在运行,所有会导致已标记的对象状态发生改变。
- 重新标记: 重新标记指修正在并发标记期间由于用户线程并行运行导致的对象引用状态发生变化的那一部分对象,这个阶段也会STW,但是这个阶段的停顿时间一般比初始标记要长,但是远比并发标记时间短。这里主要用到的三色标记法的增量更新做重新标记。
- 并发清理: 开启线程,对未被标记的对象做清理,这个阶段如果有新增的对象都不会认为是垃圾对象,不做任何处理。
- 并发重置: 重置这一轮GC过程的对象标记状态。
优点: 真正的并发收集器,停顿时间短。
缺点:
- 对 CPU 资源敏感,会和服务器抢资源;
- 无法清理浮动垃圾,在并发标记和并发清理阶段产生的垃圾无法在这一轮垃圾收集清理掉,只能在下一轮 GC 中回收。
- 使用的回收算法: 标记-清理,容易产生大量不连续的内存碎片,可以开启参数(-X:+UseCMSCompactAtFullCollection),在每次垃圾收集完整理内存。
- 垃圾收集过程的不确定性,存在这一轮的垃圾收集还没有结束,下一轮的垃圾回收又开始了。特别是在并发标记和并发清理阶段,一边回收,系统一边运行,如果还没回收完就再次触发 Full Gc,也就是"concurrent mode failure",这时候会Stop The World,使用 serial old 继续垃圾回收。
三色标记法
在 CMS 的标记阶段,使用到的标记算法叫做三色标记法。
三色标记法会将 Gc Roos 可达性分析遍历过程中遇到的对象,按 是否访问过 区分为三个颜色:
黑色: 表示对象已经被访问过,并且这个对象的所有引用也都被扫描过。黑色的对象是安全的对象,不会被清理,如果有其他引用指向黑色对象,无需重新扫描。黑色对象无法直接引用(不经过黑色对象)白色对象。
灰色: 表示对象已经被访问过,但是对象上至少还有一个引用未被扫描。
白色: 表示对象未被访问过,显然一开始的所有对象都是白色的,如果标记阶段结束,还有对象是白色的,说明这个对象不可达,即将被回收。
结合 CMS 的垃圾收集过程,这种方法会存在几个问题:
多标-浮动垃圾
在并发标记阶段,如果有方法运行结束导致 gc roots 被销毁,但这个 gc roots 引用的对象又被扫描过(可知这个对象一定不会被回收),那么这个对象在本轮 GC 是不会被回收的,这部分应该回收但没有被回收的对象,被叫做 “浮动垃圾”。浮动垃圾并不影响垃圾回收的正确性,只需要等到下一轮的 GC 再被清除即可。
另外,并发标记和并发清理开始后的对象都会被标称黑色(为了 GC 的正确性)。 这部分对象也有可能在 GC 过程中变成垃圾,这也是浮动垃圾的一部分。
漏标
漏标指的不应该被回收的对象被当成垃圾回收了,这是非常严重的bug,必须解决。
漏标产生的情形只可能有一个:
黑色对象A 原本不引用白色对象D,灰色对象引用白色对象D,当 GC 线程扫描完黑色对象A 后,GC 线程暂停。此时恰好灰色对象B 删除了对白色对象D 的引用,那么程序认为灰色对象B 的引用已经扫描完毕,设置成黑色,尽管黑色对象A 重新引用了白色对象D ,但是对象A 已经是黑色的了,程序不会再次扫描对象A。最终对象D 一直停留在白色集合里,尽管它不是垃圾对象。
总结以上过程,漏标必须满足以上两个条件:
1. 黑色对象增加了一条或多条对白色对象的引用;
2. 灰色对象删除了对白色对象的直接或间接引用。
这两个条件必须同时满足,才会出现漏标。那么只需要破坏其中一个条件,就可以防止对象消失。于是产生了两种解决方案:
1. 增量更新(Incremental Update)
增量更新破坏的是第一个条件。当黑色对象增加了对白色引用的对象时,就将这条引用记录下来。在并发标记结束后,以黑色对象为根,再次扫描即可,这样就能扫描到白色对象。即黑色对象一旦新增了对白色对象的引用,那么它就变为灰色对象(含有未被扫描的引用)了。
2. 历史快照(Snapshot At The Beginning,SATB)
历史快照破坏的是第二个条件。当灰色对象删了对白色对象的引用时,将这条引用关系记录下来。在并发标记结束后,以这条引用关系的灰色对象为根进行扫描,这样就能扫描到白色对象,将白色对象直接标成黑色,让这个对象在这轮 GC 里存活,即使他有可能成为浮动垃圾。相当于无论灰色对象是否删除了白色对象的引用,也已刚开始扫描时的那一瞬间的对象引用关系快照进行扫描。
CMS垃圾收集器是采用增量更新解决的。