JVM_垃圾收集器详解

1、 前言

JVM就是Java虚拟机,说白了就是为了屏蔽底层操作系统的不一致而设计出来的一个虚拟机,让用户更加专注上层,而不用在乎下层的一个产品。这就是JVM的跨平台,一次编译,到处运行。
而JVM中的核心功能其实就是自动化的垃圾回收机制。

在这里插入图片描述

在Java中你使用对象,使用内存,不用担心回收,只管new对象就行了,不用管垃圾的回收。
因为Java当中是自动化的垃圾回收机制。JVM有专门的垃圾回收器,把垃圾回收这件事给干了。

2、分代收集理论

当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
新生代:每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。就比如新生代中的 S0 和 S1 区域。

老年代:对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

PS:标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。

2.1、垃圾收集算法

标记-复制算法
在这里插入图片描述
标记-复制算法是把内存分为大小相等的两块,每次使用其中的一块,当这块内存使用完以后,就会把还存活的对象放到另外一块内存中去,清理刚才使用到的那块空间。分为标记和复制两个阶段:

第一阶段:标记;就是根据Gc Roots标记存活的对象。
第二阶段:复制;把存活的对象复制到另外一块内存中。
缺点:会浪费多余的存储空间。

2.2、标记-清除算法

在这里插入图片描述

标记-清除算法分为标记和清除两个阶段:
第一阶段:标记;就是根据Gc Roots标记存活的对象。
第二阶段:清除;统一回收未被标记的对象。

缺点:
效率问题 (如果需要标记的对象太多,效率不高) ;
空间问题(标记清除后会产生大量不连续的碎片)。

2.3、标记-整理算法

在这里插入图片描述

标记-整理算法是根据老年代的特点设计出的一种标记算法。他的标记过程和标记清除算法一样的,后续不同的是,它会先把所有存活的对象规整的移动到一起,然后把边界外的对象统一回收。

3、垃圾回收中的STW

对于Java项目来说,JVM进行垃圾收集会有一个很大的问题,就是STW(Stop The World)。
就是在垃圾回收的时候会把应用线程(项目中运行的线程)给停止,然后进行垃圾回收,如果STW的时间过程,用户就会感觉到明显的卡顿现象。

3.1、STW带来的问题

(1)安卓手机系统的卡顿
Google 主导的 Android 系统需要解决的一大问题就是显示卡顿问题,通过对 GC 算法的不断演进,停顿时间控制在几个ms 级别。所以这也是Android与苹果IOS系统竞争的一大利器。
(2)证券交易系统的实时性
证券交易系统的实时性要求很高,一般都是需要低卖高卖,差之毫厘谬以千里,如果STW的时间很长,到达几秒钟的级别,这几秒钟后的市场完全不同了。所以用Java来做证券系统,STW的时间越短越好。

3.2、垃圾收集器的发展

为了满足不同的业务场景,Java的GC算法和垃圾收集器都在不同的迭代,对于这些延迟敏感的应用来说,GC停顿已经成为了阻碍Java发展的一大顽疾。

近些年来,服务器的性能越来越好,各种应用可以使用的堆内存也越来越大,其实垃圾收集器的发展都是为了解决STW的停顿时间而一直迭代。为了让STW的停顿时间也跟随着堆大小的增长而不呈现指数级的增长而努力,推出了一系列垃圾收集器。

4、垃圾收集器

在这里插入图片描述

如果说垃圾回收算法是内存收回的理论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器, 我们能做的就是根据具体应用场景选择适合自己的垃圾收集器 。

4.1、单线程垃圾收集器

(1)Serial
在这里插入图片描述

Serial(串行)垃圾收集器,一看名字就知道它是一个单线程收集器。它的单线程的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾手机工作,更重要的是它在进行垃圾手机工作的时候还必须暂停其他的所有工作线程(STW),直到它收集结束。

新生代采用标记-复制算法,老年代采用标记-整理算法。

Serial 虽然是单线程,STW的时间有点长,但是它简单且高效(与其它垃圾收集器相比),它没有线程交互的开销,自然而然的可以获得很高的单线程收集效率。

参数设置:-XX:+UseSerialGC(年轻代) -XX:+UseSerialOldGC(老年代)

(2)Serial Old
Serial Old收集器是Serial收集器的老年代版本,它同样是单线程收集器。
两大用途:
一种用途是在JDK1.5 以及以前的版本中与Parallel Scavenge收集器搭配使用。
一种用途是作为CMS收集器的后备方案。

4.2、多线程垃圾收集器

