乐趣区

关于java:第-16-章-硬核-垃圾回收相关-GC细讲

第 16 章 垃圾回收相干概念

1、System.gc() 的了解

1.1、System.gc() 办法

System.gc() 办法

  1. 在默认状况下,通过 System.gc() 者 Runtime.getRuntime().gc() 的调用,会显式触发 Full GC,同时对老年代和新生代进行回收,尝试开释被抛弃对象占用的内存。
  2. 然而 System.gc() 调用附带一个免责申明,无奈保障对垃圾收集器的调用 ( 不能确保立刻失效)
  3. JVM 实现者能够通过 System.gc() 调用来决定 JVM 的 GC 行为。而个别状况下,垃圾回收应该是主动进行的,毋庸手动触发,否则就太过于麻烦了。
  4. 在一些非凡状况下,如咱们正在编写一个性能基准,咱们能够在运行之间调用 System.gc()

代码示例:手动执行 GC 操作

代码示例 1

  • 代码

public class SystemGCTest {public static void main(String[] args) {new SystemGCTest();

        System.runFinalization();}

    @Override
    protected void finalize() throws Throwable {super.finalize();
        System.out.println("SystemGCTest 重写了 finalize()");
    }
}
  • 有时候会调用 finalize() 办法,有时候并不会调用
"C:\Program Files\Java\jdk1.8.0_144\bin\java"
SystemGCTest 重写了finalize()

Process finished with exit code 0
"C:\Program Files\Java\jdk1.8.0_144\bin\java"

Process finished with exit code 0

1.2、不可达对象回收行为

手动 GC 了解不可达对象的回收行为

  • 代码

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

    public void localvarGC2() {byte[] buffer = new byte[10 * 1024 * 1024];
        buffer = null;
        System.gc();}

    public void localvarGC3() {
        {byte[] buffer = new byte[10 * 1024 * 1024];
        }
        System.gc();}

    public void localvarGC4() {
        {byte[] buffer = new byte[10 * 1024 * 1024];
        }
        int value = 10;
        System.gc();}

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

    public static void main(String[] args) {LocalVarGC local = new LocalVarGC();

        local.localvarGC1();}
}
  • JVM 参数
-XX:+PrintGCDetails

调用 localvarGC1() 办法

  • 执行 System.gc() 仅仅是将年老代的 buffer 数组对象放到了老年代(Why?buffer 数组年龄应该还没达到阈值吧~~~)
[GC (System.gc()) [PSYoungGen: 15482K->10744K(76288K)] 15482K->10976K(251392K), 0.0060965 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 10744K->0K(76288K)] [ParOldGen: 232K->10908K(175104K)] 10976K->10908K(251392K), [Metaspace: 3466K->3466K(1056768K)], 0.0048454 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
 PSYoungGen      total 76288K, used 655K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
  eden space 65536K, 1% used [0x000000076b380000,0x000000076b423ee8,0x000000076f380000)
  from space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
  to   space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
 ParOldGen       total 175104K, used 10908K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
  object space 175104K, 6% used [0x00000006c1a00000,0x00000006c24a7088,0x00000006cc500000)
 Metaspace       used 3473K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

调用 localvarGC2() 办法

  • 因为 buffer 数组对象没有援用指向它,执行 System.gc() 将被回收
[GC (System.gc()) [PSYoungGen: 15482K->872K(76288K)] 15482K->880K(251392K), 0.0008529 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 872K->0K(76288K)] [ParOldGen: 8K->668K(175104K)] 880K->668K(251392K), [Metaspace: 3466K->3466K(1056768K)], 0.0035955 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 PSYoungGen      total 76288K, used 655K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
  eden space 65536K, 1% used [0x000000076b380000,0x000000076b423ee8,0x000000076f380000)
  from space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
  to   space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
 ParOldGen       total 175104K, used 668K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
  object space 175104K, 0% used [0x00000006c1a00000,0x00000006c1aa7078,0x00000006cc500000)
 Metaspace       used 3473K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

