关于JVM:我所知道JVM虚拟机之垃圾回收相关概念的概述

34次阅读

共计 15735 个字符,预计需要花费 40 分钟才能阅读完成。

前言

从本篇开始咱们正式解说垃圾回收的相干常识了,让咱们开始吧

一、System.Gc 的了解


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

然而 System.gc()调用附带一个免责申明,无奈保障对垃圾收集器的调用(不能确保立刻失效)

简略的意思说揭示 JVM 进行垃圾回收,但不能保障垃圾收集器它进行调用

JVM 实现者能够通过 System.gc() 调用来决定 JVM 的 GC 行为。而 个别状况下垃圾回收应该是主动进行的 毋庸手动触发,否则就太过于麻烦了。在一些非凡状况下,如果咱们正在编写一个性能基准,咱们能够在运行之间调用 System.gc()

接下来咱们应用示例代码演示一下

public class SystemGCTest {public static void main(String[] args) {new SystemGCTest();
        System.gc();// 揭示 jvm 的垃圾回收器执行 gc, 然而不确定是否马上执行 gc}
    // 如果产生了 GC,这个 finalize()肯定会被调用
    @Override
    protected void finalize() throws Throwable {super.finalize();
        System.out.println("SystemGCTest 重写了 finalize()");
    }

输入后果不确定:有时候会调用 finalize() 办法,有时候并不会调用

并且咱们调用的 System.gc()背地调用又是 Runtime.getRuntime().gc()办法

public final class System {public static void gc() {Runtime.getRuntime().gc();}
}

public class Runtime {public native void gc();

}

咱们再来看一个示例代码,采纳强制调用的形式

public class SystemGCTest {public static void main(String[] args) {new SystemGCTest();
       System.runFinalization();// 强制调用应用援用的对象的 finalize()办法
    }
    // 如果产生了 GC,这个 finalize()肯定会被调用
    @Override
    protected void finalize() throws Throwable {super.finalize();
        System.out.println("SystemGCTest 重写了 finalize()");
    }
}
// 运行后果如下:SystemGCTest 重写了 finalize()

那么这时咱们再运行程序,就能够强制调用应用援用的对象的 finalize()办法

这时咱们再通过代码示例来,领会手动调用 GC 来了解 不可达对象的回收行为

public class LocalVarGC {public void localvarGC1() {byte[] buffer = new byte[10 * 1024 * 1024];//10MB
        System.gc();}

    public static void main(String[] args) {LocalVarGC local = new LocalVarGC();
        // 通过在 main 办法调用这个办法进行测试
        local.localvarGC1();}
}

这时咱们设置一下 JVM 的参数,并且运行我的项目看看具体信息如何

这时咱们实现了第一个示例代码的状况剖析,接下来进行第二个示例代码状况剖析

public class LocalVarGC {public void localvarGC2() {byte[] buffer = new byte[10 * 1024 * 1024];
        buffer = null;
        System.gc();}
    public static void main(String[] args) {LocalVarGC local = new LocalVarGC();
        // 通过在 main 办法调用这几个办法进行测试
        local.localvarGC2();}
}

与下面的 JVM 参数设置统一,这时咱们运行起来程序看看具体信息怎么样

这时咱们实现了第二个示例代码的状况剖析,接下来进行第三个示例代码状况剖析

public class LocalVarGC {public void localvarGC3() {
        {byte[] buffer = new byte[10 * 1024 * 1024];
        }
        System.gc();}
    public static void main(String[] args) {LocalVarGC local = new LocalVarGC();
        // 通过在 main 办法调用这个办法进行测试
        local.localvarGC3();}
}

与下面的 JVM 参数设置统一,这时咱们运行起来程序看看具体信息怎么样

这时咱们实现了第三个示例代码的状况剖析,接下来进行第四个示例代码状况剖析

public class LocalVarGC {public void localvarGC4() {
        {byte[] buffer = new byte[10 * 1024 * 1024];
        }
        int value = 10;
        System.gc();}
    
