垃圾回收概述,算法及垃圾回收器

参考2目录

1. 常见面试题

2. 概述

2.1 什么是垃圾

2.2 为什么需要GC(垃圾收集)?

2.3 Java的垃圾回收机制

2.4 垃圾收集主要关注的区域

3. 垃圾回收算法

3.1 判断对象是否存活

3.2 标记阶段:引用计数算法

 3.3 标记阶段:可达性分析算法

3.3.1 基本思路

 3.3.2 根节点所包括的元素

 3.4 对象的finalization机制

 3.4.1 对象的状态

3.4.2 判断是否能够回收的标记过程,要经历两次标记过程

 4.5 清除阶段:标记-清除算法(Mark-Sweep)

4.5.1 执行过程

 4.5.2 缺点

 4.6 清除阶段:复制算法

4.6.1 核心思想

 4.6.2 特点

 4.7 清除阶段:标记-压缩(整理)算法

4.7.1 执行过程

4.7.2 特点

4.8 三种常见算法小结

 4.9 分代收集算法

4.9.1 核心思想

 4.9.2 年轻代

4.9.3 老年代

4.10 增量收集算法、分区算法

4.10.1 增量收集算法

4.10.2 分区算法

5.  垃圾回收相关概念

5.1 System.gc()的理解

5.2 内存溢出和内存泄漏

5.2.1 内存溢出(OutOfMemoryError,OOM)

 5.2.2 内存泄漏

5.3  Stop The World

5.4 垃圾回收的并发和并行

5.5 安全点和安全区域

5.5.1 安全点 (Safe Point)

5.5.2 安全区域 Safe Region

 5.6 引用

5.6.1 强引用 不回收

5.6.2 软引用 内存不足即回收

5.6.3 弱引用 发现即回收

5.6.4 虚引用 对象回收跟踪

6. 垃圾回收器

 6.1 垃圾回收器分类

6.2 GC性能指标

6.2.1 吞吐量

6.2.2 暂停时间

 6.2.3 吞吐量 vs 暂停时间

6.3 垃圾回收器

 6.3.1 七种经典的垃圾回收器

 6.3.2 垃圾回收器的组合关系

 6.4 Serial回收器:串行回收

 6.5 ParNew回收器:并行回收

 6.6 Parallel回收器:吞吐量优先

 6.7 CMS(Concurrent-Mark-Sweep)回收器:低延迟

6.7.2  特点

6.7.3 优缺点

CMS的优点

CMS的弊端

6.7.4 设置的参数

6.8  G1回收器:区域化分代式 

6.8.1 概述

6.8.2  特点

G1垃圾收集器的缺点

6.8.3 G1回收器的参数设置

6.8.4 G1收集器的操作步骤

 6.8.5 G1收集器的适用场景

 6.8.6 分区Region:化整为零

 6.8.7 G1垃圾回收器的回收过程

 6.8.9 Remembered Set

 6.8.10 G1回收过程一:年轻代GC

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

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

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

 6.8.14 G1回收器优化建议

6.9  垃圾回收器总结

 那么我们如何选择垃圾回收器呢?


1. 常见面试题

蚂蚁金服

  • 你知道哪几种垃圾回收器,各自的优缺点,重点讲一下cms和G1?
  • JVM GC算法有哪些,目前的JDK版本采用什么回收算法?
  • G1回收器讲下回收过程GC是什么?为什么要有GC?
  • GC的两种判定方法?CMS收集器与G1收集器的特点

百度

  • 说一下GC算法,分代回收说下
  • 垃圾收集策略和算法

天猫

  • JVM GC原理,JVM怎么回收内存
  • CMS特点,垃圾回收算法有哪些?各自的优缺点,他们共同的缺点是什么?

滴滴

  • Java的垃圾回收器都有哪些,说下g1的应用场景,平时你是如何搭配使用垃圾回收器的

京东

  • 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,
  • 包括原理,流程,优缺点。垃圾回收算法的实现原理

阿里

  • 讲一讲垃圾回收算法。
  • 什么情况下触发垃圾回收?
  • 如何选择合适的垃圾收集算法?
  • JVM有哪三种垃圾回收器?

