共计 11625 个字符,预计需要花费 30 分钟才能阅读完成。
G1 垃圾收集器简述
全文共两部分, 有基础的读者只需要阅读第一部分 ”G1 垃圾收集器在最新几个版本的发展 ”, 第二部分为基础部分.
- G1 垃圾收集器在最新几个版本的发展
G1 垃圾收集器始见于 1.7 版本, 在后续的几个版本中对它进行了优化和改进: 在 JAVA9 中,G1 垃圾收集器增加了几个可配置选项的自动发现功能, 同时它被设置为默认的垃圾收集器(32/64 位服务器), 取代了 Parallel gc, 同时 deprecated 了 cms; 在 JAVA10 中,G1 的 FULL GC 被改进为并行以缩短时间; 在 JAVA11 中,g1 在处理 Reference 时的线程数支持自适应调整, 也是同时在这一版, 所有 gc 在 stw 阶段均支持了自适应的并行度调整.
截止此文时, 最新版的 JAVA12 在推出 Shenandoah GC, 对 zgc 支持并发类卸载的同时, 依旧对 G1 垃圾收集器进行了几点优化:
- 对 G1 和 Parallel GC 推出的体验特性, 支持 NV-DIMM 等可选设备上分配老年代(自 JAVA10 开始, 堆内存可以分配在 NV-DIMM, 关于 DIMM 的优点本文不作论述).
如果启用了这个功能, 年轻代依旧使用 DRAM 安放, 仅有老年代会存放在 NV-DIMM.G1 在任何一个给定的时间点保证提交到 DRAM 和 NV-DIMM 上的内存永远会小于 -Xms 指定的内存总量. 目前最新的实现方式是将完整的 JAVA 堆预分配到 NV-DIMM 文件系统, 这样可以避免动态的代扩容, 但是将保证 NV-DIMM 文件系统空间充足的责任甩给了用户. 启用时, 即使用户显式设置了年轻代大小, 虚拟机同时也基于 DRAM 的总可用量对年轻代进行了限定.
举例说明: 如果虚拟机在一个具备 32G 的 DRAM 和 1024G 的 NV-DIMM 内存的系统上运行, 指定了 -Xms756g, 虚拟机会对年轻代进行计算, 并使用计算结果进行限定.
如果未指定 -XX:MaxNewSize 或 -Xmn, 最大年轻代大小将设置为可用内存的百分之八十(25.6G); 指定了 -XX:MaxNewSize 或 -Xmn, 最大年轻代大小依旧以 25.6G 为封顶; 使用 -XX:MaxRAM 可告诉虚拟机有多少 DRAM 可用, 那么年轻代的大小设置为该参数指定的值的百分之八十; 使用 -XX:MaxRAMPercentage 可指定 DRAM 中有多大的百分比对于年轻代可用(默认百分之八十);
启动时, 可通过日志选项 gc+ergo=info 打印最大年轻代大小. - g1 可在并发标记周期释放内存.
在 12 版,G1 默认可以在并发标记周期将应用进程不需要的空闲的堆内存交回操作系统, 从而提升了 java 进程对内存的使用效率, 若使用 -Xms 选项将初始内存设置为最大内存, 则此功能会被禁用.
- 可终止的 g1 混合 gc
G1 有一个非常重要的目标: 在 gc 停顿阶段适配用户期望的停顿时间. 一直以来,G1 选择一段收集期内完成的大量工作信息 (一定程度上依赖于应用行为本身) 作为样本进行高度分析, 分析后选定的一组分区被称为 collection set(简称 cs, 即回收集), 一旦 cs 被选定并且 G1 开始了回收, 那么 G1 必须不停顿地回收所有这些 cs 中的存活对象. 这个行为可能会因为 G1 的启发式算法选择了过大的 cs 而导致回收时间超过用户设置的目标停顿时间. 应用行为突变是一个典型的复现场景, 它会造成启发式算法依托于 ” 脏 ” 数据, 当出现这种情况时, 可以观测到 mix gc(关于 mix gc 可参考后面更基础的描述)过程中包含了过多的老年代分区, 因此需要一个机制来发现 G1 的启发式算法是否重复选择了前面垃圾收集过程中的错误工作数据, 并在必要时让 G1 增量地按小步运行回收工作, 每一个小步完成之后, 回收工作都可以取消, 通过这样的机制,G1 可更加容易地达到或者接近用户指定的目标停顿时间要求.
具体的过程: 如果 G1 发现了启发式算法重复选取了错误的分区数, 立即切换为一个更加精细的 mix gc 方式, 首先, 将 cs 分割成两个部分, 必选和可选. 必选部分会包含 cs 中的 g1 不能细化处理的部分(如年轻代), 但为了提升效率, 它也可以包含部分老年代分区. 剩余的老年代分区便组成了可选的 cs 部分.
当 G1 完成了必选部分的回收之后, 如果有时间剩余,G1 以更细粒度回收可选部分. 回收 cs 可选部分的粒度取决于这个剩余时间, 粒度最细化的情况下限定在一个分区. 在完成可选 cs 的任何一部分回收后,G1 可以依照剩余时间来决定是否取消回收过程.
因为粒度的细化,G1 对为达到停顿时间目标而预算的 cs 变的更加精确, 可选的 cs 会在整个过程中越来越小, 最终结果是必选部分再一次包含了 cs 的所有分区. 如果某一刻启发式算法的结果变的重新不精确起来, 那么下一次回收将会重新包含 ” 必选 ” 和 ” 可选 ”. -
让 g1 在空闲时自动释放已提交但未使用的内存.
在此之前,G1 不会定时地将堆中已提交的内存释放回操作系统, 它只会在 full gc 或并发周期内做这件事, 因为 g1 一直在努力避免 full gc, 并仅会基于 java 堆的占用和内存分配活动来触发一个并发周期,G1 除非显式强制要求, 否则不会将堆内存释放. 这个行为在付费购买资源的容器化环境中是明显的劣势. 即使在空闲时, 虚拟机使用破碎的内存资源时,G1 也会持有全部的 java 堆, 结果就是云用户为这些空闲占用的资源进行了额外的买单.
如果让虚拟机有能力发现处于空闲态的 java 堆, 自动在空闲时减少堆的使用, 将会是一个大幅提升. 当然,Shenandoah 和 GenCon 收集器已经支持了类似的功能. 显然的, 对于 web 服务用户来说, 夜间的请求数量和白天的请求数量往往相差甚远, 服务器在白天频繁地处理请求, 而大部分夜晚却处于空闲状态. 当然官方还是做出了相应的调研, 通过对实时的 tomcat 服务器的昼夜服务差距估计, 此解决方案可以减少虚拟机的 85% 的内存提交量.
为了实现尽可能将无用内存释放回操作系统的目标,G1 将会在应用空闲时周期的尝试触发一个并发周期, 这用以断定 java 堆的全局使用结果, 它将会导致 java 堆中的未使用部分自动地返回给操作系统, 当然用户可以选择在这一个过程中使用 full gc 来最大化返回内存. 有两种情况,G1 会认为应用不活跃并触发周期 gc: 第一是任何一次 gc 停断后超过 ”G1PeriodicGCInterval” 指定的毫秒数且这期间没有任何正在进行的并发周期. 如果该值指定为 0, 则表示功能禁用; 第二是由 JVM 调用宿主系统的 getloadavg()方法返回的一分钟平均系统负载值低于 ”G1PeriodicGCSystemLoadThreshold” 时, 但如果指定 G1PeriodicGCSystemLoadThreshold 为 0 也会禁用. 如果两个条件均不满足, 则取消相应的周期 gc, 直到下一次 G1PeriodicGCInterval 满足了为止. 周期 gc 的类型是由选项 G1PeriodicGCInvokesConcurrent 决定的, 如果设置了该选项,G1 会开启一个并发周期来回收, 否则会使用 full GC. 在每一次回收后 G1 都会调整当前的堆大小, 悄悄地把内存还给操作系统. 新的 java 堆大小是由一些配置决定的, 包含但不限于 MinHeapFreeRatio,MaxHeapFreeRatio, 最小最大堆大小配置.
为了避免扰动应用进程,G1 默认会在周期 gc 中间开始或继续一个并发周期, 但是相对于 full gc, 明显不能释放更多的内存.
在相应的 gc 日志中, 由这个机制触发的 gc 将会打上相应的标签, 详见下面的例子.
(1) 6.084s[gc,periodic] Checking for periodic GC.[6.086s][info][gc] GC(13) Pause Young (Concurrent Start) (G1 Periodic Collection) 37M->36M(78M) 1.786ms
(2) 9.087s[gc,periodic] Checking for periodic GC.
[9.088s][info][gc] GC(15) Pause Young (Prepare Mixed) (G1 Periodic Collection) 9M->9M(32M) 0.722ms
(3) 12.089s[gc,periodic] Checking for periodic GC.
[12.091s][info][gc] GC(16) Pause Young (Mixed) (G1 Periodic Collection) 9M->5M(32M) 1.776ms
(4) 15.092s[gc,periodic] Checking for periodic GC.
[15.097s][info][gc] GC(17) Pause Young (Mixed) (G1 Periodic Collection) 5M->1M(32M) 4.142ms
(5) 18.098s[gc,periodic] Checking for periodic GC.
[18.100s][info][gc] GC(18) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 1.685ms
(6) 21.101s[gc,periodic] Checking for periodic GC.
[21.102s][info][gc] GC(20) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.868ms
(7) 24.104s[gc,periodic] Checking for periodic GC.
[24.104s][info][gc] GC(22) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.778ms
上面的例子指定了 G1PeriodicGCInterval 的值为 3000ms, 在 (1) 中应用保持不活跃一段时间后,G1 启动了一个并发周期, 标记 (Concurrent Start) 和(G1 Periodic Collection). 这次并发周期的启动立即释放了一些内存, 可以看到 (1) 到(2)的 (78M) 和(32M). 在 (2) 到(4)触发了更多的周期回收, 这一次触发的回收方式为 mix gc 并整理堆. 紧随的周期 gc(5)到 (7) 仅开始了一个并发周期, 因为 G1 的策略判断此时老年代的垃圾数量不足, 不必开始 mix gc. 本例中, 因为堆大小已经保持到最小堆 size, 周期 gc(5)到 (7) 不会进一步压缩堆内存.
在应用闲暇时间, 对象存活状态的改变 (如软引用过期) 可能会触发已提交 java 堆内存的进一步缩小.
- G1 垃圾收集器简介
本节的一些数量指标主要依托于最新 JAVA12 的文档, 有一些 ” 量化 ” 的数值可能与其他文章不一致.
按照官方文档说法,” 垃圾优先 ”(G1 即 garbage first)收集器专注于多处理器, 大内存的应用场景.(这个大内存似乎是在 zgc 等专注超大堆 gc 出现之前的描述)它试图在少量配置前提下, 满足用户指定的目标停顿时间, 同时保持一定的高吞吐量.G1 旨在应用延迟和吞吐量之间提供一个当前应用环境下的最佳平衡, 这些典型适选 G1 的应用环境的特征有:
堆大小高达数十 G 或者更大(在一些文章和问答中有 6G 的标识, 可能为旧版本), 且存活数据可占用高达 50% 的堆内存.
对象分配和晋升的速度可能会随着时间推移大幅度改变.
堆中有大量的内存碎片.
期望不长于几百毫秒的可预测的停顿时间目标, 避免长期的 gc 停顿.
G1 已替换了 cms 垃圾收集器且是默认的垃圾收集器.
- 基本概念
按照官方表述,G1 是一个分代的, 增变的, 并行的, 多数情况并发的, 会 stop-the-world 的,” 排泄 / 迁移 ”(evacuating)的垃圾收集器, 专注于每次 stop-the-world 的停顿的停顿时间. 这个 ” 分代 ” 其实是可选的, 即也有无代模式.” 增变 ” 特性在前面的新特性章节已经阐述过. 与其他垃圾收集器类似,G1 也将堆划分 (虚拟的) 为年轻代和老年代, 同样符合老规矩: 年轻代的回收是最高效的, 也是主要的工作, 偶尔也伴随一些老年代的空间回收.
在 G1 中, 为了提升吞吐量, 有一些操作永远是 stop-the-world 的. 其他的一些要长期的, 如全局标记这种要全堆进行的操作与应用程序并发进行. 为了让空间回收的 stop-the-world 停顿尽可能减少,G1 并行的分步的递增进行空间回收.G1 通过追踪此前应用行为和垃圾回收停顿的信息来构建一个与开销有关的模型. 它使用这些信息去围限停顿期间可做的工作. 举个例子,G1 首先回收最高效的区域(也即垃圾最满的区域, 因此称为垃圾 - 优先).
G1 使用 ” 排泄 ” 的方式回收大多空间, 在选定的内存区域, 存活的对象被拷贝到新的区域, 并在这个过程对他们进行压缩整理. 在排泄完成后, 存活对象之前占用的空间可以交给应用程序重新使用, 即可用来分配新的对象. - 堆布局
G1 将堆分割成一组等大小的堆区, 一个区是内存分配和回收的基本单元. 在任何一个给定的时间, 每一个区可以是空的(下图浅灰色), 也可以分配了特殊的代(年轻或老年). 当有内存分配请求到来时, 内存管理者上交空闲的分区, 把它们指派给一个代并且交给应用程序, 作为应用程序可自由分配的自由空间.
上图中, 纯红色为 eden 区, 标有 ’S’ 的为幸存者区, 与此前其他的垃圾收集器功能保持一致, 不同之处在于 G1 中这些同代的分区自身并不连续. 老年代分区由浅蓝色表示, 它的占用者可能会是跨多个分区的大型对象(带有 ’H’), 一般情况下, 应用程序会将对象分配到年轻代中的 eden 区, 大对象则直接分配到老年代. - gc 周期
G1 垃圾收集器宏观上有两个阶段, 它会在两个阶段之间往复切换. 两个阶段分别是 young-only 阶段和空间回收阶段.young-only 阶段包含一系列逐渐充满老年代可用空间的 gc. 空间回收阶段,G1 递进地回收老年代中的空间, 同时也处理年轻代. 紧接着,G1 又重新进入 young-only 阶段, 开始新一轮的循环.
上图表示 G1 的不同阶段以及有关的停顿. 可以看到图上有一些实心的圆圈, 每一个圈都表示一次 gc 停顿: 蓝色圆表示 young-only 回收停顿, 橘黄色表示包含标记过程的停顿, 红色表示混合 gc 的停顿. 这些停顿用箭头标记了循环顺序, 从 young-only 进入到混合 gc, 再回到 young-only.young-only 阶段开始于若干个 young-only gc, 图上由小蓝圈表示, 在几次 gc 后, 老年代中的对象占有超过了 InitiatingHeapOccupancyPercent 定义的阈值, 则下一次的 gc 停顿将会初始化一个标记 gc 的停顿, 上图用大蓝圈表示, 它除了标记以外, 其他的工作与 young-only 停顿一致, 同时它会为并发标记做出准备.
当运行并发标记时, 其他 young only 停顿也可能会发生, 直到 remark 停顿 (第一个大黄圈) 为止(作者认为 remark 等阶段不应简单地按字面意思翻译, 如直译为重新标记, 但是这一阶段做出的工作本身也不止如此, 就如 gc 本意 garbage collection/collector 是垃圾回收 / 器的意思, 却不止负责回收, 也影响内存分配等, 其他的阶段也类似), 在 remark 阶段,G1 完成标记. 直到 Cleanup 阶段之前, 仍旧可能会有额外的 young-only gc. 在 Cleanup 停顿之后, 将会有一个最终的 young-only gc 终止整个 young-only 阶段. 在空间回收阶段, 会发生一系列的混合 gc, 上图中用红色圈表示, 一般来讲, 它的数量会比 young-only 阶段的 young-only 停顿少, 因为 G1 努力去使空间回收尽可能的高效. 下面总结 G1 周期中的各个阶段, 阶段中的停顿和转换:
young-only 阶段: 这一阶段开始于几个普通的 young gc, 它们会提升年轻代对象到老年代.young only 阶段与空间回收阶段的转换会在老年代的占有达到一个确定阈值后开始, 这个阈值为初始堆占有阈(Initiating Heap Occupancy threshold), 在此时刻,G1 会调度一个并发开始的 young gc 并用它替换普通的 young gc.
并发开始: 它除了执行普通的 young gc 外, 还开始了标记过程, 它会并发地标记所有老年代中当前可达的存活对象以用于后续的空间回收阶段. 当标记未完成时, 普通 young gc 可能也会穿插发生. 标记完成伴随两个特殊的停顿:Remark 和 Cleanup.
remark: 此停顿会终止自身的标记过程, 它执行全局引用处理和类卸载, 以及回收完全空的分区和清空内部数据结构. 在 remark 和 cleanup 两阶段中间,G1 会并发计算后续可回收的选定的老年代空间信息, 此信息会最终在 cleanup 阶段确定.
cleanup: 这一次停顿会决定是否会有一个紧随其后的空间回收阶段, 如果有空间回收阶段发生, 那么 young-only 阶段会在最后为混合 gc 完成准备.
空间回收阶段: 这一阶段包含多个混合 gc, 除了回收年轻代, 也会排泄老年代存活对象. 这一阶段会持续一段时间, 直到 G1 发现排泄更多的老年代分区也不会得到与开销等同价值的空闲空间的回报为止.
在空间回收阶段之后,gc 循环重启, 开始新一轮的 young-only 阶段, 如果应用在收集对象存活信息过程中就已耗尽内存, 那么 G1 如其他垃圾收集器一样, 执行备选的 full gc. - G1 停顿与 cs, 分代大小
G1 在 stop-the-world 停顿中执行空间回收, 存活的对象会被从源区拷贝到目标区, 存活对象的引用也会被相应地调整到新地址.
对于非巨型对象, 一个对象的目标堆区决定于一些特定规则, 若源对象是年轻代对象 (eden 区或幸存者区), 则根据它们的年龄决定拷贝到幸存者区或老年代.
老年代对象拷贝到其他老年代. 大对象则区别对待,G1 只决定它们的存活与否, 如果它们被判定为非存活对象, 则就地回收,G1 不会移动巨型对象.
受 gc 类型影响,cs 可能包含不同种的分区. 在 young only 阶段,cs 只包含年轻代和可能被回收的潜在巨型对象区; 在空间回收阶段,cs 包含年轻代, 可能被回收的潜在巨型对象区, 一些老年代候选区.
G1 会在并发周期中选定候选回收分区, 在 remark 停顿期间,G1 选定那些较低占有率的, 也就是包含大量空余空间的分区, 这些分区会接下来在 remark 和 cleanup 之间进行后面的收集,cleanup 停顿会根据回收它们的效率进行排序, 回收效率高的分区, 即看起来回收花费时间少, 包含更多空闲空间的会在后续的混合 gc 中优先使用.
这一段话摘自官网, 官方说了 ”that contain more free space are preferred in subsequent mixed collections”, 可能会有不少看客看晕(包含作者自己),G1 不是 ” 垃圾优先 ” 吗? 它应该优先选取垃圾最多的分区去回收, 作者在 stackoverflow 上找到了老外对此的解释, 看来有此疑问的不止作者一人. 根据 stackoverflow 上的解释, 此处 ” 包含更多的空闲空间 ” 其实就是指包含更多的垃圾, 这可能是英语汉语之间的一个美妙误会吧.
从上图可见,”mostly empty” 是指包含尽可能多的 ” 可回收的垃圾 ”. 贴子作者也指出, 对于要回收的分区, 包含最大数量的可回收空间至少有两点好处: 一是可以尽快的获得最多的空间, 二是对于 G1 这种使用拷贝(标记清除整理) 的收集器, 源分区存活对象越少, 需要做的 copy 工作就越少, 就可以越高效地回收最多的空间.
提到空间的回收以及 cs 的挑选, 顺便提一提比较简单的堆空间大小调整问题.G1 遵守调整 java 堆大小的标准规则, 如 -XX:InitialHeapSize 可以指定 java 堆的最小大小, 使用 -XX:MaxHeapSize 则指定了 java 堆的最大大小,-XX:MinHeapFreeRatio 指定最小可用内存的百分比,-XX:MaxHeapFreeRatio 指定调整堆大小后最大空闲内存占比.G1 垃圾收集器仅会在 remark 和 full gc 的停顿期间调整堆的容量. 这一过程会从操作系统获取内存或释放内存.
G1 在每一次普通的 young gc 后都会为下一个增变器阶段调整年轻代的大小. 通过这种方式,G1 可以依托于长期观测实际的停顿时间来尽可能适配 -XX:MaxGCPauseTimeMillis 和 -XX:PauseTimeIntervalMillis 指定的停顿时间. 它会考虑相近大小的年轻代排泄会花费多少时间. 这会包括, 多少对象会在回收过程中 copy, 以及这些对象彼此间是怎么联系的.
如果未加其他约束,G1 会通过在 -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent 之间 (或者用 -XX:NewSize 和 -XX:MaxNewSize) 灵活自适应地调整年轻代大小的方式来尽可能达到用户设定的停顿时间标准.
空间回收阶段也伴随代大小调整. 在此阶段,G1 试图去最大化一次 gc 停顿中能回收的老年代空间量, 此时会将年轻代设置为最小允许的大小, 一般这个参数由 -XX:G1NewSizePercent 决定. 在每一个混合 gc 开始时,G1 会从候选 cs 中选出一组加入到 cs 中, 这些附加的老年代分区包含三个部分:
第一部分是在排泄阶段要保证的最小老年代分区集, 这组老年代分区是由候选 cs 的分区数除以空间回收阶段的长度决定.(空间回收阶段长度由参数 -XX:G1MixedGCCountTarget 设置)如果 G1 预测在回收最小候选 cs 后还会有时间剩余, 将会从候选 cs 添加其他老年代分区, 直到耗费剩余时间的 80%.
第三部分为一组可选的 cs 分区, 如果 G1 在本次停顿中递增地完成了另外两个部分的排泄后仍有时间剩余, 则添加这组可选的分区. 前两个分区集会在初始化收集过程中回收, 可选 cs 中的分区则会在剩余停顿时间回收, 通过这种方式实现了保证空间回收过程的同时提升了保证停顿时间和开销最小的可能性.
当候选 cs 中可回收的空间数量少于 -XX:G1HeapWastePercent 时, 空间回收阶段停止.
前面说过, 最新几版的 JAVA 对 G1 做出了不少改进, 其中一个改进就是周期 gc, 当因应用空闲而使得长久没有 gc 时, 虚拟机可能会持有大量的空闲内存, 而这些空闲内存不能用在其他地方. 为了避免这种浪费,G1 可以强制规律性的 gc, 这需要提供 -XX:G1PeriodicGCInterval 选项, 它将决定 G1 考虑执行一次 gc 的最小间隔时间, 以毫秒为单位. 如果从之前的 gc 停顿起过去了这些时间, 且没有任何过程中的并发周期, 则 G1 会触发额外的 gc, 这次触发有不同的效果.
在 young only 阶段,G1 会根据是否指定 -XX:-G1PeriodicGCInvokesConcurrent 来决定使用哪一种停顿开始一个并发标记停顿, 如果未指定, 则用 ” 并发标记停顿 ” 来开始, 否则使用 full gc. 在空间回收阶段,G1 会继续触发了适合当前过过程的停顿类型的空间回收阶段.
可使用选项 -XX:G1PeriodicGCSystemLoadThreshold 来细化是否触发一个 gc, 如果 JVM 宿主机调用 getloadavg()返回的值 (一分钟平均负载) 超过了该值, 则不会有周期 gc 运行. - 初始堆占用
初始堆占用比 (IHOP) 是一个阈值, 它表示老年代占用的比例达到该值时触发初始标记(前面说过,G1 中初始标记同时于并发开始, 并发开始包含普通 gc 和并发标记).g1 默认自动地观察标记周期中老年代对象在一定时间分配的对象情况来断定最优的 IHOP, 也就是自适应的 IHOP. 如果启动了这个功能, 并且当前没有足够的观测数据可决定一个良好的 IHOP 时, 使用 -XX:InitiatingHeapOccupancyPercent 设置的值作为 IHOP. 使用 -XX:-G1UseAdaptiveIHOP 可以关掉自适应 IHOP, 则 G1 将永远使用 -XX:InitiatingHeapOccupancyPercent 作为默认阈值.
当没有指定 IHOP 时, 使用自适应的 IHOP 将会尝试为它准备一个初值, 这个初始老年代占用阈值默认为当前最大老年代空间减去 -XX:G1HeapReservePercent(也被称为 extra buffer). - 标记
G1 的标记过程使用开始快照 (SATB) 算法. 它会在初始化标记停顿时提取当前虚拟机堆快照, 所有此时存活的对象和后续分配的对象都会被在标记的剩余过程中被当作存活(后者默认标记不需要追踪). 所以在空间回收阶段, 会对那些在标记期间死亡的对象进行冗余的工作(如果有异常发生), 但是 SATB 算法减少了 remark 阶段的停顿. 好在这些被保守地当作存活的死亡对象会在下一次标记过程中识别.
-
堆紧张时行为
当应用持续向内存中分配对象, 导致没有足够的空间 copy 时, 可能会导致排泄过程的失败. 排泄失败意味着 G1 将试着去就地完成当前的 gc(已经移动到新的位置或未来的及移动到新位置), 此时将不会再移动未移动的对象, 只会将对象间的一些引用关系进行调整. 排泄失败可能会带来一些额外开销, 但一般情况下应当和年轻代 gc 同一速度. 在这一次 gc 排泄失败之后,G1 将会假定应用如常, 相当于假定排泄失败发生在 gc 的末尾, 也就是大多对象已移动的情况, 有足够的空间保证应用继续运行, 能完成标记和开始空间回收阶段. 如果这个假定不能保持, 那么 G1 只能进行 full gc, 这将会进行就地压缩整理, 会是一个非常缓慢的过程.
-
大对象行为
大对象是指大于或等于半个分区大小的对象. 当前分区大小可以用 -XX:G1HeapRegionSize 选项来设置.
这些大对象有时会被特殊对待, 每个大对象会在老年代分区中连续分配. 对象的开始点总是在该分区组中的第一个分区的起始, 最后一个分区的剩余部分将不会在对象分配中使用, 直到整个对象回收为止.
一般情况下, 大对象只可以在标记过程的末尾的 cleanup 停顿期间进行回收, 或者在它不可达后的 full gc 中回收. 但对于一些特殊的大型对象, 如所有元素均为基本类型,G1 会在任何 gc 停顿过程中尝试碰运气回收它们. 这个特性默认开启, 可以使用选项 -XX:G1EagerReclaimHumongousObjects 关闭.大对象的分配可能会导致 gc 停顿过早地发生.G1 会在任何一个大对象分配时检查初始堆占用比(IHOP), 这可能会强制立即开始 young gc 的初始标记. 大对象即使在 full gc 中也从不移动, 也可能会导致 full gc 过程缓慢或者虽然存在大量空闲空间却因大量的内存碎片而出人意料的 oom.
- 对比其他 gc
与其他 gc 对比, 简单列举区别如下:
Parallel gc 也会整理和回收老年代空间, 但只能作为一个整体进行.G1 相当于递进地, 增量地用多个更短的 gc 过程完成了同样的工作. 这样减少了停顿时间, 也消费了一些吞吐量.
与 cms 相似,G1 并发完成老年代的空间回收, 然而 cms 不能解决老年代堆的碎片化问题, 最终只能导致长运行的 full gc.
G1 可能比上述垃圾收集器开销更大, 因为它的并发特性而影响到了吞吐量.
ZGC 的目标在于超大堆, 追求更短的停顿时间, 却更大的消费了吞吐量.
取决于 G1 的工作方式, 它有一些独有的机制来提升 gc 效率. 如 G1 可以在任何 collection 过程中回收完全空的, 大的老年代分区, 这可以避免很多其他不必要的 gc, 不太费力的释放大量空间.G1 也可以尝试并发地将堆内存中的重复字符串去重.
回收老年代中空的, 巨型对象默认开启, 可使用选项 -XX:-G1EagerReclaimHumongousObjects 开启, 字符串去重默认关闭, 可使用 -XX:+G1EnableStringDeduplication 开启.
书写心得
到此 G1 垃圾收集器简述就写完了, 主要参考资料为官方的几篇文章和文档, 未涉及复杂高深的内部实现, 最近几个版本的 java 都在不停地优化, 当然 gc 只是其中一小部分, 大到即时编译和 gc, 小到线程握手和引入 nests 等, 感觉 java 越来越重视 "内功" 了.
近几版除了 G1 进行了改良以外, 官方也推出了几种新的垃圾收集器:
Epsilon GC 是 jdk11 推出的无操作 (no-op) 垃圾收集器, 方便于我们单独观测一个应用的内存分配情况, 不受回收干扰.
zgc 是一个旨在超大堆下保持低延迟的回收器, 据官方 wiki, 在 jdk13 中已经将支持的最大堆从 4T 提升到了 16T, 回收时间不受堆内存大小的影响, 相应的着色指针技术, 读屏障技术简直让人叹为观止, 它在 jdk11 中开放体验版, 但不支持类卸载,jdk12 中补上了这一功能.
Shenandoah 也是一个超大堆低延迟的回收器,jdk12 中开放, 不同于 zgc 的是它使用了 "间接指针" 的技术实现对象拷贝过程的并发进行.
总之, 发自内心地佩服这些作者, 我辈学习之楷模.