从原理聊JVM(二):从串行收集器到分区收集开创者G1

发布时间:2023-04-25 11:07:31

       随着Java的进化,从串行到并行,从高吞吐到低延迟,出现了各种不同的垃圾回收器。最终目标是让开发人员专注于程序的代码写作,而不需要关注内存管理。

  JDK早期出现的垃圾回收器通常单独作用于不同的分代,直到G1后期出现,才能在整个区域收集。

从原理聊JVM(二):从串行收集器到分区收集开创者G1_垃圾回收

  请看前一篇关于垃圾回收器的基础知识:从原理上谈JVM(1):染色标志和垃圾回收算法2 串行收集器(Serial)

  对于较老的收集器,单线程,在收集时必须暂停工作线程,直到收集结束。但与其他收集器的单线程相比,它更简单、更有效。

  作用于新一代的收集器被称为Serial,采用标记复制算法;作用于老年人的收集器称为Serial Old,采用标记整理算法。3 并行收集器(Parallel)

  多个垃圾收集线程并行工作,在多核CPU下效率较高,但应用线程仍处于等待状态。

  并行收集器也分为ParNew和Parallel Old。可以理解,它们是Serial和Serial Old的多线程并行版本,甚至一些代码也被重复使用。

  Parnew之所以受欢迎,是因为除了Serial,它只能与CMS一起使用。然而,自JDK9以来,由于更先进的G1,官方直接取消了单独指定Parnew的参数-XX:+UseParNewGC,它被纳入CMS收集器,成为新一代特殊处理的一部分。

  而Parallel Old搭配新一代收集器ParalelScavenge成为名副其实的“吞吐量优先”搭配组合。4 ParallelScavenge

  ParallelScavenge收集器是新一代垃圾回收器,实际上与Parnew非常相似,采用标记复制算法并行收集。不同之处在于,ParaletScavenge的目标是实现可控的吞吐量(Throughput),吞吐量较高意味着利用处理器的资源最大限度地缩短垃圾的整体回收时间。ParalletScavenge有两个重要参数:

  •-XX:MaxGCPauseMillis

  收集器将尽最大努力确保内存回收的时间不超过用户的设定值。但这是以吞吐量为代价的,需要更短的时间来完成垃圾收集,所以系统需要减少新一代的大小,新一代小自然垃圾回收会更频繁,每次垃圾回收都有很多必要的工作(如等待所有线程到达安全点),所以更频繁的垃圾回收会导致整体吞吐量的减少。

  •-XX:GCTimeRatioGCTimeRatio

  这是垃圾收集时间占总时间的比例。换句话说,它意味着用户代码的运行时间是GC运行时间的X倍。例如,如果默认为99,垃圾收集时间的比例应为1/(1+99)。数量越低,用户代码的运行时间就越低。

  ParllelScavenge收集器也可以通过参数(-XX:+UseAdaptiveSizePolicy)激活自适应调节策略。激活后,无需手动指定新生代的大小(Xmn)、Eden与Survivor区的比例(XX:SurvivorRatio)、晋升年老对象的大小(XX:PretenureSizeThreshold)等待详细参数,虚拟机会根据当前系统的运行情况收集性能监控信息,为了提供最合适的停顿时间或最大吞吐量,动态调整这些参数。5 CMS收集器(Concurrent Mark Sweep)

  CMS收集器缩短暂停应用时间(Low Pause)CMS最初是为目标而设计的,只是老年收集器,后来将ParNew纳入其年轻收集器。

  与上述收集器相比,CMS是第一个允许部分阶段并发执行的收集器,不需要整个STW。

  事实上,垃圾回收主要分为两个阶段:识别垃圾和回收垃圾。CMS在这两个阶段努力减少停顿:

  •识别垃圾

  CMS分散了标记过程,同步了主要染色标记过程和用户线程,并通过增量更新解决了引用切换造成的漏标问题。

  •垃圾回收

  CMS采用清除算法。与复制和整理相比,清除算法不需要任何停顿,因为它只处理死亡对象。

  CMS操作步骤

  具体来说,CMS的整个过程分为四个步骤:

  1\. 初始标记(Initial Mark)\[STW\]

  初始标记只是标记GC Roots可以快速直接关联的对象。

  2\. 并发标记(Concurrent Marking)

  并发标记阶段是标记可回收对象。

  3\. 重新标记(Remark)\[STW\]

  重新标记阶段是修改并发标记期间用户程序继续运行导致标记变更的部分对象的标记记录。暂停时间略长于初始标记阶段,但远短于并发标记时间。

  CMS用增量更新做并发标记,也就是说,在并发标记过程中,如果一个已经标记为生存对象的引用被添加到非生存对象中,则标记为灰色,然后在重新标记阶段重新扫描这部分对象。

  4\. 并发清除(Concurrent Sweep)

  清理和删除标记阶段判断的死亡对象,因为不需要移动生存对象,所以这个阶段也可以与用户线程并发。优点

  由于收集器线程在整个过程中消耗最长的并发标记和并发清除过程中可以与用户线程一起工作,因此CMS收集器内存回收与用户并发执行,大大降低了暂停时间。缺点

  1\. 处理器资源敏感

  垃圾回收线程可以与用户线程同时执行,虽然不会导致STW,但由于处理器的计算资源共享,应用程序减慢,总吞吐量减少。

  2\. 内存敏感

  当垃圾回收和用户线程同步运行时产生的垃圾不会在标记阶段后清除,因为它已经通过了标记阶段。这部分垃圾只能在下一个GC时清除,这是浮动垃圾的问题。

  此外,由于垃圾回收和用户线程同步运行,GC不能等待填充,但需要保留部分内存,以确保用户线程在GC过程中仍然有可用的内存。为了降低GC频率,只能等待更多的垃圾来触发GC,然后用户线程中可用的内存就不多了。

  如果GC没有结束用户线程分配内存的失败,这种情况被称为“并发失败”,那么虚拟机会降级使用Serial Old来重新收集高吞吐的老一代,这样停顿时间就长了。

  触发GC的内存使用阈值应根据实际情况进行调整,参数为:-XX:CMSInitiatingOccupancyFraction。

  3\. CMS基于标记清除算法,因此内存碎片过多后,Fullll会频繁触发 GC,而且不可避免。CMS会在几次触发后合并整理内存碎片。内存整理过程涉及生存对象的移动,不能并发(在Shenandoah和ZGC出现之前)。6 G1收集器(Garbage First)

  与上述垃圾回收器相比,G1收集器具有里程碑式的创新,将堆内存划分为多个大小相等的独立区域(Region),并且可以建立“暂停时间模型”,使暂停时间可控,尽量控制-XX:MaxGCPauseMillis(默认200ms)作为暂停目标。根据Oracle官网的描述,G1是一个“软实时”的收集器,只是尽量保证垃圾收集在目标暂停时间内完成,但不能保证:

  It is important to note that G1 is not a real-time collector. It meets the set pause time target with high probability but not absolute certainty.

  可以预测的原因是,它可以避免收集整个堆,而是将整个堆分为几个小区域(Region),每个Region都是单次垃圾回收的最小单元。在系统运行过程中,G1跟踪每个Region中的垃圾堆积价值(获得的空间大小和回收所需的时间),并在后台维护优先列表,每次根据允许的收集时间优先回收最大的Region,以确保在有限的时间内获得更高的收集效率。这也是Garbage First名称的由来。G1分代模型

  G1也分为年轻一代和年老一代,但不是固定划分,而是根据运行情况动态划分每个Region。

  G1还有一个叫Humongous的特殊区域。G1将超过Region容量的一半,并存储在Humongous区域。如果对象超过Region大小,则存储在N个连续的Humongous中 在Region中。G1的大部分行为都是Humongous Region被视为老年人的一部分。

