全文共两部分,有基础的读者只需要阅读第一部分"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垃圾收集器进行了几点优化:
如果启用了这个功能,年轻代依旧使用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选择一段收集期内完成的大量工作信息(一定程度上依赖于应用行为本身)作为样本进行高度分析,分析后选定的一组分区被称为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堆内存的进一步缩小.
本节的一些数量指标主要依托于最新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垃圾收集器宏观上有两个阶段,它会在两个阶段之间往复切换.两个阶段分别是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在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对比,简单列举区别如下:
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的是它使用了"间接指针"的技术实现对象拷贝过程的并发进行. 总之,发自内心地佩服这些作者,我辈学习之楷模.