十四、垃圾回收器下篇

一、CMS回收器(低延迟)

1、CMS回收器概述

1、CMS(Concurrent Mark Sweep)收集器是HosSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作
2、CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
3、目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为 关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非 常符合这类应用的需求。
4、CMS的垃圾收集算法采用标记-清除算法,也会STW。
5、不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
6、在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。

2、CMS的工作原理

1、CMS运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤
  • 初始标记(CMS Initial Mark)
  • 并发标记(CMS Concurrent Mark)
  • 重新标记(CMS Remark)
  • 并发清除(CMS Concurrent Sweep)
2、初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-The-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快
3、并发标记(Concurrent-Mark)阶段从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
4、重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(因为并发标记和用户线程同时执行,可能导致已经标记的可达对象,在用户执行过程中改变为不可达,所以需要重新标记阶段),这个阶段的停顿时间通常会比初始标记阶段稍长一些,并且也会导致“Stop-The-World”的发生,但也远比并发标记阶段的时间短。
5、并发清除(Concurrent-Sweep)阶段此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象(因为是标记清除算法),所以这个阶段也是可以与用户线程同时并发的。

在这里插入图片描述

3、CMS的特点

1、尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和重新标记这两个阶段中仍然需要执行STW机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要STW机制,只是尽可能地缩短暂停时间。
2、由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的
3、由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用
4、因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行垃圾收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行
5、要是CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure失败,这是虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
6、CMS收集器的垃圾收集算法采用的是标记-清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配

4、使用标记清除算法而不是标记压缩算法原因

1、因为当并发清除的时候,用标记压缩整理内存的话,原来的用户线程正在使用的内存,没办法通过移动存活对象的地址,因为正在使用这些对象,地址无法被改变。
2、要保证用户线程能继续执行,前提的它运行的资源不受影响。标记压缩更适合Stop The World这种场景下使用(因为STW的时候,用户线程就不运行了,此时可以整理存活对象的内存,解决内存碎片问题)。

5、CMS优缺点

1、优点:
  • 并发收集、低延迟
2、缺点:
  • CMS收集器对CPU资源非常敏感。事实上,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说CPU的计算能力)而导致程序变慢,降低总吞吐量CMS默认启动的回收线程数是(CPU核心数量 + 3)/ 4,也就是说,如果核心数在4个或以上,并发回收时垃圾收集线程只占用不超过25%的CPU运算资源,并且会随着CPU核心数量的增加而下降。如果核心数不足4个时,CMS对用户线程的影响就可能变得很大,如果应用本来的CPU负载很高,还要分出一半的运算能力去执行垃圾收集线程,就可能导致用户程序的执行速度大幅降低。
  • CMS收集器无法处理浮动垃圾(Floating Garbage),有可能出现Concurrent Mode Failure失败而导致另一次完全Stop The WorldFull GC产生。在CMS的并发标记和并发清理阶 段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为浮动垃圾
  • 会产生内存碎片,CMS是基于标记-清除算法实现的收集器,在收集结束时会有大量的空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

6、CMS收集器常用JVM参数

1、设置JVM启动参数指定老年代使用CMS收集器执行内存回收任务-XX:+UseConcMarkSweepGC
  • 设置该参数后会自动将-XX:+UseParNewGC参数设置上。即:ParNew(Young区用)+CMS(Old区用)+Serail Old的组合
2、设置JVM启动参数指定堆内存使用率的阈值,一旦达到该阈值,便开始回收-XX:CMSInitiatingOccupancyFraction
  • JDK5及以前版本的默认值为68,即当老年代空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%
  • 如果内存使用率增长缓慢,可以适当调高该参数的值来提高CMS的触发百分比,降低内存回收频率,减少老年代回收次数,获取更好的性能。
  • 如果内存使用率增长很快,则应该降低该参数的值,以避免频繁触发老年代串行收集器。因此通过该参数便可以有效降低Full GC的执行次数
3、设置JVM启动参数指定在CMS收集器不得不进行Full GC时开启内存碎片的整理(对内存空间进行压缩整理)-XX:+UseCMSCompactAtFullCollection
  • 以避免内存碎片的产生。不过由于内存压缩整理过程中用户线程无法并发执行,因此停顿的时间就变得更长了。