调用 localvarGC3() 办法

  • 尽管出了代码块的作用域,然而 buffer 数组对象并没有被回收
[GC (System.gc()) [PSYoungGen: 15482K->10736K(76288K)] 15482K->10992K(251392K), 0.0054629 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 10736K->0K(76288K)] [ParOldGen: 256K->10908K(175104K)] 10992K->10908K(251392K), [Metaspace: 3466K->3466K(1056768K)], 0.0057568 secs] [Times: user=0.05 sys=0.06, real=0.02 secs]
Heap
 PSYoungGen      total 76288K, used 655K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
  eden space 65536K, 1% used [0x000000076b380000,0x000000076b423ee8,0x000000076f380000)
  from space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
  to   space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
 ParOldGen       total 175104K, used 10908K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
  object space 175104K, 6% used [0x00000006c1a00000,0x00000006c24a7088,0x00000006cc500000)
 Metaspace       used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K
  • 来看看字节码:实例办法局部变量表第一个变量必定是 this

  • 你有没有看到,局部变量表的大小是 2,也就是说执行 System.gc() 时,栈中还有 buffer 变量指向堆中的字节数组

调用 localvarGC4() 办法

  • 就多定义了一个局部变量 value,怎么就能把字节数组回收了呢?
[GC (System.gc()) [PSYoungGen: 15482K->808K(76288K)] 15482K->816K(251392K), 0.0010067 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 808K->0K(76288K)] [ParOldGen: 8K->668K(175104K)] 816K->668K(251392K), [Metaspace: 3466K->3466K(1056768K)], 0.0039890 secs] [Times: user=0.00 sys=0.02, real=0.01 secs]
Heap
 PSYoungGen      total 76288K, used 655K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
  eden space 65536K, 1% used [0x000000076b380000,0x000000076b423ee8,0x000000076f380000)
  from space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
  to   space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
 ParOldGen       total 175104K, used 668K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
  object space 175104K, 0% used [0x00000006c1a00000,0x00000006c1aa7078,0x00000006cc500000)
 Metaspace       used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K
  • 看,value 位于局部变量表中索引为 1 的地位

  • 局部变量表长度为 2,这阐明了出了代码块时,buffer 就出了其作用域范畴,此时没有为 value 开启新的槽,所以 value 变量占据了 buffer 变量的槽(Slot),导致堆中的字节数组没有援用再指向它,执行 System.gc() 时被回收

调用 localvarGC5() 办法

  • 这有啥好说的。。。局部变量除了办法范畴就是生效了,堆中的字节数组铁定被回收呗~
[GC (System.gc()) [PSYoungGen: 15482K->10744K(76288K)] 15482K->10968K(251392K), 0.0054897 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 10744K->0K(76288K)] [ParOldGen: 224K->10908K(175104K)] 10968K->10908K(251392K), [Metaspace: 3465K->3465K(1056768K)], 0.0048527 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 0K->0K(76288K)] 10908K->10908K(251392K), 0.0002221 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 10908K->668K(175104K)] 10908K->668K(251392K), [Metaspace: 3465K->3465K(1056768K)], 0.0055602 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
Heap
 PSYoungGen      total 76288K, used 655K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
  eden space 65536K, 1% used [0x000000076b380000,0x000000076b423ee8,0x000000076f380000)
  from space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
  to   space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
 ParOldGen       total 175104K, used 668K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
  object space 175104K, 0% used [0x00000006c1a00000,0x00000006c1aa7078,0x00000006cc500000)
 Metaspace       used 3472K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

图解剖析

2、内存溢出与内存透露

2.1、内存溢出

