一文搞懂JVM 垃圾回收机制

        在 Java 虚拟机(JVM)的世界里,垃圾回收(Garbage Collection,简称 GC)是一项至关重要的技术,它如同一位默默工作的清洁工,自动管理内存,让开发者无需手动释放不再使用的内存空间,极大地提升了开发效率和程序的稳定性。接下来,就让我们深入探究 JVM 垃圾回收的奥秘。​

一、垃圾回收的基本概念​

        垃圾回收,简单来说,就是 JVM 自动识别出程序中不再被使用的对象,并回收这些对象所占用的内存空间,以便重新分配给其他需要的对象。想象一下,我们的程序在运行过程中会不断创建对象,就像在房间里不断堆放物品,如果不及时清理不再使用的物品,房间就会变得拥挤不堪,甚至无法容纳新的物品。垃圾回收就是为 JVM 的内存空间进行 “大扫除”,确保内存资源得到高效利用 。​

二、如何判断对象已 “死亡”​

        在进行垃圾回收之前,JVM 首先需要确定哪些对象是不再被使用的,也就是判断对象是否已 “死亡”。常见的判断方法有以下两种:​

1. 引用计数法​

        引用计数法的原理是为每个对象维护一个引用计数器。当一个对象被创建并赋值给一个变量时,计数器加 1;当指向该对象的引用被赋值为 null,或者引用指向了其他对象时,计数器减 1;当计数器的值为 0 时,就表示该对象不再被任何变量引用,即对象已 “死亡”,可以被回收。例如:

class MyObject {
    // 简单的类,用于示例
}

public class ReferenceCountingExample {
    public static void main(String[] args) {
        MyObject obj1 = new MyObject(); // obj1引用MyObject对象,对象引用计数为1
        MyObject obj2 = obj1; // obj2也引用该对象,引用计数变为2
        obj1 = null; // obj1不再引用对象,引用计数减为1
        obj2 = null; // obj2也不再引用对象,引用计数变为0,该对象可被回收
    }
}

        虽然引用计数法实现简单,判断效率高,但它存在一个致命的缺陷 —— 无法解决循环引用的问题。例如,两个对象互相引用,即使它们已经不再被外部变量引用,但由于它们之间的引用关系,计数器始终不为 0,导致无法被回收。因此,在 JVM 中,引用计数法并没有被广泛采用。​

2. 可达性分析算法​

        可达性分析算法是目前 JVM 中主流的对象存活判断方法。它的基本思路是通过一系列被称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,就说明该对象是不可达的,即已 “死亡”,可以被回收。在 Java 中,可作为 GC Roots 的对象包括:​

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。​
  • 方法区中类静态属性引用的对象。​
  • 方法区中常量引用的对象。​
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。​

例如:

class A {
    B b;
}

class B {
    A a;
}

public class ReachabilityAnalysisExample {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        a.b = b;
        b.a = a;

        a = null;
        b = null;
        // 此时,虽然A和B对象互相引用,但它们到GC Roots没有引用链,会被判定为可回收对象
    }
}

三、对象的 finalization 机制​

1.finalize () 方法概述​

        finalize()方法是Object类中的一个实例方法,其源码为protected void finalize() throws Throwable { }。它是对象销毁前的回调方法,Java 语言提供该机制,允许开发人员在对象被垃圾回收器回收之前,执行自定义的处理逻辑。

        当垃圾回收器发现某个对象不再被任何引用指向,即该对象即将被回收时,总会先调用这个对象的finalize()方法 ,并且一个对象的finalize()方法在其生命周期中只会被调用一次。这使得开发者有机会在对象被回收前,进行一些资源释放、状态清理等操作,例如关闭文件流、断开数据库连接等。​

2.finalize () 方法的使用注意事项​

        虽然finalize()方法提供了对象回收前的自定义处理机会,但在实际开发中,并不推荐主动调用某个对象的finalize()方法,而应将其交给垃圾回收机制自动调用。原因主要有以下三点:

对象复活风险:在finalize()方法中,有可能通过重新建立引用关系,使原本即将被回收的对象 “复活”。例如:

public class FinalizeExample {
    public static FinalizeExample savedInstance;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        savedInstance = this;
    }

    public static void main(String[] args) {
        FinalizeExample obj = new FinalizeExample();
        obj = null;
        System.gc();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (savedInstance != null) {
            System.out.println("对象复活了!");
        }
    }
}

        在上述代码中,FinalizeExample类重写了finalize()方法,在方法中重新建立了对自身的引用,导致对象在即将被回收时复活。这种对象复活的情况可能会破坏程序的逻辑,并且使垃圾回收过程变得复杂和不可预测。​

执行时间无保障:finalize()方法的执行时间完全由 GC 线程决定,极端情况下,如果程序长时间不触发垃圾回收,或者系统垃圾回收压力较小,那么finalize()方法可能一直没有执行机会。这就意味着,依赖finalize()方法进行的资源释放等操作可能无法及时执行,从而导致资源占用、泄漏等问题 。​

