大白话详解G1垃圾回收器

2024-08-23 16:44

本文主要是介绍大白话详解G1垃圾回收器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

JVM内存结构与G1垃圾回收器概述

众所周知,JVM 的内存结构由以下五部分构成:

  • 堆(Heap)
  • 栈(Stack)
  • 方法区(Method Area)
  • 本地方法区(Native Method Area)
  • 程序计数器(Program Counter)

垃圾回收器主要管理的是堆内存。本文将详细介绍 G1 垃圾回收器 如何对堆内存进行管理。

G1垃圾回收算法

G1 垃圾回收器 采用以下算法对堆内存进行管理:

  • 分代管理
  • 复制算法

分代管理

分代管理包括:

  • 新生代(包括 Eden 和 Survivor 区)
  • 老年代(Old Generation)
  • 超大对象区(Humongous Objects)

Region

G1 逻辑上将堆内存划分为多个大小不等的 Region。每个 Region 的大小通常在 2MB 到 32MB 之间,可通过 JVM 参数进行设置。在任何给定时间,所有 Region 必定属于以下类型之一:

  • E(Eden)
  • S(Survivor)
  • O(Old)
  • H(Humongous)
  • F(Free)

划分成 Region 的好处在于,G1 能够根据需要动态调整不同代的内存大小。例如,如果新生代空间不足,G1 可以从 Free 类型的 Region 中划分一块成为 Eden 类型的 Region。

RSet (Remembered Set)

每个 Region 都会有一个自己的 RSet。RSet 用于记录不同 Region 之间的 跨 Region 引用关系。例如,有两个对象 A 和 B,且 A 在 RegionA 上,B 在 RegionB 上,对象 A 的某个属性是 B,那就意味着 A 引用着 B。此时,RegionB 对应的 RSet 上就会记录着 A 引用着 B,即 RSet 上记录的是别的区域对本区域对象的引用。

RSet 的数据结构类型

  • 稀疏模式(Sparse):通过哈希表方式实现。Key 是别的 RegionID,而值是 Card 地址数组。
  • 细粒度模式(Fine-grained):通过哈希表方式实现。Key 是别的 RegionID,而值是直接引用地址的数组。
  • 粗粒度模式(Coarse-grained):一个 RegionID 数组,里面分别是别的 Region 的 ID。

内存划分与 LAB(本地缓冲区)

当需要对新的内存区域进行划分时,了解 LAB 的概念非常重要。由于 Region 通常较大,内存申请和划分往往需要更小的粒度。因此,引入了 LAB,它允许更细粒度的内存分配。

CARD

为了更有效地管理内存,G1 将每个 Region 进一步划分为多个 Card,通常大小为 512B。这样,一个 Region 就包含多个 Card,Card 是 G1 进行内存管理和垃圾回收的最小单位。一个对象可能跨越多个 Card,或者一个 Card 内存储多个对象。

CardTable

为了全局管理 Card,引入了 CardTable。CardTable 用于记录 Card 内对象的引用情况,是 G1 垃圾回收过程中的关键数据结构。CardTable 可以被理解为一个字节数组,其中 Card 的首地址就是数组的下标,下标对应的值表示该 Card 上的对象是否发生了引用修改。

LAB (Local Allocation Buffer)

LAB 是每个线程的私有内存分配区域,减少线程间的竞争,用于加速对象的分配过程。这里的私有内存是堆内存里Eden区域中的内存,只不过对应的线程管理自己负责部分的内存区域,而且如果使用完可以重新申请LAB。


以下是对关键词进行了加红标注的版本:


以上基本概念介绍完毕,下面我们介绍一下G1垃圾回收器的整个垃圾回收流程,主要分为四步,分别是初始标记并发标记再次标记垃圾回收。简而言之,垃圾回收就是对不需要的对象进行内存释放。那么,如何找到这些不需要的对象呢?我们都知道Java的对象之间存在相互引用,类似于一个网状结构,因此确定第一个需要保留下来的对象就非常关键了。因此,GC Root的概念就出现了。

GC Root分为四类:

  1. 虚拟机栈(栈中的本地变量表)中引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI(即常说的native方法中)引用的对象。

凡是被GC Root引用的对象就会被保留下来,然后这些对象所引用的对象也要被保留下来,否则程序无法运行。通过这种方式拓扑开来,所有需要保留下来的对象都会被扫描到。这种算法叫三色标记法

三色标记法

具体来说,三色标记法的对象初始状态为白色。如果当前对象被扫描到但其引用的对象尚未被扫描完,则该对象被标记为灰色(如对象B,其引用的D对象尚未被扫描)。如果一个对象及其引用的所有对象都被扫描完毕,则该对象被标记为黑色(如A对象)。黑色和灰色对象会被保留下来,而被标记为黑色的对象不会被再次扫描,这就是三色标记法

这样看来,垃圾回收实际上可以分为两步:一步进行垃圾标记,然后对垃圾进行回收。那么,为什么G1要分为四步呢?这就要从G1的特性来说了。G1的特点是出色的响应时间以及可以设置垃圾回收时的停顿时间。

大家可以想象,代码在运行过程中会伴随着对象的修改。由于堆内存中的对象数量较大,且寻址扫描非常耗时,通常情况下,扫描一遍进行标记,扫描到的对象就保持其当前颜色。例如,在标记初期,A被标记为黑色并保留下来,而E没有被任何对象引用,因此被标记为白色并视为垃圾。然而,如果在标记阶段,垃圾回收线程和程序的工作线程同时运行,E对象可能会在标记后被A对象引用,但由于E已经被标记为白色,若作为垃圾回收,程序将会出错。这种情况也可能在垃圾清理过程中出现。因此,最简单的做法是停止应用线程,只让垃圾回收线程工作,这就是STW(Stop The World)。这种做法显然不符合G1的特性,因此G1将原来的两步分为四步。