内存溢出(OOM)

  1. 内存溢出绝对于内存透露来说,只管更容易被了解,然而同样的,内存溢出也是引发程序解体的罪魁祸首之一。
  2. 因为 GC 始终在倒退,所有个别状况下,除非应用程序占用的内存增长速度十分快,造成垃圾回收曾经跟不上内存耗费的速度,否则不太容易呈现 OOM 的状况。
  3. 大多数状况下,GC 会进行各种年龄段的垃圾回收,切实不行了就放大招,来一次独占式的 Full GC 操作,这时候会回收大量的内存,供应用程序持续应用。
  4. Javadoc 中对 OutofMemoryError 的解释是,没有闲暇内存,并且垃圾收集器也无奈提供更多内存。

内存溢出(OOM)起因剖析

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

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

    • 比方:可能存在内存透露问题;也很有可能就是堆的大小不合理,比方咱们要解决比拟可观的数据量,然而没有显式指定 JVM 堆大小或者指定数值偏小。
    • 咱们能够通过参数 -Xms、-Xmx 来调整。
  2. 代码中创立了大量大对象,并且长时间不能被垃圾收集器收集(存在被援用)

    • 对于老版本的 Oracle JDK,因为永恒代的大小是无限的,并且 JVM 对永恒代垃圾回收(如,常量池回收、卸载不再须要的类型)十分不踊跃,所以当咱们一直增加新类型的时候,永恒代呈现 OutOfMemoryError 也十分多见
    • 尤其是在运行时存在大量动静类型生成的场合;相似 intern 字符串缓存占用太多空间,也会导致 OOM 问题。
    • 对应的异样信息,会标记进去和永恒代相干:”java.lang.OutOfMemoryError:PermGen space”。
    • 随着元数据区的引入,办法区内存曾经不再那么困顿,所以相应的 OOM 有所改观,呈现 OOM,异样信息则变成了:”java.lang.OutofMemoryError:Metaspace”。间接内存不足,也会导致 OOM。

阐明

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

    • 例如:在援用机制剖析中,波及到 JVM 会去尝试回收软援用指向的对象等。
    • 在 java.nio.Bits.reserveMemory() 办法中,咱们能分明的看到,System.gc() 会被调用,以清理空间。
  2. 当然,也不是在任何状况下垃圾收集器都会被触发的

    • 比方,咱们去调配一个超大对象,相似一个超大数组超过堆的最大值,JVM 能够判断出垃圾收集并不能解决这个问题,所以间接抛出 OutofMemoryError。

2.2、内存透露

内存透露(Memory Leak)

  1. 也称作 ” 存储渗漏 ”。严格来说,只有对象不会再被程序用到了,然而 GC 又不能回收他们的状况,才叫内存透露。
  2. 但理论状况很多时候一些不太好的实际(或忽略)会导致对象的生命周期变得很长甚至导致 OOM,也能够叫做宽泛意义上的 ” 内存透露 ”。
  3. 只管内存透露并不会立即引起程序解体,然而一旦产生内存透露,程序中的可用内存就会被逐渐鲸吞,直至耗尽所有内存,最终呈现 OutofMemory 异样,导致程序解体。
  4. 留神,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘替换区设定的大小。

内存泄露的举例

官网例子

右边的图:Java 应用可达性剖析算法,最下面的数据不可达,就是须要被回收的对象。

左边的图:前期有一些对象不必了,按情理应该断开援用,然而存在一些链没有断开,从而导致没有方法被回收。

咱们的例子

  1. 单例模式

    • 单例的生命周期和应用程序是一样长的,所以在单例程序中,如果持有对外部对象的援用的话,那么这个内部对象是不能被回收的,则会导致内存透露的产生。
  2. 一些提供 close() 的资源未敞开导致内存透露

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

3、Stop the World

对于 Stop the World 的了解

  1. Stop-the-World,简称 STW,指的是 GC 事件产生过程中,会产生应用程序的进展。进展产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个进展称为 STW。
  2. 可达性剖析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程进展,为什么须要进展所有 Java 执行线程呢?

    • 剖析工作必须在一个能确保一致性的快照中进行
    • 一致性指整个剖析期间整个执行零碎看起来像被解冻在某个工夫点上
    • 如果呈现剖析过程中对象援用关系还在一直变动,则剖析后果的准确性无奈保障
  3. 被 STW 中断的应用程序线程会在实现 GC 之后复原,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以咱们须要缩小 STW 的产生。

