JVM垃圾回收笔记02-垃圾回收器
文章目录
- 前言
- 1.串行(Serial 收集器/Serial Old 收集器)
- Serial 收集器
- Serial Old 收集器
- 相关参数
- -XX:+UseSerialGC
- 2.吞吐量优先(Parallel Scavenge 收集器/Parallel Old 收集器)
- Parallel Scavenge 收集器
- Parallel Old 收集器
- 相关参数
- -XX:+UseParallelGC ~ -XX:+UseParallelOldGC
- -XX:+UseAdaptiveSizePolicy
- -XX:GCTimeRatio=ratio
- -XX:MaxGCPauseMillis=ms
- -XX:ParallelGCThreads=n
- 3.响应时间优先(ParNew 收集器/CMS 收集器)
- CMS 收集器
- 相关参数
- -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
- -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
- -XX:CMSInitiatingOccupancyFraction=percent
- -XX:+CMSScavengeBeforeRemark
- 4.G1(Garbage First收集器)
- 前言
- 介绍
- G1 VS CMS
- 1.Young Collection
- 2.Young Collection + CM
- 3.Mixed Collection
- 4.Full GC
- 5.Remark(重新标记阶段)
- ZGC 收集器
前言
如果说回收(收集)算法是内存回收的方法论,那么垃圾回收(收集)器就是内存回收的具体实现。
虽然有各种的收集器,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。
JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看):
- JDK 8: Parallel Scavenge(新生代)+ Parallel Old(老年代)
- JDK 9 ~ JDK22: G1
1.串行(Serial 收集器/Serial Old 收集器)
Serial 收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。
因为Serial(串行)收集器出现的 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
Serial 收集器优点:它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:
一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
相关参数
-XX:+UseSerialGC
-XX:+UseSerialGC 实际上是指定使用Serial(年轻代)和Serial Old(老年代)的组合。这种设置适合于数据量较小、对应用的停顿时间要求不高且资源受限(如CPU核心数量有限)的环境。
2.吞吐量优先(Parallel Scavenge 收集器/Parallel Old 收集器)
JDK1.8中默认使用收集器为Parallel Scavenge + Parallel Old收集器组合
使用 java -XX:+PrintCommandLineFlags -version 命令查看
-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能
Parallel Scavenge 收集器
Parallel Scavenge 收集器是使用标记-复制算法的多线程收集器,Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。
Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用标记-复制算法,老年代采用标记-整理算法。
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
相关参数
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
解释:UseParallelGC:工作于新生代,复制算法
UseParallelOldGC:工作于老年代,标记整理算法
注意:开启其中一个会自动开启另一个
-XX:+UseAdaptiveSizePolicy
- 功能:
- 启用自适应大小调整(新生代)策略。该策略会根据运行时的统计信息动态调整新生代(Young Generation)的大小(Eden 区和 Survivor 区)、晋升老年代的阈值等内存分配参数。JVM 会根据当前垃圾回收的性能表现自动调整这些参数,以达到更好的性能优化效果。
- 当启用此参数时,JVM 会根据应用程序的行为和垃圾回收统计数据,尝试找到最优的内存分配比例和晋升阈值,而无需手动指定诸如 -Xmn(新生代大小)或 -XX:SurvivorRatio 等参数。
-XX:GCTimeRatio=ratio
- 功能:
- 该参数用于设置吞吐量目标,它是一个控制垃圾回收时间占总运行时间比例的参数。它表示允许的最大垃圾回收时间与总运行时间的比例。
- 具体计算方式为 1 / (1 + ratio)。例如,如果你设置 ratio = 19,那么最大总垃圾回收时间占总运行时间的比例为 1 / (1 + 19) = 5%。
- JVM 会尽量调整垃圾回收器的工作,以确保垃圾回收时间不超过总运行时间的这个比例,从而保证应用程序的吞吐量,提高程序执行效率。
-XX:MaxGCPauseMillis=ms
- 功能:
- 设定垃圾回收的最大暂停时间目标,单位是毫秒。JVM 会尽力将垃圾回收的暂停时间控制在这个目标值以内。默认200ms
- 当设置此参数后,JVM 会调整堆内存大小、新生代和老年代的比例等,使每次垃圾回收的暂停时间尽可能不超过该值。
- 然而,需要注意的是,为了达到这个目标,可能会导致更频繁的垃圾回收操作,或者减少堆内存的使用量,因此需要根据实际情况进行权衡,避免因过度追求低暂停时间而影响整体性能。
注意:-XX:MaxGCPauseMillis=ms和-XX:GCTimeRatio=ratio会有冲突。
当调整 -XX:MaxGCPauseMillis 以追求更短的暂停时间时,可能会影响 -XX:GCTimeRatio 所期望的吞吐量目标。
因为为了达到更短的暂停时间,可能会频繁进行垃圾回收,这会增加垃圾回收的总时间,导致垃圾回收时间占总运行时间的比例增加,从而可能无法达到 -XX:GCTimeRatio 设定的吞吐量目标。
反之,当调整 -XX:GCTimeRatio 以提高吞吐量时(一般会将堆内存增大),可能会导致单次垃圾回收的暂停时间变长(堆内存变大导致单次时间变长),从而可能超过 -XX:MaxGCPauseMillis 设定的暂停时间限制,影响对响应时间敏感的应用程序的性能。
-XX:ParallelGCThreads=n
- 功能:
- 用于设置并行垃圾回收器的线程数量为 n。
- 在使用并行垃圾回收器(如 Parallel Scavenge 或 Parallel Old)时,该参数决定了在进行垃圾回收时同时运行的线程数量。
- 通常,n 的值可以根据 CPU 核心数来确定。如果不设置此参数,JVM 会根据实际的 CPU 核心数自动设置一个合适的值。在多核心的环境下,增加并行垃圾回收线程的数量可以提高垃圾回收的效率,但过多的线程也可能会带来额外的线程切换开销,所以要根据实际硬件和应用程序的特点来合理设置此参数。
3.响应时间优先(ParNew 收集器/CMS 收集器)
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
ParNew 收集器采用标记-复制算法
它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器配合工作。
ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。
自JDK 9开始还取消了ParNew加Serial Old以及Serial加CMS这两组收集器组合的支持,并直接取消了-XX:+UseParNewGC参数,这意味着ParNew和CMS从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。
读者也可以理解为从此以后,ParNew合并入CMS,成为它专门处理新生代的组成部分。ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
遗憾的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤(其中初始标记、重新标记这两个步骤仍然需要“Stop The World”):
- 初始标记: 短暂停顿,标记直接与 root 相连的对象(根对象);
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。(并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;)
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。(并发清除阶段清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。)
总结
初始标记时耗费时间特别少,只是标记根对象。之后进行并发标记,此时用户线程也在运行,所以可能会改变对象的引用,所以之后需要重新标记(也是stop-the-world),之后在并发清理。
在并发清理时用户线程产生的垃圾成为浮动垃圾,只能在下次垃圾回收时清理。
注意:该垃圾回收器对CPU的占用并不高,但是此时用户线程也会运行,又因为两者同时运行,所以在垃圾回收时,会对程序的吞吐量有影响。
CMS主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
CMS 垃圾回收器在 Java 9 中已经被标记为过时(deprecated),并在 Java 14 中被移除。
由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。
并行和并发概念补充:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
相关参数
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
UseConcMarkSweepGC:工作于老年代,基于标记清除的并发回收器,该垃圾回收器在运行时,其他用户线程也可以同时运行,但是在某些阶段还是需要stop-the-world。
UseParNewGC:工作于新生代,基于复制算法的垃圾回收器
SerialOld:UseConcMarkSweepGC回收器在有些情况下会出现并发失败的问题,此时会采取补救的措施,老年代会从并发回收器退化到单线程回收器。
并发失败的原因有:因为使用的基于标记清除的并发回收器,所以可能是因为内存碎片导致的,此时退化为单线程的垃圾回收器进行内存整理
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
ParallelGCThreads=n:并行线程数
ConcGCThreads=threads:并发GC线程数,一般设置为并行线程数的四分之一
-XX:CMSInitiatingOccupancyFraction=percent
在并发清理时用户线程产生的垃圾成为浮动垃圾,只能在下次垃圾回收时清理。如设置这个参数为占用老年代内存80%时会触发垃圾回收,预留一些空间存放浮动垃圾
-XX:+CMSScavengeBeforeRemark
在重新标记之前对新生代进行一次垃圾回收
4.G1(Garbage First收集器)
前言
在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。
而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。
虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。
每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
介绍
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
G1 收集器的运作大致分为以下几个步骤:
- 初始标记: 短暂停顿(Stop-The-World,STW),标记从 GC Roots 可直接引用的对象,即标记所有直接可达的活跃对象
- 并发标记:与应用并发运行,标记所有可达对象。 这一阶段可能持续较长时间,取决于堆的大小和对象的数量。
- 最终标记: 短暂停顿(STW),处理并发标记阶段结束后残留的少量未处理的引用变更。
- 筛选回收:根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。这一阶段包含一个或多个停顿(STW),具体取决于回收的复杂度。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。
G1 VS CMS
目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。当然,以上这些也仅是经验之谈,不同应用需要量体裁衣地实际测试才能得出最合适的结论,随着HotSpot的开发者对G1的不断优化,也会让对比结果继续向G1倾斜。
1.Young Collection
- 会 STW
G1会把堆内存划分为一个个大小相等的区域,每个区域都可以独立作为伊甸园、幸存区、老年代。
以上白色区域表示空闲区域,E则代表伊甸园区域,新创建的对象放在其中。
当伊甸园逐渐被占满,就会触发一次新生代的垃圾回收
垃圾回收会通过复制算法将幸存对象放入幸存区S
当幸存区中对象也比较多时,会触发新生代垃圾回收,将一些对象晋升(老年代)O区域中,不满足晋升条件的会将对象复制到其他的幸存区中。
2.Young Collection + CM
- 在 Young GC 时会进行 GC Root (根对象)的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
3.Mixed Collection
会对 E、S、O 进行全面垃圾回收
- 最终标记(Remark)会 STW
- 拷贝存活(Evacuation)会 STW
-XX:MaxGCPauseMillis=ms
- 伊甸园S中的幸存对象会被复制算法复制到幸存区中,幸存区S中不够年龄的对象也会被复制到其他幸存区中,符合晋升条件的对象会晋升到老年代O中。
- 老年代垃圾回收时也采用的复制算法,将幸存对象复制到新的O中,老年代因为需要满足最大暂停时间MaxGCPauseMillis,所以会有选择的进行回收。
- 因为有时候堆内存太大了,导致老年代的回收时间比较长。所以G1会从老年代中选择回收价值最高(垃圾最多的,可以释放更多的空间)的区域,复制的区域少了,时间自然就变短了。
- 如果回收全部老年代也满足最大暂停时间,那么会回收所有老年代区域。
4.Full GC
当G1老年代内存不足时,老年代占用堆空间比例达到阈值(默认45%)时,进行并发标记。后续进行混合收集的阶段。
G1Full GC的时机:当垃圾回收的速度跟不上垃圾产生的速度时(并发回收失败)。此时会退化为串行回收阶段
5.Remark(重新标记阶段)
- remark阶段就是为了防止出现被引用的被当成垃圾回收,没有被引用的没有被垃圾回收的情况。
- 当对象的引用发生改变时,JVM会加入一个写屏障。如B对象被A对象引用,则会在引用上加上写屏障。
- 当触发写屏障后,会将B对象加入一个队列中,并变为灰色表示没有处理完。等到并发标记结束后,进入重新标记阶段,重新标记阶段会SSW,暂停其他用户线程。将队列中的对象进行重新标记。队列的名称叫satb_mark_queue
以上图为并发标记阶段,对象的处理状态。 - 黑色表示处理完成,并且被引用。所以表示垃圾回收后会被保留下来的对象。
- 灰色表示处理当中的。
- 白色表示尚未处理的。
- 灰色、白色如果被引用,则最终会变成黑色。如果没有被引用则变为白色(并发标记后)被当成垃圾回收。
ZGC 收集器
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
ZGC 可以将暂停时间控制在几毫秒以内,且暂停时间不受堆内存大小的影响,出现 Stop The World 的情况会更少,但代价是牺牲了一些吞吐量。ZGC 最大支持 16TB 的堆内存。
ZGC 在 Java11 中引入,处于试验阶段。经过多个版本的迭代,不断的完善和修复问题,ZGC 在 Java15 已经可以正式使用了。
不过,默认的垃圾回收器依然是 G1。你可以通过下面的参数启用 ZGC:
java -XX:+UseZGC className
java -XX:+UseZGC className
在 Java21 中,引入了分代 ZGC,暂停时间可以缩短到 1 毫秒以内。
你可以通过下面的参数启用分代 ZGC:
java -XX:+UseZGC -XX:+ZGenerational className