第四章 jvm中的垃圾回收器

JVM-java中的虚拟机

第四章 jvm中的垃圾回收器


文章目录


垃圾回收器GC

概述

垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。 其中,Java堆是垃圾收集器的工作重点。

从次数上讲:

顺繁收集Young区

较少收集old区

基本不动Perm区(或元空间)

垃圾回收算法

垃圾判别阶段算法

jvm默认hotspot用的可达性分析算法

引用计数算法

引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为o,即表示对象A不可能再被使用,可进行回收。

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

缺点:引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。

可达性分析算法

相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。

相较于引用计数算法,这里的可达性分析就是Java、C#选择的 。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)。

其原理简单来说,就是将对象及其引用关系看作一个图,选定活动的对象作为GCRoots,然后跟踪引用链条,如果一个对象和GCRoots之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。

基本思路:

  • 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
    如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
    优点:
    实现简单,执行高效,有效的解决循环引用的问题,防止内存泄漏。
    GC Roots 有哪些?
    GC Roots 包括以下几类元素:在Java 语言中,
  • 虚拟机栈中引用的对象.比如:各个线程被调用的方法中使用到的参数、局部变量
  • 本地方法栈内JNI(通常说的本地方法)引用的
  • 对象类静态属性引用的对象
    比如:Java类的引用类型静态变量
  • 方法区中常量引用的对象.
    比如:字符串常量池里的引用
  • 所有被同步锁synchronized持有的对象
  • Java虚拟机内部的引用。
    基本数据类型对应的class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
  • 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

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

垃圾清除阶段算法

当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在JVM中比较常见的三种垃圾收集算法是标记一清除算法(Mark-Sweep)、复制算法(Copying)、标记-压缩算法(Mark-Compact)

标记清除算法