影响 GC 性能:如果finalize()方法编写不当,例如陷入死循环,或者执行一些耗时较长的操作,会严重影响垃圾回收器的性能。因为在调用finalize()方法时,垃圾回收器需要暂停其他操作来执行该方法,若方法执行效率低下,将导致整个垃圾回收过程的延迟,降低程序的运行效率。

3.对象在 finalization 机制下的三种状态​

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

  • 可触及的:从根节点(如虚拟机栈中引用的对象、方法区中类静态属性引用的对象等 GC Roots)开始,通过引用链可以到达的对象,即为可触及的对象。这些对象正在被程序使用,不会被垃圾回收器回收 。​
  • 可复活的:当对象的所有引用都被释放,从 GC Roots 出发无法访问到该对象时,该对象进入可复活状态。此时,垃圾回收器在回收该对象前,会先调用其finalize()方法。如果在finalize()方法中重新建立了对该对象的引用,那么这个对象就会 “复活”,重新变为可触及的对象,从而避免被回收 。​
  • 不可触及的:当对象的finalize()方法被调用后,并且在该方法中没有使对象复活,那么这个对象就会进入不可触及状态。处于不可触及状态的对象,将被垃圾回收器回收,其占用的内存空间也将被释放 。

四、垃圾回收算法​

        确定了哪些对象是垃圾后,JVM 就需要采用合适的算法来回收这些垃圾对象占用的内存空间。常见的垃圾回收算法有以下几种:​

1. 标记 - 清除算法​

        标记 - 清除算法是最基础的垃圾回收算法,它分为两个阶段:标记阶段和清除阶段。在标记阶段,GC 从 GC Roots 开始遍历所有可达对象,并为它们做上标记;在清除阶段,GC 遍历整个堆内存,将没有标记的对象(即垃圾对象)占用的内存空间回收。例如,在一个存放了很多物品的仓库中,先把有用的物品贴上标签,然后把没有标签的物品清理掉。​

        标记 - 清除算法虽然实现简单,但存在两个明显的缺点:一是效率问题,标记和清除两个过程的效率都不高;二是空间问题,由于回收后的内存空间不连续,会产生大量的内存碎片,当需要分配较大内存空间时,可能会因为无法找到连续的足够大的内存空间而导致内存分配失败。​

2. 标记 - 复制算法​

        复制算法将内存空间划分为大小相等的两块,每次只使用其中一块。当这一块内存空间用完后,GC 将这块内存中存活的对象复制到另一块内存中,然后将原来的内存空间一次性清理掉,再进行交换。就像我们有两个一模一样的房间,当一个房间堆满物品后,把有用的物品搬到另一个房间,然后把这个房间彻底打扫干净。​

        复制算法解决了标记 - 清除算法效率低和内存碎片的问题,因为复制过程不需要遍历整个内存空间,而且新的内存空间是连续的。但它的缺点是内存利用率低,只有一半的内存空间可用。在新生代中,对象的存活率通常较低,大部分对象都是 “朝生夕死” 的,因此复制算法在新生代的垃圾回收中得到了广泛应用。​

        复制算法适合存活对象少,垃圾对象多,特别适合收新生代。

3. 标记 - 压缩算法​

        标记 - 压缩算法是在标记 - 清除算法的基础上发展而来的。它同样分为标记阶段和整理阶段。在标记阶段,与标记 - 清除算法相同,先标记出所有可达对象;在整理阶段,不是直接清除垃圾对象,而是将所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。例如,在一个书架上,先把有用的书标记出来,然后把这些书整齐地摆放到书架的一端,把另一端空出来的空间清理干净。​

        标记 - 压缩算法解决了标记 - 清除算法的内存碎片问题,同时也提高了内存的利用率,相比复制算法,它不需要浪费一半的内存空间。在老年代中,对象的存活率较高,使用标记 - 整理算法更为合适。

4.分代收集算法

        分代收集算法并不是一个新的算法,而是根据对象的存活周期不同,将 Java 堆内存划分为新生代和老年代,然后针对不同代采用不同的垃圾回收算法。在新生代,由于对象存活率低,采用复制算法可以高效地回收垃圾;在老年代,由于对象存活率高,采用标记 - 整理算法可以更好地利用内存空间,避免内存碎片的产生。这种分代的思想充分利用了对象的生命周期特点,提高了垃圾回收的效率和内存的利用率。

五、垃圾回收器​

        垃圾回收算法是垃圾回收的理论基础,而垃圾回收器则是垃圾回收算法的具体实现。JVM 提供了多种垃圾回收器,不同的垃圾回收器适用于不同的场景,它们各自有不同的特点和性能表现。常见的垃圾回收器包括 Serial 回收器、ParNew 回收器、Parallel Scavenge 回收器、Serial Old 回收器、Parallel Old 回收器、CMS 回收器和 G1 回收器等。​

        按线程数可以分为单线程(串行)垃圾回收器多线程(并行)垃圾回收器。

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

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

