垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。
由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。
从不同角度分析垃圾收集器,可以将GC分为不同的类型。
小技巧学Java不同版本新特性
- 语法层面:Lambda表达式、switch、自动拆箱装箱、enum、泛型等
- API层面:Stream API、新的日期时间、Optional、String、集合框架
- 底层优化:JVM优化、GC的变化、元空间、静态域、字符串常量池位置变化
按线程数量分
按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器。
串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
Client模式
下的JVM中和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“stop-the-world
”机制。
按工作模式分
按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
按碎片处理方式分
按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器。
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
吞吐量、暂停时间、内存占用 这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。
这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果。 简单来说,主要抓住两点:
吞吐量
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即:吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)
这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的
吞吐量优先,意味着在单位时间内,STW的时间最短:0.2+0.2=0.4
暂停时间
“暂停时间”是指一个时间段内应用程序线程暂停,让GC线程执行的状态
类似我们在玩游戏的时候,画面会突然卡顿一下,这个时候往往都是因为网络延迟,网络延迟大画面停顿时间久,反之延迟小停顿时间短;同样的道理,当用户线程因GC而暂停,这也是延迟,那么当暂停时间短,就是低延迟
暂停时间优先,意味着尽可能让单次STW的时间最短(即低延迟):0.1+0.1 + 0.1+ 0.1+ 0.1=0.5
吞吐量 VS 暂停时间
高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。直觉上,吞吐量越高程序运行越快。
低暂停时间(低延迟)较好因为从最终用户的角度来看不管是GC,还是其他原因导致一个应用被挂起始终是不好的。
不幸的是”高吞吐量”和”低暂停时间”是一对相互竞争的目标(矛盾)。
在设计(或使用)GC算法时,我们必须确定我们的目标:一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折中。
现在标准:在最大吞吐量优先的情况下,降低停顿时间
垃圾收集机制是Java的招牌能力,极大地提高了开发效率。这当然也是面试的热点。
那么,Java常见的垃圾收集器有哪些?
有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection
,对应的产品我们称为Garbage Collector
。
JDK1.3.1
一起来的是串行方式的SerialGC
,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本Parallel GC
和Concurrent Mark Sweep GC(CMS GC)
跟随JDK1.4.2
一起发布·Parallel GC
(适用于新生代,对应老年代为 Parallel Old GC
)在JDK6
之后成为HotSpot默认GC
。JDK1.7u4
版本中,G1(G First)
可用。JDK9
中G1
变成默认
的垃圾收集器,以替代CMS
。JDK10
中G1垃圾回收器
的并行完整垃圾回收,实现并行性
来改善最坏情况下的延迟。JDK11
发布。引入Epsilon
垃圾回收器,又被称为 "No-Op(无操作)
“ 回收器。同时,引入ZGC
:可伸缩的低延迟垃圾回收器(Experimental测试版
)JDK12
发布。增强G1
,自动返回未用堆内存给操作系统
。同时,引入Shenandoah GC
:低停顿时间的GC(Experimental)。·JDK13
发布。增强ZGC
,自动返回未用堆内存给操作系统。JDK14
发布。删除CMS垃圾回收器。扩展ZGC在 MAC OS 和 Windows 上的应用7种经典的垃圾收集器
7款经典垃圾收集器与垃圾分代之间的关系
垃圾收集器的组合关系
Serial/Serial old
、Serial/CMS
、ParNew/Serial old
、ParNew/CMS
、Parallel Scavenge/Serial Old
、Parallel Scavenge/Parallel O1d
、G1;Serial old
作为CMS出现"Concurrent Mode Failure
"失败的后备预案。Serial+CMS
、ParNew+Serial old
这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。Parallel Scavenge
和Serial old GC
组合(JEP366)那么也许有人就会问了:为什么要有很多收集器,一个不够吗?
因为Java的使用场景很多,移动端,服务器等。所以就需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能。
虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。没有一种放之四海皆准、任何场景下都适用的完美收集器存在,更加没有万能的收集器。所以我们选择的只是对具体应用最合适的收集器。
如何查看默认的垃圾回收器?
-XX:+PrintCommandLineFlags 查看命令行相关参数(在最后就能看到默认使用的垃圾收集器)
jinfo -flag 相关垃圾回收器参数 进程ID (使用命令行指令)
Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。
Serial收集器作为HotSpot中client模式下的默认新生代垃圾收集器。
Serial收集器采用复制算法、串行回收和"stop-the-World"机制的方式执行内存回收。
除了年轻代之外,Serial 收集器还提供用于执行老年代垃圾收集的 Serial old 收集器。
Serial old收集器同样也采用了串行回收和"stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。
Client模式下默认的老年代的垃圾回收器
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)
在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。
在HotSpot虚拟机中,使用-XX:+UseSerialGC
参数可以指定年轻代和老年代都使用串行收集器。
总结
这种垃圾收集器大家了解,现在已经不用串行的了。而且在限定单核cpu才可以用。现在都不是单核的了。
对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Java web应用程序中是不会采用串行垃圾收集器的。
如果说SerialGC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。
ParNew 收集器除了采用并行回收的方式执行内存回收外,和 SerialGC 几乎没有任何区别。
ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器。
由于 ParNew 收集器是基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效?
ParNew 收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速的完成垃圾收集,提升程序的吞吐量
但是在==单个CPU的环境下,ParNew收集器不比Serial收集器高效。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效地避免多线程交互过程中产生地一些额外开销
另一方面,除Serial外,目前只有ParNew GC能与CMS收集器配合工作
在程序中,开发人员可以通过选项"-XX:+UseParNewGC
"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
-XX:ParallelGCThreads
限制线程数量,默认开启和CPU数据相同的线程数。HotSpot 的年轻代中除了拥有 ParNew收集器 是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制。
那么Parallel 收集器的出现是否多此一举?
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel收集器 在JDK1.6时提供了用于执行老年代垃圾收集的Parallel old收集器,用来代替老年代的 Serialold收集器。
Parallel old收集器采用了标记-压缩算法,但同样也是基于并行回收和"stop-the-world"机制。
在程序吞吐量优先的应用场景中,Parallel收集器 和 Parallel old收集器 的组合,在 Server模式 下的内存回收性能很不错。
参数配置
-XX:+UseParallelGC
手动指定年轻代使用Parallel并行收集器执行内存回收任务。
-XX:+UseParalleloldGC
手动指定老年代都是使用并行回收收集器。
-XX:ParallelGCThreads
设置年轻代并行收集器的线程数。一般情况下,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
3+[5*CPU Count]/8]
-XX:MaxGCPauseMillis
设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。
-XX:GCTimeRatio
垃圾收集时间占总时间的比例(=1/(N+1)
)。用于衡量吞吐量的大小。
-XX:MaxGCPauseMillis
参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。-XX:+UseAdaptiveSizePolicy
设置Parallel Scavenge收集器具有自适应调节策略
在这种模式下,年轻代的大小、Eden 和 Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。
在JDK1.5时期,Hotspot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)
收集器
这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。
CMS的垃圾收集算法采用标记-清除算法,并且也会"stop-the-world"
不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew 或者 Serial收集器 中的一个。
在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。(涉及STW的阶段主要是:初始标记 和 重新标记)
stop-the-world
”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。尽管CMS收集器采用的是并发回收(非独占式),但是在其初始标记和重新标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“stop-the-World”,只是尽可能地缩短暂停时间。
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。
因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure
” 失败,这时虚拟机将启动后备预案:临时启用Serial old收集器
来重新进行老年代的垃圾收集,这样停顿时间就很长了。
CMS收集器的垃圾收集算法采用的是标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。
那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。
那为什么CMS不适用 标记-压缩(整理)算法 呢?
答案其实很简单,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact 更适合“stop the world” 这种场景下使用
优点
缺点
会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发FullGC。
CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure"失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象(浮动垃圾)没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
设置的参数
-XX:+UseConcMarkSweepGC
手动指定使用CMS收集器执行内存回收任务。
-XX:+UseParNewGC
打开。即:ParNew(Young区用)+CMS(old区用)+Serial old
的组合。-XX:CMSInitiatingoccupanyFraction
设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
68%
,即当老年代的空间使用率达到68%时,会执行一次CMS回收。92%
-XX:+UseCMSCompactAtFullCollection
用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
-XX:CMSFullGCsBeforecompaction
设置在执行多少次 Full GC 后对内存空间进行压缩整理。
-XX:ParallelCMSThreads
设置CMS的线程数量。
(Paralle1GCThreads+3)/4
,ParallelGCThreads是年轻代并行收集器的线程数。拓展:三色标记法与读写屏障
HotSpot 有这么多的垃圾回收器,那么如果有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?
请记住以下口令:
JDK后续版本中CMS的变化
JDK9新特性:CMS被标记为 Deprecate了(JEP291)
-XX:+UseConcMarkSweepGC
来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。JDK14新特性:删除CMS垃圾回收器(JEP363)
-XX:+UseConcMarkSweepGC
的话,JVM不会报错,只是给出一个warning信息,但是不会exit。JVM会自动回退以默认GC方式启动JVM既然我们已经有了前面几个强大的GC,为什么还要发布Garbage First(G1)?
原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。
G1(Garbage-First)垃圾回收器是在Java7 update4之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。
与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。
官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。
为什么名字叫 Garbage First (G1) 呢?
因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region
)(物理上不连续的)。
G1 GC 有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
在JDK1.7版本正式启用,移除了Experimental
的标识,是JDK9以后的默认垃圾回收器,取代了CMS回收器以及Parallel+Parallel old
组合。被 Oracle 官方称为“全功能的垃圾收集器
”。
与此同时,CMS 已经在 JDK9 中被标记为废弃(Deprecated)。
在 JDK8 中还不是默认的垃圾回收器,需要使用-XX:+UseG1GC
来启用。
与其他GC收集器相比,G1使用了全新的分区算法,其特点如下所示:
并行与并发
分代收集
而是这样的
空间整合
可预测的停顿时间模型(即:软实时 soft real-time)
强实时垃圾回收算法即要求在规定时间内必须完成垃圾回收,但是软实时就会要求松一点,例如说有90%的把握可以在规定的时间内完成垃圾回收。
这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。(也就是延迟可控)
由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。
6-8GB
之间。-XX:+UseG1GC
:手动指定使用 G1垃圾收集器 执行内存回收任务
-XX:G1HeapRegionSize
设置每个 Region 的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048
个区域。默认是堆内存的1/2000。
-XX:MaxGCPauseMillis
设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
-XX:+ParallelGCThread
设置STW时GC线程数的值。最多设置为8
-XX:ConcGCThreads
设置并发标记的线程数。将n
设置为并行垃圾回收线程数(ParallelGCThreads)的1/4
左右。
-XX:InitiatingHeapOccupancyPercent
设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和FullGC,在不同的条件下被触发。
面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
还用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用 G1 可能比 CMS 好:
HotSpot 垃圾收集器里,除了 G1 以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1GC可以采用应用线程承担后台运行的GC工作
使用G1收集器时,它将整个Java堆划分成约2048个大小相同
的独立Region块
,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间
,且为2的N次幂
,即1MB,2MB,4MB,8MB,16MB,32MB
。
可以通过
-XX:G1HeapRegionsize
设定。虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
一个 region 有可能属于 Eden,Survivor或者 Old/Tenured 内存区域。
G1垃圾收集器还增加了一种新的内存区域,叫做 Humongous
内存区域,如图中的H块。
设置H的原因:
每个 Region 都是通过指针碰撞来分配空间
Thread Local Allocation Buffer
线程私有缓冲区G1GC 的垃圾回收过程主要包括如下三个环节:
按照顺时针Young GC -> Young GC + Concurrent Mark -> Mixed GC
顺序,进行垃圾回收;如果全满了,就执行 Full GC。
应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。
当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
标记完成马上开始混合回收过程。
举个例子:一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。
为什么需要 Remembered Set ?
解决方法:
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
Write Barrier
(写屏障)暂时中断这个操作;G1回收过程-年轻代GC
JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区
YGC时,首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set)
然后开始如下回收过程:
第一阶段,扫描根
根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口。
第二阶段,更新RSet
处理dirty card queue
(脏卡表)中的 card,更新 RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用。
第三阶段,处理RSet
识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
第四阶段,复制对象。
此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1
,达到阀值会被会被复制到 old 区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
第五阶段,处理引用
处理Soft,Weak,Phantom,Final,JNI Weak 等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
G1回收过程-并发标记过程
初始标记阶段:标记从根节点直接可达的对象。这个阶段是 STW 的,并且会触发一次年轻代GC。
根区域扫描(Root Region Scanning):G1 GC扫描 Survivor区 直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在 YoungGC 之前完成。
并发标记(Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被YoungGC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收(实时回收)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例),用来判断该区域值不值得回收。
再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning
(SATB)。
独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。这个阶段并不会实际上去做垃圾的收集
并发清理阶段:识别并清理完全空闲的区域。
G1回收过程 - 混合回收
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。
并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。
-XX:G1MixedGCCountTarget
设置)被回收混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。
由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。
-XX:G1MixedGCLiveThresholdPercent,默认为65%
,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%
,意思是允许整个堆内存中有10%的空间被浪费
,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收
。
G1回收可选的过程4 - Full GC
G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
要避免FullGC的发生,一旦发生需要进行调整。什么时候会发生FullGC呢?
导致 G1 Full GC的原因可能有两个:
拓展
从 Oracle 官方透露出来的信息可获知,回收阶段(Evacuation)其实本也有想过设计成与用户程序一起并发执行,但这件事情做起来比较复杂,考虑到G1只是回一部分Region,停顿时间是用户可控制的,所以并不迫切去实现,而选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC)中。
另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。
优化建议
年轻代大小
-Xmn
或-XX:NewRatio
等相关选项显式设置年轻代大小暂停时间目标不要太过严苛
截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。
GC发展阶段:Serial => Parallel(并行)=> CMS(并发)=> G1 => ZGC
不同厂商、不同版本的虚拟机实现差距比较大。HotSpot虚拟机在JDK7/8后所有收集器及组合如下图
Java垃圾收集器的配置对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升。
怎么选择垃圾收集器?
最后需要明确一个观点:
对于垃圾收集,面试官可以循序渐进从理论、实践各种角度深入,也未必是要求面试者什么都懂。但如果你懂得原理,一定会成为面试中的加分项。 这里较通用、基础性的部分如下:
另外,大家需要多关注垃圾回收器这一章的各种常用的参数