JVM第6篇-垃圾回收

垃圾回收

一、概述

在这里插入图片描述

  • 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。
  • 关于垃圾收集有三个经典问题
    • 哪些内存需要回收?
    • 什么时候回收?
    • 如何回收?
  • 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即时经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战。

1.1 什么是垃圾?

  • 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

1.2 为什么需要GC?

  • 如果不进行垃圾回收,那么内存迟早都会被消耗完
  • 便于JVM将整理出的内存分配给新的对象。
  • 没有GC就不能保证应用程序的正常进行。

1.3 GC的重点区域?

  • 频繁收集新生代
  • 较少收集老年代
  • 基本不动永久代/元空间
    在这里插入图片描述

二、垃圾回收算法

2.1 垃圾判定算法

2.1.1 引用计数算法

对每个对象保存一个整型的引用计数器。每当有一个地方引用它,计数器值就加一;当引用失效时,计数器值就减一。

优缺点

优点:

  • 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
    缺点:
  • 增加了存储空间的开销。
  • 每次赋值都要更新计数器,增加了时间开销。
  • 无法处理循环引用的情况

测试

  • jvm并不是通过引用计数法判断对象是否存活的。
public class RefCountGC {
    
    
    //这个成员属性唯一的作用就是占用一点内存
    private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB

    Object reference = null;