1. Serial 回收器​

        Serial 回收器是最基本、发展历史最悠久的垃圾回收器,它是一个单线程的回收器,在进行垃圾回收时,会暂停所有的用户线程(即 “Stop The World”),直到垃圾回收完成。Serial 回收器适用于 Client 模式下的应用程序,对于单 CPU 环境或者对暂停时间要求不高的应用,它具有简单高效的特点。例如,在一些小型的桌面应用中,Serial 回收器可以很好地工作。​

2. ParNew 回收器​

        ParNew 回收器是 Serial 回收器的多线程版本,它在 Serial 回收器的基础上,使用多个线程同时进行垃圾回收,因此在垃圾回收效率上有了显著提升。它同样会暂停所有用户线程,常用于 Server 模式下与 CMS 回收器配合使用,在新生代进行垃圾回收。​

3. Parallel Scavenge 回收器​

        Parallel Scavenge 回收器也是一个多线程的新生代垃圾回收器,它的目标是达到一个可控制的吞吐量。吞吐量是指 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。Parallel Scavenge 回收器通过自动调整新生代的大小、Eden 区和 Survivor 区的比例等参数,来尽量提高吞吐量,适合在对吞吐量要求较高的场景中使用,如后台运算任务。​

4. Serial Old 回收器​

        Serial Old 回收器是 Serial 回收器的老年代版本,同样是单线程的,采用标记 - 整理算法。它主要用于 Client 模式下的老年代垃圾回收,或者在 Server 模式下与 Parallel Scavenge 回收器配合使用,作为 Parallel Scavenge 回收器的老年代版本。​

5. Parallel Old 回收器​

        Parallel Old 回收器是 Parallel Scavenge 回收器的老年代版本,是多线程的,采用标记 - 整理算法。它的出现使得 Parallel Scavenge 回收器在 Server 模式下可以更好地发挥作用,在注重吞吐量以及 CPU 资源敏感的场景中,Parallel Scavenge 回收器和 Parallel Old 回收器的组合是一个不错的选择。​

6. CMS 回收器

        CMS(Concurrent Mark Sweep)回收器是一种以获取最短回收停顿时间为目标的垃圾回收器,它非常适合在对响应时间要求较高的应用中使用,如 Web 应用。CMS 回收器的垃圾回收过程分为四个阶段:初始标记、并发标记、重新标记和并发清除。其中,初始标记和重新标记阶段需要暂停用户线程,而并发标记和并发清除阶段可以与用户线程同时运行,因此在整体上可以减少垃圾回收对用户线程的影响。但 CMS 回收器也存在一些缺点,如会产生内存碎片、对 CPU 资源比较敏感等。​

7. G1 回收器​

        G1(Garbage-First)回收器是 JDK 1.7 后引入的一个具有划时代意义的垃圾回收器。它将 Java 堆内存划分为多个大小相等的 Region,每个 Region 都可以扮演新生代的 Eden 区、Survivor 区或者老年代的角色。G1 回收器不再像其他回收器那样区分新生代和老年代,而是基于 Region 进行垃圾回收。它的目标是在满足用户设定的停顿时间目标的前提下,尽量提高垃圾回收的效率。G1 回收器通过维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region,从而保证了在有限的时间内尽可能多地回收垃圾。G1 回收器适用于大内存、多处理器的服务器环境,在对内存占用和停顿时间有严格要求的应用中表现出色。

        以上垃圾收集器中,需要重点关注 CMS 和 G1 ,其他回收器了解即可。下图中展示了 7 种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。

六、内存溢出与内存泄露

内存溢出

        内存溢出(Out Of Memory,简称 OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出.

内存泄漏

        内存泄漏也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。

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

七、Stop the World

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

         可达性分析算法中枚举根节点(GC Roots)会导致所有Java 执行线程停顿,为什么需要停顿所有 Java 执行线程呢?

1.分析工作必须在一个能确保一致性的快照中进行

2.一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上

3.如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证,会出现漏标,错标问题

4.被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少 STW的发生。

5.越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

        STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。

八、G1回收器

1.既然我们已经有了前面几个强大的 GC,为什么还要发布Garbage First(G1)GC?

        原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC 就不能保证应用程序正常进行,而经常造成 STW的GC 又跟不上实际的需求,所以才会不断地尝试对 GC 进行优化。

        G1(Garbage-First)垃圾回收器是在 Java7 update 4 之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一. 与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。官方给 G1 设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。 G1 是一款面向服务端应用的垃圾收集器。 

2.为什么名字叫做 Garbage First(G1)呢?

        因为 G1 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的逻辑上连续的)。使用不同的 Region 来表示Eden、幸存者0 区,幸存者 1 区,老年代等。 G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。

        G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region. 由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给 G1 一个名字:垃圾优先(Garbage First)。

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

        如下图所示,G1 收集器收集器收集过程有初始标记、并发标记、最终标记、筛选回收,和 CMS 收集器前几步的收集过程很相似:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值