    public static void main(String[] args) {LocalVarGC local = new LocalVarGC();
        // 通过在 main 办法调用这个办法进行测试
        local.localvarGC4();}
}

与下面的 JVM 参数设置统一,这时咱们运行起来程序看看具体信息怎么样


这时咱们实现了第四个示例代码的状况剖析,接下来进行第五个示例代码状况剖析

public class LocalVarGC {public void localvarGC5() {localvarGC1();
        System.gc();}

    public static void main(String[] args) {LocalVarGC local = new LocalVarGC();
        // 通过在 main 办法调用这几个办法进行测试
        local.localvarGC5();}
}

与下面的 JVM 参数设置统一,这时咱们运行起来程序看看具体信息怎么样

二、内存溢出与内存泄露


内存溢出(OOM)的介绍

================================

内存溢出绝对于内存透露来说,只管更容易被了解然而同样的,内存溢出也是引发程序解体的罪魁祸首之一

因为 GC 始终在倒退所有个别状况下,除非应用程序占用的内存增长速度十分快,造成垃圾回收曾经跟不上内存耗费的速度,否则不太容易呈现 OOM 的状况

大多数状况下GC 会进行各种年龄段的垃圾回收,切实不行了就放大招,来一次独占式的 Full GC 操作,这时候会回收大量的内存供应用程序持续应用

Javadoc 中对 OutofMemoryError 的解释是:没有闲暇内存,并且垃圾收集器也无奈提供更多内存

内存溢出(OOM)起因剖析

================================

首先说没有闲暇内存的状况:阐明 Java 虚拟机的堆内存不够。起因有二:

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 能够判断出垃圾收集并不能解决这个问题,所以间接抛出 OutofMemoryError

内存透露(Memory Leak)的介绍

================================

内存透露也称作“存储渗漏”

严格来说 只有对象不会再被程序用到了,然而 GC 又不能回收他们的状况,才叫内存透露

但理论状况很多时候一些不太好的实际(或忽略)会导致 对象的生命周期变得很长甚至导致 OOM,也能够叫做宽泛意义上的“内存透露”

只管内存透露并不会立即引起程序解体,然而 一旦产生内存透露,程序中的可用内存就会被逐渐鲸吞,直至耗尽所有内存,最终呈现 OutofMemory 异样,导致程序解体

留神:这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘替换区设定的大小

内存泄露的官网例子

================================

在咱们的程序中常见的例子有:

单例模式

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

一些提供 close()的资源未敞开导致内存透露

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

三、Stop The World


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

可达性剖析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程进展

那么为什么须要进展所有 Java 执行线程呢?有以下几种起因

  • 剖析工作必须在一个能确保一致性的快照中进行
  • 一致性指整个剖析期间整个执行零碎看起来像被解冻在某个工夫点上
  • 如果呈现剖析过程中对象援用关系还在一直变动,则剖析后果的准确性无奈保障

而被 STW 中断的应用程序线程会在实现 GC 之后复原,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以咱们须要 缩小 STW 的产生

不过须要揭示的是:STW 事件和采纳哪款 GC 无关,所有的 GC 都有这个事件

哪怕是 G1 也不能完全避免 Stop-the-world 状况产生,只能说垃圾回收器越来越优良,回收效率越来越高,尽可能地缩短了暂停工夫

STW 是 JVM 在 后盾主动发动和主动实现 的。在用户不可见的状况下,把用户失常的工作线程全副停掉

所以咱们在开发中不要用 System.gc(),这会导致 Stop-the-World 的产生

接下来咱们应用示例代码领会领会 STW 是什么感触

public static class PrintThread extends Thread {public final long startTime = System.currentTimeMillis();

