【Java虚拟机】第五章、JVM--------GC算法,GC收集器,GC调优

目录

哪些是垃圾

垃圾收集算法

垃圾收集器

GC优化


上面讲完了整个流程和整个内存结构,下面就要开始进行优化了。想要知道怎么优化,就得先知道GC,有哪些内存算是垃圾,知道哪些内存是垃圾后,怎么清理垃圾,如何选择清理算法。各种垃圾收集器是用的什么算法。

很多人一开始都感觉GC是java自动的,不需要管,但是深入java之后明白,如果你要成神,这个是必不可少的,Elastic Search调优也会用到,各种用到虚拟机的都跳不过垃圾回收。

  • 哪些是垃圾

先说下哪些部分的数据会被垃圾收集吧。年轻代,老年代,方法区都会GC。我们一般称呼年轻代的回收为MinorGC,老年代的回收为MojarGC,以及所有区域的回收为FullGC。怎么算作是垃圾,算法:

  1. 引用计数算法
  2. 可达性分析算法

判断哪些是垃圾最主要还是要看”引用“。

引用计数算法:给对象添加一个引用计数器,当这个对象被引用时,计数器+1,如果引用失效,则计数器-1,最终如果计数器为0表示这个对象不能被使用。客观的说引用计数算法的实现简单,判定效率也高,在大部分情况下是个不错的算法,例如微软公司的COM(Component Object Model)技术、使用ActionScript3的FlashPlay、Python语言和在游戏脚本领域被广泛应用的Squirrel中都是用了引用技术算法进行内存管理。但是,至少主流的JAVA虚拟机里没有选用引用计数算法来管理内存,因为他有个很明显的缺点,循环引用无法被释放内存。啥叫循环引用,就是A对象里有个实例是B,B对象中有个实例是A。

可达性算法分析:从一个点(GC Root,根)散发出去的算法,如果这个点的链路中有引用,那么不可回收。没有被点关联的对象都是可回收的(不需要执行finalize方法)。有哪些对象可以算”点“:

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

从左往右三张图描述了根搜算法(可达性算法分析)的概念

其实以上两种算法,基本都和引用相关,那么java对引用的概念有哪些呢?主要分为强引用(Strong Reference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)4中。

  1. 强引用(Strong Reference):new出来的对象都是强引用,只要引用还在,就不会被收集器回收。
  2. 软引用(SoftReference):存放在SoftReference中的对象,还有用但非必需的对象,在将要发生内存溢出前,会把这些对象列进回收范围之中进行二次回收,如果还是没有足够内存,才会抛异常。比如网页缓存、图片缓存等
  3. 弱引用(WeakReference):存放在WeakReference中的对象,存活到下一次垃圾回收之前。
  4. 虚引用(PhantomReference):存放在PhantomReference中的对象,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

以上描述了哪些可以被标记为垃圾,但是还有一些情况也是要注意的,在可达性分析算法中,如果这个对象不在”点“的链路中,那么这个对象会被第一次标记并且会进行一次筛选,筛选的条件是该对象是否有finalize方法,如果没有该方法,或者该方法已经被调用过了,那么这个对象就死定了。如果这个对象有finalize方法,那么这个对象会被放置到一个F-Queue的队列中,稍后会有虚拟机自动建立的,低优先级的Finalizer线程去执行它。finalized方法时对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要拯救自己,只要与点的链路扯上关系就不会死,但是如果再次被回收的时候,哪怕有finalized方法也没用,必死无疑。

JDK1.8方法区中的垃圾,回忆一下

  •    符号引用被移到了native堆
  •    池化string对象被移到了java堆
  •    Class对象、静态变量被移到了java堆

这些都被移到了堆中。 -XX:MinMetaspaceFreeRatio=<NNN>,<NNN>是一次GC以后,为了避免增加元数据区(高水位)的大小,空闲的类元数据区的容量的最小比例,不够就会导致垃圾回收。 -XX:MaxMetaspaceFreeRatio=<NNN>,<NNN>是一次GC以后,为了避免减少元数据区(高水位)的大小,空闲的类元数据区的容量的最大比例,超过就会导致垃圾回收。

  • 垃圾收集算法