字节跳动

  • 常见的垃圾回收器算法有哪些,各有什么优劣?
  • System.gc()和Runtime.gc()会做什么事情?
  • Java GC机制?GC Roots有哪些?
  • Java对象的回收方式,回收算法。
  • CMS和G1了解么,CMS解决什么问题,说一下回收的过程。
  • CMS回收停顿了几次,为什么要停顿两次?

2. 概述

2.1 什么是垃圾

 垃圾是指在运行程序中没有任何指针指向的对象。

2.2 为什么需要GC(垃圾收集)?

对于Java来说,如果一直分配内存空间而不进行垃圾的回收的话,内存迟早会被消耗完。GC还能清理内存中的碎片,将碎片移动到内存的一段,从而可以将整理出来的内存分配给新的对象。

而经常STW(Stop-The-World)的GC以及不能跟上实际的需求了,所以现在的GC有着多种算法和方式。

2.3 Java的垃圾回收机制

Java的垃圾回收是可以自动管理的,可以降低内存泄漏和内存溢出的风险。

2.4 垃圾收集主要关注的区域

GC主要关注方法区和堆的垃圾收集,是垃圾收集器的工作重点。

  • 频繁收集Young区
  • 较少收集Old区
  • 基本不收集Perm区(元空间)

3. 垃圾回收算法

3.1 判断对象是否存活

在进行垃圾回收之前需要知道那些内存的对象是垃圾。再寻着垃圾对象的阶段我们称为垃圾标记阶段。最常用的两种方式是引用计数算法可达性分析算法

3.2 标记阶段:引用计数算法

该方法的实现原理非常简单,只需要对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。

比方说对于一个对象A,只要有对象引用了对象A那么对象A的引用计数器就加1;当引用失效时,引用计数器就减1。当对象A的引用计数器的值为0时,代表可以进行回收。

优点:实现简单,判定效率高,垃圾对象容易辨识,回收没有延迟性

缺点:增加了空间的开销,每次都需要更新计数器,增加了时间的开销。最严重的问题是无法解决循环引用的问题,所以当前的Java的垃圾回收器中没有循环使用该类算法。

循环引用:循环内部互相引用,明明三个应该被回收,却没有被回收导致内存泄漏

 3.3 标记阶段:可达性分析算法

可达性分析算法可以解决引用计数算法中循环引用的问题防止内存泄漏。Java就是采用的这种类型的算法。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)。

3.3.1 基本思路

可达性分析算法就是以根对象集合为起点,按照从上到下的方式搜索被根对象所连接的目标是否是可达的。

        所谓"GCRoots”根集合就是一组必须活跃的引用。

使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)

如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。

在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

 3.3.2 根节点所包括的元素

虚拟机栈中引用的对象

  • 比如:各个线程被调用的方法中使用到的参数、局部变量等。

本地方法栈内JNI(通常说的本地方法)引用的对象

方法区中类静态属性引用的对象

  • 比如:Java类的引用类型静态变量

方法区中常量引用的对象

  • 比如:字符串常量池(String Table)里的引用

所有被同步锁synchronized持有的对象

Java虚拟机内部的引用。

  • 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。

反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。

如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。

这点也是导致GC进行时必须“stop The World”的一个重要原因。

 3.4 对象的finalization机制

Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:

  • 在finalize()时可能会导致对象复活。
  • finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
  • 一个糟糕的finalize()会严重影响Gc的性能。

 3.4.1 对象的状态

由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。

  • 可触及的:从根节点开始,可以到达这个对象。
  • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活,在方法内重重新建立起引用。
  • 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次

只有在对象不可触及时才可以被回收。 

3.4.2 判断是否能够回收的标记过程,要经历两次标记过程

  1. 如果对象objA到GC Roots没有引用链,则进行第一次标记。
  2. 进行筛选,判断此对象是否有必要执行finalize()方法
  3. 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
  4. 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
  5. finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。

 4.5 清除阶段:标记-清除算法(Mark-Sweep)

