详细了解G1、了解G1、G1垃圾收集器详解、G1垃圾回收器简单调优

4.详细了解G1:

4.1.一:什么是垃圾回收

4.2.了解G1

4.3.G1 Yong GC

4.4.G1 Mix GC

4.5.三色标记算法

4.6.调优实践

5.G1垃圾收集器详解

5.1.G1垃圾收集器

5.2.G1的堆内存划分

5.3.G1的运行过程

5.4.三色标记

5.4.1.漏标问题

5.5.记忆集与卡表

5.6.安全点与安全区域

6.G1垃圾回收器简单调优

6.1.堆

6.2.Java 9以后开启的参数

6.3.G1的GC阶段

6.4.Young Only Phase

6.5.Mixed gc Phase

6.6.Full gc Phase

6.7.先来看看GC周期

6.8.gc日志

4.详细了解G1:

转自:https://www.cnblogs.com/lsgxeva/p/10231201.html

4.1.一:什么是垃圾回收

首先,在了解G1之前,我们需要清楚的知道,垃圾回收是什么?简单的说垃圾回收就是回收内存中不再使用的对象。

垃圾回收的基本步骤:

1.查找内存中不再使用的对象。

2.释放这些对象占用的内存。

1.查找内存中不再使用的对象

那么问题来了,如何判断哪些对象不再被使用呢?我们也有2个方法:
A:引用计数法

引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。

B:根搜索算法(可达性分析算法)

根搜索算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

现在我们已经知道如何找出垃圾对象了,如何把这些对象清理掉呢?

2.释放这些对象占用的内存

常见的方式有复制或者直接清理,但是直接清理会存在内存碎片,于是就会产生了清理再压缩的方式。

总得来说就产生了三种类型的回收算法。
A.标记--复制

它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次理掉。它的优点是实现简单,效率高,不会存在内存碎片。缺点就是需要2倍的内存来管理。

B.标记--清理

标记清除算法分为"标记"和"清除"两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。它的优点是效率高,缺点是容易产生内存碎片。

C.标记--整理

标记操作和"标记-清理"算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。因为要移动对象,所以它的效率要比"标记-清理"效率低,但是不会产生内存碎片。

3.基于分代的假设

由于对象的存活时间有长有短,所以对于存活时间长的对象,减少被gc的次数可以避免不必要的开销。这样我们就把内存分成新生代和老年代,新生代存放刚创建的存活时间比较短的对象,

4.Java垃圾收集器的历史

  • 第一阶段,Serial(串行)收集器
    在jdk1.3.1之前,java虚拟机仅仅能使用Serial收集器。Serial收集器是一个单线程的收集器,但它的"单线程"的意义并不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

PS:开启Serial收集器的方式

bash 复制代码
-XX:+UseSerialGC
  • 第二阶段,Parallel(并行)收集器
    Parallel收集器也称吞吐量收集器,相比Serial收集器,Parallel最主要的优势在于使用多线程去完成垃圾清理工作,这样可以充分利用多核的特性,大幅降低gc时间。
    PS:开启Parallel收集器的方式
bash 复制代码
-XX:+UseParallelGC -XX:+UseParallelOldGC
  • 第三阶段,CMS(并发)收集器
    CMS收集器在Minor GC时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。在Full GC时不再暂停应用线程,而是使用若干个后台线程定期的对老年代空间进行扫描,及时回收其中不再使用的对象。
    PS:开启CMS收集器的方式
bash 复制代码
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
  • 第四阶段,G1(并发)收集器
    G1收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于4GB)时产生的停顿。相对于CMS的优势而言是内存碎片的产生率大大降低。

PS:开启G1收集器的方式

bash 复制代码
-XX:+UseG1GC

4.2.了解G1

G1的第一篇paper(附录1)发表于2004年,在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。为何oracle要极力推荐G1呢,G1有哪些优点?

首先,G1的设计原则就是简单可行的性能调优

开发人员仅仅需要声明以下参数即可:

bash 复制代码
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。

其次,G1将新生代,老年代的物理空间划分取消了。

这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。

取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。

在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

PS:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。

对象分配策略

说起大对象的分配,我们不得不谈谈对象的分配策略。它分为3个阶段:

1.TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区。

2.Eden区中分配。

