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

相关文章

HTML5的input标签的`type`属性值详解和代码示例

《HTML5的input标签的`type`属性值详解和代码示例》HTML5的`input`标签提供了多种`type`属性值,用于创建不同类型的输入控件,满足用户输入的多样化需求,从文本输入、密码输入、... 目录一、引言二、文本类输入类型2.1 text2.2 password2.3 textarea(严格

C++ move 的作用详解及陷阱最佳实践

《C++move的作用详解及陷阱最佳实践》文章详细介绍了C++中的`std::move`函数的作用,包括为什么需要它、它的本质、典型使用场景、以及一些常见陷阱和最佳实践,感兴趣的朋友跟随小编一起看... 目录C++ move 的作用详解一、一句话总结二、为什么需要 move?C++98/03 的痛点⚡C++

MySQL中between and的基本用法、范围查询示例详解

《MySQL中betweenand的基本用法、范围查询示例详解》BETWEENAND操作符在MySQL中用于选择在两个值之间的数据,包括边界值,它支持数值和日期类型,示例展示了如何使用BETWEEN... 目录一、between and语法二、使用示例2.1、betwphpeen and数值查询2.2、be

python中的flask_sqlalchemy的使用及示例详解

《python中的flask_sqlalchemy的使用及示例详解》文章主要介绍了在使用SQLAlchemy创建模型实例时,通过元类动态创建实例的方式,并说明了如何在实例化时执行__init__方法,... 目录@orm.reconstructorSQLAlchemy的回滚关联其他模型数据库基本操作将数据添

Java中ArrayList与顺序表示例详解

《Java中ArrayList与顺序表示例详解》顺序表是在计算机内存中以数组的形式保存的线性表,是指用一组地址连续的存储单元依次存储数据元素的线性结构,:本文主要介绍Java中ArrayList与... 目录前言一、Java集合框架核心接口与分类ArrayList二、顺序表数据结构中的顺序表三、常用代码手动

JAVA线程的周期及调度机制详解

《JAVA线程的周期及调度机制详解》Java线程的生命周期包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING和TERMINATED,线程调度依赖操作系统,采用抢占... 目录Java线程的生命周期线程状态转换示例代码JAVA线程调度机制优先级设置示例注意事项JAVA线程

详解C++ 存储二进制数据容器的几种方法

《详解C++存储二进制数据容器的几种方法》本文主要介绍了详解C++存储二进制数据容器,包括std::vector、std::array、std::string、std::bitset和std::ve... 目录1.std::vector<uint8_t>(最常用)特点:适用场景:示例:2.std::arra

C++构造函数中explicit详解

《C++构造函数中explicit详解》explicit关键字用于修饰单参数构造函数或可以看作单参数的构造函数,阻止编译器进行隐式类型转换或拷贝初始化,本文就来介绍explicit的使用,感兴趣的可以... 目录1. 什么是explicit2. 隐式转换的问题3.explicit的使用示例基本用法多参数构造

Android使用java实现网络连通性检查详解

《Android使用java实现网络连通性检查详解》这篇文章主要为大家详细介绍了Android使用java实现网络连通性检查的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录NetCheck.Java(可直接拷贝)使用示例(Activity/Fragment 内)权限要求

MyBatis中的两种参数传递类型详解(示例代码)

《MyBatis中的两种参数传递类型详解(示例代码)》文章介绍了MyBatis中传递多个参数的两种方式,使用Map和使用@Param注解或封装POJO,Map方式适用于动态、不固定的参数,但可读性和安... 目录✅ android方式一:使用Map<String, Object>✅ 方式二:使用@Param