    public static void main(String[] args) {
    
    
        RefCountGC obj1 = new RefCountGC();
        RefCountGC obj2 = new RefCountGC();

        obj1.reference = obj2;
        obj2.reference = obj1;

        obj1 = null;
        obj2 = null;
        //显式的执行垃圾回收行为
        //这里发生GC,obj1和obj2能否被回收?
        System.gc();

        try {
    
    
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

2.1.2 可达性分析算法

  • 相对于引用计数算法而言,可达性分析算法那不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
  • 相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集

原理与基本思路

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

**基本思路: **

  • 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 使用可达性分析算法后,内存中存活的对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
    在这里插入图片描述

优点

  • 实现简单,执行高效,有效地解决了循环引用的问题,防止内存泄漏

GC Roots

在Java语言中,GC Roots 包括以下基类元素:

  • 虚拟机栈中引用的对象
    • 比如各个线程被调用的方法中使用到的参数、局部变量等。
  • 本地方法栈内JNI(通常说的本地方法)引用的对象
  • 类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁(synchronized)持有的对象
  • Java虚拟机内部的引用。
  • 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

注意点

  • 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
  • 这点也是导致GC进行时必须"Stop The World"的一个重要原因。
    • 即时是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

2.2 垃圾清除算法

2.2.1 标记-清除算法

在这里插入图片描述

缺点

  • 执行效率不稳定
  • 内存空间的碎片化问题

2.2.2 标记-复制算法

核心思想:
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换连个内存的角色,最后完成垃圾回收。
在这里插入图片描述

优缺点

优点:

  • 没有标记和清除过程,实现简单,运行高效
  • 复制过去以后保证空间的连续性,不会出现"碎片"问题。

缺点:

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

应用场景

在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
比如: IBM公司的专门研究表明,新生代中80%的对象都是"朝生夕死"的。
在这里插入图片描述

2.2.3 标记-整理算法

在这里插入图片描述
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,在进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩算法。

二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。

指针碰撞

如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump the Pointer)。

优缺点

优点:

  • 消除了标记-清除算法当中,空间碎片化的缺点
  • 消除了标记-复制算法当中,内存减半的高额代价

缺点:

  • 效率低于标记-复制算法
    • 效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
    • 对于老年代每次都有大量对象存活的区域来说,极为负重。
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
  • 移动过程中,需要全称暂停用户应用程序。即: STW(Stop The World)

2.2.4 三种垃圾回收算法对比

在这里插入图片描述

2.2.5 分代收集算法

分析

年轻代:
年轻代特点: 区域相对老年代较小,对象生命周期短,存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适合用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中两个survivor的设计得到缓解。
老年代:
老年代特点: 区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率较高的对象,复制算法明显变得不合适。一般是由标记-清除或者标记-清除与标记-整理的混合实现。

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

2.2.6 增量收集算法

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

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

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

缺点

  • 造成系统的吞吐量的下降

三、相关概念

3.1 System.gc()

  • 在默认的情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
  • System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。
  • 一般情况下,垃圾回收应该是自动进行的,无需手动触发,否则就太过于麻烦了。

测试

public class SystemGCTest {
    
    
    public static void main(String[] args) {
    
    
        new SystemGCTest();
        System.gc();//提醒jvm的垃圾回收器执行full gc,但是不确定是否马上执行gc
//        与Runtime.getRuntime().gc();的作用一样。

//        System.runFinalization();//强制调用使用引用的对象的finalize()方法
    }

    //此方法什么时候会被执行? 当一个对象首次考虑要被回收时,会调用其finalize()
    @Override
    protected void finalize() throws Throwable {
    
    
        super.finalize();
        System.out.println("SystemGCTest 重写了finalize()");
    }
}

3.2 finalize方法

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

finalize的作用

  1. finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是确定的,但Java中的finalize的调用具有不确定性。
  2. 不建议用finalize方法完成"非内存资源"的清理工作,但建议用于:
    • 清理本地对象(通过JNI创建的对象)
    • 作为确保某些非内存资源释放的一个补充: 在finalize方法中显式调用其他资源释放方法。

测试代码

public class FinalizeObj {
    
    
    public static FinalizeObj obj;//类变量,属于 GC Root


    //此方法只能被调用一次
    @Override
    protected void finalize() throws Throwable {
    
    
        super.finalize();
        System.out.println("调用当前类重写的finalize()方法");
        obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
    }


    public static void main(String[] args) {
    
    
        try {
    
    
            obj = new FinalizeObj();
            // 对象第一次成功拯救自己
            obj = null;
            System.gc();//调用垃圾回收器
            System.out.println("第1次 gc");
            // 因为Finalizer线程优先级很低,暂停2秒,以等待它
            Thread.sleep(2000);
            if (obj == null) {
    
    
                System.out.println("obj is dead");
            } else {
    
    
                System.out.println("obj is still alive");
            }
            System.out.println("第2次 gc");
            // 下面这段代码与上面的完全相同,但是这次自救却失败了
            obj = null;
            System.gc();
            // 因为Finalizer线程优先级很低,暂停2秒,以等待它
            Thread.sleep(2000);
            if (obj == null) {
    
    
                System.out.println("obj is dead");
            } else {
    
    
                System.out.println("obj is still alive");
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

finalize的执行过程(生命周期)

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

3.3 内存泄漏和内存溢出

3.3.1 内存溢出

  • 由于GC一直在发展,所以一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存的消耗速度,否则不太容易出现OOM的情况。
  • 大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。
  • javadoc中对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。

内存不够的原因?

  • Java虚拟机的堆内存设置不够。
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

OOM前必有GC?

  • 一般情况下都会GC的
  • 存在特殊情况,比如我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError。

3.3.2 内存泄漏

java中内存泄漏的8中情况

  • 静态集合类
  • 单例模式
  • 内部类持有外部类
  • 各种连接,如数据库连接、网络连接、IO连接等
  • 变量不合理的作用域
  • 改变哈希值
  • 缓存泄漏
  • 监听器和回调
静态集合类

静态集合类,如HashMap、LinkedList等。如果这些容器为静态的,那么他们的生命周期与JVM程序一致,,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

public class MemoryLeak {
    
    
	static List list = new ArrayList();
	
	public void oomTest() {
    
    
		Object obj = new Object();
		list.add(obj);
	}
}
单例模式

单例模式,和静态集合导致内存泄漏的原因类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

3.4 STW

Stop-the-World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,这个停顿称为STW。

  • 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿
  • STW事件和采用哪款GC无关,所有的GC都有这个事件。
  • STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
  • 开发中不要用System.gc();会导致Stop-the-world的发生。

3.5 安全点与安全区域

3.5.1 安全点

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

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

3.5.2 安全区域

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

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

实际执行时:

  1. 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程;
  2. 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止;

3.6 五种引用

3.6.1 强引用(不回收)

强引用是最传统的"引用"的定义,是指在程序代码之中普遍存在的引用赋值,即类似"Object obj = new Object()"这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收被引用的对象。

3.6.2 软引用(内存不足即回收)

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

3.6.3 弱引用(发现即回收)

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2版之后提供了WeakReference类来实现弱引用。

3.6.4 虚引用(对象回收跟踪)

虚引用也称为"幽灵引用"或者"幻影引用",他是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是为了能够在这个对象被收集器回收时收到一个系统通知。在JDK1.2版之后提供了PhantomReference类来实现虚引用。

3.6.5 终结器引用

  • 他用以实现对象的finalize()方法,也可以称为终结器引用。
  • 无需手动编码,其内部配合引用队列使用。
  • 在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象。

四、垃圾回收器

  • 按线程数分,可以分为从串行和并行垃圾回收器
  • 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
    • 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
    • 独占式垃圾回收器(STW)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
  • 碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器。
    • 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
      • 再分配对象空间使用: 指针碰撞
    • 非压缩式的垃圾回收器不进行这步操作。
      • 再分配对象空间使用: 空闲列表。
  • 工作的内存空间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。

4.1 GC评估指标

  • 吞吐量: 程序的运行时间(程序的运行时间+内存回收的时间)
    在这里插入图片描述

  • 垃圾收集开销: 吞吐量的补数,垃圾收集器所占时间与总时间的比例。

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

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

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

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

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

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

  • 吞吐量 —> 吞吐量越大越好!
  • 暂停时间 —> 追求低延迟

小结

现在的JVM标准: 在最大吞吐量优先的情况下,降低停顿时间。

4.2 垃圾回收器

  • 串行回收器: Serial、Serial Old
  • 并行回收器: ParNew、Parallel Scavenge、Parallel Old
  • 并发收集器: CMS、G1

7款经典收集器与垃圾分代之间的关系
在这里插入图片描述

4.2.1 如何查看默认GC?

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

测试

public class GCUseTest {
    
    
    public static void main(String[] args) {
    
    
        ArrayList<byte[]> list = new ArrayList<>();

        while(true){
    
    
            byte[] arr = new byte[100];
            list.add(arr);
//            try {
    
    
//                Thread.sleep(10);
//            } catch (InterruptedException e) {
    
    
//                e.printStackTrace();
//            }
        }
    }
}

-XX:+PrintCommandLineFlags
在这里插入图片描述
jinfo -flag UseParallelGC PID
在这里插入图片描述
在这里插入图片描述

4.3 Serial GC: 串行回收

  • Serial收集器采用复制算法、串行回收和"Stop-the-World"机制的方式执行内存回收。
  • Serial Old收集器同样也采用了串行回收和"STW"机制,只不过内存回收算法使用的是标记-压缩算法。
    • Serial Old是运行在Client模式下默认的老年代的垃圾回收器
    • Serial Old在Server模式下主要有两个用途:
      • 与新生代的Parallel Scavenge配合使用
      • 作为老年代CMS收集器的后备垃圾收集方案

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

4.3.1 优势

  • 优势: 简单而高效
  • 在用户的桌面应用场景中,可用内存一般不大

4.3.2 参数

-XX:+UseSerialGC
指定年轻代和老年代都使用串行收集器

小结

现在已经不用串行的了。而且在限定单核cpu才可以用。

4.4 ParNew GC: 并行回收

  • ParNew收集器是Serial收集器的多线程版本。
  • ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、STW机制
  • ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器。
  • 对于新生代,回收次数频繁,使用并行方式高效
  • 对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)

4.4.1 参数

-XX:+UseParNewGC
手动指定ParNew收集器执行内存回收任务。他表示年轻代使用并行收集器,不影响老年代。
-XX:ParallelGCThreads
限制线程数量,默认开启和CPU数据相同的线程数。

4.5 Parallel GC: 吞吐量优先

  • 新生代Parallel Scavenge收集器采用了复制算法、并行回收和STW机制
  • 可控制的吞吐量,自适应调节测流
  • 高吞吐量则可以高效率的利用CPU时间,尽快完成程序的运算任务主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
  • Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和STW机制
  • 在Java8中,默认的垃圾收集器

4.5.1 参数

-XX:+UseParallelGC

手动指定年轻代使用Parallel并行收集器执行内存回收任务

-XX:+UseParallelOldGC

手动指定老年代都是使用并行回收收集器。

-XX:ParallelGCThreads

设置年轻代并行收集器的线程数

-XX:MaxGCPauseMillis

设置垃圾收集器最大停顿时间(即STW的时间)

-XX:GCTimeRatio

用于衡量吞吐量,取值范围(0,100),默认99,也就是垃圾回收时间不超过1%

-XX:+UseAdaptiveSizePolicy

设置Parallel Scavenge收集器具有自适应调节策略

4.6 CMS: 低延迟

  • JDK1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器: CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,他第一次实现了让垃圾收集线程与用户线程同时工作。

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

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

  • 在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC.

4.6.1 收集过程

  • 初始标记(STW): 暂停时间非常短,标记与GC Roots直接关联的对象。
  • 并发标记(最耗时): 从GC Roots开始遍历整个对象图的过程。不会停顿用户线程。
  • 重新标记(STW): 修复并发标记环节,因为用户线程的执行,导致数据的不一致性问题
  • 并发清理(最耗时):

4.6.2 补充说明

  • 尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行STW机制暂停程序中的工作线程,不过暂停时间很短。
  • 由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
  • 当堆内存使用率达到某一阈值时,便开始进行回收
  • 使用的是标记-清除算法,会产生空间碎片

4.6.3 为什么不使用标记-整理算法呢?

因为在并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响。
标记-整理更适合STW这种场景下使用。

4.6.4 优缺点

优点:

  • 并发收集
  • 低延迟

弊端:

  • 会产生内存碎片
  • 对CPU资源非常敏感。在并发阶段,他虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
  • 无法处理浮动垃圾
    • 并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,只能在下一次执行GC时释放这些之前未被回收的内存空间。

4.6.5 参数

-XX:+UseConcMarkSweepGC

手动指定使用CMS收集器执行内存回收任务。

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

-XX:CMSInitiatingOccupanyFraction

设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
- JDK6及以上版本默认值92%
- 通过该选项便可以有效降低Full GC的执行次数。

-XX:+UseCMSCompactSAtFullCollection

用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。

-XX:CMSFullGCsBeforeCompaction

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

4.7 G1 GC: 区域化分代式

  • G1是一款面向服务端应用的垃圾收集器,兼顾吞吐量和停顿时间的GC实现。
  • 是JDK9以后的默认GC选项,取代了CMS回收器
    在这里插入图片描述

4.7.1 为什么还需要G1 GC?

由于业务越来越庞大、复杂,用户越来越多
与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。
官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起"全功能收集器"的重任与期望。

4.7.2 为什么叫G1(Garbage First)呢?

  • G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个有限列表,每次根据允许的收集时间,优先回收价值最大的Region。
  • 由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以给G1一个名字: 垃圾优先(Garbage First)。

4.7.3 特点

  • 并行与并发
  • 分代收集
  • 空间整合
    • CMS: “标记-清除算法”、内存碎片、若干次GC后进行一次碎片整理
    • G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿时间模型

4.7.4 参数

-XX:+UseG1GC

手动指定使用G1收集器执行内存回收任务

-XX:G1HeapRegionSize

设置每个Region的大小。值是2的幂

-XX:MaxGCPauseMills

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

-XX:ParallelGCThread

设置STW时GC线程数的值。最多设置为8

-XX:ConcGCThreads

设置并发标记的线程数。将n设置为并行垃圾回收线程数的1/4左右。

-XX:InitiatingHeapOccupancyPercent

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

4.7.5 操作步骤

  • 开启G1垃圾收集器
  • 设置堆的最大内存
  • 设置最大的停顿时间

4.7.6 适用场景

  • 面向服务端应用,针对具有大内存、多处理器的机器。

4.7.7 垃圾回收过程

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

4.8 各GC使用场景

在这里插入图片描述

4.9 GC日志分析

4.9.1 GC日志参数

-verbose:gc

输出gc日志信息,默认输出到标准输出

-XX:+PrintGC

输出GC日志。类似: -verbose:gc

-XX:+PrintGCDetails

在发生垃圾回收时打印内存回收详细的日志,并在进程退出时输出当前内存各区域分配情况

-XX:+PrintGCTimeStamps

输出GC发生时的时间戳

-XX:+PrintGCDateStamps

输出GC发生时的时间戳

-XX:j+PringHeapAtGC

每一次GC前和GC后,都打印堆信息

-Xloggc:<file>

表示把GC日志写入到一个文件中,而不是打印到标准输出中。

猜你喜欢

转载自blog.csdn.net/qq_43478625/article/details/121460612
今日推荐