4、设置JVM启动参数指定在执行多少Full GC后对内存空间进行压缩整理-XX:CMSFullGCsBeforeCompaction
5、设置JVM启动参数指定CMS收集器线程的数量-XX:ParallelCMSThreads
  • CMS默认启动的回收线程数是(ParallelGCThreads + 3)/ 4
  • ParallelGCThreads是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。

7、如何选择回收器

1、HotSpot有这么多的垃圾回收器,那么如果有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?
  • 如果你想要最小化地使用内存和并行开销,请选Serial GC(减少上下文切换)
  • 如果你想要最大化应用程序的吞吐量,请选Parallel GC(最大化吞吐量)
  • 如果你想要最小化GC的中断或停顿时间,请选CMS GC(减少STW,增高低延迟)
2、JDK后续版本中CMS的变化
  • 如果对JDK9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃
  • 如果在JDK14中使用-XX:+UseConcMarkSweepGC的话,JVM不会报错,只是给出一个warning信息,但是不会exit。JVM会自动回退以默认GC方式启动JVM

二、G1回收器(区域化分代式)

1、G1出现背景

1、由于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,因此才会不断对GC进行优化。
2、G1(Garbage-First)垃圾回收器是在Java7 Update 4之后引入的一个新的垃圾回收器,是当今收集器技术发展最前沿的成果之一。与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量
3、官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才当起“全功能收集器”的重任与期望

2、G1的含义(Region分区)

1、G1是一个并行回收器,它把连续的Java堆内存划分为多个大小相等的独立区域(Region),使用不同的Region来表示新生代的Eden空间、Survivor空间(Survivor0区、Survivor1区),或者老年代空间
2、G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小及回收所需要时间的经验值),在后台维护一个优先列表每次根据允许的收集时间,优先回收价值最大的Region
3、由于这种方式的侧重点在于回收垃圾最大量的区间(Region),因此G1也称为:垃圾优先(Garbage First)。
4、G1(Garbage First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征
5、在JDK1.7版本正式启用,移除了Experimental的标识,是JDK9以后的默认垃圾回收器,取代了CMS回收器以及Parallel + Parallel Old的组合,被Oracle官方称为全功能的垃圾收集器
  • 补充说明:CMS在JDK9中被标记为废弃(deprecated)。
6、G1收集器Region分区示意图

在这里插入图片描述

3、G1回收器的特点(优势)

与其他的GC收集器相比,G1使用了全新的分区算法,其特点如下:
1、并行与并发
  • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。
  • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般不会在整个回收阶段发生完全阻塞应用程序的情况
2、分代收集
  • 从分代上看,G1仍然属于分代型垃圾回收器,它会区分老年代和年轻代,年轻代(有Eden区和Survivor区)。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量
  • 堆空间分为若干区域(Region),这些区域中包含了逻辑上的年轻代和老年代。和之前的各类的回收器不同,它同时兼顾年轻代和老年代
  • G1之前的所有回收器(包括CMS在内),垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC),而G1可以面向堆内存任何部分来组成回收集(Collection Set,简称CSet)进行回收,衡量的标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大(即G1收集器的Mixed GC模式)。
3、空间整合
  • CMS回收器的垃圾收集算法采用的是标记-清除算法,不可避免地将会产生一些内存碎片,因此在若干次GC后进行一次碎片整理
  • G1将内存划分为一个个的Region。内存回收是以Region作为基本单位。Region之间是复制算法(Survivor0区、Survivor1区),但整体上实际可以看作是标记压缩算法,这两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发一次Full GC。尤其是当Java堆非常大的时候,G1的优势会更加明显。
4、可预测的停顿时间模型(即:软实时soft real-time),这是G1相对于CMS的另一大优势,G1除了追求低停顿,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗垃圾收集上的时间不得超过N毫秒。
  • 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
  • G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小及回收所需要时间的经验值),在后台维护一个优先列表每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限时间内可以获取尽可能高的收集效率
  • 相比于CMS,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好得多。

4、G1回收器的缺点

1、相较于CMS,G1还不具备全方位、压倒性优势。 比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
2、从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

5、G1回收器常用JVM参数