    public void run() {
        try {while (true) {
                // 每秒打印工夫信息
                long t = System.currentTimeMillis() - startTime;
                System.out.println(t / 1000 + "." + t % 1000);
                Thread.sleep(1000);
            }
        } catch (Exception ex) {ex.printStackTrace();
        }
    }
    public static void main(String[] args) {PrintThread p = new PrintThread();
        p.start();}
}
// 运行后果如下:
0.1
1.1
2.1
.....

咱们能够察看得出后果:以后工夫距离与上次工夫距离 ** 根本 ** 是每隔 1 秒打印一次

这时咱们再增加多一个线程做事件,当满足某些条件的时候执行 gc

public static class WorkThread extends Thread {List<byte[]> list = new ArrayList<byte[]>();

    public void run() {
        try {while (true) {for(int i = 0;i < 1000;i++){byte[] buffer = new byte[1024];
                    list.add(buffer);
                }
                if(list.size() > 10000){list.clear();
                    System.gc();// 会触发 full gc,进而会呈现 STW 事件}
            }
        } catch (Exception ex) {ex.printStackTrace();
        }
    }
}

这时咱们新建一个 Demo 类将这两个类执行起来,看看 gc 对咱们原有输入有什么影响

public class StopTheWorldDemo {WorkThread w = new WorkThread();
    PrintThread p = new PrintThread();
    w.start();
    p.start();}
// 运行后果如下:0.0
1.7
2.8
3.10
4.11
5.13

咱们能够察看得出后果:以后工夫距离与上次工夫距离相差 1.3s 呈现 Stop the World 的感觉

四、垃圾回收的并行与并发


并发(Concurrent)的介绍

================================

在操作系统中指 一个时间段 中有几个程序都处于已启动运行到运行结束之间,且这几个程序都是在同一个处理器上运行

并发不是真正意义上的“同时进行”,只是 CPU 把一个时间段划分成几个工夫片段(工夫区间),而后在这几个工夫区间之间来回切换。因为 CPU 解决的速度十分快,只有工夫距离解决切当,即可让用户感觉是多个应用程序同时在进行

并行(Parallel)的介绍

================================

当零碎有一个以上 CPU 时,当一个 CPU 执行一个过程时,另一个 CPU 能够执行另一个过程,两个过程互不抢占 CPU 资源,能够 同时 进行,咱们称之为并行(Parallel)

其实决定并行的因素不是 CPU 的数量,而是 CPU 的外围数量,比方一个 CPU 多个核也能够并行

并发与并行的比照

================================

并发,指的是多个事件,在 同一时间段内同时产生 了。

并行,指的是多个事件,在 同一时间点上(或者说同一时刻)同时产生 了。

并发的多个工作之间 是相互抢占资源的 。并行的多个工作之间 是不相互抢占资源的

只有在多 CPU 或者一个 CPU 多核的状况中,才会产生并行。否则看似同时产生的事件,其实都是并发执行的。

垃圾回收的并发与并行

================================

并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于期待状态

串行(Serial):相较于并行的概念,单线程执行。如果内存不够,则程序暂停,启动 JVM 垃圾回收器进行垃圾回收(单线程)

并发(Concurrent):指 用户线程与垃圾收集线程同时执行(但不肯定是并行的,可能会交替执行),垃圾回收线程在执行时不会进展用户程序的运行

五、平安点与平安区域


平安点介绍

================================

程序执行时并非在所有中央都能停顿下来开始 GC,只有在特定的地位能力停顿下来开始 GC,这些地位称为“平安点(Safepoint)”

Safe Point 的抉择很重要,如果太少可能导致 GC 期待的工夫太长,如果太频繁可能导致运行时的性能问题

大部分指令的执行工夫都十分短暂,通常会依据“是否具备让程序长时间执行的特色”为规范

比方:抉择一些执行工夫较长的指令作为 Safe Point,如办法调用、循环跳转和异样跳转等

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

领先式中断:(目前没有虚拟机采纳了)首先中断所有线程。如果还有线程不在平安点,就复原线程,让线程跑到平安点

主动式中断:设置一个中断标记,各个线程运行到 Safe Point 的时候 被动轮询 这个标记,如果中断标记为真,则将本人进行中断挂起

平安区域介绍

================================

平安区域是指在一段代码片段中,对象的援用关系不会发生变化,在这个区域中的任何地位开始 GC 都是平安的。咱们也能够把 Safe Region 看做是被扩大了的 Safepoint

咱们说 Safepoint 机制保障了程序执行时,在不太长的工夫内就会遇到可进入 GC 的 Safepoint。然而,程序“不执行”的时候呢?

例如线程处于 Sleep 状态或 Blocked 状态,这时候线程无奈响应 JVM 的中断请求,“走”到平安点去中断挂起,JVM 也不太可能期待线程被唤醒。对于这种状况,就须要平安区域(Safe Region)来解决

理论执行时

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

当线程行将来到 Safe Region 时,会查看 JVM 是否曾经实现 GC,如果实现了则持续运行,否则线程必须期待直到收到能够平安来到 Safe Region 的信号为止

六、再谈援用:概述


咱们心愿能形容这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是很缓和,则能够摈弃这些对象

那么要想形容这样一类的对象就要讲讲援用:强援用、软援用、弱援用、虚援用

先看一道偏门高频的面试题:强援用、软援用、弱援用、虚援用有什么区别?具体应用场景是什么?

在 JDK1.2 版之后,Java 对援用的概念进行了裁减,将援用分为:

  • 强援用(Strong Reference)
  • 软援用(Soft Reference)
  • 弱援用(Weak Reference)
  • 虚援用(Phantom Reference)

这 4 种援用强度顺次逐步削弱。除强外其余 3 种援用均能够在 java.lang.ref 包中找到它们的身影

Reference 子类中只有终结器援用是包内可见的,其余 3 种援用类型均为 public,能够在应用程序中间接应用

接下来咱们概述行的阐明一下这些援用是什么意思

强援用(StrongReference):

最传统的“援用”的定义是指在程序代码之中普遍存在的援用赋值,即相似“object obj=new Object()”这种援用关系

无论任何状况下只有强援用关系还存在,垃圾收集器就永远不会回收掉被援用的对象。宁肯报 OOM,也不会 GC 强援用

软援用(SoftReference):

在零碎将要产生内存溢出之前,将会把这些对象列入回收范畴之中进行第二次回收

如果这次回收后还没有足够的内存,才会抛出内存溢出异样

弱援用(WeakReference)

被弱援用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱援用关联的对象

虚援用(PhantomReference):

一个对象是否有虚援用的存在,齐全不会对其生存工夫形成影响,也无奈通过虚援用来取得一个对象的实例。为一个对象设置虚援用关联的惟一目标就是 能在这个对象被收集器回收时收到一个零碎告诉

六、再谈援用:强援用


在 Java 程序中,最常见的援用类型是强援用(一般零碎 99% 以上都是强援用),也就是咱们最常见的一般对象援用,也是默认的援用类型

当在 Java 语言中应用 new 操作符创立一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强援用

只有强援用的对象是可涉及的,垃圾收集器就永远不会回收掉被援用的对象。只有强援用的对象是可达的,jvm 宁肯报 OOM,也不会回收强援用

接下来咱们应用一个示例代码领会一下强援用造成的输入

public class StrongReferenceTest {public static void main(String[] args) {StringBuffer str = new StringBuffer ("Hello, 小明");
        StringBuffer str1 = str;

        str = null;
        System.gc();

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

        System.out.println(str1);
    }
}
// 输入后果如下:Hello, 小明

在下面代码中局部变量 str 指向 stringBuffer 实例所在堆空间,通过 str 能够操作该实例,那么 str 就是 stringBuffer 实例的强援用对应内存构造

在下面的代码中咱们能够晓得强援用大略有以下几点特点:

  • 强援用能够间接拜访指标对象。
  • 强援用所指向的对象在任何时候都不会被零碎回收,虚拟机宁愿抛出 OOM 异样,也不会回收强援用所指向对象。
  • 强援用可能导致内存透露

所以对于一个一般的对象,如果没有其余的援用关系,只有超过了援用的作用域或者显式地将相应(强)援用赋值为 null,就是能够当做垃圾被收集了,当然具体回收机会还是要看垃圾收集策略

绝对的,软援用、弱援用和虚援用的对象是软可涉及、弱可涉及和虚可涉及的,在肯定条件下,都是能够被回收的。所以,强援用是造成 Java 内存透露的次要起因之一

七、再谈援用:软援用


软援用是用来形容一些还有用但非必须的对象。只被软援用关联着的对象,在零碎将要产生内存溢出异样前,会把这些对象列进回收范畴之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异样。留神:这里的第一次回收是不可达的对象

个别咱们软援用通常 用来实现内存敏感的缓存 。比方: 高速缓存就有用到软援用。如果还有闲暇内存,就能够临时保留缓存,当内存不足时清理掉,这样就保障了应用缓存的同时,不会耗尽内存

在 JDK1.2 版之后提供了 SoftReference 类来实现软援用
Object obj = new Object();// 申明强援用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; // 销毁强援用

垃圾回收器 在某个时刻决定回收软可达的对象的时候会清理软援用,并可选地把援用寄存到一个援用队列(Reference Queue)

相似弱援用,只不过 Java 虚构机会尽量让软援用的存活工夫长一些,无可奈何才清理

接下来咱们应用示例代码领会领会软援用是什么状况

public static class User {public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int id;
    public String name;

