聊聊GC

128 阅读15分钟

本文基于javaguide.cn/java/jvm/jv…做部分补充。

image.png

HotSpot 为什么要分为新生代和老年代?

HotSpot虚拟机将Java堆内存分为新生代(Young Generation)和老年代(Old Generation)的主要原因是为了提高垃圾回收(Garbage Collection,GC)的效率。这种内存划分策略基于一个观察到的现象:大多数Java对象的生命周期都很短暂,即“弱代假说”(Weak Generational Hypothesis)。

新生代和老年代的划分,使得垃圾回收器可以针对不同区域采用不同的回收算法,从而提高回收效率。

  1. 新生代:新创建的对象会被分配到新生代。新生代又分为Eden区、两个Survivor区(S0和S1)。新创建的对象首先被分配到Eden区,当Eden区满时,触发Minor GC(也称为Young GC),将存活的对象复制到Survivor区。经过多次Minor GC后,仍然存活的对象会被晋升到老年代。新生代采用复制算法进行垃圾回收,由于大部分对象的生命周期短暂,回收效率较高。
  2. 老年代:经过多次Minor GC仍然存活的对象会被晋升到老年代。老年代的对象生命周期较长,回收频率较低。老年代采用标记-清除-整理(Mark-Sweep-Compact)算法或标记-清除(Mark-Sweep)算法进行垃圾回收。当老年代空间不足时,触发Major GC(也称为Full GC),对整个堆进行回收。

通过将内存分为新生代和老年代,HotSpot虚拟机可以针对不同区域采用不同的回收算法,从而提高垃圾回收的效率。同时,这种划分策略还有助于减少内存碎片,提高内存利用率。

jvm中新生代为什么有三个区域?

JVM中的新生代是用于存放新创建的对象的内存区域,它被划分为Eden区、Survivor0区和Survivor1区三个部分。其中,Eden区是对象最初被创建的地方,Survivor0和Survivor1是两个相同大小的区域,用于存放从Eden区中经过一次垃圾回收后仍然存活的对象。这种设计是为了实现垃圾回收的复制算法,即将存活的对象复制到Survivor区域,然后清空Eden区和上一次使用的Survivor区域,这样就可以保证新生代的内存空间始终是可用的。同时,通过两个Survivor区域的轮换使用,可以避免在进行垃圾回收时出现内存碎片的问题。

一般来说,Eden区会被设置得比Survivor区大,这是因为大部分对象都是“朝生夕死”的,即很多对象在创建后很快就会变得不可达,所以将更大的空间分配给Eden区可以提高内存的使用效率。常见的Eden区与Survivor区的大小比例是8:1。

模拟两次young gc时Eden区和Survivor区的使用情况:

假设我们有一个Eden区和两个Survivor区(S0和S1),并且假设所有区域在开始时都是空的。我们也假设Eden区和每个Survivor区的大小都是一样的,为了简化,我们假设每次GC后,只有一半的对象存活。

  1. 在开始时,所有新创建的对象都被分配到Eden区。
  2. 当Eden区满时,进行第一次Young GC。GC后,Eden区中存活的对象被复制到S0区,Eden区和S1区此时都是空的。
  3. 继续在Eden区创建对象,直到Eden区再次满。
  4. 进行第二次Young GC。这次GC后,Eden区和S0区中存活的对象被复制到S1区(因为S0区中的对象年龄增加,需要移动到另一个Survivor区)。此时,Eden区和S0区都是空的,存活的对象都在S1区。

这就是两次连续的Young GC后,Eden区和Survivor区的使用情况。请注意,实际的情况可能会因为对象的存活时间、对象的大小、JVM的具体实现等因素而有所不同。

垃圾收集器及其对应的收集算法