3.Humongous区分配。

TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。

对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。

最后,G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面我们将分别介绍一下这2种模式。

4.3.G1 Yong GC

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

这时,我们需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。

在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。

但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。

需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为"0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

Young GC阶段:

  • 阶段1:根扫描

    静态和本地对象被扫描。

  • 阶段2:更新RS

    处理dirty card队列更新RS。

  • 阶段3:处理RS

    检测从年轻代指向年老代的对象。

  • 阶段4:对象拷贝

    拷贝存活的对象到survivor/old区域。

  • 阶段5:处理引用队列

    软引用,弱引用,虚引用处理。

4.4.G1 Mix GC

Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

它的GC步骤分2步:

  • 全局并发标记(global concurrent marking)
  • 拷贝存活对象(evacuation)

在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎样的呢?

在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:

  • 初始标记(initial mark,STW)

    在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。

  • 根区域扫描(root region scan)

    G1 GC在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。

  • 并发标记(Concurrent Marking)

    G1 GC在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被STW 年轻代垃圾回收中断。

  • 最终标记(Remark,STW)

    该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。

  • 清除垃圾(Cleanup,STW)

    在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。

4.5.三色标记算法

提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。

  • 黑色:根对象,或者该对象与它的子对象都被扫描。
  • 灰色:对象本身被扫描,但还没扫描完该对象中的子对象。
  • 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象

当GC开始扫描对象时,按照如下图步骤进行对象的扫描:

根对象被置为黑色,子对象被置为灰色。

继续由灰色遍历,将已扫描了子对象的对象置为黑色。

遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。

这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就可能改变。这样的话,我们就会遇到一个问题:对象丢失问题。

我们看下面一种情况,当垃圾收集器扫描到下面情况时:

这时候应用程序执行了以下操作:

bash 复制代码
A.c = C
B.c = null

这样,对象的状态图变成如下情形:

这时候垃圾收集器再标记扫描的时候就会下图成这样:

很显然,此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用在运行的时候,GC标记的对象不丢失呢?有如下2种可行的方式:

1.在插入的时候记录对象。

2.在删除的时候记录对象。

刚好这对应CMS和G1的2种不同实现方式:

在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:

1,在开始标记的时候生成一个快照图标记存活对象

2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)

3,可能存在游离的垃圾,将在下次被收集

这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作"混合式"是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:

混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。

至此,混合式GC告一段落了。下一小节我们讲进入调优实践。

4.6.调优实践

MaxGCPauseMillis调优:

前面介绍过使用GC的最基本的参数:

bash 复制代码
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

前面2个参数都好理解,后面这个MaxGCPauseMillis参数该怎么配置呢?这个参数从字面的意思上看,就是允许的GC最大的暂停时间。G1尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。 那G1是如何做到最大暂停时间的呢?这涉及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域集合。

  • Young GC:选定所有新生代里的region。通过控制新生代的region个数来控制young GC的开销。
  • Mixed GC:选定所有新生代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。

在理解了这些后,我们再设置最大暂停时间就好办了。 首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。

其他调优参数

bash 复制代码
-XX:G1HeapRegionSize=n

设置的 G1 区域的大小。值是2的幂,范围是1 MB到32 MB 之间。目标是根据最小的Java 堆大小划分出约2048个区域。

bash 复制代码
-XX:ParallelGCThreads=n

设置STW工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为8。

如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的5/8左右。这适用于大多数情况,除非是较大的SPARC系统,其中n的值可以是逻辑处理器数的5/16左右。

bash 复制代码
-XX:ConcGCThreads=n

设置并行标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。

bash 复制代码
-XX:InitiatingHeapOccupancyPercent=45

设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个Java堆的45%。

避免使用以下参数:

避免使用-Xmn选项或-XX:NewRatio等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。

触发Full GC

在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?

  • 并发模式失败

    G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。

  • 晋升失败或者疏散失败

    G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:

a,增加-XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为"目标空间"增加预留内存量。

b,通过减少-XX:InitiatingHeapOccupancyPercent提前启动标记周期。

c,也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

  • 巨型对象分配失败
    当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。

由于篇幅有限,G1还有很多调优实践,在此就不一一列出了,大家在平常的实践中可以慢慢探索。最后,期待java 9能正式发布,默认使用G1为垃圾收集器的java性能会不会又提高呢?

