JVM 垃圾回收器

Posted 百事yyds

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM 垃圾回收器相关的知识,希望对你有一定的参考价值。

评估GC的性能指标

  1. 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)

  2. 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。

  3. 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。

  4. 收集频率:相对于应用程序的执行,收集操作发生的频率。

  5. 内存占用:Java堆区所占的内存大小。

  6. 快速:一个对象从诞生到被回收所经历的时间。

  7. 吞吐量、暂停时间、内存占用这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。

  8. 这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果。

  9. 简单来说,主要抓住两点:

    • 吞吐量优先(必然需要降低内存回收的执行频率
    • 暂停时间优先(降低每次执行内存回收时的暂停时间
  10. 现在标准:在最大吞吐量优先的情况下,降低停顿时间

垃圾回收器的发展史

  1. 1999年随JDK1.3.1一起来的是串行方式的Serial GC,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本
  2. 2002年2月26日,Parallel GC和Concurrent Mark Sweep GC跟随JDK1.4.2一起发布·
  3. Parallel GC在JDK6之后成为HotSpot默认GC。
  4. 2012年,在JDK1.7u4版本中,G1可用。
  5. 2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS。
  6. 2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
  7. 2018年9月,JDK11发布。引入Epsilon 垃圾回收器,又被称为 "No-Op(无操作)“ 回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)
  8. 2019年3月,JDK12发布。增强G1,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC:低停顿时间的GC(Experimental)。
  9. 2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。
  10. 2020年3月,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macOS和Windows上的应用 

7款经典的垃圾收集器

  1. 串行回收器:Serial、Serial old
  2. 并行回收器:ParNew、Parallel Scavenge、Parallel old
  3. 并发回收器:CMS、G1

 

  1. 新生代收集器:Serial、ParNew、Parallel Scavenge;

  2. 老年代收集器:Serial old、Parallel old、CMS;

  3. 整堆收集器:G1

 

  1. 两个收集器间有连线,表明它们可以搭配使用:

    • Serial/Serial old
    • Serial/CMS (JDK9废弃)
    • ParNew/Serial Old (JDK9废弃)
    • ParNew/CMS
    • Parallel Scavenge/Serial Old (预计废弃)
    • Parallel Scavenge/Parallel Old
    • G1
  2. 其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案。

  3. (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。

  4. (绿色虚线)JDK14中:弃用Parallel Scavenge和Serial Old GC组合(JEP366)

  5. (青色虚线)JDK14中:删除CMS垃圾回收器(JEP363)

查看默认垃圾收集器

  1. -XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
  2. 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID

Serial回收器:

                    Serial   复制算法 新生代      Serial Old 标记压缩算法 老年代 【串行】

  1. Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。

  2. Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。

  3. Serial收集器采用复制算法、串行回收和"Stop-the-World"机制的方式执行内存回收。

  4. 除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial old收集器同样也采用了串行回收和"Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。

  5. Serial Old是运行在Client模式下默认的老年代的垃圾回收器,Serial Old在Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用(JDK9取消)②作为老年代CMS收集器的后备垃圾收集方案(JDK14取消)

 

Serial 回收器的优势

  1. 优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。运行在Client模式下的虚拟机是个不错的选择。
  2. 在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。
  3. 在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。
    • 等价于新生代用Serial GC,且老年代用Serial Old GC

总结

  1. 这种垃圾收集器大家了解,现在已经不用串行的了。而且在限定单核CPU才可以用。现在都不是单核的了。

  2. 对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Java Web应用程序中是不会采用串行垃圾收集器的。

ParNew 回收器:

                                                 并行回收(新生代) 复制算法

  1. 如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。
    • Par是Parallel的缩写,New:只能处理新生代
  2. ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。

ParNew 回收器与 Serial 回收器比较

Q:由于ParNew收集器基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效?

A:不能

  1. ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。
  2. 但是在单个CPU的环境下,ParNew收集器不比Serial收集器更高效。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。
  3. 除Serial外,目前只有ParNew GC能与CMS收集器配合工作

设置 ParNew 垃圾回收器

  1. 在程序中,开发人员可以通过选项"-XX:+UseParNewGC"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。

  2. -XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数。

Parallel回收器:吞吐量优先

            Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制

            Parallel Old收集器采用了标记-压缩算法,基于并行回收和"Stop-the-World"机制

  1. HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制。

  2. 那么Parallel收集器的出现是否多此一举?

    • 和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
    • 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。(动态调整内存分配情况,以达到一个最优的吞吐量或低延迟)
  3. 高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。

  4. Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器。

  5. 在Java8中,默认是此垃圾收集器。

Parallel Scavenge 回收器参数设置

  1. -XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。

  2. -XX:+UseParallelOldGC:手动指定老年代都是使用并行回收收集器。

    • 分别适用于新生代和老年代

    • 上面两个参数分别适用于新生代和老年代。默认jdk8是开启的。默认开启一个,另一个也会被开启。(互相激活)

  3. -XX:ParallelGCThreads:设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。

    1. 在默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量。

    2. 当CPU数量大于8个,ParallelGCThreads的值等于3+[5*CPU_Count]/8]

  4. -XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。

    1. 为了尽可能地把停顿时间控制在XX:MaxGCPauseMillis 以内,收集器在工作时会调整Java堆大小或者其他一些参数。
    2. 对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
    3. 该参数使用需谨慎。
  5. -XX:GCTimeRatio垃圾收集时间占总时间的比例,即等于 1 / (N+1) ,用于衡量吞吐量的大小。

    1. 取值范围(0, 100)。默认值99,也就是垃圾回收时间占比不超过1。

    2. 与前一个-XX:MaxGCPauseMillis参数有一定矛盾性,STW暂停时间越长,Radio参数就容易超过设定的比例。

  6. -XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自适应调节策略

    1. 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。

    2. 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。

CMS回收器:低延迟

这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作采用标记-清除算法   运用与老年代

  1. CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
    • 目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
  2. 不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作(因为实现的框架不一样,没办法兼容使用),所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
  3. 在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。

工作原理

 

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

CMS分析

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

CMS 的优点与弊端

优点

  1. 并发收集
  2. 低延迟

弊端

  1. 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
  2. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
  3. CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure"失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。

CMS 参数配置

  • -XX:+UseConcMarkSweepGC:手动指定使用CMS收集器执行内存回收任务。

    开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区)+CMS(Old区)+Serial Old(Old区备选方案)的组合。

  • -XX:CMSInitiatingOccupanyFraction:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。

  1. JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%

  2. 如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。

  • -XX:+UseCMSCompactAtFullCollection:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。

  • -XX:CMSFullGCsBeforeCompaction:设置在执行多少次Full GC后对内存空间进行压缩整理。

  • -XX:ParallelCMSThreads:设置CMS的线程数量。

  1. CMS默认启动的线程数是 (ParallelGCThreads + 3) / 4,ParallelGCThreads是年轻代并行收集器的线程数,可以当做是 CPU 最大支持的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。 

小结

HotSpot有这么多的垃圾回收器,那么如果有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?

  1. 如果你想要最小化地使用内存和并行开销,请选Serial GC;
  2. 如果你想要最大化应用程序的吞吐量,请选Parallel GC;
  3. 如果你想要最小化GC的中断或停顿时间,请选CMS GC。

G1回收器:区域化分代式

  1. 为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。
  2. 官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。
  3. 后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
  4. 面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
  5. JDK9以后的默认垃圾回收器
  6. G1在JDK8中还不是默认的垃圾回收器,需要使用-XX:+UseG1GC来启用。

G1的优势

  1. 并行与并发兼备
    • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
    • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  2. 分代收集
    • 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
    • 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
    • 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;

Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。

可预测的停顿时间模型

可预测的停顿时间模型(即:软实时soft real-time)

这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

  1. 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
  2. G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
  3. 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。

G1 回收器的缺点

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

G1 参数设置

  • -XX:+UseG1GC:手动指定使用G1垃圾收集器执行内存回收任务

  • -XX:G1HeapRegionSize:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。

  • -XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间指标,JVM会尽力实现,但不保证达到。默认值是200ms

  • -XX:+ParallelGCThread:设置STW工作线程数的值。最多设置为8

  • -XX:ConcGCThreads:设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGcThreads)的1/4左右。

  • -XX:InitiatingHeapOccupancyPercent:设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。

 G1垃圾回收流程

  • 年轻代GC(Young GC)
  • 老年代并发标记过程(Concurrent Marking)
  • 混合回收(Mixed GC)
  • (如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。)

