JVM-垃圾回收

目录

1、GC过程

2、垃圾回收算法

2.1、标记-清除

2.2、标记-整理

2.3、复制

2.4、分代收集算法

3、TLAB

4、对象如何进入老年代

5、卡片标记

6、HotSpot垃圾回收器

6.1、年轻代垃圾回收器

6.2、老年代垃圾回收器

6.3、如何配置垃圾回收器

6.4、STW

7、CMS垃圾回收器

7.1、定义

7.2、设计目标

7.3、CMS回收过程

7.4、内存碎片

7.5、CMS的停顿(STW)

7.6、CMS缺点

8、G1垃圾回收器

8.1、为什么需要G1?

8.2、G1的设计目标

8.3、CMS和G1的区别?

[8.4、G1 有年轻代和老年代的区分吗?](#8.4、G1 有年轻代和老年代的区分吗?)

8.5、G1的优点

8.6、G1的数据结构

8.8、G1的垃圾回收过程

8.8.1、年轻代回收(STW)

8.8.2、并发标记

[8.8.3、混合回收(Mixed GC)](#8.8.3、混合回收(Mixed GC))

9、ZGC


1、GC过程

先找到活跃的对象,然后把其他不活跃的对象判定为垃圾,然后删除。所以垃圾回收只与活跃的对象有关,和堆的大小无关。

2、垃圾回收算法

2.1、标记-清除

标记存活的对象,然后将未标记的对象统一清除。缺点是会产生内存碎片。

2.2、标记-整理

标记存活的对象,然后把整个堆中未标记的对象压缩到堆的其中一块,从而避免了内存的碎片化问题。

2.3、复制

将内存一分为二,每次只使用其中一半,满了之后将存活的对象复制到另一半内存中,然后将剩下的碎片一次性擦除。缺点是需要两倍的内存空间。

2.4、分代收集算法

  • 新生代:复制。(新生代存活对象少,复制代价小)
  • 老年代:标记清除,标记整理

年轻代 = 1*Eden + 2*survivor

当年轻代中的 Eden 区分配满的时候,就会触发年轻代的 GC(Minor GC)。具体过程如下:

1、在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区(以下简称from);

2、Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理。存活的对象会被复制到 to 区;接下来,只需要清空 from 区就可以了。

所以在这个过程中,总会有一个 Survivor 分区是空置的。Eden、from、to 的默认比例是 8:1:1,所以只会造成 10% 的空间浪费。( -XX:SurvivorRatio 进行配置的(默认为 8))

3、TLAB

Thread Local Allocation Buffer,JVM 默认给每个线程开辟一个 buffer 区域,用来加速对象分配。这个 buffer 就放在 Eden 区中。

对象的分配优先在 TLAB上 分配,但 TLAB 通常都很小,所以对象相对比较大的时候,会在 Eden 区的共享区域进行分配。

4、对象如何进入老年代

  • 提升

对象在新生代的GC存活次数阈值:‐XX:+MaxTenuringThreshold(最大为15)。即一个对象,在15次GC后,依然存活,那么将该对象提升到老年代。

  • 分配担保

当 Survivor 空间不够,就需要依赖其他内存(指老年代)进行分配担保。这个时候,对象也会直接在老年代上分配。

  • 大对象直接分配到老年代
  • 动态对象年龄判断

比如,如果幸存区中相同年龄对象大小的和,大于幸存区的一半,大于或等于 age 的对象将会直接进入老年代。

5、卡片标记

老年代是被分成众多的卡页(card page)的(一般数量是 2 的次幂)。

卡表(Card Table)就是用于标记卡页状态的一个集合,每个卡表项对应一个卡页。

如果年轻代有对象分配,而且老年代有对象指向这个新对象, 那么这个老年代对象所对应内存的卡页,就会标识为 dirty,卡表只需要非常小的存储空间就可以保留这些状态。

垃圾回收时,就可以先读这个卡表,进行快速判断。

6、HotSpot垃圾回收器

6.1、年轻代垃圾回收器

  • Serial:单线程,垃圾回收时暂停用户线程,复制算法。
  • ParNew:多线程,垃圾回收时暂停用户线程。追求降低用户停顿时间,适合交互式应用。强交互弱计算。
  • Parallel Scavenge:多线程垃圾回收器,追求 CPU 吞吐量,能够在较短时间内完成指定任务,适合没有交互的后台计算。弱交互强计算。

6.2、老年代垃圾回收器

  • Serial Old:单线程,垃圾回收时暂停用户线程,标记-整理算法。
  • Parallel Old:Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。
  • CMS:垃圾回收和用户线程并发执行。以获取最短 GC 停顿时间为目标的收集器,它在垃圾收集时使得用户线程和 GC 线程能够并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

6.3、如何配置垃圾回收器

  • -XX:+UseSerialGC 年轻代和老年代都用串行收集器
  • -XX:+UseParNewGC 年轻代使用 ParNew,老年代使用 Serial Old
  • -XX:+UseParallelGC 年轻代使用 ParallerGC,老年代使用 Serial Old
  • -XX:+UseParallelOldGC 新生代和老年代都使用并行收集器
  • -XX:+UseConcMarkSweepGC,表示年轻代使用 ParNew,老年代的用 CMS
  • -XX:+UseG1GC 使用 G1垃圾回收器
  • -XX:+UseZGC 使用 ZGC 垃圾回收器

6.4、STW

Stop the world,在垃圾回收时,暂停用户的一切线程。垃圾回收器是为了让GC时间更短,减少停顿,并不能完全消除停顿。

7、CMS垃圾回收器

7.1、定义

  • 并发标记清除垃圾收集器
  • 年轻代:复制算法
  • 老年代:并发标记-清除算法

7.2、设计目标

  • 避免在老年代 GC 时出现长时间的卡顿。
  • 它把最耗时的一些操作,做成了和应用线程并行。

7.3、CMS回收过程

初始标记(STW状态:(时间短))

1、标记直接关联GC ROOT的对象;

2、标记年轻代中对象的引用。

并发标记(并行)

1、标记所有可达的对象,耗时长,但可与用户线程并行执行。

2、将老年代中发生变化的卡页,标记为dirty状态。

并发预清理(并行)

1、重新标记老年代中状态为dirty的卡页,并清除掉dirty的状态;

2、将老年代中发生变化的卡页,标记为dirty状态。

并发可取消的预清理(可选)(并行)

在满足某些条件的时候,可以终止,比如迭代次数、有用工作量、消耗的系统时间等。

最终标记(SWT)

完成老年代中所有存活对象的标记。(处理老年代中的所有状态为dirty的卡页,且不会再增加新的dirty状态的卡页)

并发清除(并发)

删除不可达对象,并回收空间。(新产生的垃圾对象留待下次GC处理,被称作浮动垃圾)

并发重置(并发)

重置 CMS 算法相关的内部数据,为下一次 GC 循环做准备。

7.4、内存碎片

一般情况下,当老年代的使用率达到 70%,就会触发 GC。

可通过参数 -XX:CMSInitiatingOccupancyFraction 用来配置这个比例。

CMS 对老年代回收时,并没有内存的整理阶段。所以,CMS提供了两个参数来解决这个问题:

  • UseCMSCompactAtFullCollection(默认开启):表示在要进行 Full GC 的时候,进行内存碎片整理。内存整理的过程是无法并发的,所以停顿时间会变长。
  • CMSFullGCsBeforeCompaction:每隔多少次不压缩的 Full GC 后,执行一次带压缩的 Full GC。默认值为 0,表示每次进入 Full GC 时都进行碎片整理。

7.5、CMS的停顿(STW)

  1. 初始标记,这部分的停顿时间较短;
  2. Minor GC(可选),在预处理阶段对年轻代的回收,停顿由年轻代决定;
  3. 重新标记,由于 preclaen 阶段的介入,这部分停顿也较短;
  4. Serial-Old 收集老年代的停顿,主要发生在预留空间不足的情况下,时间会持续很长;
  5. Full GC,永久代空间耗尽时的操作,由于会有整理阶段,持续时间较长。

7.6、CMS缺点

  1. CPU敏感:并发标记、清除过程中因占用一部分线程可能导致应用程序变慢。
  2. 并发清除过程中会产生浮动垃圾。
  3. 标记清除算法会存在空间碎片的情况。

8、G1垃圾回收器

8.1、为什么需要G1?

CMS在发生 Minor GC 时,由于 Survivor 区已经放不下了,多出的对象只能提升(promotion)到老年代。但是此时老年代因为空间碎片的缘故,会发生 concurrent mode failure 的错误。这个时候,就需要降级为 Serail Old 垃圾回收器进行收集。

8.2、G1的设计目标

分而治之,部分收集。

在任意 1 秒的时间内,停顿不得超过 10ms。-XX:MaxGCPauseMillis=10。(控制STW时间)

8.3、CMS和G1的区别?

  • CMS回收器,是对某个年代的整体收集,无法控制收集时间;
  • G1回收器,把堆分成了很多份,把每一份当做一个小目标,控制收集时间。

8.4、G1 有年轻代和老年代的区分吗?

G1堆内存划分

  • G1也会分年轻代和老年代,只不过它们在内存上是不连续的,而是由一小份一小份(Region)组成,被称作小堆区。
  • 对象超过Region大小的50%,会被分配到黄色区域,被称作Humongous Region(连续的小堆区)。
  • 可通过-XX:G1HeapRegionSize=<N>M设置Region大小。
  • 垃圾回收的时候,会优先回收垃圾最多的小堆区。

8.5、G 1 的优点

  • 任意1秒内停顿时间不会超过10ms;
  • 停顿时间不会随着堆的增大而增大;
  • 复制算法和标记整理算法,保证无空间碎片问题。

8.6、G1的数据结构

Rset

  • 用于记录和维护Region之间的对象引用关系。
  • 把 RSet 理解成一个 Hash,key 是引用的 Region 地址,value 是引用它的对象的卡页集合。
  • 年轻代的Rset:只保存来自老年代的引用,因为年轻代的回收是针对所有年轻代Region;
  • 老年代的Rset:只保存老年代对它的引用,因为老年代回收之前,会先对年轻代进行垃圾回收。

CSet

收集集合,保存一次GC中将执行垃圾回收的区间(Region)。

8.8、G1的垃圾回收过程

8.8.1、年轻代回收(STW)

Minor GC,Eden区满的时候,会发生年轻代的垃圾回收。

跨代引用使用 RSet 数据结构来追溯,会一次性回收掉年轻代的所有 Region。

  1. 扫描根:根,可以看作是我们前面介绍的 GC Roots,加上 RSet 记录的其他 Region 的外部引用。
  2. 更新RSet:RSet 可以准确的反映老年代对所在的内存分段中对象的引用。
  3. 处理RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的 Eden 中的对象被认为是存活的对象。
  4. 复制对象:Eden 区内存段中存活的对象会被复制到 Survivor 区中空的 Region。
  5. 处理引用:处理 Soft、Weak、Phantom、Final、JNI Weak 等引用。结束收集。

8.8.2、并发标记

当整个堆内存使用达到一定比例(默认是 45%),并发标记阶段就会被启动。

可通过参数 -XX:InitiatingHeapOccupancyPercent 进行配置。

  1. 初始标记(STW):GC ROOT扫描
  2. Root 区扫描:扫描对老年代的引用,并标记被引用的对象。
  3. 并发标记:从 GC Roots 开始对 heap 中的对象标记,标记线程与应用程序线程并行执行,并且收集各个 Region 的存活对象信息。
  4. 重新标记(STW):标记那些在并发标记阶段发生变化的对象。
  5. 清理阶段:如果发现 Region 里全是垃圾,在这个阶段会立马被清除掉。不全是垃圾的 Region,并不会被立马处理,它会在 Mixed GC 阶段,进行收集。

8.8.3、混合回收(Mixed GC)

在 Minor GC 之后,如果判断这个占比达到了某个阈值(垃圾占比),下次就会触发 Mixed GC。这个阈值,由 -XX:G1HeapWastePercent 参数进行设置(默认是堆大小的 5%)。

9、ZGC

  • 停顿时间不会超过 10ms;
  • 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下);
  • 可支持几百 M,甚至几 T 的堆大小(最大支持 4T)。
  • 连逻辑上的年轻代和老年代也去掉了,只分为一块块的 page,每次进行 GC 时,都会对 page 进行压缩操作,所以没有碎片问题。

以上内容为个人学习理解,如有问题,欢迎在评论区指出。

部分内容截取自网络,如有侵权,联系作者删除。

相关推荐
ThisIsClark33 分钟前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉1 小时前
【jvm】内存泄漏与内存溢出的区别
jvm
大G哥3 小时前
深入理解.NET内存回收机制
jvm·.net
泰勒今天不想展开3 小时前
jvm接入prometheus监控
jvm·windows·prometheus
东阳马生架构1 天前
JVM简介—3.JVM的执行子系统
jvm
程序员志哥1 天前
JVM系列(十三) -常用调优工具介绍
jvm
后台技术汇1 天前
JavaAgent技术应用和原理:JVM持久化监控
jvm
程序员志哥1 天前
JVM系列(十二) -常用调优命令汇总
jvm
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭1 天前
聊聊volatile的实现原理?
java·jvm·redis