标记清除算法是一种非常基础和常见的垃圾收集算法。

所谓清除并不是真正的置空,而是将清楚的对象放在空闲地址列表中。下次有新对象需要加载时,判断垃圾的位址空间是否足够,如果足够就存放覆盖原地址。

4.5.1 执行过程

当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除

  • 标记:从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
  • 清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收

 4.5.2 缺点

标记清除算法的效率不算高

在进行GC的时候,需要停止整个应用程序,用户体验较差

这种方式清理出来的空闲内存是不连续的,产生碎片,需要维护一个空闲列表

 4.6 清除阶段:复制算法

解决了标记-清除算法在垃圾收集效率方面的缺陷

4.6.1 核心思想

内存空间分为两块,每次使用其中的一块,另外一块作为复制时使用。在垃圾回收时将正在使用的内存中的存活对象复制到空闲的内存块中,再将复制之前的内存块清除,交换两个内存的角色,完成垃圾回收。是不是有点类似幸存者1,和0区。

常见的应用场景就是新生代的垃圾回收。

 4.6.2 特点

优点:没有标记清除过程,实现简单,运行高效。并且复制过去后能够保持空间连续性,不会出现碎片问题。适用于空间不大,垃圾占比多的场景

缺点:此算法的缺点也是很明显的,就是需要两倍的内存空间。对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小

 4.7 清除阶段:标记-压缩(整理)算法

复制算法只有在存活对象少,垃圾对象多的时候才能保证高效性。它的特点能够完美的适合新生代,但是在老年代中存活对象较多,复制成本较大,而且额外内存开销也会很大。

标记-清除算法可以用于老年代,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片。

所以标记清除算法由此诞生。

4.7.1 执行过程

  1. 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
  2. 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
  3. 之后,清理边界外所有的空间

标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销,只需要采取指针碰撞算法。

4.7.2 特点

优点

  • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
  • 消除了复制算法当中,内存减半的高额代价。

缺点

  • 从效率上来说,标记-整理算法要低于复制算法。
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
  • 移动过程中,需要全程暂停用户应用程序。即:STW

4.8 三种常见算法小结

 4.9 分代收集算法

4.9.1 核心思想

同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。目前几乎所有的GC都采用分代收集算法执行垃圾回收的。

 4.9.2 年轻代

年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。

这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

4.9.3 老年代

老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。

这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现

  • Mark阶段的开销与存活对象的数量成正比。
  • Sweep阶段的开销与所管理区域的大小成正相关。
  • Compact阶段的开销与存活对象的数据成正比。

以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

4.10 增量收集算法、分区算法

4.10.1 增量收集算法

在垃圾回收中,随着垃圾的增多,STW的时间也会增多,影响用户体验。

一次性的将所有大量的垃圾处理掉,会造成系统产时间停顿。每次,垃圾收集线程只收集一小片区域的内存空间接着切换到应用程序线程。依次反复,直到垃圾收集完成。可以减少每次停顿的时间。

使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

4.10.2 分区算法

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

后续会详细讲解

5.  垃圾回收相关概念

5.1 System.gc()的理解

在默认情况下,通过system.gc()或者Runtime.getRuntime().gc() 的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

但是!!!

System.gc() 无法保证对垃圾收集器的调用(不能确保立即生效)。这个gc()函数的作用只是提醒虚拟机:程序员希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。

所以我们尽量不要频繁的使用gc(),而是保持代码的健壮性,及时的将不用的变量变为null,让虚拟机去管理内存。

5.2 内存溢出和内存泄漏

5.2.1 内存溢出(OutOfMemoryError,OOM)

 除非程序占用内存增长的速度快过垃圾回收的速度,否则现在并不太容易出现OOM错误。在大多数情况下会通过各个年龄段的垃圾回收来避免出现OOM,或者直接FULL GC清理大量内存。

Java文档中对OOM的解释是没有空闲内存,并且垃圾收集器也无法提供更多内存

堆内存不够的原因:

Java虚拟机的堆内存设置不够。

  • 比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过参数-Xms-Xmx来调整。