当堆中的有效内存空间(availablememory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
标记 :Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。标记的是垃圾对象
清除 :Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

缺点:

1、效率比较低:递归与全堆对象遍历两次

2、在进行GC的时候,需要停止整个应用程序,导致用户体验差3、这种方式清理出来的空闲内存是不连续的,产生内存碎片。

注意:何为清除?

这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。

复制算法

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

优点:

没有标记和清除过程,实现简单,运行高效

复制过去以后保证空间的连续性,不会出现"碎片"问题。

缺点:

此算法的缺点也是很明显的,就是需要两倍的内存空间

对于G1这种分拆成为大量region的GC,复制而不是移动,意味着Gc需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。

特别的:适用于存活对象很少

如果系统中的存活对象很多,复制算法不会很理想。因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。

应用场景:

新生代 ,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。

所以现在的商业虚拟机都是用这种收集算法回收新生代。

比如:IBM 公司的专门研究表明,新生代中80%的对象都是"朝生夕死"的。

标记压缩算法

执行过程:

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

优点:(此算法消除了"标记-清除"和"复制"两个算法的弊端。)

消除了标记/清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。I

消除了复制算法当中,内存减半的高额代价。

缺点:

从效率上来说,标记-压缩算法要低于复制算法。效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。对于老年代每次都有大量对象存活的区域来说,极为负重。

移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。

移动过程中,需要全程暂停用户应用程序。即:STW

上面三个算法对比

分代收集算法

分代收集算法是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率 。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接 ,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

增量收集算法

上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop theWorld 的状态。在stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生。
基本思想

如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。

缺点:

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

分区算法

G1 GC使用的算法

分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

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

相关概念

手动调用gc

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

然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。

JVM实现者可以通过System.gc()调用来决定JVM的Gc行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。

其中在调用System.gc()会执行finalize()方法,

finalize()方法详解,前言,finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。

首先,大致描述一下**finalize流程:**当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象"复活"

使用建议

不建议用finalize方法完成"非内存资源"的清理工作,但建议用于:

1清理本地对象(通过JNI创建的对象);

2作为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其他资源释放方法。

finalize()方法执行处于该对象已经标记为垃圾后,执行垃圾回收前,垃圾回收器回收成功也行,回收失败也可以

内存泄漏和内存溢出

内存溢出:造成垃圾回收已经跟不上内存消耗的速度,出现OOM的情况。

内存不够原因?

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

可以通过参数-Xms、-Xmx来调整。

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

oom前必有GC吗?

不是必然的。比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出outofMemoryErron

内存溢出

什么情况会出现内存溢出

  • 静态集合类
    静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用但是因为长生命周期对象持有它的引用而导致不能被回收。
java 复制代码
public class MemoryLeak {
static List list = new ArrayList();
public void oomTests(){
Object obj=new Object();//局部变量
List.add(obj);
}}
  • 单例模式
    单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
  • 内部类持有外部类
    内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
  • 各种连接,如数据库连接、网络连接和IO连接等
  • 变量不合理的作用域
    变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为nul1,很有可能导致内存泄漏的发生。
java 复制代码
public class UsingRandom {
private String msg;
public void receiveMsg(){
//private String msg;//原本定义为局部变量,扩大了其作用域为全局变量
readFromNet();//从网络中接受数据保存到msg中
saveDB();//把msg保存到数据库中
//msg = null;
}
  • 改变哈希值
    改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。
    否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。
java 复制代码
public class ChangeHashCode {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        Person p1 = new Person(1001, "AA");
        Person p2 = new Person(1002, "BB");

        set.add(p1);
        set.add(p2);
        p1.name = "CC";
        set.remove(p1);//这个地方因为上一步进行过修改,所以无法进行删除了,导致内存泄漏了
        System.out.println(set);//2个对象!

//        set.add(new Person(1001, "CC"));
//        System.out.println(set);
//        set.add(new Person(1001, "AA"));
//        System.out.println(set);
    }
}
  • 缓存泄漏
    内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据
    对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
  • 监听器和回调
    内存泄漏另一个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显式的取消,那么就会积聚。
    需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。
安全点和安全区域

安全点(Safepoint)

程序执行时并非在所有地方都能停顿下来开始 GC,只有在特定的位置才能停顿下来开始GC,这些位置称为"安全点(Safepoint)"

SafePoint的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据"是否具有让程序长时间执行的特征"为标准。比如:选择一些执行时间较长的指令作为SafePoint,如方法调用、循环跳转和异常跳转等。

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?

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

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

主动式中断:

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

安全区域(Safe Region)
Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入GC 的Safepoint。但是,程序"不执行"的时候呢?例如线程处于Sleep状态或Blocked状态,这时候线程无法响应JVM 的中断请求,"走"到安全点去中断挂起,JVM 也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(SafeRegion)来解决。

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

实际执行时:

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

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

5种引用
  • 强引用:不回收
  • 软引用:内存不足即回收
  • 弱引用:发现即回收
  • 虚引用:对象回收跟踪
    它的回收非常干脆。当 GC 发现一个对象只剩下虚引用时,会直接回收该对象,并将虚引用放入关联的引用队列中。后台线程监听到队列变化后,就可以立刻执行清理逻辑(比如释放堆外内存)。整个过程只需要一次 GC,不会导致对象"复活"或延迟回收。
  • 终结器引用:
    它用以实现对象的finalize()方法,也可以称为终结器引用。
    无需手动编码,其内部配合引用队列使用。
    在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次Gc时才能回收被引用对象。

垃圾回收器

GC分类

串行vs并行:按线程数。

串行:同一时间段只允许一个cpu用于执行垃圾回收

并行:同一时间段只允许多个cpu用于执行垃圾回收

并发式vs独占式:工作模式

并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间

独占式垃圾回收器(Stoptheworld)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。

压缩式vs非压缩式:碎片处理方式

压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。

再分配对象空间使用:指针碰撞

非压缩式的垃圾回收器不进行这步操作。

再分配对象空间使用:空闲列表

年轻代vs老年代:工作内存空间

GC评估指标

主要通过吞吐量和暂停时间和内存占用

吞吐量 :程序的运行时间(程序的运行时间十内存回收的时间)

吞吐量=程序的运行时间/(程序的运行时间十内存回收的时间)

垃圾收集开销:吞吐量的补数,垃圾收集器所占时间与总时间的比例。
暂停时间 :执行垃圾收集时,程序的工作线程被暂停的时间。

收集频率:相对于应用程序的执行,收集操作发生的频率。
内存占用 :Java堆区所占的内存大小。

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

吞吐量优先:单位时间内,STW的时间最短:0.2+0.2=0.4

响应时间优先:尽可能让单次STW的时间最短:0.1+0.1+0.1+0.1+0.1=0.5

三项共同构成一个"不可能三角"。三者总体的表现会随着技术进步而越来越好。款优秀的收集器通常最多同时满足其中的两项。

这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍

垃圾回收器都有那些

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

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

Serial GC(串行收集器)
  • Seria1收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。
  • Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。
  • Serial收集器采用复制算法、串行回收和"Stop-the-World"机制的方式执行内存回收。
  • 除了年轻代之外,Seria1收集器还提供用于执行老年代垃圾收集的Serial old收集器Serial Old 收集器同样也采用了串行回收和"Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。
    Serial old是运行在Client模式下默认的老年代的垃圾回收器
    Serial old在Server模式下主要有两个用途:
    1 与新生代的ParallelScavenge配合使用
    2 作为老年代CMS收集器的后备垃圾收集方案

    这个收集器是一个单线程的收集器,但它的"单线程"的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(StopTheWorld)

优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说 ,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

运行在Client模式下的虚拟机是个不错的选择。

在用户的桌面应用场景中,可用内存一般不大 (几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。

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

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

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

Parallel GC(并行收集器)
  • HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,ParallelScavenge收集器同样也采用了复制算法、并行回收和"Stop theWorld"机制。
  • 那么Parallel 收集器的出现是否多此一举?
    1.和ParNew收集器不同,ParallelScavenge收集器的目标则是达到一个可控制的吞吐量,它也被称为吞吐量优先的垃圾收集器。
    2.自适应调节策略也是ParallelScavenge与ParNew一个重要区别。cms默认也没有启动这个参数
  • 高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
  • Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel old收集器,用来代替老年代的Serialold收集器。
  • Parallel old收集器采用了标记-压缩算法,但同样也是基于并行回收和Stop-the-World"机制。

    在程序吞吐量优先的应用场景中,Parallel收集器和Parallel old收集器的组合在Server模式下的内存回收性能很不错。
    在Java8中,默认是此垃圾收集器。

参数配置

  • -XX:+UseParallelGC手动指定年轻代使用Paral1e1并行收集器执行内存回收任务
  • -XX:+UseParallel0ldGC手动指定老年代都是使用并行回收收集器。
    分别适用于新生代和老年代。默认jdk8是开启的。
    上面两个参数,默认开启一个,另一个也会被开启。(互相激活)
  • -XX:ParallelGCThreads 设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
    在默认情况下,当CPU 数量小于8个,ParallelGcThreads的值等于CPU 数量。
    当CPU 数量大于8个,ParallelGcThreads 的值于3+[5*CPU_Count]/8]
  • -XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒
    为了尽可能地把停顿时间控制在MaxGCPauseMil1s以内,收集器在工作时会调整Java堆大小或者其他一些参数。
    对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。该参数使用需谨慎。
  • -XX:GCTimeRatio垃圾收集时间占总时间的比例(=1/(N+1))。用于衡量吞吐量的大小。
    1.取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1%。
    2.与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。
CMS(并发标记清除收集器)
  • 在JDK1.5时期,HotSpot 推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

  • CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。

    目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

  • CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-the-world"

不幸的是 ,CMS 作为老年代的收集器,却无法与 JDK 1.4.0 中已经存在的新生代收集器ParallelScavenge 配合工作,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。

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

初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为'Stop-the-World"机制而出现短暂的暂停,这个阶段的主要任务仅仅**只是标记出GC Roots能直接关联到的对象。**一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。

并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程 ,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。

重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 (比如:由不可达变为可达对象的数据,可达对象变成不可达的时候,这些对象就称为浮动垃圾,本次不回收等下次回收),这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的己经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行

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

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

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

参数配置

  • -XX:+UseConcMarkSweepGC 手动指定使用CMS 收集器执行内存回收任务。
  • 开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区用)+CMS(OLd区用)+Serial old的组合。
  • -XX:CMSlnitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收
    JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS 回收。JDK6及以上版本默认值为92%
    如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMs的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低FullGC 的执行次数。
  • XX:+UseCMSCompactAtFullCollection 用于指定在执行完Ful1GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
  • XX:CMSFul1GCsBeforeCompaction 设置在执行多少次Ful1l Gc后对内存空间进行压缩整理。

