JVM面试“核武器” 垃圾回收器串讲及HotSpot的细节实现
一、三色标记
并发标记时候使用三色标记算法
在三色标记法之前有一个算法叫 Mark-And-Sweep(标记清除)。这个算法会设置一个标志位来记录对象是否被使用。最开始所有的标记位都是 0,如果 发现对象是可达的就会置为 1,一步步下去就会呈现一个类似树状的结果。等标记的步骤完成后,会将未被标记的对象统一清理,再次把所有的标记位 设置成 0 方便下次清理。 这个算法最大的问题是 GC 执行期间需要把整个程序完全暂停,不能异步进行 GC 操作。因为在不同阶段标记清扫法的标志位 0 和 1 有不同的含义, 那么新增的对象无论标记为什么都有可能意外删除这个对象。对实时性要求高的系统来说,这种需要长时间挂起的标记清扫法是不可接受的。所以就需 要一个算法来解决 GC 运行时程序长时间挂起的问题,那就三色标记法。 三色标记最大的好处是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个 GC。
黑色:根对象、或该对象的子对象都被扫描
灰色: 本身被扫描,但并没有扫描完该对象的子对象
白色:未扫描的对象,白色既是垃圾对象
虽然三色标记能够解决GC长时间挂起的问题,但是它也有缺点:GC并发情况下的漏标问题
线程1和线程2同时工作,线程1完成所有标记,线程2尚未完成
此时,引用发生变化,线程1中A对象引用了C,线程2中B对象不在引用C
此时,线程B也已经完成工作,那么C对象并没有被线程1或者线程2任何一方标记,C对象为白色
针对上面的问题,CMS和G1的解决方案如下:
CMS解决方案----> Incremental(增加) Update 算法
当一个白色对象被黑色对象引用的时,将黑色对象重新标记为灰色,让垃圾回收器重新扫描
G1解决方案—>SATB(snapshot-at-the-beginning)
刚开始做一个快照,当 B 和 C 消失的时候要把这个引用推到 GC 的堆栈(最重要的是要把这个引用推到 GC 的堆栈),如果一旦某一个引用消失掉了,因为我把它放到栈,其实还是能找到它的,下回直接扫扫描他就行了
对比
SATB 算法是关注引用的删除。(B->C 的引用) Incremental Update 算法关注引用的增加。(A->C 的引用) G1 如果使用 Incremental Update 算法,因为变成灰色的成员还要重新扫,重新再来一遍,效率太低了
所以 G1 在处理并发标记的过程比 CMS 效率要高,这个主要是解决漏标的算法决定的
二、G1中技术细节
跨代引用
老年代对象引用了新生代的对象,那么回收新生代的话,需要跟踪从老 年代到新生代的所有引用,所以要避免每次 YongGC 时扫描整个老年代,减少开销。
RSet(记忆集)
RSet 的价值在于使得垃圾收集器不需要扫描整个堆,找到谁引用了当前分区中的对象,只需要扫描 RSet 即可。
RSet 本身就是一个 Hash 表,如果是在 G1 的话,则是在一个 Region 区里面。
CardTable
是一个字节数组每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。 一般来说,卡页大小 都是以 2 的 N 次幂的字节数,假设使用的卡页是 2 的 10 次幂,即 1M,内存区域的起始地址是 0x0000 的话,数组 CARD_TABLE 的第 0、1、2 号元素,分别 对应了地址范围为 0x0000~0x03FF、0x0400 ~ 0x07FF、0x0800~0x011FF 的卡页内存
总结
这里描述的是 G1 处理跨代引用的细节,其实在 CMS 中也有类似的处理方式,比如 CardTable,也需要记录一个 RSet 来记录,我们对比一下,在 G1 中是每 一个 Region 都需要一个 RSet 的内存区域,导致有 G1 的 RSet 可能会占据整个堆容量的 20%乃至更多。但是 CMS 只需要一份,所以就内存占用来说,G1
占用的内存需求更大,虽然 G1 的优点很多,但是我们不推荐在堆空间比较小的情况下使用 G1,尤其小于 6 个 G。
安全点
用户线程暂停,GC 线程要开始工作,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以 JVM 会在字节码指令中,选一些指令, 作为“安全点”,比如方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点。 为什么它叫安全点,是这样的,GC 时要暂停业务线程,并不是抢占式中断(立马把业务线程中断)而是主动是中断。 主动式中断是设置一个标志,这个标志是中断标志,各业务线程在运行过程中会不停的主动去轮询这个标志,一旦发现中断标志为 True,就会在自己最近 的“安全点”上主动中断挂起。
安全区域
为什么需要安全区域?
要是业务线程都不执行(业务线程处于 Sleep 或者是 Blocked 状态),那么程序就没办法进入安全点,对于这种情况,就必须引入安全区域。 安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区 城看作被扩展拉伸了的安全点
三、GC 日志详解
GC 常用参数
-Xmn -Xms -Xmx –Xss | 年轻代 最小堆 最大堆 栈空间 |
---|---|
-XX:+UseTLAB | 使用 TLAB,默认打开 |
-XX:+PrintTLAB | 打印 TLAB 的使用情况 |
-XX:TLABSize | 设置 TLAB 大小 |
-XX:+DisableExplicitGC | 启用用于禁用对的调用处理的选项 System.gc() |
-XX:+PrintGC | 查看 GC 基本信息 |
-XX:+PrintGCDetails | 查看 GC 详细信息 |
-XX:+PrintHeapAtGC | 每次一次 GC 后,都打印堆信息 |
-XX:+PrintGCTimeStamps | 启用在每个 GC 上打印时间戳的功能 |
-XX:+PrintGCApplicationConcurrentTime | 打印应用程序时间(低) |
-XX:+PrintGCApplicationStoppedTime | 打印暂停时长(低) |
-XX:+PrintReferenceGC | 记录回收了多少种不同引用类型的引用(重要性低) |
-verbose:class | 类加载详细过程 |
-XX:+PrintVMOptions | 可在程序运行时,打印虚拟机接受到的命令行显示参数 |
-XX:+PrintFlagsFinal -XX:+PrintFlagsInitial | 打印所有的 JVM 参数、查看所有 JVM 参数启动的初始值(必须会用) |
-XX:MaxTenuringThreshold | 升代年龄,最大值 15, 并行(吞吐量)收集器的默认值为 15,而 CMS 收集器的默认值为 6 |
Parallel 常用参数
-XX:SurvivorRatio | 设置伊甸园空间大小与幸存者空间大小之间的比率。默认情况下,此选项设置为 8 |
---|---|
-XX:PreTenureSizeThreshold | 大对象到底多大,大于这个值的参数直接在老年代分配 |
-XX:MaxTenuringThreshold | 升代年龄,最大值 15, 并行(吞吐量)收集器的默认值为 15,而 CMS 收集器的默认值为 6 |
-XX:+ParallelGCThreads | 并行收集器的线程数,同样适用于 CMS,一般设为和 CPU 核数相同 |
-XX:+UseAdaptiveSizePolicy | 自动选择各区大小比例 |
CMS 常用参数
-XX:+UseConcMarkSweepGC | 启用 CMS 垃圾回收器 |
---|---|
-XX:+ParallelGCThreads | 并行收集器的线程数,同样适用于 CMS,一般设为和 CPU 核数相同 |
-XX:CMSInitiatingOccupancyFraction | 使用多少比例的老年代后开始 CMS 收集,默认是 68%(近似值),如果频繁发生 SerialOld 卡顿,应该调小,(频繁 CMS 回 收) |
-XX:+UseCMSCompactAtFullCollection | 在 FGC 时进行压缩 |
-XX:CMSFullGCsBeforeCompaction | 多少次 FGC 之后进行压缩 |
-XX:+CMSClassUnloadingEnabled | 使用并发标记扫描(CMS)垃圾收集器时,启用类卸载。默认情况下启用此选项 |
-XX:CMSInitiatingPermOccupancyFraction | 达到什么比例时进行 Perm 回收,JDK 8 中不推荐使用此选项,不能替代 |
-XX:GCTimeRatio | 设置 GC 时间占用程序运行时间的百分比(不推荐使用) |
-XX:MaxGCPauseMillis | 停顿时间,是一个建议时间,GC 会尝试用各种手段达到这个时间,比如减小年轻代 |
G1 常用参数
-XX:+UseG1GC | 启用 CMS 垃圾收集器 |
---|---|
-XX:MaxGCPauseMillis | 设置最大 GC 暂停时间的目标(以毫秒为单位)。这是一个软目标,并且 JVM 将尽最大的努力(G1 会尝试调整 Young 区的块数来)来实 现它。默认情况下,没有最大暂停时间值 |
-XX:GCPauseIntervalMillis | GC 的间隔时间 |
-XX:+G1HeapRegionSize | 分区大小,建议逐渐增大该值,1 2 4 8 16 32。随着 size 增加,垃圾的存活时间更长,GC 间隔更长,但每次 GC 的时间也会更长 |
-XX:G1NewSizePercent | 新生代最小比例,默认为 5% |
-XX:G1MaxNewSizePercent | 新生代最大比例,默认为 60% |
-XX:GCTimeRatioGC | 时间建议比例,G1 会根据这个值调整堆空间 |
-XX:ConcGCThreads | 线程数量 |
-XX:InitiatingHeapOccupancyPercent | 启动 G1 的堆空间占用比例,根据整个堆的占用而触发并发 GC 周期 |