新生代收集器:

  1. Serial收集器:单线程收集器,适用于新生代。采用复制算法进行垃圾回收。在进行垃圾回收时,会暂停所有应用线程(Stop-The-World)。启用Serial收集器的JVM参数:-XX:+UseSerialGC
  2. ParNew收集器:多线程版本的Serial收集器,适用于新生代。采用复制算法进行垃圾回收。在进行垃圾回收时,会暂停所有应用线程(Stop-The-World),但使用多个线程并行执行垃圾回收任务。启用ParNew收集器的JVM参数:-XX:+UseParNewGC
  3. Parallel Scavenge收集器:多线程收集器,适用于新生代。采用复制算法进行垃圾回收。在进行垃圾回收时,会暂停所有应用线程(Stop-The-World),并使用多个线程并行执行垃圾回收任务。启用Parallel Scavenge收集器的JVM参数:-XX:+UseParallelGC

老年代收集器:

  1. Serial Old收集器:单线程收集器,适用于老年代。采用标记-整理(Mark-Compact)算法进行垃圾回收。在进行垃圾回收时,会暂停所有应用线程(Stop-The-World)。启用Serial Old收集器的JVM参数:-XX:+UseSerialGC(与Serial收集器一起使用)。
  2. Parallel Old收集器:多线程收集器,适用于老年代。采用标记-整理(Mark-Compact)算法进行垃圾回收。在进行垃圾回收时,会暂停所有应用线程(Stop-The-World),并使用多个线程并行执行垃圾回收任务。启用Parallel Old收集器的JVM参数:-XX:+UseParallelOldGC
  3. CMS(Concurrent Mark Sweep)收集器:以获取最短回收停顿时间为目标的收集器,适用于老年代。采用标记-清除(Mark-Sweep)算法进行垃圾回收。它的垃圾回收过程主要与应用线程并发执行,减少了应用线程的暂停时间。启用CMS收集器的JVM参数:-XX:+UseConcMarkSweepGC

同时适用于新生代和老年代的收集器:

  1. G1(Garbage-First)收集器:面向服务端应用的收集器,适用于新生代和老年代。采用标记-整理(Mark-Compact)算法进行垃圾回收。它将堆划分为多个区域(Region),并根据区域内垃圾回收价值优先进行回收。G1收集器旨在实现高吞吐量和可预测的停顿时间。启用G1收集器的JVM参数:-XX:+UseG1GC

在选择垃圾收集器时,需要根据应用的特点和性能需求进行权衡。例如,如果需要降低延迟,可以选择CMS或G1收集器;如果需要提高吞吐量,可以选择Parallel Scavenge和Parallel Old收集器。

ParNew和Parallel Scavenge的区别是什么?

ParNew和Parallel Scavenge都是新生代的垃圾收集器,它们都采用复制算法进行垃圾回收,并使用多线程并行执行垃圾回收任务。但它们之间还是存在一些区别:

  1. 目标:ParNew收集器的主要目标是与CMS收集器配合使用,降低垃圾回收停顿时间。而Parallel Scavenge收集器的主要目标是在多核CPU环境下提高吞吐量。
  2. 自适应调整策略:Parallel Scavenge收集器支持自适应调整策略(-XX:+UseAdaptiveSizePolicy),可以根据应用的运行情况自动调整新生代大小、Eden区与Survivor区的比例以及晋升老年代的对象年龄等参数,以达到最佳的吞吐量。而ParNew收集器不支持自适应调整策略。
  3. 与老年代收集器的搭配:ParNew收集器通常与CMS收集器搭配使用,以降低垃圾回收停顿时间。而Parallel Scavenge收集器通常与Parallel Old收集器搭配使用,以提高吞吐量。
  4. 启用参数:启用ParNew收集器的JVM参数是-XX:+UseParNewGC,启用Parallel Scavenge收集器的JVM参数是-XX:+UseParallelGC

总之,ParNew和Parallel Scavenge都是新生代的多线程垃圾收集器,但它们的目标和特性有所不同。ParNew主要用于降低垃圾回收停顿时间,通常与CMS收集器搭配使用;而Parallel Scavenge主要用于提高吞吐量,支持自适应调整策略,并通常与Parallel Old收集器搭配使用。在选择垃圾收集器时,需要根据应用的特点和性能需求进行权衡。

为什么Parallel Scavenge通过调整参数可以提高吞吐量呢?