JDK9新特性:CMS被标记为Deprecate了(JEP291)

如果对JDK 9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC来开启CMSs收集器的话,用户会收到一个警告信息,提CMS未来将会被废弃。
JDK14新特性:删除CMS垃圾回收器(JEP363)

移除了CMs垃圾收集器,如果在JDK14中使用-XX:+UseConcMarkSweepGC的话,JVM不会报错,只是给出一个warning信息,但是不会exit。JVM会自动回退以默认GC方式启动JVM

G1(Garbage First 收集器)
  • G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
  • 在jdk8中还不是默认的垃圾回收器,需要使用-XX:+UseG1GC来启用。
  • G1(Garbage-First)是一款面向服务端应用的垃圾收集器,兼顾吞吐量和停顿时间的GC实现。
  • 在JDK1.7版本正式启用,是JDK9以后的默认GC选项,取代了CMS 回收器。

设置H的原因:

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

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

  • 并行与并发
    并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
    并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  • 分代收集
    从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
  • 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
  • 空间整合:与CMS的"标记~清理"算法不同,G1从整体来看是基于"标记整理"算法实现的收集器;从局部上来看是基于"复制"算法实现的。
  • 可预测的停頓:这是G1相对于CMS的另一个大优势,降低停破时间是G1和CMS共同的关注点,但G1除了追求低停敏外,还能建立可预测的停频时间機型,能让使用者明编指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。

参数

  • 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。

使用场景

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

年轻代GC(Young GC)

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