具体步骤:

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

G1回收过程一:年轻代 GC

  1. 第一阶段,扫描根

    根是指GC Roots,根引用连同RSet记录的外部引用作为扫描存活对象的入口。

  2. 第二阶段,更新RSet

  3. 第三阶段,处理RSet

    识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。

  4. 第四阶段,复制对象。

    • 此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象
    • 如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。
    • 如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
  5. 第五阶段,处理引用

    处理Soft,Weak,Phantom,Final,JNI Weak 等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

G1回收过程二:并发标记过程

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

G1回收过程三:混合回收过程

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

G1 回收可选的过程四:Full GC

  1. G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。

  2. 要避免Full GC的发生,一旦发生Full GC,需要对JVM参数进行调整。什么时候会发生Ful1GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到Full GC,这种情况可以通过增大内存解决。

导致G1 Full GC的原因可能有两个:

  1. EVacuation的时候没有足够的to-space来存放晋升的对象;
  2. 并发处理过程完成之前空间耗尽。

七种垃圾回收器总结

 

  1. 优先调整堆的大小让JVM自适应完成。
  2. 如果内存小于100M,使用串行收集器
  3. 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
  4. 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
  5. 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
  6. 官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。

最后需要明确一个观点:

  1. 没有最好的收集器,更没有万能的收集算法
  2. 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

GC日志分析       推荐:GCeasy

  1. -XX:+PrintGC :输出GC日志。类似:-verbose:gc
  2. -XX:+PrintGCDetails :输出GC的详细日志
  3. -XX:+PrintGCTimestamps :输出GC的时间戳(以基准时间的形式)
  4. -XX:+PrintGCDatestamps :输出GC的时间戳(以日期的形式,如2013-05-04T21: 53: 59.234 +0800)
  5. -XX:+PrintHeapAtGC :在进行GC的前后打印出堆的信息
  6. -Xloggc:…/logs/gc.log :日志文件的输出路径

  Major GC

 

  Full GC

ZGC(了解 以后可能会成为主流)

  1. ZGC与Shenandoah目标高度相似,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停颇时间限制在十毫秒以内的低延迟。

  2. 《深入理解Java虚拟机》一书中这样定义ZGC:ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-压缩算法的,以低延迟为首要目标的一款垃圾收集器。

  3. ZGC的工作过程可以分为4个阶段:并发标记 - 并发预备重分配 - 并发重分配 - 并发重映射 等。

  4. ZGC几乎在所有地方并发执行的,除了初始标记的是STW的。所以停顿时间几乎就耗费在初始标记上,这部分的实际时间是非常少的。