Stop the World 的注意事项

  1. STW 事件和采纳哪款 GC 无关,所有的 GC 都有这个事件。
  2. 哪怕是 G1 也不能完全避免 Stop-the-world 状况产生,只能说垃圾回收器越来越优良,回收效率越来越高,尽可能地缩短了暂停工夫。
  3. STW 是 JVM 在后盾主动发动和主动实现的。在用户不可见的状况下,把用户失常的工作线程全副停掉。
  4. 开发中不要用 System.gc(),这会导致 Stop-the-World 的产生。

代码感触 Stop the World

  • 代码

public class StopTheWorldDemo {
    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();}
                }
            } catch (Exception ex) {ex.printStackTrace();
            }
        }
    }

    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) {WorkThread w = new WorkThread();
        PrintThread p = new PrintThread();

        p.start();}
}
  • 敞开工作线程 w,察看打印输出:以后工夫距离与上次工夫距离最多相差 0.1s
0.0
1.0
2.1
3.1
4.2
5.2
6.3
7.4
8.4
  • 开启工作线程 w,察看打印输出:以后工夫距离与上次工夫距离最多相差 0.4s,能够显著感触到 Stop the World 的存在
0.1
1.2
2.4
3.5
4.6
5.6
6.10
7.11
8.14
9.15
10.15

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

4.1、并发的概念

并发

  1. 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行结束之间,且这几个程序都是在同一个处理器上运行
  2. 并发不是真正意义上的 ” 同时进行 ”,只是 CPU 把一个时间段划分成几个工夫片段(工夫区间),而后在这几个工夫区间之间来回切换
  3. 因为 CPU 解决的速度十分快,只有工夫距离解决切当,即可让用户感觉是多个应用程序同时在进行

4.2、并行的概念

并行

  1. 当零碎有一个以上 CPU 时,当一个 CPU 执行一个过程时,另一个 CPU 能够执行另一个过程,两个过程互不抢占 CPU 资源,能够同时进行,咱们称之为并行(Parallel)
  2. 其实决定并行的因素不是 CPU 的数量,而是 CPU 的外围数量,比方一个 CPU 多个核也能够并行
  3. 适宜科学计算,后盾解决等弱交互场景

并发与并行的比照

  1. 并发,指的是多个事件,在同一时间段内同时产生了。
  2. 并行,指的是多个事件,在同一时间点上同时产生了。
  3. 并发的多个工作之间是相互抢占资源的。并行的多个工作之间是不相互抢占资源的。
  4. 只有在多 CPU 或者一个 CPU 多核的状况中,才会产生并行。否则,看似同时产生的事件,其实都是并发执行的。

4.3、垃圾回收的并行与串行

垃圾回收的并行与串行

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

    • 如 ParNew、Parallel Scavenge、Parallel Old
  2. 串行(Serial)

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

4.4、垃圾回收的并发

垃圾回收的并发

  1. 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不肯定是并行的,可能会交替执行),垃圾回收线程在执行时不会进展用户程序的运行。
  2. 比方用户程序在持续运行,而垃圾收集程序线程运行于另一个 CPU 上;
  3. 典型垃圾回收器:CMS、G1

5、平安点与平安区域

平安点(Safepoint)

  1. 程序执行时并非在所有中央都能停顿下来开始 GC,只有在特定的地位能力停顿下来开始 GC,这些地位称为 ” 平安点(Safepoint)”。
  2. Safe Point 的抉择很重要,如果太少可能导致 GC 期待的工夫太长,如果太频繁可能导致运行时的性能问题。
  3. 大部分指令的执行工夫都十分短暂,通常会依据 ” 是否具备让程序长时间执行的特色 ” 为规范。
  4. 比方:抉择一些执行工夫较长的指令作为 Safe Point,如办法调用、循环跳转和异样跳转等。