(1)Parallel Scavenge
在这里插入图片描述
Parallel收集器是Serial收集器的多线程版本,除了使用多线程垃圾收集以外,其余和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。

新生代采用标记-复制算法,老年代采用标记-整理算法。

参数设置:-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)

(2)Parallel Old
Parallel Old收集器是Parallel Scavenge收集器的老年代版本。
使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。

(3)ParNew

ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
新生代采用标记-复制算法,老年代采用标记-整理算法。
参数设置:-XX:+UseParNewGC

4.3、多线程+并发收集器

(1)CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体 验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从它的名字中可以看出它是一种 “标记-清除” 算法的实现。

在这里插入图片描述

GC步骤:

初始标记(STW):暂停所有的其它线程,期间会STW,并记录GC Roots直接能引用的对象,速度很快。
并发标记:并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但 是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
重新标记(STW):重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。
并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。
并发重置:重置本次GC过程中的标记数据。
优点:并发收集、低停顿。

缺点:
对CPU资源敏感(会和服务抢资源);
无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数(- XX:+UseCMSCompactAtFullCollection)可以让jvm在执行完标记清除后再做整理 。
执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并 发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入STW,用serial old垃圾收集器来回收。

核心参数设置:
参数 含义

-XX:+UseConcMarkSweepGC	启用CMS
-XX:ConcGCThreads	并发的GC线程数
-XX:+UseCMSCompactAtFullCollection	FullGC之后做压缩整理(减少碎片)
-XX:CMSFullGCsBeforeCompaction	多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
-XX:CMSInitiatingOccupancyFraction	当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
-XX:+UseCMSInitiatingOccupancyOnly	
只使用设定的回收阈值
(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整 
-XX:+CMSScavengeBeforeRemark	在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
-XX:+CMSParallellnitialMarkEnabled	表示在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled	在重新标记的时候多线程执行,缩短STW

(2)G1
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC 停顿时间要求的同时,还具备高吞吐量性能特征。
在这里插入图片描述
G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。
每个Region大小等于堆除以Region的个数,当然也可以用参数(-XX:G1HeapRegionSize)手动指定,但是不推荐。
G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。

默认年轻代对堆内存的占比是5%,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和 Survivor对应的region也跟之前一样,默认8:1:1。

一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。
G1对于大对象有特殊的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。
Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
在这里插入图片描述
GC流程:
初始标记(STW):暂停所有的其它线程,并记录下GC Roots直接能引用的对象,速度很快;
并发标记:同CMS的并发标记;
最终标记(STW):同CMS的并发标记;
筛选回收(STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期 望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划。

:比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200ms,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。
筛选回收如何实现?

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回 收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。

G1垃圾收集器分类:
Yong GC:YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时 间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC
Mixed GC:不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的 Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做 MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够 的空region能够承载拷贝对象就会触发一次Full GC
Full GC:停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这 个过程是非常耗时的。

特点:
并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop- The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式 让java程序继续执行。
分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
空间整合:与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器;从局部 上来看是基于“复制”算法实现的。
可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了 追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"- XX:MaxGCPauseMillis"指定)内完成垃圾收集。默认的停顿目标为200ms。
核心参数设置:

参数 含义
-XX:+UseG1GC 使用G1收集器
-XX:ParallelGCThreads 指定GC工作的线程数量
-XX:G1HeapRegionSize 指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis 目标暂停时间(默认200ms) -XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent 新生代内存最大空间
-XX:TargetSurvivorRatio Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个 年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold 最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent 老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合 收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了
-XX:G1MixedGCLiveThresholdPercent (默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这 个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget 在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一 会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1HeapWastePercent (默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都 是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清 理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立 即停止混合回收,意味着本次混合回收就结束了。

什么场景适合使用G1:
50%以上的堆被存活对象占用。
对象分配和晋升的速度变化非常大。
垃圾回收时间特别长,超过1秒。
8GB以上的堆内存(建议值) 。
停顿时间是500ms以内。

4.4、ZGC

jdk11 低延迟,浮动垃圾多,适合几百G 以上

5 总结

本章主要讲了JVM中垃圾收集器的发展,从单线程到多线程,再到GC线程和用户线程并发收集的垃圾收集器。
如何选择垃圾收集器:
优先调整堆的大小让服务器自己来选择
如果内存小于100M,使用串行收集器
如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
如果允许停顿时间超过1秒,选择并行或者JVM自己选
如果响应时间最重要,并且不能超过1秒,使用并发收集器
4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC。

猜你喜欢

转载自blog.csdn.net/chuige2013/article/details/129764585
今日推荐