面向大堆的 AliGC

     AliGC是阿里巴巴JVM团队基于G1算法,面向大堆(LargeHeap)应用场景。

JVM垃圾回收机制 (垃圾判断,垃圾回收算法,垃圾回收器,五种引用)jvm

👨‍🎓博主主页爪哇贡尘拾Miraitow
📆传作时间:🌴2022年1月9日🌴
📒内容介绍:最近在学习JVM所以会时不时更新有关内容
📚参考资料:黑马JVM 码云
🔗参考链接:👉JVM垃圾回收机制
⏳简言以励:列位看官,且将新火试新茶,诗酒趁年华
📝内容较多有问题希望能够不吝赐教🙏
🎃 欢迎点赞 👍 收藏 ⭐留言 📝


大家都知道的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。

1.1 如何判断对象可以回收♻

1、 引用计数器法

引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。但是他有一个缺点是不能解决循环引用的问题。

我们从上面可以看到有这个过程
A对象引用对象B B的计数加一
B对象引用对象A A的计数加一
各自的引用计数不能归零,导致这两个对象不能作为垃圾回收,造成了内存泄漏

2、可达性分析算法

首先要确定一系列根对象,何为根对象?

可以理解为 肯定不能被当成垃圾回收的对象。 在垃圾回收之前,我们首先会对堆内存中的对象进行扫描,判断每一个对象是不是被 根对象直接或间接的引用,如果是,那么这个对象就不能被垃圾回收,反之就可以作为垃圾回收。

举个栗子🌰:

我们夏天吃的葡萄,葡萄向上一提,连🍇根部的葡萄果,就是不可回收的,落在盘子中的葡萄就可以作为垃圾♻回收

  • Java 虚拟机中的垃圾回收机器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root 对象 为起点的引用链找到该对象,如果能找到,表示这个对象需要保留,若找不到,表示可以回收
  • 哪些对象可以作为 GC Root ?
    • 通过 Memory Analyzer ( MAT )工具,可以形象的看到哪些 GC Root 对象
    • 下载地址:https://www.eclipse.org/downloads/download.php?file=/mat/1.8/rcp/MemoryAnalyzer-1.8.0.20180604-win32.win32.x86_64.zip&mirror_id=1290

通过使用MAT以后出现下表,可以看到哪些对象是根对象,且其把根对象分为4大类

