JVM的各种垃圾收集器详解(CMS、G1)

我们可以把垃圾回收器分为3类

串行、吞吐量优先、响应时间优先

其中,串行是单线程,吞吐量优先和响应时间优先是多线程

本文重点讨论的是分代收集器,ZGC会另外做一个文章专门讲解

1 串行(单线程)

Serial收集器(复制算法)——新生代

Serial Old收集器(标记-整理算法)——老年代

  • 单线程
  • 对内存较小,适合个人电脑
  • 采用复制算法-新生代,标记整理算法-老年代

-XX:+UseSerialGC=Serial+SerialOld——开启串行GC

回收过程

在这里插入图片描述

安全点:

GC时,所有用户线程都会在一个地方停止下来,这个地方就是安全点,目的是防止用户线程引用的内存地址因垃圾回收而混乱

2 吞吐量优先(多线程)

Parallel Scavenge收集器(标记-复制算法)——新生代

Parallel Old收集器(标记-复制算法)——老年代

追求高吞吐量,高效利用CPU(垃圾回收时CPU占用会到100%),吞吐量一般为99%,吞吐量=用户线程时间/(用户线程时间+GC线程时间)

  • 多线程
  • 堆内存较大,多核cpu
  • 让单位时间内,STW的时间最短 0.2 0.2 = 0.4
  • 采用复制算法-新生代,标记整理算法-老年代
  • 垃圾回收时间对总程序的时间占比越低,则吞吐量越高

开启使用

-XX:+UseParallelGC — -XX:+UseParallelOldGC——开启吞吐量优先GC,开启其中一个,另一个自动开启(JDK1.8默认开启)

-XX:+UseAdaptiveSizePolicy——采用自适应的新生代大小调整

-XX:GCTimeRatio=ratio——调整吞吐量的目标,垃圾回收时间不能超过总时间的(1/(1+ratio)),ratio默认为99,一般设置为19,如果达不到设定的吞吐量,就会把堆内存设置得更大,这样垃圾回收的次数就不频繁,但单次GC暂停时间就会越长

-XX:MaxGCPauseMillis=ms——调整最大暂停时间,默认200ms,与GCTimeRatio冲突

-XX:ParallelGCThreads=n——设置线程数

回收过程

在这里插入图片描述

3 响应时间优先(多线程)

ParNew收集器(标记-复制算法)——新生代,Serial收集器的多线程版本

CMS(Concurrent Mark Sweep)收集器(标记-清除算法)——老年代

用户线程与GC线程并发进行,用户线程与GC线程会竞争抢占CPU

  • 多线程
  • 堆内存较大,多核cpu
  • 尽可能让单次STW的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
  • 在垃圾回收的过程中可能会产生新垃圾,被称为浮动垃圾
  • 采用标记清除算法-老年代,复制算法-新生代

开启使用

-XX:+UseConcMarkSweepGC — -XX:+UseParNewGC — SerialOld——可以与用户线程并发执行,用于老年代,在并发开启失败后(由于内存碎片过多),会退化为串行

-XX:ParallelGCThreads=n — -XX:ConcGCThreads=threads——ParallelGCThreads是并行的线程数,ConcGCThreads是并发的线程数,一般设置为ParallelGCThreads的1/4

-XX:CMSInitiatingOccupancyFraction=percent——为了应对浮动垃圾,预先设置触发GC的内存阈值,一般设置为80,早期JDK默认设置为65

-XX:+CMSScavengeBeforeRemark——在重新标记之前,要对新生代进行垃圾回收

回收过程

在这里插入图片描述

初始标记: 仅仅标记GC ROOTS的直接关联对象,即只标记根对象,世界暂停

并发标记: 使用GC ROOTS TRACING算法,进行跟踪标记,世界不暂停

重新标记: 因为之前并发标记,其他用户线程不暂停,可能产生了新垃圾,所以重新标记,世界暂停,且只追踪在并发标记过程中产生变动的对象

问题

  1. 会产生浮动垃圾
  2. 会产生过多的内存碎片,导致CMS退化为串行,响应时间增加

4 G1

概述

定义: Garbage First(Garbage One),一种新型垃圾收集器,相比于CMS收集器,G1收集器有两个最突出的改进

  • 基于 标记-整理 算法,不会产生内存碎片
  • 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收

发展历史

  • 2004 论文发布
  • 2009 JDK6 体验
  • 2012 JDK7 官方支持
  • 2017 JDK9 默认

适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,堆内存越大,G1对比CMS的优势就越大,会将堆划分为多个大小相等的 Region(每个区域1,2,4,8…,每个区域都可以作为Eden、Servivor、old…)
  • 整体上是 标记+整理 算法(解决了CMS的内存碎片问题),两个区域之间是 复制 算法

相关JVM参数

-XX:+useG1GC——JDK9以前

-XX:G1HeapRegionSize=size

-XX:MaxGCPauseMillis=time

G1 垃圾回收阶段

在这里插入图片描述

新生代垃圾收集、新生代垃圾收集+并发标记、混合收集三个阶段,循环发生

第一个阶段——Young Collection(新生代)

每个region可以作为任意区域,新生成的对象会任选一个region放入,该region就会变成Eden区(简称E区)

在这里插入图片描述

E区资源紧张,会触发GC,把依然存活的对象放入一个新的region,该region就会变成Survivor区(简称S区),该过程会STW