平安点的中断实现形式

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

  1. 领先式中断:(目前没有虚拟机采纳了)首先中断所有线程。如果还有线程不在平安点,就复原线程,让线程跑到平安点。
  2. 主动式中断:设置一个中断标记,各个线程运行到 Safe Point 的时候被动轮询这个标记,如果中断标记为真,则将本人进行中断挂起。(有轮询的机制)

平安区域(Safe Region)

  1. Safepoint 机制保障了程序执行时,在不太长的工夫内就会遇到可进入 GC 的 Safepoint。然而,程序 ” 不执行 ” 的时候呢?
  2. 例如线程处于 Sleep 状态或 Blocked 状态,这时候线程无奈响应 JVM 的中断请求,” 走 ” 到平安点去中断挂起,JVM 也不太可能期待线程被唤醒。
  3. 对于这种状况,就须要平安区域(Safe Region)来解决。
  4. 平安区域是指在一段代码片段中,对象的援用关系不会发生变化,在这个区域中的任何地位开始 GC 都是平安的。咱们也能够把 Safe Region 看做是被扩大了的 Safepoint。

平安区域的执行流程

  1. 当线程运行到 Safe Region 的代码时,首先标识曾经进入了 Safe Region,如果这段时间内产生 GC,JVM 会疏忽标识为 Safe Region 状态的线程
  2. 当线程行将来到 Safe Region 时,会查看 JVM 是否曾经实现 GC,如果实现了,则持续运行,否则线程必须期待直到收到能够平安来到 Safe Region 的信号为止;

6、再谈援用:强援用

再谈援用

  1. 咱们心愿能形容这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是很缓和,则能够摈弃这些对象。
  2. 既偏门又十分高频的面试题:强援用、软援用、弱援用、虚援用有什么区别?具体应用场景是什么?
  3. 在 JDK1.2 版之后,Java 对援用的概念进行了裁减,将援用分为:

    • 强援用(Strong Reference)
    • 软援用(Soft Reference)
    • 弱援用(Weak Reference)
    • 虚援用(Phantom Reference)
  4. 这 4 种援用强度顺次逐步削弱。除强援用外,其余 3 种援用均能够在 java.lang.ref 包中找到它们的身影。如下图,显示了这 3 种援用类型对应的类,开发人员能够在应用程序中间接应用它们。

四中援用类型的举例

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

  1. 强援用(StrongReference):最传统的 ” 援用 ” 的定义,是指在程序代码之中普遍存在的援用赋值,即相似 ”object obj=new Object()” 这种援用关系。无论任何状况下,只有强援用关系还存在,垃圾收集器就永远不会回收掉被援用的对象。
  2. 软援用(SoftReference):在零碎将要产生内存溢出之前,将会把这些对象列入回收范畴之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异样。
  3. 弱援用(WeakReference):被弱援用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱援用关联的对象。
  4. 虚援用(PhantomReference):一个对象是否有虚援用的存在,齐全不会对其生存工夫形成影响,也无奈通过虚援用来取得一个对象的实例。为一个对象设置虚援用关联的惟一目标就是能在这个对象被收集器回收时收到一个零碎告诉。

强援用

  1. 在 Java 程序中,最常见的援用类型是强援用(一般零碎 99% 以上都是强援用),也就是咱们最常见的一般对象援用,也是默认的援用类型。
  2. 当在 Java 语言中应用 new 操作符创立一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强援用。
  3. 只有强援用的对象是可涉及的,垃圾收集器就永远不会回收掉被援用的对象。
  4. 对于一个一般的对象,如果没有其余的援用关系,只有超过了援用的作用域或者显式地将相应(强)援用赋值为 null,就是能够当做垃圾被收集了,当然具体回收机会还是要看垃圾收集策略。
  5. 绝对的,软援用、弱援用和虚援用的对象是软可涉及、弱可涉及和虚可涉及的,在肯定条件下,都是能够被回收的。所以,强援用是造成 Java 内存透露的次要起因之一。

