大白话详解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

相关文章

MySQL中的交叉连接、自然连接和内连接查询详解

《MySQL中的交叉连接、自然连接和内连接查询详解》:本文主要介绍MySQL中的交叉连接、自然连接和内连接查询,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、引入二、交php叉连接(cross join)三、自然连接(naturalandroid join)四

Go 语言中的select语句详解及工作原理

《Go语言中的select语句详解及工作原理》在Go语言中,select语句是用于处理多个通道(channel)操作的一种控制结构,它类似于switch语句,本文给大家介绍Go语言中的select语... 目录Go 语言中的 select 是做什么的基本功能语法工作原理示例示例 1:监听多个通道示例 2:带

mysql的基础语句和外键查询及其语句详解(推荐)

《mysql的基础语句和外键查询及其语句详解(推荐)》:本文主要介绍mysql的基础语句和外键查询及其语句详解(推荐),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋... 目录一、mysql 基础语句1. 数据库操作 创建数据库2. 表操作 创建表3. CRUD 操作二、外键

Spring Boot项目部署命令java -jar的各种参数及作用详解

《SpringBoot项目部署命令java-jar的各种参数及作用详解》:本文主要介绍SpringBoot项目部署命令java-jar的各种参数及作用的相关资料,包括设置内存大小、垃圾回收... 目录前言一、基础命令结构二、常见的 Java 命令参数1. 设置内存大小2. 配置垃圾回收器3. 配置线程栈大小

鸿蒙中@State的原理使用详解(HarmonyOS 5)

《鸿蒙中@State的原理使用详解(HarmonyOS5)》@State是HarmonyOSArkTS框架中用于管理组件状态的核心装饰器,其核心作用是实现数据驱动UI的响应式编程模式,本文给大家介绍... 目录一、@State在鸿蒙中是做什么的?二、@Spythontate的基本原理1. 依赖关系的收集2.

Redis实现延迟任务的三种方法详解

《Redis实现延迟任务的三种方法详解》延迟任务(DelayedTask)是指在未来的某个时间点,执行相应的任务,本文为大家整理了三种常见的实现方法,感兴趣的小伙伴可以参考一下... 目录1.前言2.Redis如何实现延迟任务3.代码实现3.1. 过期键通知事件实现3.2. 使用ZSet实现延迟任务3.3

C语言函数递归实际应用举例详解

《C语言函数递归实际应用举例详解》程序调用自身的编程技巧称为递归,递归做为一种算法在程序设计语言中广泛应用,:本文主要介绍C语言函数递归实际应用举例的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录前言一、递归的概念与思想二、递归的限制条件 三、递归的实际应用举例(一)求 n 的阶乘(二)顺序打印

Python Faker库基本用法详解

《PythonFaker库基本用法详解》Faker是一个非常强大的库,适用于生成各种类型的伪随机数据,可以帮助开发者在测试、数据生成、或其他需要随机数据的场景中提高效率,本文给大家介绍PythonF... 目录安装基本用法主要功能示例代码语言和地区生成多条假数据自定义字段小结Faker 是一个 python

Java Predicate接口定义详解

《JavaPredicate接口定义详解》Predicate是Java中的一个函数式接口,它代表一个判断逻辑,接收一个输入参数,返回一个布尔值,:本文主要介绍JavaPredicate接口的定义... 目录Java Predicate接口Java lamda表达式 Predicate<T>、BiFuncti

详解如何通过Python批量转换图片为PDF

《详解如何通过Python批量转换图片为PDF》:本文主要介绍如何基于Python+Tkinter开发的图片批量转PDF工具,可以支持批量添加图片,拖拽等操作,感兴趣的小伙伴可以参考一下... 目录1. 概述2. 功能亮点2.1 主要功能2.2 界面设计3. 使用指南3.1 运行环境3.2 使用步骤4. 核