# JVM G1GC Q\&A

前面的文章中，我们援引了美团技术博客的文章，简单介绍和了解了G1GC。但是心中还是有很多疑问，所以这篇文章，我就将这些疑惑一一列举出来，并一一解答。争取做到对G1GC有一个深入的详细的了解。

G1 指的是 Garbage First,GC 在JVM 中有两个含义，内存的分配和针对已分配内存的回收。

## 参考

* 《JVM G1 源码分析和调优》-- 彭成寒编著
* [G1GC 参数调优](https://www.oracle.com/cn/technical-resources/articles/java/g1gc.html)
* [The Garbage First Garbage Collector](https://www.oracle.com/java/technologies/javase/hotspot-garbage-collection.html)

## G1 的基本概念

### G1GC的优势

#### 并行与并发

* 并行性:G1在回收期间，可以有多个 GC 线程同时工作，有效利用多核计算能力。
* 并发性:G1拥有与应用程序交替执行的能力，部分工作可以和应用程序同时执行，因此，一般来说，不会在整个回收阶段发生完全阻塞应用程序的情况

#### 分代收集

* 从分代上看，G1依然属于分代型垃圾回收器，它会区分年轻代和老年代，年轻代依然有Eden区和Survivor区。但从堆的结构上看，它不要求整个Eden区、年轻代或者老年代都是连续的，也不再坚持固定大小和固定数量。
* 将堆空间分为若干个区域（Region），这些区域中包含了逻辑上的年轻代和老年代。
* 和之前的各类回收器不同，它同时兼顾年轻代和老年代。对比其他回收器，或者工作在年轻代，或者工作在老年代；

#### 空间整合

* CMS：“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理
* G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法，但整体上实际可看作是标记-压缩（Mark-Compact）算法，两种算法都可以避免内存碎片。这种特性有利于程序长时间运行，分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候，G1的优势更加明显。

#### 可预测的停顿时间模型

**可预测的停顿时间模型（即：软实时soft real-time**

这是G1相对于CMS的另一大优势，G1除了追求低停顿外，还能建立可预测的停顿时间模型，能让使用者明确指定在一个长度为M毫秒的时间片段内，消耗在垃圾收集上的时间不得超过N毫秒。

* 由于分区的原因，G1可以只选取部分区域进行内存回收，这样缩小了回收的范围，因此对于全局停顿情况的发生也能得到较好的控制。
* G1跟踪各个Region里面的垃圾堆积的价值大小（回收所获得的空间大小以及回收所需时间的经验值），在后台维护一个优先列表，每次根据允许的收集时间，优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
* 相比于CMS GC，G1未必能做到CMS在最好情况下的延时停顿，但是最差情况要好很多。

### 如何设置Heap Region的大小

**G1 中每个Region的的大小都是相同的，如何设置Heap Region的大小？设置HR的时候有哪些考虑**?

HR 的大小直接影响分配和回收的效率。HR过大，可以存放多个对象，分配效率高，但是回收效率低。过小，则分配效率低下。为了均衡二者，所以HR设置了一个上下限，分别是1MB,2MB,4MB,...,32MB,也就是2的指数次幂。默认情况下，整个堆空间有2048个HR(该值可以通过最小的堆分区大小计算出来)。

* G1HeapRegionSize 可以用来指定HR的大小，一般默认为0.
* 不指定的时候，由G1自行推断。

按照默认值来计算的话，G1可以管理的最大内存为 2048×32MB = 64G。假设 xms=32G,xmx=128G,那么计算过程如下：

* average\_heap\_size=(32GB+128GB)/2= 80GB 判断是否设置过分区大小，如果有就使用，没有则根据初始内存和最大分配内存，获得平均值
* region\_size=max(80GB/2048,1m)= 40M  并根据HR的个数得到Region的大小,和Region的下限进行比较，取两者的最大值。
* region\_size= 32M  对region\_size 按2的幂次进行对齐，保证其落在上下限范围内

那这样计算出来的每个 HR 的大小就是32MB，则根据最小内存和最大内存的计算，HR个数的变化范围就是从1024 到4096个。

### G1 大对象不使用 新生代，直接进入老年代，那什么是大对象？

简单来说，就是region\_size 的一半。

### 新生代大小如何设置？

新生代大小指的是新生代内存空间的大小。G1中还新增了两个参数G1MaxNewSizePercent 和G1NewSizePercent用于控制新生代的大小。 整体逻辑如下：

* 如果设置新生代最大值（MaxNewSize）和最小值（NewSize），可以根据这些计算出最大的分区和最小的分区，注意设置了Xmn等价与设置了MaxNewSize和NewSize，且 NewSize=MaxNewSize。
* 如果设置 最大值（MaxNewSize）和最小值（NewSize） ，又设置了NewRatio，则忽略NewRatio。
* 如果没有设置最大值（MaxNewSize）和最小值（NewSize），但是设置了NewRatio，则最大值和最小值相同 (= 整个heap/(NewRatio+1))
* 如果没有设置最大值（MaxNewSize）和最小值（NewSize），或者只设置了其中一个，那么G1将根据G1MaxNewSizePercent （默认60）和G1NewSizePercent（默认5）占整个堆空间的比例来计算最大最小值。

### 分配新的分区时，如何扩展，一次扩展多少？

G1 是自适应扩展内存的， 参数 -XX:GCTimeRatio 表示GC与应用的耗时时间比，默认为9.即G1 GC时间与总时间占比不超过10%时，不需要动态扩展，当GC超过这个值时，可以动态扩展。计算方式是 100 × （1.0 /(1.0 + GCTimeRatio）。扩展时有一个参数，G1ExpandByPercentOfAvailable(默认20),即每次都从未提交的内存中申请20%。

### CardTable And RSet(Remember Set)

CardTable 在之前的GC中就已经存在了，它存在的目的是为了对内存的引用关系做标记。从而根据引用关系遍历活跃对象。 关于两者的介绍可以参看前面的文章。

### 参数介绍和调优

* G1HeapRegionSize 指定堆分区的大小，分区大小可以指定，也可以不指定，不指定时由内存管理器自动推断堆分区大小。
* xms/xmx 指定堆空间最小/最大值
* GCTimeRatio 指的是GC时间与整体时间的占比。前面我们已经介绍过计算，增大该值，能够减少GC占用，但是后果就是动态扩展内存更容易发生。
* G1NewSizePercent是一个实验参数。需要使用-XX:+UnlockExperimentalVMOptions 配合才能改变选项。有实验表明G1在回收Eden分区的时候，大概每GB需要100ms，所以可以根据停顿时间进行相应的调整。这个值在内存比较多大的时候，可以相应的减少。

注意： G1 中不需要设置MaxNewSize、NewSize、Xmn和NewRatio，原因是，一，G1对内存的管理是不连续的，即使重新分配一个堆分区代价也不高，G1的目标满足 垃圾收集停顿，如果设置了固定分区，则G1不能调整新生代的大小，可能就满足不了垃圾收集停顿了。

## G1 的对象分配

G1 提供了两种对象分配策略： 基于线程本地分配缓冲区（Thread Local Allocation Buffer，TLAB）的快速分配和慢速分配。 无论快速分配还是慢速分配，都应该在STW（Stop the World）之外进行调用。

在分配线程对象时，从JVM 堆中分配一个固定大小的内存区域将于作为线程的私有缓冲区，这个缓冲区就是TLAB。只有为线程分配TLAB时候才需要锁定JVM。也就是加锁。其实这个比较好理解，在Java中线程是资源分配的最小单位。不同线程不共享TLAB.

当我们需要去分配一个对象时，优先从当前线程的TLAB去分配，不需要全局锁，因此就达到了快速分配的目的。

当不能进行快速分配时，就会进入慢速分配，而且慢的过程中会有一些 诸如大对象直接分配到老年代，分配不会收需要先进行GC等，失败一定次数之后，则分配失败。

### G1垃圾回收的时机

* 分配时发生回收，快速分配和慢速分配时都有可能存在内存不足，都有可能发生回收，回收之后再继续分配。
* 外部调用的回收，例如显式地调用了system.gc 。 JNI（Java Native Interface） 代码进入了临界区(synchronized)，为了保证安全需要加锁，加锁又发出了GC请求，导致GC等。

### 参数介绍和调优

* 在优化调试TLAB的时候，在调试环境可以打开PrintTLAB来观察TLAB的分配和使用情况
* UseTLAB，指是否打开TLAB，大量的实验证明使用TLAB能够加速TLAB的分配和使用的情况
* ResizeTLAB，是否允许动态调整。基准测试表明，使用动态调整TLAB大小，效率更高
* TLABSize，指设置TLAB的大小，实际使用中不要设置，设置之后就不能动态调整了。

## 新生代回收

内存分配时，剩余空间不能满足分配要求就会优先触发新生代回收（Young GC ，YGC）。

为了便于理解，下面用自己的话进行整理描述：

* 收集之前先STW.
* 选择要收集的区域，对于YGC来说，要收集的就是整个新生代。
* 进行并行任务处理。
* 将符合条件的对象复制到新的Survivor区,对象的field入栈等待复制处理.
* 处理老年代到新生代的代际引用，即更新RSet（前面的文章中有介绍）
* 对栈中的对象进行深度递归遍历复制对象。

### 参数介绍和调优

* ParallelGCThreads ,默认值为0，表示并行执行GC的线程个数。G1可以根据CPU的核数自动推断线程数。GC是CPU密集型，通常来说，线程个数不应该超过CPU核数，一般不用设置该值。
* ResizePLAB，默认为true。表示在垃圾回收之后会根据内存的使用情况来调整PLAB（promotion local allocation buffer）的大小，在一些测试中发现关闭这个参数可能有更好的效果。
* SurvivorRatio，默认值为8，指Eden和一个Survivor分区之间的比例。默认8:1:1。G1 中并会因为增大这个值，就导致Eden变小，因为Eden时根据GC的时间来预测的。

## 混合回收

混合回收可以总结为两个阶段：

* 第一阶段： 并发标记，目的就是识别老年代分区中的活跃对象，并计算分区中垃圾对象所占空间的多少，用于垃圾回收过程中判断是否回收分区
  * 初始标记子阶段
  * 并发标记子阶段
  * 再标记子阶段
  * 清理阶段
* 第二阶段： 垃圾回收。这个过程和新生代回收的步骤完全一致，重用了新生代的逻辑。最大的区别就是，不仅要回收新生代，还要回收并发标记中识别到的垃圾多的老年代分区。

下面就根据混合回收发生的逻辑顺序依次介绍一下这些阶段：

#### 初始标记子阶段

标记所有的根对象（根对象，全局对象，JNI对象）,根是对象图的起点，需要STW.混合回收的初始标记与YGC的初始标记几乎一样。实际上就是借用了YGC之后的结果，即Survivor分区作为根，所以混合回收一定发生再YGC之后，且不需要再一次进行初始标记。

#### 并发标记子阶段

当YGC执行结束之后，如果发现满足并发标记的条件，并发线程就开始并发标记。根据新生代的Survivor分区以及老年代的Rset开始并发标记。并发标记会对所有的分区进行标记，这个阶段并不需要STW,这时标记线程和应用程序线程同时运行。

#### 再标记子阶段

这是最后一个标记阶段。这时，G1需要暂停一下，找出所有未被访问到的对象，同时完成存活内存数据计算。

这个阶段也是并行执行的，通过参数 -XX:ParallelGCThreads 可以设置GC 暂停时可用端的GC 线程数。

#### 清理子阶段

再标记之后进入清理子阶段，也是需要STW的。清理子阶段需要完成虾米那的操作。

* 统计存活对象，主要是利用RSet和位图来实现。
* 交换标记位图，并为下次并发标记做准备
* 重置RSet，此时老年代已经标记完，如果标记后的分区没有引用对象，这说明引用已经发生改变，可以删除原来RSet里面的引用关系。
* 把空闲分区放到空闲分区列表中，这里的空闲指的是全部是垃圾对象的分区，如果分区还有任何分区活跃对象都不会被释放，真正释放是在混合GC中。

这个阶段容易让人误解，清理阶段并没有真正的GC，也不会执行存活对象的拷贝，极端情况下，该阶段结束之后，空闲分区列表和JVM内存的使用情况可能毫无变化。

#### 混合回收阶段

这个阶段是辉进行真正的GC的。 与YGC 一样，第一个步骤是从分区中选出若干个进行回收，这些被选中的分区称为Collect Set（简称CSet）；第二个步骤是把存活的对象复制到空闲的分区中区，同时把这些已经被回收的分区放到空闲分区列表中。垃圾回收总是要在一次新的YGC开始才会发生。

### 并发标记法---三色标记法

三色标记法是一个逻辑上的抽象

* 白色，没有被收集器表标记的对象
* 灰色，自身被标记到，但是其拥有的field字段引用到其他对象还没有被处理。
* 黑色, 自己本身被标记，同时field引用的对象也被标记。

这里虽然讲的很简单，但是实际实现过程中却非常复杂，这里先略过了。

### 参数介绍和调优

* InitiationHeapOccupancyPercent(IHOP),默认值是45，这个值是启用并发标记的先决条件。只有当老年代内存占总空间45%之后才会启动并发标记任务。增加该值，可能导致 并发标记花费更多时间。也会导致YGC或者MixedGC收集时的分区变少，还有可能导致FullGC。根据经验，这个值通常根据整体应用占用的平均值来设置，比平均内存稍微高一点此时性能最好（即YGC 和 MixedGC 比较快，FGC比较少），IHOP的设置非常有用，但是设置IHOP并不容易，需要不断尝试。
* G1ReservePercent,默认是10，当发现GC晋升失败导致FGC，可以增大这个值。
* HeapSizePerGCThread，默认为64M，可以简单地理解为每64M分配一个线程。

## FULL GC

DK 10 之前的FGC都是串行GC,但是不管是串行还是并行，过程和步骤都是一样的。而且采用的标记清除算法。

* 标记活跃对象
* 计算新对象的地址
* 把所有的引用都更新到新的地址上面
* 移动对象完成压缩

### 参数介绍和调优

* MinHeapFreeRatio，用于判断是否可以扩展堆空间。增大该值扩展概率变小，减少该值扩展几率变大
* MaxHeapFreeRatio，用于判断是否可以收缩堆空间。增大该值收缩概率变小，减少该值收缩几率变大

## 实践

找到一台运行Java程序的机器，运行`jps`命令,找到相应的进程ID，然后运行 `jmap -heap pid`就可以查看到该进程的堆栈以及GC信息。

```
jmap -heap 15784
Picked up JAVA_TOOL_OPTIONS: -Duser.timezone=UTC -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8
Attaching to process ID 15784, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.121-b13

using thread-local object allocation.
Garbage-First (G1) GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 5368709120 (5120.0MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 3221225472 (3072.0MB)
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 2097152 (2.0MB)

Heap Usage:
G1 Heap:
   regions  = 2560
   capacity = 5368709120 (5120.0MB)
   used     = 1002454008 (956.0146408081055MB)
   free     = 4366255112 (4163.9853591918945MB)
   18.67216095328331% used
G1 Young Generation:
Eden Space:
   regions  = 436
   capacity = 3353346048 (3198.0MB)
   used     = 914358272 (872.0MB)
   free     = 2438987776 (2326.0MB)
   27.267041901188243% used
Survivor Space:
   regions  = 14
   capacity = 29360128 (28.0MB)
   used     = 29360128 (28.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:
   regions  = 29
   capacity = 1986002944 (1894.0MB)
   used     = 58735608 (56.01464080810547MB)
   free     = 1927267336 (1837.9853591918945MB)
   2.9574783953593173% used
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://www.selinux.tech/java/core/jvm-g1gc-qa.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