上面讲了哪些是垃圾,下面讲讲用了哪些方法去除垃圾的吧。

  1. 标记-清除算法
  2. 复制算法
  3. 标记-整理算法
  4. 三色标记算法
  5. 分代收集算法

标记-清除,最基础的收集算法,顾名思义把需要回收的对象标记,然后统一清除掉,这个有个很直观的缺点,清除掉之后,会有很多分散的空间,不易于存储大对象,因为如果过多的存储大对象就会导致提前GC。还有个缺点就是效率问题,标记和清除两个过程的效率都不高。

复制算法,这种算法内存永远会浪费10%的空间,因为eden区把还存活的对象(红色部分)移动到To(或者From,只要哪个是全空的就移到哪个)区域, 然后清理掉Eden区非存活对象,然后From区同样操作。接下来如果还是要进行复制,就把Eden区存活的对象放到From区,To也把存活的对象放到From区,等From(或者To)满了,进行一次MinorGC。如果不分代,那么可能永远会浪费50%的空间!看下图

标记-整理就是解决浪费50%空间的其中一个办法,它沿用了标记清理方法,但是清理完后,会把存活的对象都向一端移动,然后清理掉边界以外的对象。

分代收集就是根据内存周期的不同,将内存划分为几个区域,根据不同的区域特点采用不同的垃圾收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对他进行担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

三色标记算法

并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。黑色:根对象,或者该对象与它的子对象都被扫描,灰色:对象本身被扫描,但还没扫描完该对象中的子对象,白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象。

当GC开始扫描对象时,按照如下图步骤进行对象的扫描:根对象被置为黑色,子对象被置为灰色。

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

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

枚举根节点(安全点,安全区域,OOPMap)即为GC Roots的一个枚举,为什么要讲这个,因为我们都知道程序是在不断运行中的,那么垃圾回收时,内存是在不断变化的,那么我们需要不断的对对象做统计,以保证哪些对象是需要被回收的,哪些对象是不需要的,需要不断的更新状态。如果并发的去获取状态,这些状态也是不准的,最好是让程序极短的停顿,生成一个对象状态快照,然后并行的去统计,清理。也就是说这里说的就是如何保证分析结果的正确性。

使用OopMap标记对象引用

在HotSpot中,使用一组OopMap的数据结构来标记对象引用的位置。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来。在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举。

什么是安全点?

OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。实际上,HotSpot也的确没有为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。

如何选择安全点

安全点的选定是以“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。

在安全点暂停的方式

对于Sefepoint,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension),其中抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。而主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

安全区域

使用Safepoint似乎已经完美地解决了如何进入GC的问题,但实际情况却并不一定。Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

引用:https://www.sohu.com/a/217640660_812245

  • 垃圾收集器

上图的含义为新生代和老年代的垃圾收集器搭配。每种收集器各有特点,有老有新。

新生代发生的GC(MinorGC)使用到的器

  1. Serial
  2. ParNew
  3. Parallel Scavenge(PS)
  4. G1

Serial收集器

最基本,发展历史最悠久的收集器。该收集器是一个单线程收集器,这个单线程的含义不是说只是用一条线程或者或者一个CPU去进行垃圾收集工作,他的含义更多的是在进行垃圾收集前需要进行停顿,暂停其他工作线程,直到它收集结束。client模式下默认新生代收集器。采用复制算法

优点:限定单个CPU来说,简单而高效,因为没有线程交互的开销,专心于垃圾收集。

缺点:用户停顿感,体验差

ParNew收集器

ParNew就是Serial的多线程版本,除了使用多线程并行收集,其余包括Serial的控制参数也同样生效(例如:-XX:SurvivorRatio(分配Eden、Survivor比例)、-XX:PretenurdSizeThreshold(设置分配到老年代的大小M)、-XX:HadlePromotionFailure(是否允许分配担保失败)等)、收集算法、STW、对象分配规则、回收策略等都和Serial一样。使用此收集器的配置:-XX:+UseConcMarkSweepGC表示老年代使用CMS收集器收集,此时新生代默认使用ParNew收集器,只有ParNew收集器能与CMS收集器配合工作,还有个配置-XX:+UseParNewGC强制指定新生代使用该收集器。另外该收集器还有个配置-XX:ParallelGCThreads来设置开启的线程数。

