HotSpot 垃圾回收器
全网最硬核 JVM TLAB 分析
HotSpot虚拟机垃圾收集优化指南
垃圾回收的第一步,就是找出活跃的对象。根据 GC Roots 遍历所有的可达对象,这个过程,就叫作标记。标记完成后,把其他不活跃的对象判定为垃圾,然后删除。所以垃圾回收只与活跃的对象有关,和堆的大小无关。
JVM GC可以分为:
- Minor GC:发生在年轻代的 GC。
- Major GC:发生在老年代的 GC。
- Full GC:全堆垃圾回收。比如 Metaspace 区引起年轻代和老年代的回收。
年轻代垃圾回收器
- Serial 垃圾收集器: 单线程处理 GC ,使用复制算法, 回收的过程中暂停一切用户线程
- ParNew 垃圾收集器: 多线程处理 GC ,回收的过程中暂停一切用户线程
-
Parallel Scavenge 垃圾收集器: 另一个多线程版本的垃圾回收器, 追求 CPU 吞吐量, 适合弱交互强计算
老年代垃圾回收器
老年代的对象存活率一般是比较高的, 空间又比较大使用复制算法不划算, 所以一般使用“标记-清除”、“标记-整理”算法,采取就地收集的方式。
- Serial Old 垃圾收集器: Serial收集器的老年代版本,单线程, 使用标记-整理算法。
- Parallel Old 垃圾收集器: Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。
- CMS 垃圾收集器: CMS(Concurrent Mark Sweep)收集器是以获取最短 GC 停顿时间为目标的收集器,它在垃圾收集时使得用户线程和 GC 线程能够并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。从后续更高版本的JDK版本提示来看, CMS 垃圾回收器逐步被G1 等垃圾回收器取代掉。
- G1、ZGC 垃圾收集器: 启动参数添加-XX:+UseG1GC, -XX:+UseZGC
TLAB(Thread Local Allocation Buffer)
查看GC配置信息
通过-XX:+PrintCommandLineFlags
参数,可以查看当前 Java 版本默认使用的垃圾回收器-XX:InitialHeapSize=126596288 -XX:MaxHeapSize=2025540608 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
JDK1.8默认使用UseParallelGC, 由ParallelScavenge(年轻代) + ParallelOld(老年代)组成, 可通过自带的工具jmc查看相关信息, 启用飞行记录器在JVM启动时需要添加参数-XX:+UnlockCommercialFeatures -XX:+FlightRecorder
CMS垃圾回收器
把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色(三色标记):
- 白色:尚未被GC访问过的对象,如果全部标记已完成依旧为白色的,称为不可达对象,既垃圾对象。
- 黑色:本对象已经被GC访问过,且本对象的子引用对象也已经被访问过了。
- 灰色:本对象已访问过,但是本对象的子引用对象还没有被访问过,全部访问完会变成黑色,属于中间态。
步骤:
初始标记 -> 并发标记 -> 并发预清理 -> 可中止的并发预清理 -> 重新标记 -> 并发清理 -> 并发重置
也可以简化理解为四个阶段: 初始标记->并发标记->重新标记->并发清理
. 有两个阶段会发生stop-the-world(初始标记和重新标记阶段),其他阶段都是并发执行的。
G1垃圾回收器
G1 的回收过程主要分为 3 类:
- G1“年轻代”的垃圾回收,同样叫 Minor GC,这个过程和我们前面描述的类似,发生时机就是 Eden 区满的时候。
- 老年代的垃圾收集,严格上来说其实不算是收集,它是一个“并发标记”的过程,顺便清理了一点点对象。
- 真正的清理,发生在“混合模式”,它不止清理年轻代,还会将老年代的一部分区域进行清理。
简单来说, 就是原本内存连续的堆切分成了均等大小的名字叫作小堆区(Region), Region的大小1M 到 32M 字节之间的一个 2 的幂值数。小堆区可以是 Eden 区,也可以是 Survivor 区,还可以是 Old 区。所以 G1 的年轻代和老年代的概念都是逻辑上的. 垃圾最多的小堆区,会被优先收集。这就是 G1 名字的由来
步骤
初始标记->Root 区扫描(Root Region Scan)->并发标记->重新标记->清理阶段
G1的过程和 CMS 垃圾回收器的回收过程非常类似,初始标记和重新标记阶段也是STW
- 并发标记: 这个阶段从 GC Roots 开始对 heap 中的对象标记,标记线程与应用程序线程并行执行,并且收集各个 Region 的存活对象信息。
- 清理阶段: 如果发现 Region 里全是垃圾,在这个阶段会立马被清除掉。不全是垃圾的 Region,并不会被立马处理,它会在 Mixed GC 阶段,进行收集。
CSet
全称是 Collection Set,即收集集合,保存一次 GC 中将执行垃圾回收的区间(Region)。GC 是在 CSet 中的所有存活数据(Live Data)都会被转移。
SATB 算法
全称是 Snapshot At The Beginning,它作用是保证在并发标记阶段的正确性。
这个快照是逻辑上的,主要是有几个指针,将 Region 分成个多个区段。如图所示,并发标记期间分配的对象,都会在 next TAMS 和 top 之间。
RSet
RSet 是一个空间换时间的数据结构。RSet 的功能与卡表(Card Table)类似,它的全称是 Remembered Set,用于记录和维护 Region 之间的对象引用关系。RSet 记录了其他 Region 中的对象引用本 Region 中对象的关系,属于 points-into 结构(谁引用了我的对象),有点倒排索引的味道。
混合回收(Mixed GC)
能并发清理老年代中的整个整个的小堆区是一种最优情形。混合收集过程,不只清理年轻代,还会将一部分老年代区域也加入到 CSet 中。
ZGC垃圾回收器
所以垃圾回收器本身的优化和升级,从来都没有停止过。最新的 ZGC 垃圾回收器,
就有 3 个令人振奋的 Flag:
- 停顿时间不会超过 10ms;
- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下);
- 可支持几百 M,甚至几 T 的堆大小(最大支持 4T)。
内存回收算法
- 复制算法(Copy):复制算法是所有算法里面效率最高的,缺点是会造成一定的空间浪费。
- 标记-清除(Mark-Sweep):效率一般,缺点是会造成内存碎片问题。
- 标记-整理(Mark-Compact):效率比前两者要差,但没有空间浪费,也消除了内存碎片问题。
所以,没有最优的算法,只有最合适的算法。
卡片标记(card marking)
该技术解决老年代到新生代的跨代引用问题。具体是,使用卡表(Card Table)和写屏障(Write Barrier)来进行标记并 加快 对GC Roots的扫描, 老年代是被分成众多的卡页(card page)的(一般数量是 2 的次幂)。卡表(Card Table)就是用于标记卡页状态的一个集合,每个卡表项对应一个卡页。
在进行 Minor GC 时, 只需要扫描由Dirty标记的区域(老年代引用了新生代对象的区域)即可, 大大加快了扫描的速度, 使GC停顿的时间减少
对象如何进入老年代
-
提升(Promotion)
每当发生一次 Minor GC,存活下来的对象年龄都会加 1。直到达到一定的阈值,该对象提升到老年代。这些对象如果变的不可达,直到老年代发生 GC 的时候,才会被清理掉。这个阈值,可以通过参数 ‐XX:+MaxTenuringThreshold
进行配置,最大值是 15
-
分配担保
看一下年轻代的图,每次存活的对象,都会放入其中一个幸存区,这个区域默认的比例是 10%。但是我们无法保证每次存活的对象都小于 10%,当 Survivor 空间不够,就需要依赖其他内存(指老年代)进行分配担保。这个时候,对象也会直接在老年代上分配。
-
大对象直接在老年代分配
超出某个大小的对象将直接在老年代分配。这个值是通过参数 -XX:PretenureSizeThreshold
进行配置的。默认为 0,意思是全部首选 Eden 区进行分配。
-
动态对象年龄判定
有的垃圾回收算法,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法。比如,如果幸存区中相同年龄对象大小的和,大于幸存区的一半,大于或等于 age 的对象将会直接进入老年代。
这些动态判定一般不受外部控制,我们知道有这么回事就可以了。通过下图可以看一下一个对象的分配逻辑。
参考
通过 JFR 与日志深入探索 JVM - TLAB 原理详解
关于栈上分配和TLAB的理解
CMS垃圾收集器
CMS与三色标记算法
一文看透垃圾回收,深入剖析,浅入深出
JVM调优:CardTable简介
一篇文章彻底搞懂CMS与G1
Java之CMS GC的7个阶段