老年代并发标记过程(Concurrent Marking)

  • 1.初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。
  • 2.根区域扫描(Root Region Scanning):G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在youngGC之前完成。
  • 3.并发标记(Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被youngGC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
  • 4.再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMs更快的初始快照算法:snapshot-at-the-beginning(SATB)。
  • 5.独占清理(cleanup,STW):计算各个区域的存活对象和Gc回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。(这个阶段并不会实际上去做垃圾的收集)
  • 6.并发清理阶段:识别并清理完全空闲的区域。
    混合回收(Mixed GC)
  • 当越来越多的对象晋升到老年代oldregion时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个YoungRegion,还会回收一部分的oldRegion。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是MixedGC并不是FullGC。
  • 混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。
  • 由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。
  • 混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。
    FullGC
    G1的初衷就是要避免FullGC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
ZGC(Z 收集器)

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

ZGC:是一款基于Region内存布局的分区模型,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-压缩算法的,以低延迟为首要目标的一款垃圾收集器。
分区模型

ZGC的Region可以具有如图3-19所示的大、中、小

  • 小型Region(SmallRegion):容量固定为2MB,用于放置小于256KB的小对象。
  • 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
  • 大型Regjion(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作"大型Region",但它的实际容量完全有可能小于中型Regjion,最小容量可低至4MB。大型Regjion在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到)的,因为复制一个大对象的代价非常高昂。

染色指针?



初始标记

标记与GCRoots直连的对象。STW暂停很短。STW的时间<1ms:跟堆的大小没有关系

且不会随着堆的大小增加而增加STW
并发标记/对象重新定位

处理时间很长并发。不暂停。

采用三色标记(黑白灰) 存在漏标问题,下一步解决

对象重新定位:上次gc中,对象根据最后一步产生新对象进行重新指向新的对象内存地址

再标记

解决漏标的对象

STW暂停时间:很短处理漏标对象。(不多)<1ms.跟堆的大小没有关系
并发转移准备

他跟G1是有区别的,zgc是会采用复制算法,找一个空闲region区域将存活对象复制过去,这个时间段主要找相关区域等做准备
初始转移

跟初始标记一样,先转移跟GC roots相关对象,进行STW
并发转移

不会进行STW,标记其他对象

其中有个这个阶段也不进行指针重新分配新的对象的引用地址,会在下一次gc中的并发标记阶段才进行对象指针新分配 ,这阶段会在被复制内存中生成一个转发表 ,记录对象旧内存地址和新内存地址,注意有一种情况会直接分配,但是发生概率极低,需要满足条件是

1.处于并发转移阶段

2.用户线程某个线程正好在这个阶段访问这个对象了

ZGC触发时机

ZGC目前有4中机制触发GC:

  • 定时触发,默认为不使用,可通过ZCollectionlnterval参数配置。
  • 预热触发,最多三次,在堆内存达到10%、20%、30%时触发,主要时统计GC时间,为其他GC机制使用。
  • 分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC(耗尽时间-一次GC最大持续时间-一次GC检测周期时间)。
  • 主动触发,(默认开启,可通过ZProactive参数配置)距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的间隔时间跟(49*一次
    GC的最大持续时间),超过则触发。
各GC使用场景
如何选择

如何选择垃圾收集器

1.优先调整堆的大小让服务器自己来选择

2.如果内存小于100M,使用串行收集器

3.如果是单核,并且没有停顿时间的要求,串行或JVM自己选择

4.如果允许停顿时间超过1秒,选择并行或者JVM自己选

5.如果响应时间最重要,并且不能超过1秒,使用并发收集器

4G以下可以用parallel,4-8G可以

用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC

GC发展
相关推荐
阿正呀2 小时前
Redis怎样实现本地缓存的高效失效通知
jvm·数据库·python
九转成圣2 小时前
Java 性能优化实战:如何将海量扁平数据高效转化为类目字典树?
java·开发语言·json
2501_901200532 小时前
mysql如何设置InnoDB引擎参数_优化innodb_buffer_pool
jvm·数据库·python
直奔標竿3 小时前
Java开发者AI转型第二十七课!Spring AI 个人知识库实战(六)——全栈闭环收官,解锁前端流式渲染终极技巧
java·开发语言·前端·人工智能·后端·spring
金銀銅鐵3 小时前
[java] 编译之后的记录类(Record Classes)长什么样子(上)
java·jvm·后端
m0_495496413 小时前
mysql处理复杂SQL性能_InnoDB优化器与MyISAM差异
jvm·数据库·python
forEverPlume4 小时前
PHP怎么使用Eloquent Attribute Composition属性组合_Laravel通过组合构建复杂属性【方法】
jvm·数据库·python
2301_809204705 小时前
mysql在docker容器中如何部署_利用docker-compose快速启动
jvm·数据库·python