优点:在大于两个CPU的场景下,效率比Serial要高。

缺点:STW!!!!!

Parallel Scavenge收集器(记不住怎么拼写,用PS)

采用复制算法,但又和其他收集器关注点不同,其他收集器主要关注的是GC时,用户线程的停顿时间。它主要关注吞吐量,运行用户代码时间/(运行用户代码时间+GC时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量能高效率的利用CPU时间,尽快完成运算任务,主要是和后台运算而不需要太多交互的任务。

PS提供了两个参数用于精确控制吞吐量,-XX:MaxGCPauseMillis控制最大垃圾收集停顿时间,-XX:GCTimeRatio直接设置吞吐量大小。还有个参数-XX:+UseAdaptiveSizePolicy,当设置这个参数后,就不需要手工指定新生代大小,Eden和Survivor比例,晋升老年代对象年龄等细节参数,这些参数都会进行动态调整。

-XX:MaxGCPauseMillis设置时,大家不要认为如果把停顿时间的值设置小一点就能使垃圾收集变快,时间换空间,GC停顿时间所短是以牺牲吞吐量和新生代空间来换取的:系统把新生代设置的小一点,收集300M比收集500M的速度肯定快,这也直接导致垃圾收集发生的更频繁,原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。

优点:可以精确控制吞吐量和停顿时间

缺点:停顿时间设置不好掌控

老年代发生的GC(MajorGC)使用到的器

  1. CMS
  2. Serial Old(MSC)
  3. Parallel Old
  4. G1

Serial Old收集器

在介绍Serial的时间附上的一张图同时也描述了该收集器的模型,采用的是整理算法,单线程。在server模式下,主要两个作用:在JDK1.5之前与PS搭配使用,另一种用途是作为CMS的后备方案,在Concurrent Mode Failure时使用。后面会讲。

Parallel Old收集器

PS收集器的老年代版本,使用标记整理算法,>=JDK1.6才有。吞吐量优先。看图吧。

CMS收集器

CMS收集器是一种以获得最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清除算法实现的,它的运作过程相对来说更复杂一点,分为四个步骤:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

其中,初始标记、重新标记这两个步骤仍然需要STW,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行追溯的过程,而重新标记阶段是解决初始标记至重新标记这个过程中因用户程序继续运作发生而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记的停顿时间要长,但比并发标记的时间短。而整个过程的并发标记和并发清除是和用户线程一起并发运行的。

优点:并发收集,低停顿

缺点:如果用户线程比较大,占用了CPU较多资源,那么并发标记和并发清理会严重影响用户线程变慢,有人发明了i-cms,与用户线程交替执行,效果不大。

          无法清理浮动垃圾,什么叫浮动垃圾?就是在并发清理的过程中,用户线程产生了垃圾,这种垃圾只能下一次清理,但是这样的话还需要留些空间给用户线程使用,所以不能老年代内存满了才清理。JDK1.5默认是老年代使用了68%就开始垃圾收集,在JDK1.6中,CMS收集器的启动阀值已经提升到92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure 失败,这是虚拟机将启动后备预案:临时启用Serial Old来重新进行老年代的垃圾收集,这样停顿时间就很长了。当然也可以使用参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比。通过上面我们也知道这个数值不能太高,也不能太低。

         还有个缺点就是他的收集模型,标记-清除,这个缺点我就不复述了,但该收集器采用了-XX:+UseCMSCompactAtFullCollection开发参数(默认开启),意思是在Full GC之前是否需要进行内存碎片合并整理。需要停顿,不能并发。还有个参数-XX:CMSFullGCsBeforeCompaction,这个参数的意思是设置多少次不压缩的Full GC后,来一次带压缩的(默认0,标识每次进入FullGC都进行碎片整理)搭配使用。

G1收集器

借鉴:https://tech.meituan.com/2016/09/23/g1.html

https://www.cnblogs.com/duanxz/p/6102580.html

G1垃圾收集器是一种工作在堆内不同分区(region)上的并发收集器。分区既可以归属于老年代,也可以归属新生代,同一个代的分区不需要保持连续。为老年代设计分区的初衷是我们发现并发后台线程在回收老年代中没有引用的对象时,有的分区垃圾对象的数量很多,另一些分区垃圾对象相对较少。

虽然分区的垃圾收集工作实际还是要暂停应用线程,不过由于G1收集器专注于垃圾最多的分区,最终的效果是花费较少的时间就能回收这些分区的垃圾。这种只专注于垃圾最多的分区的方式就是G1垃圾收集器的名称由来,即首先收集垃圾最多的分区。

这一算法并不适用新生代的分区,新生代进行垃圾回收时,整个新生代空间要么被回收,要么被晋升。那么新生代也采用分区的原因是因为:采用预定义的分区能够便于代的大小调整。

在G1的实现过程中,引入了一些新的概念,对于实现高吞吐、没有内存碎片、收集时间可控等功能起到了关键作用。下面我们就一起看一下G1中的这几个重要概念。

region

之前当我看完CMS、PS之后,我想着说难道不能采用一个块一块的方式去存储对象,把垃圾回收控制到每个块,那就不存在过大的浪费空间和避免过多的碎片。而region我自己感觉好像就是这么个意思。

传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址(逻辑地址,下同)是连续的。G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。如下图所示:

在上图中,我们注意到还有一些Region标明了H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。

H-obj在global concurrent marking(全局并发标记)阶段的cleanup 和 full GC阶段回收。 在分配H-obj之前先检查是否超过 initiating heap occupancy percent(使用阀值,超过内存使用率就开始GC)和the marking threshold(标记阀值), 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures(拷贝存活对象失败) 和 full GC。

为了减少连续H-objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size。

一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数。如果不设定,那么G1会根据Heap大小自动决定。

SATB(Snapshot-At-The-Beginning)

全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 那么它是怎么维持并发GC的正确性的呢?就是上面讲的三色算法

由于并发阶段的存在,Mutator和Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的情况,这种情况发生的前提是:当黑对象被赋值成一个白对象时,也就是说黑对象的引用变成了白对象的引用。太抽象了,如下图

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

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完成后,会重新释放空间。

SATB也是有副作用的,如果被替换的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是浮动垃圾。因为SATB的做法精度比较低,所以造成的float garbage也会比较多。

RSet

类似于卡页、卡表。简单来说就是Remembered Set是建立在卡表的基础上的,其结构为Hash Table,key记录了其他region的起始地址,value是一个集合,记录的是卡页的索引。那么为什么key记录的是其他region的地址,因为它是points-into结构,意思是谁引用了我的对象,我就要记录他。如下图:每个region分为多个512Bytes的卡页,每个当region1引用了region2的对象后,RSet for Region2的key记录了region1的地址,value记录了region2的card索引,这个索引>>9位就是卡页所在位置。

而维系RSet中的引用关系靠post-write barrier和Concurrent refinement threads来维护。那么到底RSet到底是怎么用于G1的呢?post-write barrier(写屏障)记录了跨Region的引用更新,更新日志缓冲区(SATB快照)则记录了那些包含更新引用的Cards。一旦缓冲区满了,Post-write barrier(写屏障)就停止服务了,会在重新标记阶段由Concurrent refinement threads处理这些缓冲区日志。 RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。

CSet收集集合

一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间、或者老年代。CSet会占用不到整个堆空间的1%大小。

Pause Prediction Model停顿预测模型

G1 GC是一个响应时间优先的GC算法,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms,不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?就需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。

在G1 GC过程中,每个可测量的步骤花费的时间都会记录到TruncateSeq(继承了AbsSeq)中,用来计算衰减均值、衰减变量,衰减标准偏差等:

G1提供了两种GC模式

  1. G1 Young GC
  2. G1 Mix GC

G1 Young GC

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

其实上面已经讲的差不多了,有一种场景老年代持有新生代的对象,怎么快速的定位到这个对象呢,上面说的RSet可以解决,只要扫描RSet里面的跟对象就行了。再看一张图:

Old区域C对象持有了Eden区的D对象,所以Old区B和C所在的卡页被记录为持有D和E对象,EDEN区的RS的key则记录了Old区B和C的起始地址,value记录了D和E所在卡页的索引。

Young GC 阶段:

  • 阶段1:根扫描(初始标记)
                          静态和本地对象被扫描

  • 阶段2:更新RS(并发标记)
                          处理dirty card队列更新RS

  • 阶段3:处理RS(重新标记)
                         检测从年轻代指向年老代的对象

  • 阶段4:对象拷贝(清除)
                         拷贝存活的对象到survivor/old区域

  • 阶段5:处理引用队列(清除)
                        软引用,弱引用,虚引用处理

G1 Mix GC

它的GC步骤分2步:

  1. 全局并发标记(global concurrent marking)
  2. 拷贝存活对象(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 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。

以下是一些G1优化参数

参数含义
-XX:G1HeapRegionSize=n设置Region大小,并非最终值
-XX:MaxGCPauseMillis设置G1收集过程目标时间,默认值200ms,不是硬性条件
-XX:G1NewSizePercent新生代最小值,默认值5%
-XX:G1MaxNewSizePercent新生代最大值,默认值60%
-XX:ParallelGCThreadsSTW期间,并行GC线程数
-XX:ConcGCThreads=n并发标记阶段,并行执行的线程数
-XX:InitiatingHeapOccupancyPercent设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous
-XX:+UseG1GC启用G1 GC
-XX:G1ReservePercent=n预留多少内存,防止晋升失败的情况,默认值是10

GC优化

4种情况会触发这类的Full GC

G1收集器同CMS收集器一样,在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。有的时候你会在垃圾回收日志中观察到Full GC,这些日志是一个信号,表明我们需要进一步调优(方式很多,甚至很可能要分配更多的堆空间)才能提升应用程序的性能。主要有4种情况会触发这类的Full GC,如下:

1、并发模式失效

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

GC日志如下的示例:

解决办法:发生这种失败意味着堆的大小应该增加了,或者G1收集器的后台处理应该更早开始,或者需要调整周期,让它运行得更快(如,增加后台处理的线程数)。

2、晋升失败

(to-space exhausted或者to-space overflow)

G1收集器完成了标记阶段,开始启动混合式垃圾回收,清理老年代的分区,不过,老年代空间在垃圾回收释放出足够内存之前就会被耗尽。(G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用),由此触发了Full GC。

下面日志中(可以在日志中看到(to-space exhausted)或者(to-space overflow)),反应的现象是混合式GC之后紧接着一次Full GC。

这种失败通常意味着混合式收集需要更迅速的完成垃圾收集:每次新生代垃圾收集需要处理更多老年代的分区。

解决这种问题的方式是:

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

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

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

3、疏散失败

(to-space exhausted或者to-space overflow)

进行新生代垃圾收集是,Survivor空间和老年代中没有足够的空间容纳所有的幸存对象。这种情形在GC日志中通常是:

这条日志表明堆已经几乎完全用尽或者碎片化了。G1收集器会尝试修复这一失败,但可以预期,结果会更加恶化:G1收集器会转而使用Full GC。

解决这种问题的方式是:

  1. 增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
  2. 通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
  3. 也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目

4、巨型对象分配失败

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

下面列出能够避免发生Full GC的方法:

  1. 通过增加总的堆空间大小或者调整老年代、新生代之间的比例来增加老年代空间的大小。
  2. 增加后台线程的数量(假设我们有足够的CPU资源运行这些线程)。
  3. 以更高的频率进行G1的后台垃圾收集活动。
  4. 在混合式垃圾收集周期中完成更多的垃圾收集工作。

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值