代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

  • 对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space"。
    随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的ooM有所改观,出现OOM,异常信息则变成了:“java.lang.OutofMemoryError:Metaspace"。直接内存不足,也会导致OOM。

这里面隐含着一层意思是,在抛出OutOfMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间。

  • 例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等。
  • java.nio.BIts.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。

当然,也不是在任何情况下垃圾收集器都会被触发的

  • 当我们分配的对象的空间大于JVM堆的总空间,则就不需要触发垃圾收集器,而是直接抛出OOM。

 5.2.2 内存泄漏

对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。

尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutOfMemory异常,导致程序崩溃。

单例模式

单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。

一些提供close的资源未关闭导致内存泄漏

数据库连接(dataSourse.getConnection() ),网络连接(socket)和io连接必须手动close,否则是不能被回收的。

5.3  Stop The World

Stop-the-World,简称STW,是指再垃圾回收器期间,所有其他线程都需要停止。

可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。

  • 分析工作必须在一个能确保一致性的快照中进行
  • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
  • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

被STW中断的应用程序线程会在完成GC之后恢复。

所有的GC都不能避免发生STW。

开发中不要用System.gc() 会导致Stop-the-World的发生。

5.4 垃圾回收的并发和并行

 并行:

指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。如ParNew、Parallel Scavenge、Parallel Old;

串行:

相较于并行的概念,单线程执行。如果内存不够,则程序暂停,启动JM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。

并发:

指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;如:CMS、G1

5.5 安全点和安全区域

5.5.1 安全点 (Safe Point)

程序执行时,并非在所有的时刻和执行位置都能够进行GC的。只有程序运行到特定的位置才能进行GC,这个能够进行GC的位置就成为安全点。

安全点太少:GC等待时间较长

安全点太多:GC过于频繁,导致性能问题

选择一些执行时间较长的指令作为安全点,如方法调用、循环跳转和异常跳转等,减少STW的停顿感受。

抢先式中断:(目前没有虚拟机采用了)

首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。\

主动式中断

设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。(有轮询的机制)

5.5.2 安全区域 Safe Region

 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始Gc都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。

如线程处于阻塞 状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。

当线程运行到Safe Region的代码时,首先标识已经进入了Safe Relgion,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程

当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止;

 5.6 引用

强软弱虚,强度依次减弱。

5.6.1 强引用 不回收

Object obj = new Object()这种引用关系。

无论任何情况下,强引用的对象是可触及的,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

强引用是造成Java内存泄漏的主要原因之一。

5.6.2 软引用 内存不足即回收

软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

Object obj = new Object(); // 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引用

5.6.3 弱引用 发现即回收

弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。

由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。

软引用、弱引用都非常适合来保存那些可有可无的缓存数据

Object obj = new Object(); // 声明强引用
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; //销毁强引用

弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。

5.6.4 虚引用 对象回收跟踪

为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知

它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。

由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。

Object obj = new Object(); // 声明强引用
ReferenceQueue phantomQueue = new ReferenceQueue();
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;

6. 垃圾回收器

 6.1 垃圾回收器分类

按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。

按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。

按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器。

按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。

如果你想要最小化地使用内存和并行开销,请选Serial GC;

如果你想要最大化应用程序的吞吐量,请选Parallel GC;

如果你想要最小化GC的中断或停顿时间,请选CMS GC。

6.2 GC性能指标

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

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

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

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

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

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

 简单来说,主要抓住两点:吞吐量、暂停时间

6.2.1 吞吐量

吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间)。高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。

6.2.2 暂停时间

“暂停时间”是指一个时间段内应用程序线程暂停,让GC线程执行的状态。GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的。

 6.2.3 吞吐量 vs 暂停时间

不幸的是”高吞吐量”和”低暂停时间”是一对相互竞争的目标(矛盾),尽量在最大吞吐量优先的情况下,降低停顿时间。

因为如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。

相反的,如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。

