JVM垃圾收集器

1. 垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

虽然我们对各个收集器进行比较,但并非是为了挑选出一个最好的收集器。即使是现在的ZGC,在对吞吐量优先的场景,ZGC可能并不适合。我们需要做的就是根据具体的应用场景选择合适的垃圾收集器。

1.1 Serial收集器

Serial,音标[ˈsɪəriəl]

配置参数:-XX:+UseSerialGC -XX:+UseSerialOldGC
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。从名字上就能猜到这是个单线程收集器。它的单线程意义不仅仅意味着只会使用一条垃圾收集器线程去完成垃圾收集工作,更重要的是在进行垃圾收集工作的时候必须暂停其他所有的工作线程(Stop The World),直到它收集结束。

强烈怀疑荒木飞吕彦也是个程序员。

新生代采用复制算法老年代采用标记-整理算法

image.png

虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短。

但是Serial收集器有没有优于其他垃圾收集器的地方?

当然有,它简单且高效(与其他单线程垃圾收集器相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两个用途:一个用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案

1.2 ParNew收集器

音标:[pɑː(r)] [njuː]

配置参数:-XX:+UseParNewGC

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为**(控制参数、收集算法、回收策略等等)和Serial收集器完全一样**。默认的收集线程数跟CPU核数相同,当然也可以使用参数 -XX:ParallelGCThreads指定收集线程数。

image.png

新生代采用复制算法老年代采用标记-整理算法

它是许多运行在Server模式下的虚拟机的首要选择。除了Serial收集器之外,只有它能与CMS收集器配合工作。

1.3 Parallel Scavenge收集器

音标:[ˈpærəlel]

adj.平行的;极相似的;同时发生的;相应的;对应的;并行的
n.(尤指不同地点或时间的)极其相似的人(或情况、事件等);相似特征;相似特点;(地球或地图的)纬线,纬圈
v.与…相似;与…同时发生;与…媲美;比得上
adv.平行地,并列地

配置参数:-XX:+UseParallelGC(年轻代)-XX:+UseParallelOldGC(老年代)

Parallel Scavenge收集器类似于ParNew收集器,是Server模式下的默认收集器。

特点:

  • Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
  • 提供了很多的参数供用户找到最合适的停顿时间或最大吞吐量,如果对收集器不太了解的话,可以选择把内存管理优化交给虚拟机去完成。
  • 新生代采用复制算法,老年代采用标记-整理算法

image.png

1.4 CMS收集器

配置参数:

  • -XX:UseConcMarkSweepGC 启用CMS
  • -XX:ConcGCThreads 并发GC线程数
  • -XX:+UseCMSCompactAtFullCollection FullGC之后再做压缩整理(减少碎片)
  • -XX:CMSFullGCsBeforeCompaction多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
  • -XX:CMSInitiatingOccupancyFraction 当老年代使用达到该比例时会触发FullGC(默认是92%)
  • -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
  • -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在remark阶段

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合注重用户体验的应用上使用,他是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集器与用户线程(基本上)同时工作。

从名字的Mark Sweep可以看出,这是一种 标记-清除 算法实现的,它的运作过程相比于前几种垃圾收集器来说更加复杂。

步骤:

  • 初始标记暂停所有线程,并记录GC Roots直接引用的对象,速度很快
  • 并发标记同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记重新标记阶段就是为了修正并发标记起家因为用户程序继续运行而导致标记产生变动的那一部分的标记记录,这个阶段的停顿时间一般会比初始标记节点的时间稍长,远远比并发标记阶段短
  • 并发清理开启用户线程,同时GC线程开始对未标记的区域做清扫

image.png

CMS的优点:并发收集低停顿

缺点:

  • 对CPU资源敏感(会和服务抢资源)
  • 无法处理浮动垃圾(在并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次GC再清理)
  • 回收算法:标记-清理,会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让JVM在执行完标记清除之后再做整理
  • 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段,一边回收,系统一边运行,也许没回收完就再次触发Full GC,也就是 concurrent mode failure ,此时会进入STW,用Serial Old收集器来回收

1.5 G1收集器

配置参数:

参数参数含义
-XX:+UseG1GC使用G1收集器
-XX:ParallelGCThreads指定GC工作的线程数量
-XX:G1HeapRegionSize指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis目标暂停时间(默认200ms)
-XX:G1NewSizePercent新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent新生代内存最大空间
-XX:TargetSurvivorRatioSurvivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了
-XX:G1HeapWastePercent(默认5%) gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。

G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对于配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备吞吐量性能的特征

image.png

G1将Java划分为多个相等的独立区域(Region),JVM最多可以用2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,可以通过参数 -XX:G1HeapRegionSize 手动指定Region大小。

G1收集器保留了年轻代和老年代的概念,但不再是物理隔阂了,他们都是(可以不连续)Region的集合

默认年轻代对堆内存的占比时5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过 -XX:G1NewSizePercent新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是更多新生代的占比不会超过60%,可以通过 -XX:G1MaxNewSizePercent 调整。年轻代中的Eden和Survivor对应的Region也跟之前的一样,默认8:1:1,假设年轻代现在有1000个Region,Eden区对应800个,s0对应100个,s1对应100个

一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。

G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的RegionHumongous [hjuːˈmʌŋɡəs]区,而不是让大对象直接进入老年代的Region中。G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。

Humongous区专门存放短期巨型对象不用直接进入老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。

Full GC的时候除了收集年轻代和老年代之外,也会讲Humongous区一并回收

G1收集器第一次GC运作过程大致分为以下几个步骤:

  • 初始标记(initial mark,STW):会暂停所有的其他线程,并记录下GC Roots直接能引用的对象,速度很快
  • 并发标记(Concurrent Marking)同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。(同CMS的并发标记)
  • 最终标记(Remark,STW)重新标记阶段就是为了修正并发标记起家因为用户程序继续运行而导致标记产生变动的那一部分的标记记录,这个阶段的停顿时间一般会比初始标记节点的时间稍长,远远比并发标记阶段短。(同CMS重新标记。)
  • 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间,可以通过参数 -XX:MaxGCPauseMilis指定来制定回收计划。比如老年代此时有1000个Region都满了,但是根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800Region,尽量把GC导致的停顿时间控制在我们指定的范围之内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分的Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代还是老年代,回收算法主要用的是复制算法,将一个Region中的存活对象赋值到另一个Region中,这种不会像CMS那么回收完因为有很多内存碎片还需要再整理一次,G1采用复制算法回收几乎不会有太多的内存碎片

image.png

被JDK1.7以上版本Java虚拟机的一种重要进化特征。具备以下特点

  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者
    CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执
    行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留
    了分代的概念
  • 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法
    实现的收集器;从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同
    的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指
    定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收
    集。

1.6 ZGC

以下内容转载于知乎新一代垃圾回收器ZGC的探索与实践,这里只做部分内容截取。

ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器,它的设计目标包括:

  • 停顿时间不超过10ms
  • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加
  • 支持8MB~4TB级别的堆(未来支持16TB)。

与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。

ZGC垃圾回收周期如下图所示:

ZGC只有三个STW阶段:初始标记再标记初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

ZGC关键技术

ZGC通过着色指针读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址

那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针

1.6.1 ZGC重要配置参数

参数参数含义
-Xms -Xmx堆的最大内存和最小内存,这里都设置为10G,程序的堆内存将保持10G不变。
-XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize设置CodeCache的大小, JIT编译的代码都放在CodeCache中,一般服务64m或128m就已经足够。我们的服务因为有一定特殊性,所以设置的较大,后面会详细介绍。
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC启用ZGC的配置。
-XX:ConcGCThreads并发回收垃圾的线程。默认是总核数的12.5%,8核CPU默认是1。调大后GC变快,但会占用程序运行时的CPU资源,吞吐会受到影响。
-XX:ParallelGCThreadsSTW阶段使用线程数,默认是总核数的60%。
-XX:ZCollectionIntervalZGC发生的最小时间间隔,单位秒。
-XX:ZAllocationSpikeToleranceZGC触发自适应算法的修正系数,默认2,数值越大,越早的触发ZGC。
-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive是否启用主动回收,默认开启,这里的配置表示关闭。
-Xlog设置GC日志中的内容、格式、位置以及每个日志的大小。

1.6.2 ZGC触发时机

相比于CMS和G1的GC触发机制,ZGC的GC触发机制有很大不同。ZGC的核心特点是并发,GC过程中一直有新的对象产生。如何保证在GC完成之前,新产生的对象不会将堆占满,是ZGC参数调优的第一大目标。因为在ZGC中,当垃圾来不及回收将堆占满时,会导致正在运行的线程停顿,持续时间可能长达秒级之久。

ZGC有多种GC触发机制,总结如下:

  • 阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。
  • 基于分配速率的自适应算法:最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。自适应算法的详细理论可参考彭成寒《新一代垃圾回收器ZGC设计与实现》一书中的内容。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。我们通过调整此参数解决了一些问题。日志中关键字是“Allocation Rate”。
  • 基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是“Timer”。
  • 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。日志中关键字是“Proactive”。
  • 预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。
  • 外部触发:代码中显式调用System.gc()触发。日志中关键字是“System.gc()”。
  • 元数据分配触发:元数据区不足时导致,一般不需要关注。日志中关键字是“Metadata GC Threshold”。
# JVM  ZGC  GC 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×