第一类:System Class :系统类,由启动类加载类加载的类,且肯定不会被垃圾回收(试想系统类没了还怎么跑程序

第二类:Native Stack:Java虚拟机在执行时,偶尔需调用操作系统的方法,本地方法栈

第三类:Thread:活动线程。正在运行的线程,能把活动线程中所使用的对象当成垃圾回收吗?显然不行,线程正在运行,这时我们把它正在使用的对象当成垃圾回收了,那就没法继续运行了

每次方法调用都会产生一个栈帧,即栈帧内所使用的对象,可以作为根对象

下图显示的是,主线程栈帧内用到的一些变量情况

注意,要把引用变量和对象分开,就好比下面代码,list 只是一个引用,它存在于活动栈帧中,它是一个局部变量,而 new ArrayList<>() 是存储在 堆 里的

看下图的 ArrayList ,那么它是不是由我们上面代码 list 的引用所引用?它就是一个根对象,在活动线程执行过程中,局部变量所引用的对象,是可以作为根对象的

包括方法参数 String[] args,所引用的字符串数组对象,也是根对象

List<Object> list = new ArrayList<>();

第四类:Busy Monitor:正在加锁的对象,比如同步锁机制,synchronized 关键字,被 synchronized 加锁的对象不能当成垃圾回收,如果被回收,将来谁来解锁?

2.list 置空之前,存储一个快兆名为:b.bin

从下图可以发现,没有 ArrayList 那个对象了,为什么没有了呢?

代码 list = null ,局部变量已经 置为 null 了,也就是它不再引用 ArrayList 对象,而我们执行了 live 所以进行垃圾回收,垃圾回收就会把不再有人引用它的ArrayList对象给回收掉,所以在根对象列表中就找不到它了

1.2 Java 中的五种引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

上图中,所有实线都表示强引用,虚线表示:软、弱、虚、终结器引用。

其实像我们平时用的所有引用,都属于强引用,比如 创建了一个对象,把这个对象通过 “ = ” 赋值给了一个变量,那么这个对象就 强引用 了这个对象。

  1. 强引用:只要沿着 GC Root 的引用链能够找到它,那么它就不会被垃圾回收。比如上图,沿着 C对象(GC Root) 能找到 A1对象,那么 A1对象 就不能被垃圾回收。只有 GC Root 对象对 A1对象 的引用都断开时,才会被垃圾回收。

  2. 软引用:还是参照上图,只要 A2对象 没有被直接的 强引用 所引用(上图 A2对象被 B对象 直接引用,不能被回收),那么当发生垃圾回收时,它就有可能被垃圾回收。比如上图中, A2对象 被 C对象(GC Root)间接引用。那么 A2对象 什么时候才能被垃圾回收呢?当发生垃圾回收时,并且内存不够时,就发生垃圾回收,但垃圾回收一次后,发现内存仍不够,这时就会把 软引用 所引用的对象释放掉,它认为 软引用 所引用的对象不够重要。

  3. 弱引用:只要发生垃圾回收,不管内存够不够,都会把 弱引用 引用的对象回收

    软、弱引用还可以配合 引用队列 一起工作,什么意思呢?就是当 软、弱引用 的对象被回收掉后,那么 软、弱引用 其实本身也是一个对象,那如果再创建它们时为其分配了 引用队列 ,那么 当 软、弱引用 的对象被回收掉后,它们就会进入一个 引用队列

    那么问题来了,为什么要做这么一个处理呢?因为,不管是 软、弱引用 ,它们自身也要占用一定的内存空间,那么如果想对它们占用的内存空间进行 释放,那么就需要用到 引用队列 来找到它们。比如它们可能还被强引用 所引用,那么就可以在 引用队列 中遍历它们,然后释放

  4. 虚引用:与 软、弱引用不同,虚引用 必须配合 引用队列 使用,也就是创建 虚引用 对象时,它就会关联一个 引用队列。在创建 ByteBuffer 实现对象时,它就会创造一个 Cleaner 的虚引用对象,ByteBuffer 会分配一块 直接内存,并且会把 直接内存 地址传给 虚引用对象,那么为何要做这个操作?将来如果 ByteBuffer 没有强引用所引用它了,那么 ByteBuffer 就可能被垃圾回收,但它被垃圾回收了,它所分配的 直接内存 并不能被 Java 的垃圾回收机制管理,那怎么解决?当 ByteBuffer 被垃圾回收时,让 虚引用 对象进入 引用队列 ,而 虚引用 所在的 引用队列 会由一个叫 Reference Handler 的线程定时去 引用队列 找,看看有无一个 新入队 的 Cleaner ,如果有,那么它就会调用 Cleaner 对象的 clean() 方法, clean() 方法就会根据 直接内存地址调用 Unsafe.freeMemory() 方法,把直接内存释放掉,这样就不会由 直接内存 导致的内存泄漏。

  5. 终结器引用:与 软、弱引用不同,终结器引用 必须配合 引用队列 使用,也就是创建 虚引用 对象时,它就会关联一个 引用队列。我们都知道,所有的 Java 对象,都会继承一个 Object 父类,而 Object 父类都有一个 finalize() 终结方法,当对象 重写了 终结方法,并且没有被 强引用 所引用,那么它就可以被垃圾回收,那么问题来了,这个 finalize() 终结方法 什么时候会被调用?其实,你重写了 finalize() 终结方法,你就希望这个终结方法将来在这个对象垃圾回收时被调用吧?其实它就是靠这个 终结器引用来达到目的的。如上图,当 A4 对象被垃圾回收时,终结器引用就会被加入 引用队列 ,但注意,此时 A4对象 还没被垃圾回收,即不是立刻回收,而是先将 终结器引用 放入 引用队列,再由一个 优先级很低的线程去查看 引用队列 中是否有 终结器引用,如果有,就会根据这个 终结器引用 找到 要作为垃圾回收的对象 ,并且调用 finalize()方法,等下一次垃圾回收时,就能把这个对象占用的内存垃圾回收掉。效率低

1.3 垃圾回收算法

定义:具体的垃圾回收,其实也依赖于一些 垃圾回收算法,常见的有:标记清除、标记整理、复制。这三种算法

1、标记清除

具体步骤:

  1. 先标记,看看哪些对象可以是垃圾,把没有被引用的对象标记出来

  2. 清除垃圾,把被标记的内存空间释放

注意:这里可能会产生一个误区,释放,是不是意味着要把内存的每个字节进行清0操作呢?

其实并不会,只需要把这个被清除的对象的 起始、结束地址 记录下来,放在空闲的地址列表里就可以,下次再分配新对象时,就到这个空闲的地址列表里去找,看看有没有一块足够的空间容纳新对象,并不会把占用的内存做清0操作。

优点:速度快,只需把垃圾对象内存的起始、结束地址做记录就可以,无需额外处理

缺点:产生内存碎片, 即清除后不会再对内存空间进行整理操作,所以当我们再次分配一个较大的对象时,比如 数组,而 数组 的分配需要一段连续的内存空间,但是清除后的每一个内存空间都不够 数组 存放,而其实总的内存空间却可以容纳我的数组对象,但由于清理后的内存空间不连续,所以造成新对象仍不能有一个有效的内存给新的数组对象用,所以会造成内存溢出问题

2、标记整理

具体步骤:

  1. 先标记,看看哪些对象可以是垃圾,把没有被引用的对象标记出来

  2. 为了避免 “标记清除” 算法产生内存碎片。在清理垃圾的过程中,会把可用的对象向前移动,让内存更为紧凑,整理之后,我们发现,内存空间更为紧凑了,这样就不会造成 “标记清除” 算法产生内存碎片

优点:没有内存碎片

缺点:由于清理的过程涉及到 对象的移动,那么效率自然就变低。比如我们有一些局部变量,而这些局部变量引用了这个移动的对象,所以自然需要改变引用的引用地址,涉及到内存区块的拷贝移动,还要把所有引用的地址改变,所以效率低,速度慢

3、复制算法

定义:把内存区划成大小相等的两个区,即下图的 FROMTO ,其中, TO 这个内存区始终空闲,里面一个对象都没有

步骤:

  1. 先标记,看看哪些对象可以是垃圾,把没有被引用的对象标记出来

  2. 然后从 FROM 区,把存活的对象(没被垃圾回收的对象)转移到 TO 内存区,复制的过程中,会完成碎片的整理,即不会产生内存碎片,复制完,清空 FROM 内存区的垃圾

  3. 交换 FROMTO 的位置,原来的 TO 变成 FROM , 原来的 FROM变成 TO,即 TO 总是空闲的区域

优点:不会产生内存碎片

缺点:会占用双倍的内存空间

1.4 分代垃圾回收

1、定义

前面我们学习了三种垃圾回收算法,但实际上 JVM 虚拟机不会单独采用某一种算法,而是结合三种算法协同工作,具体的实现称为:分代垃圾回收。

把整个堆内存分为两块:新生代、老年代。而新生代又分为3个部分,即:伊甸园、幸存区 From、幸存区 To。

那么问题来了,为什么要做区域划分呢?主要是因为 Java 中有的对象需要长期使用,长时间使用的对象,就把其放到 老年代 中,而那些用完了就可以回收掉的对象,就可以放在 新生代 中,这样就可以根据对象的生命周期不同,进行不同的垃圾回收策略,老年代的垃圾回收机制,就很久触发一次,而新生代垃圾回收触发的几率就多一点,这样针对不同的区域我们采用不同的算法就可以对垃圾回收有一个更好的管理

2、分代垃圾回收机制工作原理

  1. 当我们创建一个新的对象时,那么这个对象默认就会使用 伊甸园 这块空间,接下来可能会有很多对象被创建,所以也会分配到 伊甸园 中。而随着对象创建,内存逐渐增加,当内存不够时,若再想往 伊甸园 中添加对象,这时就会触发一次 垃圾回收。

  2. 新生代的垃圾回收一般称为:Minor GC ,而 Minor GC 触发后,就会采用 可达性分析算法 沿着 GC Root 引用链去 伊甸园 中查找,看这些对象有用或者可以被当成垃圾回收,即先 标记,标记完成后,就会采用 复制 算法,把存活的对象复制到 幸存区To ,而 复制到幸存区To的对象,寿命就会加1,而至于 伊甸园 中的对象,就可以全部被当成垃圾回收。

  3. 但我们知道,完成一次 复制 算法后,FromTo 的位置就会互换,但内存空间不会变,即只是交换位置。这就是第一次垃圾回收产生的效果

  4. 完成第一次垃圾回收后,此时 伊甸园 内存空间足够了,又可以往里面添加对象了

  5. 又过了一段时间,此时 伊甸园 的内存空间又满了,又需进行 第二次垃圾回收 ,第二次垃圾回收,除了要把 伊甸园 存活的对象找到以外,还需在 幸存区To 中判断有无需要继续存活的对象,即 幸存区To 中的对象也有可能在第二次垃圾回收中被回收,与第一次垃圾回收类似,把存活的对象复制到 幸存区To ,而 复制到幸存区To的对象,寿命就会加1,而至于 伊甸园 中的对象,就可以全部被当成垃圾回收。且完成一次 复制 算法后FromTo 的位置需要互换

  6. 但 幸存区 中的对象不会一直存在,当超过一定寿命时(默认 15 ),就会把该对象存到 老年代

一个冷知识为什么默认为15?
对象的GC年龄肯定和对象相关,信息肯定保存在对象的某块区域,我们平时看不到是因为Java对开发者屏蔽了一些数据。

我们平时写代码,编写的只是对象的实例数据,但其实Java对象除了自身的实例数据外,还包括头信息和对齐字节,如下图所示:

对象的GC年龄就保存在对象的头信息里,除此之外,头信息还记录了对象的锁标记,大家常常说的“Java锁的是对象而不是代码”就是这个道理,上锁修改的是头信息中的锁标记。

对象的头信息内存分配不同的JVM实现不一样,一般来说32位占8字节,64位占16字节(开启压缩指针占12字节)。

因为Object Header采用4个bit位来保存年龄,4个bit位能表示的最大数就是15!

  1. 但当 老年代新生代 内存空间同时不足时,这时就会触发一次 Full GC ,进行 老年代 的垃圾回收,此时就会完成一整轮垃圾回收,从新生代到老年代

注意: 其实,当发生 Minor GC 时,就会发生一次:stop the world。什么意思呢?其实就是在发生垃圾回收时,必须暂停其它用户线程,由垃圾回收线程完成垃圾回收,当把对象从 伊甸园、幸存区From 拷贝到 幸存区To 时,即等垃圾回收的动作做完后,其它的用户线程才能继续运行。

那么问题来了,为什么需要把其它用户线程都暂停呢?
这是因为在垃圾回收的过程中,涉及到对象的复制,也就是对象地址会发生改变,而这种情况下,如果多个用户线程都在运行,就会造成混乱,即对象都在移动,其它的线程再根据原来的地址访问这个对象,就访问不到了。

3、相关 VM 参数

1.5 垃圾回收器

1、类型

  1. 串行
    • 单线程
    • 堆内存较小,适合个人电脑
  2. 吞吐量优先
    • 多线程
    • 堆内存较大,多核 CPU
    • 让单位时间内,STW 的时间最短 ,如 0.2 + 0.2 = 0.4,垃圾回收时间占比最低,这样就称 吞吐量高
  3. 响应时间优先
    • 多线程
    • 堆内存较大,多核 CPU
    • 尽可能让单次 STW 的时间最短,如 0.1 + 0.1 + 0.1 + 0.1 + 0.1

2、串行

1、需要配置以下信息

-XX:+UseSerialGC

-XX:+UseSerialGC = Serial + SerialOld
Serial:工作在 新生代,采用的回收算法是:复制
SerialOld:工作在 老年代,采用的回收算法是:标记整理
且 新生代和老年代的垃圾回收器是分别运行的
若 新生代的内存不足,会采用 Serial 完成垃圾回收
若 老年代的内存不足,会采用 Serial 完成 Minor GC,SerialOld 完成 Full GC

那么具体它的回收过程是怎样的呢?如上图,假设我们现在有多核CPU,刚开始这些线程都在运行,运行一段时间后,发现堆内存不够了,触发了一次垃圾回收,这时要让这些线程在一个安全点停下来,那为什么要让这些线程停下来呢?因为可能在垃圾回收的过程中,部分对象的地址要发生改变,为了保证安全的使用这些对象地址,则需要所有的用户线程到达一个安全点停下来,这时完成垃圾回收就不会有其它线程干扰了,否则如果移动了对象,地址改变了,其它线程来访问这个对象,就可能找到错误的地址的对象,程序就会出问题。

Serial + SerialOld 都是单线程的垃圾回收器,所以只有一个垃圾回收线程在运行,当这个垃圾回收线程在运行时,其它的用户线程就会进入 阻塞 状态,等待垃圾回收线程的结束。完事后再继续运行

3、吞吐量优先

1、需要配置以下信息

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
在 jdk 1.8 默认开启上面两个开关
-XX:+UseParallelGC:新生代并发垃圾回收器,采用复制算法
-XX:+UseParallelOldGC:老年代并发垃圾回收器,采用标记整理算法

-XX:+UseAdaptiveSizePolicy 采用自适应的新生代大小调整策略
-XX:GCTimeRatio=ratio 与 MaxGCPauseMillis 冲突
-XX:MaxGCPauseMillis=ms 最大暂停毫秒数 默认200ms
-XX:ParallelGCThreads=n 控制垃圾回收线程数

2、工作流程

  1. 刚开始这些线程都在运行,运行一段时间后,发现堆内存不够了,触发了一次垃圾回收,这时要让这些线程在一个安全点停下来。与 串行 不同,垃圾回收器会开启多个垃圾回收线程一起回收,垃圾回收线程数默认与CPU核数相关,但因为同时开启多个垃圾回收线程,所以在回收时,CPU会瞬间飚到100%。
  2. 但是 垃圾回收线程 数可以通过 -XX:ParallelGCThreads=n 来控制
  3. ParallelGC 比较智能,可以根据设置的参数,调整堆的大小以达到期望目标
  4. -XX:GCTimeRatio=ratio 调整垃圾回收时间与总时间占比 1/(1+ratio)
  5. -XX:MaxGCPauseMillis=ms 最大暂停毫秒数 默认200ms。

4、响应时间优先

  1. 相关参数

    -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
    UseConcMarkSweepGC:concurrent(并发)、Mark(标记)、Sweep(清除),一款基于标记清除的并发回收器
    
    
    -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=thread
    -XX:CMSInitiatingOccupancyFraction=percent
    -XX:+CMSScavengeBeforeRemark 一个开关
    
  2. 运行流程

    首先多个 CPU 开始并行执行,老年代发生了内存不足,线程在安全点停下来,这时 UseConcMarkSweepGC垃圾回收器开始工作,执行 初始标记 动作,在这个 初始标记 动作时,仍需要 STW ,即其它用户线程进入 阻塞 ,暂停下来,但是 初始标记 很快,因为只完成标记根对象,完成该动作后,用户线程就可以运行了,与此同时,垃圾回收线程继续 并发标记 ,把剩余的垃圾找出来,但这时与其它用户线程是 并发进行的,当完成 并发标记 后,还需再 重新标记 ,这时又需要 STW ,为什么呢?因为 我们在 并发标记 的同时,用户线程也在工作,工作的时候,现有的对象的引用可能就会改变,所以 并发标记 结束后,仍需再进行 STW,完成后,再进行 并发清理

    整个过程,只有 初始标记重新标记 需要 STW,整个过程时间短,符合 响应时间优先

    虽然这种垃圾回收器对CPU的占用没有 UseParallelGC 高,就拿下图的例子,4核的CPU,只用了1核去做垃圾回收,所以对 CPU 的占用并不高,但是,用户线程也在运行,本来用户工作线程可以满负荷工作的,即本来4核CPU都能使用上,但是其中1核被垃圾回收占用了,所以用户工作线程只能占用原来的 3/4 的CPU的数量,所以对整个应用程序的吞吐量有一定影响,

  3. 参数解读

    • UseConcMarkSweepGC 一款基于标记清除的,工作在老年代并发回收器,与之配合的是 UseParNewGC ,是一款工作在 新生代的 基于 复制算法的垃圾回收器,并发,指我们在进行垃圾回收的同时,其它用户线程也能同时进行,即用户线程和垃圾回收器的并发执行,但其在某几个阶段也需要进行 STW 。且有的时候,UseConcMarkSweepGC 会发生并发失败的情况,这时会采取补救措施,让老年代的垃圾回收器,从UseConcMarkSweepGC 并发垃圾回收器退化到 SerialOld 单线程垃圾回收器。
    • ParallelGCThreads 并行垃圾回收线程数
    • ConcGCThreads 并发垃圾回收线程数,建议设置为 ParallelGCThreads 的 1/4,如4核CPU,设置1个线程去进行垃圾回收,剩下3个留个用户线程去工作
    • CMSInitiatingOccupancyFraction=percent CMS 垃圾回收器在工作过程中,由于其它用户线程还可以继续运行,这时也可能产生垃圾,但是 并发清理 的同时不能把这些新的垃圾回收掉,所以就得等到下一次垃圾回收时才能清理,我们把这些垃圾称为 浮动垃圾,这时产生了新的问题,因为在垃圾回收时可能产生新的垃圾,它又不能像其它垃圾回收器那样,等到整个堆内存不足了再垃圾回收,那样的话,那些新垃圾就无处可放了,所以得预留一定空间保留这些浮动垃圾。这个参数就是控制我们何时进行垃圾回收的时机,参数类型是百分比,比如设置为:80。表示只有老年代的内存占用到达 80% 时,就执行一次垃圾回收
    • CMSScavengeBeforeRemark重新标记 之前,对 新生代 进行垃圾回收。有可能 新生代的对象会引用老年代的对象,这时在 重新标记会扫描整个堆,然后通过新生代扫描引用老年代做可达性分析,但这样堆性能影响大,因为新生代创建的对象有点多,其中可能有很多都是垃圾对象 ,所以就算找到了,将来也要被回收掉,所以相当于做了一些无用功
  4. 并发失败

    由于该垃圾回收器采用 标记清除算法,所以可能产生较多的垃圾碎片,这样就会造成将来如果 分配对象时,经历一次 Minor GC后不足,由于老年代碎片过多也不足,这样就会造成 并发失败,及由于碎片过多造成并发失败,这时 UseConcMarkSweepGC 老年代垃圾回收器就不能正常工作了,这时UseConcMarkSweepGC 就会退化为 SerialOld ,做一次 单线程的、串行的 垃圾回收,清理完碎片才能继续工作。

    如果发生并发失败了,垃圾回收时间就会邹增,导致本来是 响应时间优先 变成 响应时间过长

5、Garbage First(G1)

1、简介

  1. JDK9 默认的垃圾回收器

  2. 适用场景

    • 同时注重 吞吐量(Throughput)和 低延迟(Low latency),默认的暂停目标是 200ms。
      • 也可在垃圾回收线程运行的同时,其它用户线程继续运行
    • 超大堆内存,会将 堆 划分为多个大小相等的 Region(区域),每个 Region 都可作为独立的 Region,每个Region 都可以作为 伊甸园、幸存区、老年代 。
      • CMS 垃圾回收器都属于 并发的垃圾回收器,在堆内存较小的情况下,暂停时间不相上下。但若随着堆内存越来越大,那么 G1 的优势就比 CMS 明显了
    • 整体上采用了:标记整理算法,避免产生垃圾碎片。两个Region之间是 复制 算法。
  3. 相关 JVM 参数

    -XX:+UseG1GC	// jdk 1.8 不是默认的,需做设置
    -XX:+G1HeapRegionSize = size //划分区域
    -XX:MaxGCPauseMillis = time // 暂停目标时间
    

2、G1垃圾回收阶段

3、G1-新生代回收

Young Collection

  • 会 STW

    首先,G1 垃圾回收器会把整个堆内存划分成很多个 Region,每个 Region 都可以独立作为 伊甸园、幸存区、老年代,白色框框 表示 空闲的区域,当执行类加载时新创建的一些对象,就会分配到 E 伊甸园 区,随着 E 伊甸园 区被占满,会触发一次 Young Collection,这时也会 STW,当然这个时间比较短

    新生代垃圾回收就会把 幸存的对象,以 复制 算法放入 幸存区S

    随着对象的增多,幸存区 内存不足或者幸存区的对象超过一定年龄,又会触发 新生代垃圾回收,这时,幸存区一部分对象就会晋升到 老年代O,而不够年龄的幸存区对象,会继续 复制 到其它幸存区S

4、Young Collection + CM

**定义:**新生代的垃圾回收和并发阶段

  • 我们在进行垃圾回收时,需要进行 初始 标记和 并发标记。初始标记,就是要找到那些 GC Root(根对象),而 并发标记 就是从 RC Root(根对象)出发,顺着引用链找到其它对象。

  • 初始标记 在 新生代垃圾回收 时就发生了,注意,初始标记并不会占用 并发标记 的时间

  • 什么时候进行并发标记呢?当老年代占用堆空间达到一定阈值时,这时就会发生 并发标记(不会 STW),由以下 JVM 参数设置

  • -XX:InitiatingHeapOccupancyPercent=percent(默认 45%)
    

5、Mixed Collection

会对 伊甸园、幸存区、老年代 进行全面垃圾回收

  • 最终标记(Remark)会 STW
    • 防止之前 并发标记 中可能会漏掉一些对象,因为在并发标记的同时,其它用户线程也在工作,可能会产生一些新的垃圾,改变对象的引用,所以可能会被结果产生影响,所以需要在 Mixed Collection 阶段,先 STW,然后执行一个 最终标记
  • 拷贝存活(Evacuation)会 STW
-XX:MaxGCPauseMillis=ms  最大暂停时间

分析上图:

进行一次垃圾回收后,伊甸园E 的幸存对象 会被 复制 到 幸存区S ,另一些 幸存区S 的对象,不够年龄的也会被复制到 幸存区S,有一些符合晋升条件的就会晋升到 老年代O

经过几轮并发标记后,发现老年代O 里也有一些对象没用了,可以回收了。看上图,为什么没有把所有老年代O箭头都指向右下角那个老年代O呢?那是因为 G1垃圾回收器,会根据 -XX:MaxGCPauseMillis=ms 最大暂停时间 ,进行有选择的垃圾回收,怎么理解呢?有时候我们堆内存空间很低,老年代的垃圾回收时间就可能会很长,因为采用的 复制 算法,有大量的对象要从一个 Region 复制到 另一个 Region,这时如果时间长了,就达不到 我们预期设置的 -XX:MaxGCPauseMillis=ms 最大暂停时间,那怎么办呢?为了达到这个设置的最大暂停时间,G1垃圾回收器就会从所有老年代中挑选回收价值最高的几个 Region,也就是这几个 Region 被回收后能释放更大的空间,所以就只会挑几个 Region,这时 Region 少了,最大暂停时间也就能达到了。当然,如果要复制的对象没那么多,最大暂停时间 这个目标也能达到,那么就会把所有 Region 都复制走,复制,一方面是为了保存 存活对象,另一方面是为了 整理, 减少内存空间。

这时就验证了 为什么要把这个垃圾回收器称为 :G1,即优先回收垃圾最大的 Region。主要目的就是为了达到 最大暂停时间

注意

当 垃圾回收速度 < 垃圾产生速度 ,这时 并发收集 就失败了,这时就会退化为 串行 的收集,这时就称为 Full GC了。

6、Young Collection 跨代引用

  • 新生代回收的跨代引用(老年代引用新生代)问题

我们先回忆一下新生代垃圾回收,首先就是找到 GC Root(根对象),然后 GC Root 进行可达性分析,再找到存活对象,存活对象进行 复制,复制到 幸存区。

这时就有一个问题,我们要找 新生代对象的 GC Root,通过 GC Root就行查找,那首先得找 GC Root,而 GC Root 又有一部分来自老年代,但老年代的存活对象又很多,如果我们通过遍历整个老年代找到根对象,显然效率很低,因此,采用了 卡表(card table) 的技术,把老年代的Region,再进行细分,分成一个个的 card,每个 card 分别为 512K ,如果老年代中有一个对象引用了新生代的对象,那么这个对应的 card 就标记为:脏卡。这样做的好处就是做 GC Root 遍历时 不用去遍历整个老年代,而是只需要去关注 脏卡 的区域就好了。

上图中,粉色区域 代表 脏卡,它们都有对象引用新生代中的对象。

新生代中有一个 :Remember Set,会记录外部对新生代区域的引用,也就是记录都有哪些 脏卡。将来对 新生代进行垃圾回收时,就可以通过 Remember Set 去知道有哪些脏卡,然后再到这些脏卡中,遍历 GC Root。这样就减少了 GC Root 的遍历时间。

在进行对象引用创建时,会有一个查找过程,查找该引用是否被其它区域对象所引用,若被引用,则在 Remember Set 集合中标注,也就是 脏卡

但这时又有一个问题,我们需要标记脏卡,这些脏卡其实是通过下面 post-write barrier + dirty card queue //写屏障 ,在每次对象的引用发送变更时,都要去更新脏卡,即把卡表中的卡标记为 脏卡。这是一个 异步操作,即不会立刻完成脏卡的更新,会把更新指令放在脏卡的队列中,将来由一个线程完成脏卡的更新操作

产生跨代引用(老年代引用新生代对象)的老年代区域称为脏卡区域

7、Remark(重新标记)

  • pre-write barrier+satb_mark_queue

上图表示的是:并发标记阶段,对象的处理状态

  • 黑色:已处理完成,且有引用在引用它们,所以结束时会存活下来
  • 灰色:正在处理,上图中 灰色 的方框被引用,所以最后还是会存活
  • 白色:未处理,上图中右下角的 白色 的方框被引用,所以最后还是会存活,上面那个白色 因为无人引用,最后还是白色,还是会被回收

案例一

假如现在处理到 灰色B ,因为有强引用引用它,所以,就把它变成黑色,将来会存活,当我们处理到 白色C 时,因为是 并发标记 ,就表示 可能会有 用户线程 对 白色C 的引用做修改,比如把 B–>C 的引用断开,所以处理 C 时,发现已经没被引用了,所以等整个 并发标记 完成后,C仍然是白色,最后就会被回收。

案例二

  1. 在 C 被处理完后,并发标记可能还没有结束,这时用户线程又改变 C 的引用地址,比如把 A—>C 。这时问题就来了,因为之前 C 已经被处理过了,且 A 是黑色的,所以也不会处理 A了,所以等到整个并发标记结束后,C就会漏处理了,但我们仍然认为 C 是白色的,要把其回收掉,但这样就错误了,为什么呢?这时候有一个强引用引用它,若再将其回收掉,这时伤害就大了,所以,需对对象的引用做进一步的检查,怎么做呢?其实就是 Remark,重新标记阶段。就是为了防止这种现象发生,那具体怎么做呢?

  2. 就是当对象的引用发送改变时,JVM 就会为其加入一个 写屏障。什么叫写屏障? 只要你的对象引用发生改变,写屏障 的代码就会被执行,比如把 C的引用 给 A的一个属性,这说明 C的引用 发生了变化,既然发生变化,写屏障的代码就会被执行,那写屏障的指令做了什么呢?它就会把 A加入队列中,并且把 A 变成灰色,即表示还没处理完,等到整个 并发标记 结束了,接下来进入 重新标记 阶段,重新标记会 STW,让其它用户线程暂停,这时 重新标记 就会把 队列 中的对象一个个取出,再做一次检查,发现是 灰色的,还需进一步判断处理,结果发现有强引用,再把其变成黑色

以上是关于JVM 垃圾回收器的主要内容,如果未能解决你的问题,请参考以下文章

jvm垃圾回收算法

JVM探究之 —— 垃圾回收

JVM GC-----垃圾回收算法

最新 JVM 垃圾回收器 Shenandoah GC 的实践案例

JVM——垃圾回收(GC)

java中是怎样进行垃圾回收的?