1、设置JVM启动参数指定使用G1收集器执行内存回收任务-XX:+UseG1GC
2、设置JVM启动参数指定每个Region的大小-XX:G1HeapRegionSize
  • 取值范围为1MB-32MB,且应为2的N次幂。目标是根据最小的Java堆大小划分出约2048个区域,默认是堆内存的1/2000。
3、设置JVM启动参数设置期望达到的最大GC停顿时间-XX:MaxGCPauseMillis
  • JVM会尽力实现,但不保证达到,默认值200ms,根据这个时间,优先处理回收价值收益最大的那些Region。
4、设置JVM启动参数指定STW时GC线程数的值,最多设置为8-XX:ParallelGCThreads
5、设置JVM启动参数设置并发标记的线程数-XX:ConcGCThreads
  • 将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右
6、设置JVM启动参数设置触发并发GC周期的Java堆占用率阈值,超过该值就触发GC,默认是45-XX:InitiatingHeapOccupancyPercent

6、G1回收器的常见操作步骤

1、G1的设计原则就是简化JVM性能调优,只需要三步即可完成调优:
  • 第一步开启G1垃圾收集器:-XX:+UseG1GC
  • 第二步设置堆的最大内存(例如:-Xms、-Xmx
  • 第三步设置最大停顿时间:-XX:MaxGCPauseMillis
2、G1中提供了三种垃圾回收模式在不同条件下触发:
  • Young GC
  • Mixed GC
  • Full GC

7、G1回收器的应用场景

1、面向服务端应用,针对具有大内存、多处理的机器(在普通大小的堆里表现并不好)。
2、最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案。
  • 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)。
3、用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比CMS好:
  • 超过50%的Java堆被活动数据占用。
  • 对象分配频率或年代提升频率变化很大。
  • GC停顿时间过长(长于0.5至1秒)。
4、HotSpot垃圾收集器里
  • 除了G1以外,其他的垃圾收集器均使用内置的JVM线程执行GC的多线程操作。
  • 而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

8、分区Region(化整为零)

1、使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。
2、-XX:G1HeapRegionSize参数可以设定Region大小。所有的Region大小相同,且在JVM生命周期内不会被改变
3、虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的动态集合。通过Region的动态分配方式实现逻辑上的连续。
4、一个Region有可能属于Eden,Survivor或者Old/Tenured内存区域。但是一个Region只可能属于一个角色。下图中的E表示该Region属于Eden内存区域,S表示属于Survivor内存区域,O表示属于Old内存区域。图中空白的表示未使用的内存空间。
5、G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,G1认为只要大小超过单个Region容量一半的对象(即0.5个Region),就放到H。而对于那些超过整个Region容量的超大对象,将会被存放到N个连续的H之中。

在这里插入图片描述

6、设置Humongous内存区域原因:
  • 对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个生命周期很短的大对象,就会对垃圾收集器造成负面影响。因为老年代垃圾收集的频率比较低,而这个短期的对象会存活很长时间,始终占有内存空间(宽泛意义上的内存泄漏)
  • 为了解决这个问题,G1划分了一个Humongous区,用来专门存放大对象。
  • 如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。
7、Region的内部结构
  • 每个Region都是通过指针碰撞来分配空间。
  • 每个Region都有TLAB,提高对象分配的效率。

在这里插入图片描述

三、G1回收器的回收流程

1、G1回收器回收流程的主要环节

1、主要有以下三个环节:
  • 年轻代GC(Young GC)
  • 老年代并发标记过程(Concurrent Marking)
  • 混合回收(Mixed GC)
  • (如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。)
2、图示
  • 顺时针方向,Young GC -> Young GC + Concurrent Mark -> Mixed GC顺序进行垃圾回收

在这里插入图片描述

3、详细说明
  • 当年轻代的Eden区用尽时开始年轻代回收过程。G1的年轻代收集阶段是一个并行独占式收集器。在年轻代回收期间,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后将年轻代中存活对象移动到Survivor区或者Old区
  • 当堆内存使用率到达一定值(默认45%)时,开始老年代并发标记过程。标记完成之后马上开始混合回收过程。
  • 对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲空间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代的Region和年轻代一起被回收的。
4、举例说明
  • 一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。

2、G1回收器回收流程的记忆集