6.3 垃圾回收器

 6.3.1 七种经典的垃圾回收器

  • 串行回收器:Serial、Serial Old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel old
  • 并发回收器:CMS、G1
  • 新生代收集器:Serial、ParNew、Parallel Scavenge;
  • 老年代收集器:Serial Old、Parallel Old、CMS;
  • 整堆收集器:G1;

 6.3.2 垃圾回收器的组合关系

  1. 两个收集器间有连线,表明它们可以搭配使用:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、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和Serialold GC组合(JEP366)
  5. (绿色虚框)JDK14中:删除CMS垃圾回收器(JEP363)

-XX:+PrintCommandLineFlags查看命令行相关参数(包含使用的垃圾收集器)

使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID

 6.4 Serial回收器:串行回收

Serial回收器

作为HotSpot中client模式下的默认新生代垃圾收集器。

采用复制算法串行回收和"stop-the-World"机制的方式执行内存回收。

 Serial Old收集器

用于执行老年代垃圾收集

采用了串行回收和"Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法

  • Serial old是运行在Client模式下默认的老年代的垃圾回收器
  • Serial 0ld在Server模式下主要有两个用途:① 与新生代的Parallel scavenge配合使用 ② 作为老年代CMS收集器的后备垃圾收集方案

 特点:

单线程:只会使用一个CPU或一条收集线程去完成垃圾收集工作,并且工作时要STW。

简单而高效(与其他收集器的单线程比):对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销

-XX:+UseSerialGC参数同时设定新生代用Serial GC,且老年代用Serial Old GC。

在Java web应用程序中是不会采用串行垃圾收集器的!!!

 6.5 ParNew回收器:并行回收

ParNew收集器则是Serial收集器的多线程版本。Par是Parallel的缩写,New:只能处理的是新生代。ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器

并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。

对于新生代,回收次数频繁,使用并行方式高效。

对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)

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

因为除Serial外,目前只有ParNew GC能与CMS收集器配合工作

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

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

 6.6 Parallel回收器:吞吐量优先

Parallel Scavenge收集器是并行回收的,同样也采用了复制算法、并行回收和"Stop the World"机制。和上一个收集器不同的是ParallelScavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。

适合在后台运算而不需要太多交互的任务。

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

在程序吞吐量优先的应用场景中,Parallel 收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很不错

 参数配置:

  • -XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。
  • -XX:+UseParallelOldGC 手动指定老年代都是使用并行回收收集器。
  • 分别适用于新生代和老年代。默认jdk8是开启的。
  • 上面两个参数,默认开启一个,另一个也会被开启。(互相激活)
  • -XX:ParallelGCThreads 设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
  • -XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STw的时间)。单位是毫秒。
  • 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数。
  • 对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
  • -XX:GCTimeRatio 垃圾收集时间占总时间的比例(=1/(N+1))。用于衡量吞吐量的大小。
  • 取值范围(0, 100)。默认值99,也就是垃圾回收时间不超过1%。
  • 与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。
  • -XX:+UseAdaptivesizePolicy 设置Parallel Scavenge收集器具有自适应调节策略
  • 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。
  • 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。

 6.7 CMS(Concurrent-Mark-Sweep)回收器:低延迟

这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。适用于强交互应用,CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间

CMS作为老年代的收集器,算法采用标记-清除算法,并且也会"Stop-the-World",新生代只能选择ParNew或者Serial收集器中的一个。

6.7.1 步骤

CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段

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

6.7.2  特点

尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“stop-the-World”,只是尽可能地缩短暂停时间。

由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。

另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

CMS收集器的垃圾收集算法采用的是标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。

有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact?

答案其实很简单,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact更适合“Stop the World” 这种场景下使用。

6.7.3 优缺点

CMS的优点

  • 并发收集
  • 低延迟

CMS的弊端

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

6.7.4 设置的参数

  • -XX:+UseConcMarkSweepGC手动指定使用CMS收集器执行内存回收任务。
    开启该参数后会自动将-xx:+UseParNewGC打开。即:ParNew(Young区用)+CMS(Old区用)+ Serial Old的组合。
  • -XX:CMSInitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。

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

  • 如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Ful1Gc的执行次数。
  • -XX:+UseCMSCompactAtFullCollection 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
  • -XX:CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理。
  • -XX:ParallelcMSThreads 设置CMS的线程数量。
  • CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。