5.G1垃圾收集器详解

5.1.G1垃圾收集器

以下转自:https://blog.csdn.net/u022812849/article/details/107692963

GC收集器的三个考量指标:

  • 占用的内存(Capacity)
  • 延迟(Latency)
  • 吞吐量(Throughput)
    随着硬件的成本越来越低,机器的内存也越来越大,GC收集器占用的内存基本上可以容忍。而吞吐量可以通过集群(增加机器)来解决。

随着JVM中内存的增大,STW的时间成为JVM急迫解决的问题,如果还是按照传统的分代模型,使用传统的垃圾收集器,那么STW的时间将会越来越长。

在传统的垃圾收集器中,STW的时间是无法预测的,有没有一种办法,能够首先定义一个停顿时间,然后反向推算收集内容呢?就像是领导在年初制定KPI一样,分配的任务多就多干些,分配的任务少就少干点。

G1的思路说起来也类似,它不要求每次都把垃圾清理的干干净净,它只是努力做它认为对的事情。

我们要求G1,在任意1秒的时间内,停顿不得超过10ms,这就是在给它制定KPI。G1会尽量达成这个目标,它能够反向推算出本次要收集的大体区域,以增量的方式完成收集。

这也是使用G1垃圾回收器(-XX:+UseG1GC)不得不设置的一个参数:-XX:MaxGCPauseMillis=10。

5.2.G1的堆内存划分

为了实现STW的时间可预测,首先要有一个思想上的改变。G1将堆内存"化整为零",将堆内存划分成多个大小相等独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region可能是Eden,也有可能是Survivor,也有可能是Old,另外Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的进行回收大多数情况下都把Humongous Region作为老年代的一部分来进行看待。

G1在逻辑上还是划分Eden、Survivor、old,但是物理上他们不是连续的。

5.3.G1的运行过程