强援用代码举例

  • 代码

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,&#x5C1A;&#x7845;&#x8C37;
  • 局部变量 str 指向 stringBuffer 实例所在堆空间,通过 str 能够操作该实例,那么 str 就是 stringBuffer 实例的强援用对应内存构造:
StringBuffer str = new StringBuffer("hello mogublog");

  • 如果此时,再运行一个赋值语句
StringBuffer str = new StringBuffer("hello mogublog");
StringBuffer str1 = str;

  • 在程序中咱们将 str = null; 则 原来堆中的对象也不会被回收,因为还有其它对象指向该区域

总结

本例中的两个援用,都是强援用,强援用具备以下特点:

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

7、再谈援用:软援用

软援用(Soft Reference):内存不足即回收

  1. 软援用是用来形容一些还有用,但非必须的对象。只被软援用关联着的对象,在零碎将要产生内存溢出异样前,会把这些对象列进回收范畴之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异样。留神,这里的第一次回收是不可达的对象
  2. 软援用通常用来实现内存敏感的缓存。比方:高速缓存就有用到软援用。如果还有闲暇内存,就能够临时保留缓存,当内存不足时清理掉,这样就保障了应用缓存的同时,不会耗尽内存。
  3. 垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软援用,并可选地把援用寄存到一个援用队列(Reference Queue)。
  4. 相似弱援用,只不过 Java 虚构机会尽量让软援用的存活工夫长一些,无可奈何才清理。
  5. 一句话概括:当内存足够时,不会回收软援用可达的对象。内存不够时,会回收软援用的可达对象

在 JDK1.2 版之后提供了 SoftReference 类来实现软援用


Object obj = new Object();

SoftReference<Object> sf = new SoftReference<>(obj);
obj = null;

软援用代码举例

  • 代码

public class SoftReferenceTest {
    public static class User {
        public int id;
        public String name;

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

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

    public static void main(String[] args) {SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));

        System.out.println(userSoftRef.get());

        System.gc();
        System.out.println("After GC:");
        System.out.println(userSoftRef.get());

        try {byte[] b = new byte[1024 * 1024 * 7];
        } catch (Throwable e) {e.printStackTrace();
        } finally {System.out.println(userSoftRef.get());
        }
    }
}
  • 在 JVM 内存不足时,会清理软援用对象
[id=1, name=songhk]
After GC:
[id=1, name=songhk]
java.lang.OutOfMemoryError: Java heap space
    at com.atguigu.java1.SoftReferenceTest.main(SoftReferenceTest.java:51)
null

8、再谈援用:弱援用

弱援用(Weak Reference)发现即回收

  1. 弱援用也是用来形容那些非必须对象,只被弱援用关联的对象只能生存到下一次垃圾收集产生为止。在零碎 GC 时,只有发现弱援用,不论零碎堆空间应用是否短缺,都会回收掉只被弱援用关联的对象。
  2. 然而,因为垃圾回收器的线程通常优先级很低,因而,并不一定能很快地发现持有弱援用的对象。在这种状况下,弱援用对象能够存在较长的工夫。
  3. 弱援用和软援用一样,在结构弱援用时,也能够指定一个援用队列,当弱援用对象被回收时,就会退出指定的援用队列,通过这个队列能够跟踪对象的回收状况。
  4. 软援用、弱援用都非常适合来保留那些可有可无的缓存数据。如果这么做,当零碎内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源短缺时,这些缓存数据又能够存在相当长的工夫,从而起到减速零碎的作用。
  5. 弱援用对象与软援用对象的最大不同就在于,当 GC 在进行回收时,须要通过算法查看是否回收软援用对象,而对于弱援用对象,GC 总是进行回收。弱援用对象更容易、更快被 GC 回收。