6.8  G1回收器:区域化分代式 

可以参考:

G1GC参考2参考3参考4

适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。

6.8.1 概述

因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等。

G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region

由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。

G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。

在JDK1.7版本正式启用,移除了Experimenta1的标识,是JDK9以后的默认垃圾回收器,取代了CMS回收器以及Parallel+Parallel Old组合。被Oracle官方称为“全功能的垃圾收集器”。

与此同时,CMS已经在JDK9中被标记为废弃(deprecated)在jdk8中还不是默认的垃圾回收器,需要使用-XX:+UseG1GC来启用

6.8.2  特点

与其他GC收集器相比,G1使用了全新的分区算法,其特点如下所示:

并行与并发

  • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
  • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况

分代收集

  • 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
  • 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
  • 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;

空间整合

  • CMS:“标记-清除”算法、内存碎片、若干次Gc后进行一次碎片整理
  • G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。

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

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

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

G1垃圾收集器的缺点

相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。

从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

6.8.3 G1回收器的参数设置

  • -XX:+UseG1GC:手动指定使用G1垃圾收集器执行内存回收任务
  • -XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
  • -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms(人的平均反应速度)
  • -XX:+ParallelGCThread 设置STW工作线程数的值。最多设置为8(上面说过Parallel回收器的线程计算公式,当CPU_Count > 8时,ParallelGCThreads 也会大于8)
  • -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
  • -XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。

6.8.4 G1收集器的操作步骤

G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:

  • 第一步:开启G1垃圾收集器
  • 第二步:设置堆的最大内存
  • 第三步:设置最大的停顿时间

G1中提供了三种垃圾回收模式:Young GC、Mixed GC和Full GC,在不同的条件下被触发。

 6.8.5 G1收集器的适用场景

面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)

最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)。

用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比CMS好:

  • 超过50%的Java堆被活动数据占用;
  • 对象分配频率或年代提升频率变化很大;
  • GC停顿时间过长(长于0.5至1秒)

HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

 6.8.6 分区Region:化整为零

使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。

可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。

虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。

一个region有可能属于Eden,Survivor或者Old/Tenured内存区域。但是一个region只可能属于一个角色。图中的E表示该region属于Eden内存区域,S表示属于survivor内存区域,O表示属于Old内存区域。图中空白的表示未使用的内存空间。

G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如果超过1.5个region,就放到H。

设置H的原因:对于堆中的对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。

每个Region都是通过指针碰撞来分配空间

 6.8.7 G1垃圾回收器的回收过程

G1GC的垃圾回收过程主要包括如下三个环节:

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

应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。

堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程

标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。

举个例子:一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。

 6.8.9 Remembered Set

存在的问题

一个对象被不同区域引用的问题

一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?

在其他的分代收集器,也存在这样的问题(而G1更突出)回收新生代也不得不同时扫描老年代?

这样的话会降低MinorGC的效率;

解决方法:

无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:

每个Region都有一个对应的Remembered Set

每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;

然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);

如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;

当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。

 6.8.10 G1回收过程一:年轻代GC

JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。

年轻代垃圾回收只会回收Eden区和Survivor区。

首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。

然后开始如下回收过程:

  • 第一阶段,扫描根。根是指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停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

 6.8.11 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的。这个阶段并不会实际上去做垃圾的收集
  • 并发清理阶段:识别并清理完全空闲的区域。

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

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

并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收

混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。

由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。

混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

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

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

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

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

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

 6.8.14 G1回收器优化建议

年轻代大小

  • 避免使用-Xmn-XX:NewRatio等相关选项显式设置年轻代大小
  • 固定年轻代的大小会覆盖暂停时间目标

暂停时间目标不要太过严苛

  • G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
  • 评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。

6.9  垃圾回收器总结

 那么我们如何选择垃圾回收器呢?

Java垃圾收集器的配置对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升。

怎么选择垃圾收集器?

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

最后需要明确一个观点:

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值