1、存在问题
  • 一个对象被不同区域引用的问题
  • 一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中的对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确。
  • 在其他的分代收集器,也存在这样的问题(而G1更突出,因为G1主要针对大堆,如果通过扫描整个堆来判断,代价太大)
2、记忆集Remembered Set是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,可以避免把整个老年代加进GC Roots扫描范围
3、解决方法
  • 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描。
  • 每个Region都有一个对应的Remembered Set
  • 每次Reference类型数据写操作时,都会产生一个写屏障(Write Barrier)暂时中断操作;
  • 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器,检查老年代对象是否引用了新生代对象);
  • 如果不同,通过Card Table把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
  • 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏。

在这里插入图片描述

3、G1年轻代GC

1、JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程年轻代垃圾回收只会回收Eden区和Survivor区
2、YGC时,首先G1停止应用程序的执行(STW),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集合包含年轻代Eden区和Survivor区所有的内存分段

在这里插入图片描述

4、G1年轻代GC回收过程

1、第一阶段,扫描根(GCRoots)寻找可达对象
  • 根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。
  • 根引用连同记忆集(RSet)记录的外部引用作为扫描存活对象的入口。
2、第二阶段,更新记忆集
  • 处理dirty card queue(见备注)中的card,更新RSet。
  • 此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用
3、第三阶段,处理记忆集
  • 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
4、第四阶段,复制对象。(Eden中的对象复制到Survivor中)
  • 此阶段,对象树被遍历,Eden区内存分段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存分段中存活的对象会判断对象年龄的阈值
  • 如果年龄未达到阈值,年龄会加1,达到会被复制到Old区中空的内存分段
  • 如果Survivor内存空间不足,Eden空间中的部分数据会直接晋升到Old空间中
5、第五阶段,处理引用
  • 处理Soft,Weak,Phantom,Final,JNI Weak 等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
备注:
  • 对于应用程序的引用赋值语句oldObject.field=new Object(),JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card (卡表)。
  • 在年轻代回收的时候,G1会对Dirty Card Queue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。
  • 那为什么不在引用赋值语句处直接更新RSet呢?这是为了性能的需要,RSet的处理需要线程同步,开销会很大,使用队列性能会好很多。

5、G1并发标记过程 (和CMS流程类似)

1、初始标记阶段
  • 标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代的GC
  • 由于该阶段是STW的,因此只扫描根节点可达的对象,以节省时间。
2、根区域扫描(Root Region Scanning)
  • G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。
  • 这一过程必须在Young GC之前完成,因为Young GC会使用复制算法对Survivor区进行GC。
3、并发标记(Concurrent Marking)
  • 在整个堆中进行并发标记(和应用程序并发执行),此过程可能被YGC中断。
  • 在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收
  • 同时并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
4、再次标记(Remark)
  • 由于应用程序持续进行,需要修正上一次的标记结果。是STW的。
  • G1中采用了比CMS更快的初始快照算法:Snapshot-At-The-Beginning(SATB)。
5、独占清理(Clean UP)
  • 计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。
  • 为下阶段做铺垫。是STW的。这个阶段并不会实际上去做垃圾的收集。
6、并发清理阶段
  • 识别并清理完全空闲的区域。

6、G1混合回收概述

1、当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC, 该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region
2、这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC

在这里插入图片描述

7、G1混合回收过程

1、并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。
2、默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收。
3、混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。
4、由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。
5、混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

8、G1回收器回收可选过程Full GC

1、G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(STW),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
2、要避免Full GC的发生,一旦发生需要进行调整。什么时候会发生Full GC?
  • 比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到Full GC,这种情况可以通过增大内存解决。
3、导致Full GC的原因可能有两个:
  • Evacuation的时候没有足够的to-space来存放晋升的对象。
  • 并发处理过程完成之前空间耗尽。

9、G1回收器优化建议

1、年轻代大小
  • 避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小。
  • 固定年轻代的大小会覆盖暂停时间。
2、暂停时间不要太过严苛
  • G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间。
  • 评估G1 GC的吞吐量时,暂停时间不要太严苛。目标太过严苛表示要承受更多的垃圾回收开销,而这些会直接影响到吞吐量。

四、垃圾回收器总结

1、七款垃圾回收器的比较

截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器
GC发展阶段:Serial => Parallel(并行)=> CMS(并发)=> G1 => ZGC