在这里插入图片描述

如果S区资源紧张,会触发GC,把可以放入老年代的对象放入一个新的region,该region就会变成Old区(简称O区),该过程会STW

在这里插入图片描述

第二阶段——Young Collection+CM(新生代垃圾回收+并发标记)

  • 在Young GC时会进行GC Root的初始标记

  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),该过程与CMS的并发标记类似,阈值由下面的JVM参数决定

    -XX:InitiatingHeapOccupancyPercent=percent——默认45

在这里插入图片描述

第三阶段——Mixed Collection(混合收集)

会对E、S、O区进行全面垃圾回收

  • 最终标记(Remark)会STW,主要标记之前并发标记时漏掉的对象,这个地方也与CMS的重新标记类似

  • 拷贝存活(Evacuation)会STW,对老年代来说,G1 会根据最大暂停时间,部分回收O区的垃圾,只回收垃圾最多的区域,这也是为什么叫Garbage First的原因

    -XX:MaxGCPauseMillis=ms——最大停顿时间

在这里插入图片描述

图中,橘色虚线边框的O区并没有发生GC,就是因为G1会采取Garbage First的策略

所以,G1之所以能做到既保证高吞吐量,又保证低响应时间,就是因为它并不会对老年代中所有的对象进行回收,而是选择性地进行回收,G1之所以能这样做,是基于其分区的策略,即把整个堆划分为了一个一个region。

每种垃圾回收器发生Full GC的时机
  • SerialGC

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC

    • 新生代内存不足发生的垃圾收集 - minor gc

    • 老年代内存不足发生的垃圾收集 - full gc

  • CMS

    • 新生代内存不足发生的垃圾收集 - minor gc

    • 老年代内存不足,并发失败 - full gc

  • G1

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足(老年代占整个堆内存的45%时),且垃圾回收速度跟不上用户产生垃圾速度 - full gc
Young Collection 跨代引用

新生代的根对象查找,有一部分来自老年代,老年代存活对象很多,如果挨个遍历,效率会很低,这个时候我们就会采用一种卡表技术(card table)

我们会把O区细分为很多个card(512k),如果老年代中有一个对象引用了新生代的对象,我们就把这个对象对应的card标记为脏卡,这样,新生代的根对象查找时,我们只用遍历脏卡,大大缩小了遍历区域

在这里插入图片描述

  • 卡表与 Remembered Set
  • 在引用变更时,是异步进行的,通过 post-write barrier+dirty card queue 技术(这个地方笔者精力有限,没有深入研究,感兴趣的读者可以自行查阅别的资料),将待更改的card放入一个card queue中进行更新
  • concurrent refinement threads 更新 Remembered Set

在这里插入图片描述

Remark

pre-write barrier——写屏障技术,在对象引用改变前,将对象让如队列satb_mark_queue

在这里插入图片描述

黑色的是已经处理完成的且有引用的对象,灰色的是在处理中的对象,白色的是还没有处理的对象

其它版本的优化

JDK 8u20 字符串去重

在新生代回收时检查是否有重复字符串,如果有,则让他们指向同一个对象

优点:节省大量内存

缺点:略微增加新生代回收时间

-XX:+UseStringDeduplication 默认开启

JDK 8u40 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

-XX:+ClassUnloadingWithConcurrentMark 默认开启

JDK 8u60 回收巨型对象

  • 一个对象大于 region 的一半时,称之为巨型对象
  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉

JDK 9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC

  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent

  • JDK 9 可以动态调整

    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值

    • 进行数据采样并动态调整

    • 总会添加一个安全的空档空间

JKD 9 更高效的回收

之后的优化这里就不一一列出了,感兴趣的读者可以去查阅Oracle官方文档:https://docs.oracle.com/en/java/javase/12/gctuning

以上便是JVM中垃圾收集器的全部内容

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM (Java Virtual Machine) G1 (Garbage-First) 垃圾收集是一种用于 Java 应用程序的垃圾收集算法。它是自JDK 7u4版本后引入的一种全新的垃圾收集。 G1垃圾收集的设计目标是为了解决传统的分代垃圾收集可能遇到的一些问题,如停顿时间长、内存碎片化等。它采用了一种基于区域的垃圾收集方式,可以将内存划分为多个大小相等的区域,每个区域可以是Eden、Survivor或Old区。 G1垃圾收集的工作原理如下: 1. 初始标记(Initial Mark):标记所有从根对象直接可达的对象。 2. 并发标记(Concurrent Mark):在并发执行程序的同时,标记那些在初始标记阶段无法访问到的对象。 3. 最终标记(Final Mark):为并发标记阶段中发生改变的对象进行最终标记。 4. 筛选回收(Live Data Counting and Evacuation):根据各个区域的回收价值来优先回收价值低的区域。 G1垃圾收集具有以下特点: - 并发执行:在执行垃圾收集过程时,尽可能减少应用程序的停顿时间。 - 分区回收:将整个堆划分为多个区域,可以根据需要优先回收垃圾较多的区域,从而避免全堆回收带来的长时间停顿。 - 内存整理:G1垃圾收集会对内存进行整理,减少内存碎片化,提高内存利用率。 需要注意的是,G1垃圾收集并不适用于所有情况。在特定的场景下,如大堆情况下的长时间运行、对延迟要求非常高的应用等,可能需要考虑其他垃圾收集的使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值