垃圾回收器
垃圾回收类型
串行
单线程
适合堆内存小的时候。
STW
Stop The World 的简称。这是因为串行的机制,在垃圾回收的线程运行的时候,其它工作线程都要阻塞。
在垃圾回收过程中,对象的地址会发生改变。如果其它线程不阻塞,则可能会发生对象引用错误的问题。
吞吐量优先
- 多线程
- 适合堆内存较大,且多核CPU的情况。
- 在单位时间内,STW时间最短。
响应时间优先
多线程
适合堆内存较大,且多核CPU的情况。
尽可能让单次 STW 时间最短。
比如 1小时,垃圾回收两次,每次10分钟。(吞吐量优先)
和每5分种垃圾回收一次,1小时内回收4次。(响应时间优先)
垃圾收集器
Serial 收集器
- XX:+UseSerialGC -XX:+UseSerialOldGC
使用串行回收器进行回收,这个参数会使新生代和老年代都采用串行回收器。SerialOld是Serial的老年代版本。
新生代使用复制算法,老年代使用标记-整理算法。
Serial收集器是最基本、历史最悠久的收集器。它是一个单线程收集器,一旦收集器工作,系统会停止。
在垃圾回收线程运行时,其它工作线程都要阻塞。
优点是简单而高效,单线程没有线程开销,有很高的单线程收集效率。
Parallel Scavenge收集器
- XX:+UseParallelGC(年轻代) -XX:+UseParallelOldGC(老年代)
Parallel收集器其实就是 Serial收集器的多线程版本
。
默认的收集线程数与 CPU 核数一致。(可以修改线程数,但是为了吞吐量不建议修改)
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用CPU)。
CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
Parallel Scavenge收集器和Parallel Old收集器 是JDK8默认的新生代和老年代收集器。
ParNew 收集器
- XX:+UseParNewGC
ParNew收集器其实跟 Parallel 收集器很类似,区别主要在于它可以和CMS收集器配合使用
。
CMS收集器
- XX:+UseConcMarkSweepGC(old)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
使用 标记-清除
算法。
标记-清除过程
初始标记
暂停用户线程(STW),记录下GC Roots 能
直接引用
的对象。并发标记
开始遍历 GC Roots 能直接引用的对象。
比如 GC Roots → A → B,开始从 A 遍历,遍历整个对象图。
不需要暂停用户线程,这个过程耗时较长,用户线程可以与垃圾回收线程一起运行。
但是因为用户线程继续运行,可能导致已标记的对象状态发生变化。
重新标记
需要暂停用户线程(STW),重新标记阶段就是为了修复并发标记阶段,对象状态产生变化的对象。
这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法 (见下面详解) 做重新标记。
并发清理
开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
并发重置
重置本次GC过程中的标记数据。
CMS收集器的特点
优点
- 并发收集
- 低停顿
缺点
CPU 资源敏感(GC线程会抢占资源)
无法处理GC过程中新产生的垃圾
并发标记和并发清理阶段产生的新垃圾不做操作,只能等到下次 GC。
标记-清除算法会导致大量内存碎片
可以通过参数
XX:+UseCMSCompactAtFullCollection
让jvm在执行完标记清除后再做整理。执行过程不确定
可能在本次 GC还未完成的时候,由于用户线程的原因,触发新一次的 GC。
三色标记法
垃圾回收算法判断对象是否需要被回收时,一般采用可达性分析算法。
而在以减少系统停顿时间为目的的 CMS 收集器中,标记-清除
过程中存在垃圾回收线程和用户线程并发的情况,可能导致对象状态发生变化。
三色标记法将对象的状态按照三种颜色划分,不仅能解决和用户线程并发的问题,还能缩短 STW 的时间。
白色
没有被 GC 扫描到的节点都是白色,意味着可以被回收。
在初始标记开始前,所有节点的状态都是白色。
灰色
该节点表示正在标记的节点,至少存在一个引用没有被扫描到。是一种中间状态。
最终节点状态只会停留在黑色或者白色。
黑色
被 GC 扫描过的节点标记为黑色,表示节点是安全的。
标记过程
初始标记阶段。从虚拟机栈中 GC Roots 开始扫描直接引用对象,找到 Node1 和 Node5,将 Node1 和 Node5 标记为灰色。
并发标记阶段。
从 Node1 和 Node5 开始遍历,查到 Node2 和 Node6,此时将 Node2 和 Node6 标记为灰色。同时将 Node1 和 Node5 标记为黑色。
并发清除阶段。
经历过标记阶段之后,只剩下Node4 和 Node7 两个节点没有被引用,可以被回收。
将白色阶段回收之后,剩下的就都是黑色节点。
标记问题
多标 - 浮动垃圾
在并发-标记过程中,如果运行的方法被销毁。该方法作为 GC Roots 引用的对象,之前如果已经被扫描过标记为黑色了。那么本轮 GC不会再回收这些对象,这些本该被回收的垃圾对象成为浮动垃圾。
在并发-标记(并发-清理)过程中,如果产生了新对象,通常做法直接设置为黑色。本轮也不会进行垃圾回收,放到下一轮回收。
如果这部分对象在本次 GC 有垃圾对象产生,也是浮动垃圾。
浮动垃圾并不会影响垃圾回收的正确性,只需要等到下一次垃圾回收就可以被清除。
漏标
应该被标记为黑色回收的对象,漏标。导致存活对象被垃圾回收,严重影响程序功能。
漏标必须满足的条件
漏标只有在以下两个条件都满足的情况下才会产生:
条件 1:有至少一个黑色对象在自己被标记之后指向了这个白色对象。(黑色对象本身不会被再次扫描)
条件 2:灰色对象在自己引用扫描完成之前删除了对白色对象的引用。
只有同时满足两个条件才会发生漏标的情况,所以只要破坏其中一个条件,就能解决漏标问题。
漏标解决方案
增量更新
破坏条件 1(有至少一个黑色对象在自己被标记之后指向了这个白色对象)。
在并发标记阶段当我们黑色对象(B)引用关联白色对象(E),记录下B黑色对象。
在重复标记阶段,将 B黑色对象变为灰色对象,然后扫描 B 的整个引用链。
记录黑色对象的过程结合了写屏障来实现的。
增量更新指的是保存引用关系,在重复标记阶段再次更新。
- 在并发-标记阶段,当 Node5 引用 Node3 时,记录 q 指针的引用关系。
- 在重复标记阶段,将q 指针对应的 Node5 变为灰色对象,然后扫描 Node5 的整个调用链。最终将 Node3 标记为黑色,不被清除。
原始快照
破坏条件 2(灰色对象在自己引用扫描完成之前删除了对白色对象的引用)
当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来。
在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次。这样就能扫描到白色的对象,将白色对象直接标记为黑色 (目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)。
原始快照指的是保留删除前的引用关系,将要被删除的引用对象标记为黑色,下次 GC 再重新扫描。
当 p 指针引用关系要被删除时,记录下 p 指针。
在重复扫描过程中,以灰色对象为根,重新扫描一次。
将扫描到的白色对象直接标记为黑色。
这样做的目的是让对象在本轮 GC 中能够存活下来,等到下次 GC时再重新扫描。
写屏障
写屏障的主要作用是确保在并发编程环境中,对共享数据的写操作能够被正确地同步和管理。
特别是在垃圾回收过程中,确保对象状态的改变能够被垃圾回收器感知。
当应用程序的线程对一个对象进行写操作时,写屏障会被触发,通知垃圾回收器对象的状态变化,从而避免漏标等问题。
解决方案
- CMS 收集器
- 写屏障 + 增量更新
- G1 收集器
- 写屏障 - 原始快照(SATB)
- ZGC:
- 读屏障