在这里插入图片描述

2、怎么选择垃圾回收器

Java垃圾收集器的配置对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升。
  • 优先调整堆的大小让JVM自适应完成。
  • 如果内存小于100M,使用串行收集器。
  • 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器。
  • 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择。
  • 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器。
  • 官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。

五、GC日志分析

1、常用参数配置

-XX:+PrintGC:输出GC日志。类似-verbose:gc
-XX:+PrintGCDetails:输出GC的详细日志
-XX:+PrintGCTimeStamps:输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps:输出GC的时间戳(以日期的形式,如2021-10-27T21: 53: 59.234 +0800)
-XX:+PrintHeapAtGC:在进行GC的前后打印出堆的信息
-Xloggc:./log/gc.log:日志文件的输出路径,./表示项目的根目录

2、各参数使用说明

/**
 * 测试各个日志参数
 * 设置JVM启动参数:-Xms60m -Xmx600m
 */
public class GCLogTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 500; i++) {
            //100MB数组
            byte[] b = new byte[1024 * 100];
            list.add(b);
        }
    }
}
1、设置GC日志参数为:-XX:+PrintGC

在这里插入图片描述

说明:
  • GC、Full GC:GC的类型,GC只在新生代上进行,Full GC包括永久代,新生代,老年代。
  • Allocation Failure:表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。
  • 15263K->14362K:堆内存在GC前的大小和GC后的大小。
  • 58880K:堆总大小。
  • 0.0053829 secs:GC持续的时间。
2、设置GC日志参数为:-XX:+PrintGCDetails

在这里插入图片描述

说明:
  • GC、Full GC:GC的类型,GC只在新生代上进行,Full GC包括永久代,新生代,老年代。如果有Full则说明GC发生了STW。
  • Allocation Failure:表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。
  • PSYoungGen:使用了Parallel Scavenge并行垃圾收集器的新生代GC前后大小的变化。
  • ParOldGen:使用了Parallel Old并行垃圾收集器的老年代GC前后大小的变化。
  • Metaspace:元数据区GC前后大小的变化,JDK1.8中引入元数据区以替代永久代。
  • x.xxxxx secs:GC花费的时间。
  • Times: user:指的是垃圾收集器花费的所有CPU时间。
  • sys:花费在等待系统调用或者系统事件的时间。
  • real:GC从开始到结束的时间,包括其他进程占用时间片的实际时间。
  • [PSYoungGen: 15263K->2536K(17920K)] 15263K->14370K(58880K)
    • 中括号内:GC回收前年轻代大小,回收后大小,(年轻代总大小)
    • 括号外:GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)
  • 补充说明:user代表用户态回收耗时,sys内核态回收耗时,real实际耗时。由于多核线程切换的原因,时间总和可能会超过real时间。
  • 使用Serial收集器在新生代的名字是Default New Generation,因此显示的是DefNew
  • 使用ParNew收集器在新生代的名字会变成ParNew,意思是"Parallel New Generation"
  • 使用Parallel scavenge收集器在新生代的名字是PSYoungGen
3、设置GC日志参数为:-XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps,日志带上了时间和日期

在这里插入图片描述

4、Young GC图示

在这里插入图片描述

5、Full GC图示

在这里插入图片描述

3、GC代码举例

/**
 * 设置JVM启动参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 * 堆初始大小、最大大小20M;新生代大小10m;Eden:Survivor=8:2
 */
public class GCLogTest1 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        testAllocation();
    }

    private static void testAllocation() {
        byte [] allocation1,allocation2,allocation3,allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }
}
1、JDK7中的情况
  • 首先会将3个2M的数组存放到Eden区,然后后面4M的数组来了后,将无法存储,因为Eden区只剩下2M的剩余空间了,那么将会进行一次Young GC操作,将原来Eden区的内容,存放到Survivor区,但是Survivor区也存放不下,那么就会直接晋级存入Old区。

在这里插入图片描述

  • 然后将4M对象存入到Eden区中

在这里插入图片描述

2、JDK8中的情况
  • 与JDK7不同的是,JDK8直接判定4M的数组为大对象,直接放到老年代

在这里插入图片描述

4、常用日志分析工具

常用的日志分析工具有:GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等
推荐使用:GCViewer、GCEasy
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值