从原理聊JVM(二):从串行收集器到分区收集开创者G1_JVM原理_02

TAMS(Top at mark start)

  为了确保Region也可以在垃圾回收过程中使用,G1为每个Region设计了两个名为TAMS的指针,即Previous TAMS(PTAMS)、Next TAMS(NTAMS)。在并发标记阶段开始之前,TAMS指针指向Region内存的边界。在并发标记阶段,G1默认指针上的对象不是生存对象,当对象分配时,用户线程直接分配到指针上。这就保证了扫描行为和对象分配不会相互干扰。

从原理聊JVM(二):从串行收集器到分区收集开创者G1_G1_03

G1如何判断Region的“价值”?

  G1运行期间,收集每个Region的价值信息,如回收耗时、记忆集中的脏卡数量等,计算每个Region回收的成本性能。G1的停顿预测模型是在用户预期的时间内找到更高回收率的Region组合。Remembered Sets

  G1堆中的每个Region都有一份Rememberd Set,也叫RSet,它的功能是为每个Region记录哪些Region含有引用。

从原理聊JVM(二):从串行收集器到分区收集开创者G1_串行收集器_04

  RSet的更新需要线程同步处理。由于对象引用变化非常频繁,如果同步写卡表消耗量很大,通常会将更新信息存储在队列中,然后异步更新RSet。这个队列叫做Dirty Card Queue。G1垃圾回收工艺