    @Override
    public String toString() {return "[id=" + id + ", name=" + name + "]";
    }
}

此时增加 main 办法,运行程序看看是什么后果

public static void main(String[] args) {User u1 = new User(1,"songhk");
    SoftReference<User> userSoftRef = new SoftReference<User>(u1);
    u1 = null;// 勾销强援用

    // 从软援用中从新取得强援用对象
    System.out.println(userSoftRef.get());
}

运行后果如下:[id = 1,name = songhk]

当初咱们失常运行输入没有什么问题,此时咱们设置堆空间大小并运行 System.gc 后看看是否还在

public static void main(String[] args) {
    // 创建对象,建设软援用
    //SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));
    // 下面的一行代码,等价于如下的三行代码
    User u1 = new User(1,"songhk");
    SoftReference<User> userSoftRef = new SoftReference<User>(u1);
    u1 = null;// 勾销强援用

    // 从软援用中从新取得强援用对象
    System.out.println(userSoftRef.get());
    System.gc();
    System.out.println("After GC:");
    // 垃圾回收之后取得软援用中的对象
    System.out.println(userSoftRef.get());// 因为堆空间内存足够,所有不会回收软援用的可达对象。}
// 运行后果如下:[id = 1,name = songhk]
After GC:
[id = 1,name = songhk]

此时堆空间还是足够的,并没有回收软援用,此时咱们操作一下将内存资源缓和缓和

public static void main(String[] args) {User u1 = new User(1,"songhk");
    SoftReference<User> userSoftRef = new SoftReference<User>(u1);
    u1 = null;// 勾销强援用

    // 从软援用中从新取得强援用对象
    System.out.println(userSoftRef.get());
    System.out.println("--- 目前内存还不缓和 ---");
    System.gc();
    System.out.println("After GC:");
    // 垃圾回收之后取得软援用中的对象
    System.out.println(userSoftRef.get());
    System.out.println("--- 上面开始内存缓和了 ---");
    try {
        // 让零碎认为内存资源缓和、不够
        byte[] b = new byte[1024 * 1024 * 7];
    } catch (Throwable e) {e.printStackTrace();
    } finally {
        // 再次从软援用中获取数据
        System.out.println(userSoftRef.get());
    }
}

// 运行后果如下:[id=1, name=songhk] 
--- 目前内存还不缓和 ---
After GC:
[id=1, name=songhk] 
--- 上面开始内存缓和了 ---
null
java.lang.OutOfMemoryError: Java heap space
 at com.atguigu.java1.SoftReferenceTest.main(SoftReferenceTest.java:48)

此时咱们就能够看到在堆空间不报 OOM 时,垃圾回收期会回收软援用的可达对象

一句话概括:当内存足够时,不会回收软援用可达的对象。内存不够时,会回收软援用的可达对象

八、再谈援用:弱援用


弱援用也是用来形容那些非必须对象,被弱援用关联的对象只能生存到下一次垃圾收集产生为止。在零碎 GC 时,只有发现弱援用,不论零碎堆空间应用是否短缺,都会回收掉只被弱援用关联的对象

然而因为垃圾回收器的线程通常优先级很低,因而并不一定能很快地发现持有弱援用的对象。在这种状况下,弱援用对象能够存在较长的工夫

弱援用和软援用一样在结构弱援用时,也能够指定一个援用队列,当弱援用对象被回收时就会退出指定的援用队列,通过这个队列能够跟踪对象的回收状况

在 JDK1.2 版之后提供了 WeakReference 类来实现弱援用
// 申明强援用
Object obj = new Object();
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; // 销毁强援用

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

接下来咱们通过示例来领会一下弱援用

public static class User {public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int id;
    public String name;

    @Override
    public String toString() {return "[id=" + id + ", name=" + name + "]";
    }
}

这时咱们增加 main 办法调用这个 User 类,看看咱们 gc 后还存在不

public static void main(String[] args) {
    // 结构了弱援用
    WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk"));
    // 从弱援用中从新获取对象
    System.out.println(userWeakRef.get());

    System.gc();
    // 不论以后内存空间足够与否,都会回收它的内存
    System.out.println("After GC:");
    // 从新尝试从弱援用中获取对象
    System.out.println(userWeakRef.get());
}
// 运行后果如下:[id=1, name=songhk] 
After GC:
null

这时咱们就能够晓得执行垃圾回收后,软援用对象必然被革除

软援用、弱援用都非常适合来保留那些可有可无的缓存数据。如果这么做,当零碎内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源短缺时,这些缓存数据又能够存在相当长的工夫,从而起到减速零碎的作用

九、再谈援用:虚援用


虚援用也称为“幽灵援用”或者“幻影援用”,是所有援用类型中最弱的一个

一个对象是否有虚援用的存在,齐全不会决定对象的生命周期。

如果一个对象仅持有虚援用,那么它和没有援用简直是一样的,随时都可能被垃圾回收器回收

它不能独自应用,也无奈通过虚援用来获取被援用的对象。当试图通过虚援用的 get()办法获得对象时,总是 null。即通过虚援用无奈获取到咱们的数据

一个对象设置虚援用关联的惟一目标在于跟踪垃圾回收过程。比方:能在这个对象被收集器回收时收到一个零碎告诉

在 JDK1.2 版之后提供了 PhantomReference 类来实现虚援用
// 申明强援用
Object obj = new Object();
// 申明援用队列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 申明虚援用(还须要传入援用队列)PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;

咱们应用示例领会一下虚援用是什么,先领会看看是否获取

public class PhantomReferenceTest {

    public static PhantomReferenceTest obj;// 以后类对象的申明
    
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;// 援用队列
    
    public static void main(String[] args) {phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
        
        obj = new PhantomReferenceTest();
    
        // 结构了 PhantomReferenceTest 对象的虚援用,并指定了援用队列
        PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
        
        System.out.println(phantomRef.get());
    
    }
}
// 运行后果如下:null

第一次尝试获取虚援用的值,发现无奈获取的,这是因为虚援用是无奈间接获取对象的值

接下来咱们增加一些代码进来,再看看是个什么状况

public class PhantomReferenceTest {
    
    public static PhantomReferenceTest obj;// 以后类对象的申明
    
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;// 援用队列
    
    @Override
    protected void finalize() throws Throwable { //finalize()办法只能被调用一次!super.finalize();
        System.out.println("调用以后类的 finalize()办法");
        obj = this;
    }
}
public static void main(String[] args) {phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
    obj = new PhantomReferenceTest();
    
    // 结构了 PhantomReferenceTest 对象的虚援用,并指定了援用队列
    PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
  
    System.out.println(phantomRef.get());

    // 将强援用去除
    obj = null;
    System.gc();
    Thread.sleep(1000);
    if (obj == null) {System.out.println("obj 是 null");
    } else {System.out.println("obj 可用");
    }
}
// 运行后果如下:null
第 1 次 gc
调用以后类的 finalize()办法
obj 可用

此时咱们进行第一次 GC,因为会调用 finalize 办法,将对象复活了,所以对象没有被回收。

那么这关咱们虚援用什么事呢?不急让咱们再增加一些代码阐明一下

public class PhantomReferenceTest {

    public static PhantomReferenceTest obj;// 以后类对象的申明
    
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;// 援用队列

    public static class CheckRefQueue extends Thread {
        @Override
        public void run() {while (true) {if (phantomQueue != null) {
                    PhantomReference<PhantomReferenceTest> objt = null;
                    try {objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();} catch (InterruptedException e) {e.printStackTrace();
                    }
                    if (objt != null) {System.out.println("追踪垃圾回收过程:PhantomReferenceTest 实例被 GC 了");
                    }
                }
            }
        }
    }
}
public static void main(String[] args) {Thread t = new CheckRefQueue();
    t.setDaemon(true);// 设置为守护线程:当程序中没有非守护线程时,守护线程也就执行完结。t.start();

    phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
    obj = new PhantomReferenceTest();
    // 结构了 PhantomReferenceTest 对象的虚援用,并指定了援用队列
    PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);

    try {
        // 不可获取虚援用中的对象
        System.out.println(phantomRef.get());
        System.out.println("第 1 次 gc");
        // 将强援用去除
        obj = null;
        // 第一次进行 GC, 因为对象可复活,GC 无奈回收该对象
        System.gc();
        Thread.sleep(1000);
        if (obj == null) {System.out.println("obj 是 null");
        } else {System.out.println("obj 可用");
        }
        System.out.println("第 2 次 gc");
        obj = null;
        System.gc(); // 一旦将 obj 对象回收,就会将此虚援用寄存到援用队列中。Thread.sleep(1000);
        if (obj == null) {System.out.println("obj 是 null");
        } else {System.out.println("obj 可用");
        }
    } catch (InterruptedException e) {e.printStackTrace();
    }
}

咱们 main 办法里启动一个线程进行虚援用 Object 对象的判断,始终 while 循环进行

当调用第二次 GC 操作的时候,因为 finalize 办法只能执行一次,所以就触发了 GC 操作,将对象回收了,同时将会触发第二个操作就是将待回收的对象存入到援用队列中

所以此时的输入后果就是

null
第 1 次 gc
调用以后类的 finalize()办法
obj 可用
第 2 次 gc
追踪垃圾回收过程:PhantomReferenceTest 实例被 GC 了
obj 是 null

十、再谈援用:终结器援用


它用于实现对象的 finalize() 办法,也能够称为终结器援用

无需手动编码,其外部配合援用队列应用

在 GC 时,终结器援用入队。由 Finalizer 线程通过终结器援用找到被援用对象调用它的 finalize()办法,第二次 GC 时才回收被援用的对象

参考资料


尚硅谷:JVM 虚拟机(宋红康老师)

正文完
 0