Parallel Scavenge收集器通过调整参数来提高吞吐量的原因在于它支持自适应调整策略(Adaptive Size Policy)。自适应调整策略可以根据应用的运行情况自动调整新生代大小、Eden区与Survivor区的比例以及晋升老年代的对象年龄等参数,以达到最佳的吞吐量。

吞吐量是指在单位时间内完成的工作量,对于垃圾收集器来说,吞吐量主要体现在以下几个方面:

  1. 减少垃圾收集次数:通过调整新生代大小和晋升老年代的对象年龄阈值,可以减少垃圾收集的次数。较大的新生代可以容纳更多的对象,从而减少Minor GC的次数;较高的对象年龄阈值可以减少对象在新生代和老年代之间的来回拷贝,降低Full GC的次数。
  2. 提高垃圾收集效率:通过调整Eden区与Survivor区的比例,可以提高垃圾收集的效率。合适的比例可以使得大部分对象在新生代被回收,减少晋升到老年代的对象数量,从而降低Full GC的开销。
  3. 平衡新生代与老年代的回收开销:通过调整新生代与老年代的大小,可以在新生代与老年代之间找到一个合适的平衡点,使得新生代的回收开销与老年代的回收开销达到最佳比例,从而提高整体的吞吐量。

Parallel Scavenge收集器在运行过程中会持续监控应用的运行情况,根据实际情况动态调整这些参数,以达到最佳的吞吐量。这种自适应调整策略使得Parallel Scavenge收集器在多核CPU环境下能够更好地利用系统资源,提高应用的运行效率。

减少垃圾收集次数等目标的主要原因是减少STW(Stop-The-World)的时间,从而提高吞吐量。 在垃圾收集过程中,STW会导致应用线程暂停,进而影响应用的响应速度和吞吐量。因此,减少垃圾收集次数和缩短每次垃圾收集的时间是提高吞吐量的关键。

CMS的垃圾回收过程