从原理聊JVM(二):从串行收集器到分区收集开创者G1_G1_05

  当Eden中不能分配对象时,触发Young GC。

  当老一代占比达到45%时,等待下一次Young 并发标记GC时。

  并在标记结束后立即执行Mixed GC。

  当Mixed GC对内存的清理速度赶不上分配新对象的速度,触发Fulll GC,G1Full GC采用单线程(JDK11后改为多线程)执行标记整理算法,耗时巨大。YoungG1 GC触发时机

  当JVM无法在Eden中分配对象时。回收范围

  Eden区和Survivor区运行过程(STW在所有阶段)

  1\. 根扫描

  所有Eden区域的GC 外部引用Root和RSet记录作为扫描生存对象的入口。

  2\. 更新RSet

  通过Dirty Card Queue中的card更新RSet,以确保RSet能够准确反映老年人是否引用Region。

  3\. 处理RSet

  将Eden区域中RSet指向的对象标记为生存对象。

  4\. 对象复制

  判断生存对象的年龄,如果没有达到“阈值”,则复制到Surviver区域,否则复制到Old区域。如果Surviver空间不够,则直接将部分对象复制到Old区域。

  5\. 处理引用

  处理软引用、弱引用、虚引用等,最后清空所有Eden区域。此时,清理过的内存空间没有内存碎片。G1的Mixed GC触发时机

  老年人占用的空间超过整个堆的45%(可以通过参数)-XX:InitiatingHeapOccupancyPercent进行设置)

  事实上,它不会立即触发,等待下一个Young GC,初始标记步骤同步进行。回收范围

  通过价值计算动态选择并发标记的Region。运行过程

从原理聊JVM(二):从串行收集器到分区收集开创者G1_垃圾回收_06

  1\. 初始标记(Initial Marking)\[STW\]

  标记GC Roots直接关联对象,修改TAMS指针值。值得注意的是,这个阶段不是单独执行的,而是在Minor GC同步完成。所以这个阶段没有额外的停顿。

  2\. 并发标记(Concurrent Marking)

  与用户线程并发执行,遵循GC Root递归标记。标记完成后,重新扫描SATB记录中引用变化的对象。如果此时发现空的Region,则直接清空。

  3\. 重新标记(Remark)\[STW\]

  由于并发标记是并发执行的,并发标记结束后仍有少量引用变化的对象,因此在此阶段,STW可以处理这部分剩余的对象。并开始计算所有Region的活动。

  4\. 清理(Clean Up)\[STW\]

  根据用户预期的停顿时间制定回收计划,选择所有非生存对象的OLD区域和回收利润较高的Region加入回收集。清空记忆集。重置清空的Region(这一步不是STW的)。

  5\. 拷贝(Coping)\[STW\]

  将回收集中的生存对象复制到空的Region中,最后清空这些旧的Region。

  现阶段算法和Young GC完全一致,但默认分8次执行(参数可以由参数执行)-XX:G1MixedGCCountarget设置)。因此,每次清理的回收集包括Eden区、Survivor区和八分之一的Old区。低存活率(垃圾多)的Region清理速度快,G1优先回收。

  混合回收不需要8次。有一个阈值-XX :G1HeapWastePercent(默认值为10%),这意味着整个堆内存中10%的空间被浪费,这意味着如果发现可回收垃圾占堆内存的比例小于10%,则不再混合回收。优点

  与以往的垃圾回收器相比,G1最大的变化是将堆放分成几个小Region,以减少GC的范围,从而达到“低延迟”的目的。

  G1的垃圾回收过程采用标记复制算法,避免了空间碎片化的问题。缺点

  1.内存占用率高。因为G1分区比CMS多,每个Region都需要建立卡表。其中,新一代对象变化频繁,增加了卡表维护成本。

  2.G1不仅需要写前屏障来更新卡表,还需要写后屏障来跟踪并发时的指针变化,以实现快照搜索算法(SATB)。虽然增量更新算法可以减少并发标记和重新标记阶段的消耗,但用户程序运行时的计算负载很高。

  3.G1和CMS也有“并发回收”的能力,所以如果垃圾回收的速度跟不上用户创建新对象的速度,就会触发Full GC可以获得更多的内存。将预期停顿时间设置为一两百毫秒或两三百毫秒是合理的。最佳实践

  1.不要设置年轻一代的大小,年轻一代的大小应由G1控制,并设置为固定值,以覆盖暂停时间的目标

  2.暂停时间目标不要太严格G1为Young GC可以缩短时间,减少Eden区的数量,所以Young GC会更频繁。Mixed 如果GC想要达到停顿目标,就需要减少回收垃圾的数量。如果回收速度低于新对象的分配速度,就会导致Fulll GC。

  3.CMS和G1的选择在小内存应用中仍然优于G1,而G1在大内存应用中可以发挥其优势,Java堆容量平衡点通常在6GB到8GB之间。7 总结

  在GC的选择上,也是“没有银弹”。不同的收集器有自己的特点和适用场景,甚至Epsilon也会在特定场合发挥作用。我们应该根据不同的业务特点和系统条件选择最合适的垃圾回收器,而不是盲目创新。

上一篇 addErrors与saveErrors有什么区别
下一篇 五分钟理解Java算法的时间复杂度

文章素材均来源于网络,如有侵权,请联系管理员删除。

标签: Java教程Java基础Java编程技巧面试题Java面试题