在 JDK1.2 版之后提供了 WeakReference 类来实现弱援用


Object obj = new Object();

WeakReference<Object> sf = new WeakReference<>(obj);
obj = null;

面试题:你开发中应用过 WeakHashMap 吗?

WeakHashMap 用来存储图片信息,能够在内存不足的时候,及时回收,防止了 OOM

  • 来,看源码,WeakHashMap 类中同样也保护了一个 Entry[] 数组
Entry<K,V>[] table;
  • Entry 类继承了 WeakReference 类
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

软援用代码举例

  • 代码

public class WeakReferenceTest {
    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 + "]";
        }
    }

    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

9、再谈援用:虚援用

虚援用(Phantom Reference):对象回收跟踪

  1. 也称为 ” 幽灵援用 ” 或者 ” 幻影援用 ”,是所有援用类型中最弱的一个
  2. 一个对象是否有虚援用的存在,齐全不会决定对象的生命周期。如果一个对象仅持有虚援用,那么它和没有援用简直是一样的,随时都可能被垃圾回收器回收。
  3. 它不能独自应用,也无奈通过虚援用来获取被援用的对象。当试图通过虚援用的 get() 办法获得对象时,总是 null,即通过虚援用无奈获取到咱们的数据
  4. 为一个对象设置虚援用关联的惟一目标在于跟踪垃圾回收过程。比方:能在这个对象被收集器回收时收到一个零碎告诉。
  5. 虚援用必须和援用队列一起应用。虚援用在创立时必须提供一个援用队列作为参数。当垃圾回收器筹备回收一个对象时,如果发现它还有虚援用,就会在回收对象后,将这个虚援用退出援用队列,以告诉应用程序对象的回收状况。
  6. 因为虚援用能够跟踪对象的回收工夫,因而,也能够将一些资源开释操作搁置在虚援用中执行和记录。

在 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 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 了");
                    }
                }
            }
        }
    }

    @Override
    protected void finalize() throws Throwable {super.finalize();
        System.out.println("调用以后类的 finalize() 办法");
        obj = this;
    }

    public static void main(String[] args) {Thread t = new CheckRefQueue();
        t.setDaemon(true);
        t.start();

        phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
        obj = new PhantomReferenceTest();

        PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);

        try {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 可用");
            }
            System.out.println("第 2 次 gc");
            obj = null;
            System.gc();
            Thread.sleep(1000);
            if (obj == null) {System.out.println("obj 是 null");
            } else {System.out.println("obj 可用");
            }
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }
}
  • 第一次尝试获取虚援用的值,发现无奈获取的,这是因为虚援用是无奈间接获取对象的值,而后进行第一次 GC,因为会调用 finalize 办法,将对象复活了,所以对象没有被回收
  • 然而调用第二次 GC 操作的时候,因为 finalize 办法只能执行一次,所以就触发了 GC 操作,将对象回收了,同时将会触发第二个操作就是将待回收的对象存入到援用队列中。
null
&#x8C03;&#x7528;&#x5F53;&#x524D;&#x7C7B;&#x7684;finalize()&#x65B9;&#x6CD5;
obj &#x53EF;&#x7528;
&#x7B2C; 2 &#x6B21; gc
&#x8FFD;&#x8E2A;&#x5783;&#x573E;&#x56DE;&#x6536;&#x8FC7;&#x7A0B;&#xFF1A;PhantomReferenceTest&#x5B9E;&#x4F8B;&#x88AB;GC&#x4E86;
obj &#x662F; null

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

  1. 它用于实现对象的 finalize() 办法,也能够称为终结器援用
  2. 无需手动编码,其外部配合援用队列应用
  3. 在 GC 时,终结器援用入队。由 Finalizer 线程通过终结器援用找到被援用对象调用它的 finalize() 办法,第二次 GC 时才回收被援用的对象

你只管学习,我来负责记笔记???? 关注公众号!, 更多笔记,等你来拿,谢谢



退出移动版