初始标记阶段通过找到GC Root引用的对象,这一步需要STW,但非常快速。接下来是并发标记阶段,这一步主要对堆内存中的对象进行拓扑扫描和三色标记。由于对象数量庞大,这一环节耗时较长,因此不能STW,而是让应用线程和垃圾回收线程同时工作。被标记为黑色的对象不会被再次扫描,但这也可能出现前面提到的漏标问题,对应的也可能出现多标的问题,比如之前标记为黑色或者灰色,但是并行标记过程中不再被引用,那么这些被多标的对象就会成为浮游垃圾,本次垃圾回收不会对其回收,下次垃圾回收会回收掉,为了解决漏标问题,出现了两种理论:SATB(原始快照)和增量更新G1采用前者,而另一种并发垃圾回收器则采用后者。这两种理论的解决方案都基于写屏障技术,写屏障分为写前屏障写后屏障

SATB基于写前屏障增量更新则基于写后屏障。例如,在扫描初期,a.e = e1,此时e1被标记为不可回收,因为它被对象a引用了。在并发标记阶段,应用线程执行a.e = e2;执行这一句时会触发写屏障写前屏障在读取e2对象的引用时记录下该引用到一个队列里,而写后屏障则记录a对象的引用。因此,这种方式可以将并发标记过程中出现的变化记录下来。

接着进入再次标记过程,这次标记实际上是重新扫描并发标记过程中记录下来的对象,因此也需要STW。由于扫描的对象较少,所以STW时间非常短。最后一步是垃圾回收,这就是整个G1垃圾回收的大致过程。

至于为什么叫原始快照,其实原始快照并不是真实存在的数据结构,而是一个概念。从初始标记并发标记,中间发生变化的对象会保持原貌被记录下来,这样相当于实现了一个快照的作用。

这是你提供的内容,经过调整并加红关键词的版本:


其实G1有一个很大的优势,这一点不同于其他垃圾回收器,就是对堆内存的管理。其他垃圾回收器虽然也将堆内存划分为新生代(Eden和两个Survivor区)以及老年代(Old),但它们在一开始就指定了这些区域的比例。而G1则是根据Region管理的,每个Region具体属于哪个代(新生代或老年代),是根据程序运行情况和算法动态划分的。

这种动态管理堆内存的方式相较于一开始就固定比例,优势明显。因此,你会发现G1的参数配置都是配置的目标效果,算法会根据你想要的效果来管理堆内存,尽可能达到配置目标效果。


有了以上内容的铺垫我想你基本上了解的大概差不多了,上面只是介绍了大概流程,这样你脑海里大流程通顺了,具体的细节后面进一步补充,比如minorGC和mixGC的时候,上面说垃圾回收扫描堆,其实就是扫描Region,minorGC回收的是新生代,具体其实就是那些被标记为E和S的Region,以及可能存在新生代对象,虽然在新生代,但是并没有被新生代的对象引用,而是被老年代引用,这些对象其实也是要被留下来的,如何不扫描老年代又能发现这些对象呢,要是连老年代一块扫描了,那岂不是FullGC了,就不叫MinorGC了,这就要说一下Rset了,

这篇关于大白话详解G1垃圾回收器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1099938

相关文章

Debezium 与 Apache Kafka 的集成方式步骤详解

《Debezium与ApacheKafka的集成方式步骤详解》本文详细介绍了如何将Debezium与ApacheKafka集成,包括集成概述、步骤、注意事项等,通过KafkaConnect,D... 目录一、集成概述二、集成步骤1. 准备 Kafka 环境2. 配置 Kafka Connect3. 安装 D

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

Spring Cloud LoadBalancer 负载均衡详解

《SpringCloudLoadBalancer负载均衡详解》本文介绍了如何在SpringCloud中使用SpringCloudLoadBalancer实现客户端负载均衡,并详细讲解了轮询策略和... 目录1. 在 idea 上运行多个服务2. 问题引入3. 负载均衡4. Spring Cloud Load

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud

如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解

《如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解》:本文主要介绍如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别的相关资料,描述了如何使用海康威视设备网络SD... 目录前言开发流程问题和解决方案dll库加载不到的问题老旧版本sdk不兼容的问题关键实现流程总结前言作为

SQL 中多表查询的常见连接方式详解

《SQL中多表查询的常见连接方式详解》本文介绍SQL中多表查询的常见连接方式,包括内连接(INNERJOIN)、左连接(LEFTJOIN)、右连接(RIGHTJOIN)、全外连接(FULLOUTER... 目录一、连接类型图表(ASCII 形式)二、前置代码(创建示例表)三、连接方式代码示例1. 内连接(I

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.

Java中八大包装类举例详解(通俗易懂)

《Java中八大包装类举例详解(通俗易懂)》:本文主要介绍Java中的包装类,包括它们的作用、特点、用途以及如何进行装箱和拆箱,包装类还提供了许多实用方法,如转换、获取基本类型值、比较和类型检测,... 目录一、包装类(Wrapper Class)1、简要介绍2、包装类特点3、包装类用途二、装箱和拆箱1、装

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要