CMS(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的垃圾回收器。它主要用于回收老年代(Old Generation)的垃圾。CMS的垃圾回收过程分为以下四个阶段:

  1. 初始标记(Initial Mark) :这个阶段是“Stop The World”(STW)阶段,即在此阶段,所有的应用线程都会被暂停。垃圾回收器会标记老年代中所有的GC Roots直接可达的对象(其实就是找GC Roots)。这个阶段的目标是找到所有的根对象。
  2. 并发标记(Concurrent Mark) :在这个阶段,垃圾回收器会在后台线程中遍历老年代中所有从GC Roots可达的对象,并进行标记。这个阶段是与应用线程并发执行的,不会导致应用线程暂停。
  3. 重新标记(Remark) :这个阶段也是“Stop The World”(STW)阶段。由于在并发标记阶段,应用线程仍在运行,可能会导致一些对象的引用关系发生变化。因此,在重新标记阶段,垃圾回收器需要重新检查并修正这些发生变化的对象的标记状态。为了提高效率,这个阶段通常采用增量更新(Incremental Update)算法。
  4. 并发清除(Concurrent Sweep) :在这个阶段,垃圾回收器会在后台线程中清除所有未被标记的对象,回收它们所占用的内存空间。这个阶段也是与应用线程并发执行的,不会导致应用线程暂停。

CMS垃圾回收器的优点是在大部分时间内与应用线程并发执行,减少了应用线程的暂停时间。但它的缺点是会产生内存碎片,并且在并发清除阶段可能会导致应用线程的吞吐量下降。

详细说一说G1

G1(Garbage-First)收集器是一款面向服务端应用的垃圾收集器,适用于新生代和老年代。它采用标记-整理(Mark-Compact)算法进行垃圾回收。G1收集器旨在实现高吞吐量和可预测的停顿时间。启用G1收集器的JVM参数:-XX:+UseG1GC

G1收集器的主要特点如下:

  1. 划分区域(Region) :G1收集器将Java堆划分为多个大小相等的区域(Region),每个区域可以是Eden、Survivor或Old区。这种划分方式使得G1能够更灵活地管理堆内存,减少内存碎片。
  2. 并发与并行:G1收集器在垃圾回收过程中,部分阶段与应用线程并发执行,减少了应用线程的暂停时间。同时,G1收集器也利用多核CPU并行执行垃圾回收任务,提高垃圾回收效率。
  3. Garbage-First策略:G1收集器根据区域内垃圾回收价值优先进行回收。它首先收集垃圾最多的区域,从而尽可能高效地回收内存。这种策略有助于实现可预测的停顿时间。
  4. 可预测的停顿时间:G1收集器支持设置停顿时间目标(-XX:MaxGCPauseMillis),它会根据目标值调整垃圾回收策略,尽量保证每次垃圾回收的暂停时间不超过设定的目标值。
  5. 自适应调整:G1收集器在运行过程中会持续监控应用的运行情况,根据实际情况动态调整新生代大小、Eden区与Survivor区的比例以及晋升老年代的对象年龄等参数,以达到最佳的吞吐量和停顿时间。

总之,G1收集器是一款面向服务端应用的垃圾收集器,它通过划分区域、并发与并行、Garbage-First策略、可预测的停顿时间和自适应调整等特点,实现了高吞吐量和可预测的停顿时间。在选择垃圾收集器时,如果需要降低延迟并保持较高的吞吐量,可以考虑使用G1收集器。

G1中多个区域在Eden、Survivor或Old区是怎么分配的,比例是什么?

在G1收集器中,Java堆被划分为多个大小相等的区域(Region),这些区域可以是Eden、Survivor或Old区。G1收集器会根据应用的运行情况和内存需求动态调整这些区域的分配比例。

G1收集器的Eden、Survivor和Old区分配比例没有固定值,它们会根据实际情况自适应调整。以下是一些影响区域分配比例的因素:

  1. 堆内存大小:Java堆的大小会影响区域的数量。堆内存越大,可用于分配的区域数量越多。
  2. 对象分配速率:应用程序中对象分配的速率会影响Eden区的大小。对象分配速率越高,Eden区可能会相应地变大,以容纳更多的对象。
  3. 对象存活时间:对象在新生代中存活的时间会影响Survivor区的大小。对象存活时间越长,Survivor区可能会相应地变大,以容纳更多的存活对象。
  4. 晋升阈值:晋升到老年代的对象年龄阈值会影响新生代和老年代的大小。晋升阈值越高,新生代可能会相应地变大,以容纳更多的存活对象。
  5. 停顿时间目标:G1收集器支持设置停顿时间目标(-XX:MaxGCPauseMillis),它会根据目标值调整垃圾回收策略,从而影响区域分配比例。

G1收集器在运行过程中会持续监控应用的运行情况,根据实际情况动态调整这些区域的分配比例,以达到最佳的吞吐量和停顿时间。因此,G1收集器的Eden、Survivor和Old区分配比例是不固定的,会根据应用的实际需求进行自适应调整。

jdk1.8默认的垃圾收集器是什么?

在JDK 1.8中,默认的垃圾收集器组合是:

  • 新生代(Young Generation):使用Parallel Scavenge收集器
  • 老年代(Old Generation):使用Parallel Old收集器

这种组合也被称为Parallel GC或Throughput GC,它的目标是在多核CPU环境下提高吞吐量。Parallel Scavenge和Parallel Old收集器在执行垃圾回收时会暂停应用线程(Stop-The-World),并使用多个线程并行执行垃圾回收任务,以提高回收效率。

需要注意的是,虽然JDK 1.8默认使用Parallel GC,但你可以根据实际需求选择其他垃圾收集器组合,例如:

  • 使用-XX:+UseConcMarkSweepGC选项启用CMS(Concurrent Mark Sweep)收集器作为老年代收集器。
  • 使用-XX:+UseG1GC选项启用G1(Garbage-First)收集器,它适用于新生代和老年代的垃圾回收。

在选择垃圾收集器时,需要根据应用的特点和性能需求进行权衡。例如,如果需要降低垃圾回收停顿时间,可以考虑使用CMS或G1收集器。