G1的运行过程与CMS大体一致,分为以下四个步骤:

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。

  • 并发标记( Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,并发时有引用变动的对象会产生漏标问题,G1中会使用SATB(snapshot-at-the-beginning)算法来解决,后面会详细介绍。

  • 最终标记(Final Marking):对用户线程做一个短暂的暂停,用于处理并发标记阶段仍遗留下来的最后那少量的SATB记录(漏标对象)。

  • 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。

TAMS是什么?要达到GC与用户线程并发运行,必须要解决回收过程中新对象的分配,所以G1为每一个Region区域设计了两个名为TAMS(Top at Mark Start)的指针,从Region区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围。

5.4.三色标记

在三色标记法之前有一个算法叫Mark-And-Sweep(标记清除)。这个算法会设置一个标志位来记录对象是否被使用。最开始所有的标记位都是0,如果发现对象是可达的就会置为1,一步步下去就会呈现一个类似树状的结果。等标记的步骤完成后,会将未被标记的对象统一清理,再次把所有的标记位设置成0方便下次清理。

这个算法最大的问题是GC执行期间需要把整个程序完全暂停,不能实现用户线程和GC线程并发执行。因为在不同阶段标记清扫法的标志位0和1有不同的含义,那么新增的对象无论标记为什么都有可能意外删除这个对象。对实时性要求高的系统来说,这种需要长时间挂起的标记清扫法是不可接受的。所以就需要一个算法来解决GC运行时程序长时间挂起的问题,那就是三色标记法。

三色标记最大的好处是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个GC。

三色标记法很简单。首先将对象用三种颜色表示,分别是白色、灰色和黑色。

  • 黑色:表示根对象,或者该对象与它引用的对象都已经被扫描过了。
  • 灰色:该对象本身已经被标记,但是它引用的对象还没有扫描完。
  • 白色:未被扫描的对象,如果扫描完所有对象之后,最终为白色的为不可达对象,也就是垃圾对象。

5.4.1.漏标问题

1.假设此时,对象A及其引用的对象都已经被扫描完,那么对象A将会被标记为黑色。。

2.用户线程将对象B和对象C之间的引用断开,将对象A指向对象C,此时对象C会被当成对象,会产生漏标问题,因为对象A不会被扫描。

漏标问题在CMS和G1收集器中有着不同的解决方案。

  • CMS:采用IncrementalUpdate(增量更新)算法,在并发标记阶段时如果一个白色对象被一个黑色对象引用时,会将黑色对象重新标记为灰色,让垃圾收集器在重新标记阶段重新扫描。
  • G1:采用SATB(snapshot-at-the-beginning),在初始标记时做一个快照,当B和C之间的引用消失时要把这个引用推到GC的堆栈,保证C还能被GC扫描到,在最终标记阶段扫描STAB记录。

两种漏标解决方案的对比:

  • SATB算法关注的是引用的删除(B->C的引用)。
  • Incremental Update算法关注的是引用的增加(A->C 的引用),需要重新扫描,效率低。

5.5.记忆集与卡表

跨代引用:堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么回收新生代的话,需要扫描所有从老年代到新生代的所有引用,所以要避免每次YGC时扫描整个老年代,减少开销。

记忆集(RSet,Remembered Set):用来记录从其他Region中的对象到本Region的引用,是一种抽象的数据结构。每一个Region都设有一个RSet,有了这个数据结构,在回收某个Region的时候,就不必对整个堆内存的对象进行扫描了,它使得部分收集成为了可能。

对于年轻代的Region,它的RSet只保存了来自老年代的引用,这是因为年轻代的回收是针对所有年轻代Region的,没必要画蛇添足。所以说年轻代Region的RSet有可能是空的。

而对于老年代的Region来说,它的RSet也只会保存老年代对它的引用。这是因为老年代回收之前,会先对年轻代进行回收。这时,Eden区变空了,而在回收过程中会扫描Survivor分区,所以也没必要保存来自年轻代的引用。

RSet通常会占用很大的空间,大约5%或者更高(最高可能20%)。不仅仅是空间方面,很多计算开销也是比较大的。

RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定年轻代的RSet作为GC ROOTs,这些RSet记录了old->young的跨代引用,避免了扫描整个老年代。 而mixed gc的时候,老年代中记录了old->old的RSet,young->old的引用从Survivor区获取(老年代回收之前,会先对年轻代进行回收,存活的对象放在Survivor区),这样也不用扫描全部老年代,所以RSet的引入大大减少了GC的工作量。

5.6.安全点与安全区域

用户线程暂停,GC线程要开始工作,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以JVM会在字节码指令中,选一些指令,作为"安全点",比如方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点。

为什么它叫安全点,是这样的,GC时要暂停用户线程,并不是抢占式中断(立马把业务线程中断)而是主动式中断。

主动式中断是设置一个标志,这个标志是中断标志,各用户线程在运行过程中会不停的主动去轮询这个标志,一旦发现中断标志为 True,就会在自己最近的"安全点"上主动中断挂起。

为什么需要安全区域?要是用户线程都不执行(用户线程处于Sleep或者是Blocked状态),那么程序就没办法进入安全点,对于这种情况,就必须引入安全区域。安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间里JVM要发起GC就不必去管这个线程了。

当线程要离开安全区域时,它要检查JVM是否已经完成了根节点枚举或者其他GC中需要暂停用户线程的阶段:

1、如果完成了,那线程就当作没事发生过,继续执行。

2、否则它就必须一直等待,直到收到可以离开安全区域的信号为止。

6.G1垃圾回收器简单调优

以下转自:https://www.codercto.com/a/99945.html

在G1中,为了提升吞吐量,有一些操作永远是(STW) stop-the-world 的。其他的一些要长期的,如全局标记这种要全堆进行的操作与应用程序并发进行。为了让空间回收的 STW尽可能减少,G1并行的分步的递增进行空间回收。G1通过追踪此前应用行为和垃圾回收停顿的信息来构建一个与开销有关的模型(Pause Prediction Model)。它使用这些信息停顿期间可做的工作。举个例子,G1首先回收最高效的区域(也即垃圾最满的区域,因此称为垃圾---优先)。

6.1.堆

G1把堆分成n个大小相同的region。

  • E是eden region
  • S是survivor region
  • O是old region
  • H是humongous(老年代可以是humongous,可以看出,他可以跨多个连续regions。直接分配到老年代,防止反复拷贝移动)。

6.2.Java 9以后开启的参数

自从Java 9后,引入的统一的日志,也就是Xlog参数。下面是建议的GCLog参数:

bash 复制代码
-Xlog:gc*:file=your.log:tags,time,uptime,level:filecount=5,filesize=100

6.3.G1的GC阶段

我们明白,调优的基本步骤就是

1.Measure收集相关诊断信息(例如收集详细的gclog,默认log level info可以满足大部分情况)

2.Understand理解发生了什么

3.Tune调优

只有明白了GC内部发生了什么,才能针对性的对其进行调整。

下面通过一些正常的GC log来理解GC的三种方式GC做了什么。

6.4.Young Only Phase

先用张图来简单理解 Young GC过程。

  • 垃圾回收的过程就是Allocated->eden; eden -> survivor; survivor -> survivor; survivor -> old;
  • 可以看到,这里有eden,survivor,old还有个free region。
    - - 橙色就是或者的对象
  • G1会把橙色对象拷贝到free region
    -- 当拷贝完毕,free region就会晋升为survivor region,以前的eden就被释放了。
  • 如果Young gc中,花费了大量的时间。
  • 正常来说,大部分在 Young 的对象都不会存活很长时间。
  • 如果不符合这个规则 (大部分在 Young 的对象都不会存活很长时间),你可能需要调整一下 Young 区域占比。来降低 Young 对象的拷贝时间。
  1. -XX:G1NewSizePercent (默认:5) Young region 最小值
  2. -XX:G1MaxNewSizePercent (默认: 60) Young region 最大值

6.5.Mixed gc Phase

Mixed gc 会选取所有的 Young region + 收益高的若干个 Old region。


  • 同样的,被回收的 region 就变回 free region了。
  • 从上图可以了解到 Mixed gc 只能回收部分的老年代
  • G1是如何选择要回收的regions的?
  1. -XX:G1MaxNewSizePercent与Young 关联。
  2. -XX:MixedGCCountTarget与old关联。
  • -XX:MixedGCCountTarget默认是8,意味着要在8次以内回收完所有的old region
    A: 换句话说,如果你有 800 个 old region, 那么一次 mixed gc 最大会回收 100 个 old region
    B: G1也可以被调整成不做这么多工作,也就是回收少点,浪费堆内存,导致更堆使用
  1. -XX:G1MixedGCLiveThresholdPercent(默认:85)可能会提高堆使用率。
  2. -XX:G1HeapWastePercent (默认:5) 如果可回收低于这个值, 那么将不会启动Mixed gc

6.6.Full gc Phase

Full gc是不应该发生的。

6.7.先来看看GC周期

G1 有两个阶段,它会在这两个阶段往返,分别是Young-only,Space Reclamation.

  • Young-only包含一系列逐渐填满 old gen的gc
  • Space Reclamation G1 会递进地回收old gen的空间,同时也处理Young region

图是来自oracle上对gc周期的描述,实心圆都表示一次GC停顿。

  • 蓝色Young-only。
  • 黄色 标记过程的停顿。
  • 红色Mixed gc停顿。

在几次gc后,old gen 的对象占有比超过了 InitiatingHeapOccupancyPercent,gc就会进入并发标记准备(concurrent mark)。

  • G1在每一次 Young 回收中都会查找活对象(有引用的对象)。
  • G1在 old region 并发查找活对象。
    A: 叫concurrent marking。
    B: 可能花费很长时间。
    C: 不会停止 Java 应用。
  • G1 没有活对象的引用信息是不能进行垃圾回收的。
    A: Mixed gc 依赖 concurrent mark。

回到full gc,从上面简单分析得出,full gc 发生是没有足够的 free region,如果堆是足够大的,Mixed gc没有回收足够的old region,或者concurrent mark 没法及时完成,都可能会导致full gc。

6.8.gc日志

bash 复制代码
[gc,start ] GC(78) Pause Young (Normal) (G1 Evacuation Pause)
[gc,task ] GC(78) Using 10 workers of 10 for evacuation
[gc,phases ] GC(78) Pre Evacuate Collection Set: 3.2ms
[gc,phases ] GC(78) Evacuate Collection Set: 28.8ms
[gc,phases ] GC(78) Post Evacuate Collection Set: 1.8ms
[gc,phases ] GC(78) Other: 1.1ms
[gc,heap ] GC(78) Eden regions: 538->0(871)
[gc,heap ] GC(78) Survivor regions: 69->33(76)
[gc,heap ] GC(78) Old regions: 1041->1077
[gc,heap ] GC(78) Humongous regions: 3->1
[gc,metaspace ] GC(78) Metaspace: 71777K->71777K(1114112K)
[gc ] GC(78) Pause Young (Normal) (G1 Evacuation Pause) 3300M->2220M(6144M) 34.907ms
[gc,cpu ] GC(78) User=0.24s Sys=0.05s Real=0.04s
[gc,start ] GC(79) Pause Young (Concurrent Start) (G1 Humongous Allocation)
[gc,task ] GC(79) Using 10 workers of 10 for evacuation
[gc,phases ] GC(79) Pre Evacuate Collection Set: 0.2ms
[gc,phases ] GC(79) Evacuate Collection Set: 22.3ms
[gc,phases ] GC(79) Post Evacuate Collection Set: 0.9ms
[gc,phases ] GC(79) Other: 1.8ms
[gc,heap ] GC(79) Eden regions: 569->0(656)
[gc,heap ] GC(79) Survivor regions: 33->55(113)
[gc,heap ] GC(79) Old regions: 1077->1077
[gc,heap ] GC(79) Humongous regions: 1->1
[gc,metaspace ] GC(79) Metaspace: 71780K->71780K(1114112K)
[gc ] GC(79) Pause Young (Concurrent Start) (G1 Humongous Allocation) 3357M->2264M(6144M) 25.305ms
[gc,cpu ] GC(79) User=0.21s Sys=0.00s Real=0.03s
[gc ] GC(80) Concurrent Cycle
[gc,marking ] GC(80) Concurrent Clear Claimed Marks
[gc,marking ] GC(80) Concurrent Clear Claimed Marks 0.147ms
[gc,marking ] GC(80) Concurrent Scan Root Regions
[gc,marking ] GC(80) Concurrent Scan Root Regions 16.125ms
[gc,marking ] GC(80) Concurrent Mark (373.358s)
[gc,marking ] GC(80) Concurrent Mark From Roots
[gc,task ] GC(80) Using 4 workers of 4 for marking
[gc,marking ] GC(80) Concurrent Mark From Roots 57.029ms
[gc,marking ] GC(80) Concurrent Preclean
[gc,marking ] GC(80) Concurrent Preclean 0.454ms
[gc,marking ] GC(80) Concurrent Mark (373.358s, 373.415s) 57.548ms
[gc,start ] GC(80) Pause Remark
[gc,stringtable] GC(80) Cleaned string and symbol table, strings: 36361 processed, 315 removed, symbols: 192117 processed, 500 removed
[gc ] GC(80) Pause Remark 2326M->956M(6144M) 14.454ms
[gc,cpu ] GC(80) User=0.08s Sys=0.03s Real=0.02s
[gc,marking ] GC(80) Concurrent Rebuild Remembered Sets
[gc,marking ] GC(80) Concurrent Rebuild Remembered Sets 38.843ms
[gc,start ] GC(80) Pause Cleanup
[gc ] GC(80) Pause Cleanup 974M->974M(6144M) 0.660ms
[gc,cpu ] GC(80) User=0.00s Sys=0.00s Real=0.00s
[gc,marking ] GC(80) Concurrent Cleanup for Next Mark
[gc,marking ] GC(80) Concurrent Cleanup for Next Mark 16.673ms
[gc ] GC(80) Concurrent Cycle 146.748ms
[gc,start ] GC(81) Pause Young (Prepare Mixed) (G1 Evacuation Pause)
[gc,task ] GC(81) Using 10 workers of 10 for evacuation
[gc,mmu ] GC(81) MMU target violated: 61.0ms (60.0ms/61.0ms)
[gc,phases ] GC(81) Pre Evacuate Collection Set: 0.1ms
[gc,phases ] GC(81) Evacuate Collection Set: 76.8ms
[gc,phases ] GC(81) Post Evacuate Collection Set: 0.9ms
[gc,phases ] GC(81) Other: 1.1ms
[gc,heap ] GC(81) Eden regions: 211->0(136)
[gc,heap ] GC(81) Survivor regions: 55->17(34)
[gc,heap ] GC(81) Old regions: 392->443
[gc,heap ] GC(81) Humongous regions: 3->1
[gc,metaspace ] GC(81) Metaspace: 71780K->71780K(1114112K)
[gc ] GC(81) Pause Young (Prepare Mixed) (G1 Evacuation Pause) 1320M->919M(6144M) 78.857ms
[gc,cpu ] GC(81) User=0.41s Sys=0.37s Real=0.08s
[gc,start ] GC(82) Pause Young (Mixed) (G1 Evacuation Pause)
[gc,task ] GC(82) Using 10 workers of 10 for evacuation
[gc,phases ] GC(82) Pre Evacuate Collection Set: 0.1ms
[gc,phases ] GC(82) Evacuate Collection Set: 22.1ms
[gc,phases ] GC(82) Post Evacuate Collection Set: 0.8ms
[gc,phases ] GC(82) Other: 0.9ms
[gc,heap ] GC(82) Eden regions: 136->0(142)
[gc,heap ] GC(82) Survivor regions: 17->11(20)
[gc,heap ] GC(82) Old regions: 443->367
[gc,heap ] GC(82) Humongous regions: 1->1
[gc,metaspace ] GC(82) Metaspace: 71780K->71780K(1114112K)
[gc ] GC(82) Pause Young (Mixed) (G1 Evacuation Pause) 1191M->757M(6144M) 23.970ms
[gc,cpu ] GC(82) User=0.15s Sys=0.08s Real=0.03s
[gc,start ] GC(83) Pause Young (Mixed) (G1 Evacuation Pause)
[gc,task ] GC(83) Using 10 workers of 10 for evacuation
[gc,phases ] GC(83) Pre Evacuate Collection Set: 0.1ms
[gc,phases ] GC(83) Evacuate Collection Set: 5.0ms
[gc,phases ] GC(83) Post Evacuate Collection Set: 0.8ms
[gc,phases ] GC(83) Other: 1.1ms
[gc,heap ] GC(83) Eden regions: 142->0(783)
[gc,heap ] GC(83) Survivor regions: 11->10(20)
[gc,heap ] GC(83) Old regions: 367->294
[gc,heap ] GC(83) Humongous regions: 1->1
[gc,metaspace ] GC(83) Metaspace: 71780K->71780K(1114112K)

上面是连续几次GC的日志,可以对照着gc周期来看。为了方便排版,把时间相关的tag给精简掉了。

  • GC(78)是一次普通的young gc,里面信息有各种region的变化
    A: 这里简单说一下humongous对象的处理
    B: humongous对象在G1中是被特殊对待的,G1只决定它们是否生存,回收他们占用的空间,从不会移动它们。
    C: Young-Only阶段,humongous regions可能会被回收。
    D: Space-Reclamation,humongous regions可能会被回收。
  • GC(79)开始进入并发阶段
  • GC(80)完成了Cleanup,紧接着一个Prepare Mixed GC(81)的垃圾收集,对应周期虚线右边的蓝实心圆
  • GC(82)之后就是Space Reclamation阶段了,多个Mixed GC会进行。

根据日志,可以简单看到每个步骤花费的时间,以及对应区域垃圾的回收情况,结合GC参数,可以定位出什么问题,针对性的调整参数。

吞吐量跟低延时是无法兼得的,低延时意味着GC工作会更加频繁,相对的,会占用应用的资源,吞吐量降低。需要大吞吐量,那么GC工作就会减少,相对的,每次回收的垃圾就会多,暂停时间就会增加,延时就会增加。-XX:MaxGCPauseMillis G1 会尽量满足这个参数设定的目标时间,通过此参数可以平衡应用需要的吞吐量以及延时。

相关推荐
m0_571957581 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
pianmian13 小时前
python数据结构基础(7)
数据结构·算法
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟5 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
好奇龙猫5 小时前
【学习AI-相关路程-mnist手写数字分类-win-硬件:windows-自我学习AI-实验步骤-全连接神经网络(BPnetwork)-操作流程(3) 】
人工智能·算法
P.H. Infinity6 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天6 小时前
java的threadlocal为何内存泄漏
java
sp_fyf_20246 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-01
人工智能·深度学习·神经网络·算法·机器学习·语言模型·数据挖掘