对象什么时候是垃圾?

前言:在之前的文章中,讲到一个对象是如何被创建出来的,对象的增多会导致的问题(OOM),那么,我们该如何去解决对象太多的问题呢?换句话说,我们该如何清理掉多余的、无用的内存空间呢?

哪些区域是需要被回收的?

首先,我们要了解,在JVM内存区域中,哪几个区域是需要我们去释放内存的。
我们把五大区域按线程是否私有分为两大块:

  1. 线程私有:程序计数器、线程栈(虚拟机栈)、本地方法栈。
  2. 线程共享:Java堆、方法区

程序计数器所占内存微乎其微,并不会导致内存溢出,所以无需考虑。
线程栈和本地方法栈以栈的形式有序的入栈和出栈(入栈开辟内存、出栈后释放内存)、每一个栈帧在类结构确定下来后即可知道其内存(即编译器可知),因此,线程栈和本地方法栈在内存分配时具有确定性,所以无需考虑回收问题。

而对于Java堆和方法区而言,我们无法确定程序具体会创建哪些对象、创建多少个对象。也无法知道一个接口的实现类所占多少内存、哪些实现类被加载(多态的灵活性)。

所以,在分析哪些区域需要被回收时,我们重点关注的应是Java堆方法区两大板块。

如何判断对象可回收?

  1. 引用计数法
    注:Java虚拟机不用该算法
    引用计数算法模型

概念: 每个对象信息中添加一个计数器,当对象被一次引用时,该计数器就会被+1,当该计数器的值为0时,表示该对象已经没有被任何其他对象引用了,也就证明没有利用价值了。
优点: 原理简单、效率高
缺点: 无法解决循环依赖的问题(若A对象仅被B对象所引用,B对象也仅被A对象引用,从逻辑上来讲二者已经完全没有利用价值了,但由于计数器始终为1,所以无法被判定为垃圾对象)
采用该算法的案例: 微软COM技术、使用ActionScript 3 的 FlashPlayer 、 Python、游戏脚本等。

  1. 可达性分析算法

可达性分析算法模型
上述这张图就可解释可达性分析算法的核心概念:以GC Roots 为起点,根据引用关系往下搜索,搜索路径被称为 “引用链” ,在该引用链上的对象都是需要存活的,反之,都为可回收的对象。

何为GC Roots?

如下几种情况的对象可被判定为GC Roots对象:

  1. 虚拟机栈中(栈帧中的局部变量表)引用的对象。
  2. 方法区中类静态属性引用的对象(被static 修饰)
  3. 方法区中常量引用的对象(被final修饰、StringTable里的引用)
  4. 本地方法栈中引用的对象。
  5. 虚拟机内部的引用(基本数据类型对应的Class对象、常驻异常对象(NullPointException等)、系统类加载器)
  6. 被同步锁(synchronized)持有的对象。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  8. 根据垃圾回收器的不同可能还有其他对象“临时加入”。

四种引用类型

在我们的狭隘概念中,reference类型的数据中存储的数值是堆内存中的具体对象地址值(大部分情况下),这也就代表着一个对象reference所引用。也就是说,一个对象,只有“被引用”和“未被引用”两种状态,这显然有点粗糙了。
我们可能希望存在更多的引用关系,如:一个对象确实被引用着,但它的价值并没有那么突出,希望能够在内存紧张的时候,这类对象可以被垃圾回收。针对描述需求,引用关系被划分为以下四种:
强引用:Object obj = new Object() , 一个对象通过 “=” 被引用,我们称之为强引用,只要引用关系存在,这类对象就永远不会被回收。

代码:

/**
 * 四种引用
 * VM Args:-Xmx20M -XX:+PrintGCDetails
 */
public class ReferenceType {

    private static final int _4M = 4 * 1024 * 1024;

    public static void main(String[] args) {
        strongReference();
    }

    /**
     * 强引用
     */
    public static void strongReference() {
        ArrayList<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            byte[] strongReference = new byte[_4M];
            list.add(strongReference);
            System.out.println("放入时:" + strongReference);
        }
        for (byte[] strongReference : list) {
            System.out.println("最终:" + strongReference);
        }
    }
}

异常信息如下:

[GC (Allocation Failure) [PSYoungGen: 5632K->512K(6144K)] 5632K->4632K(19968K), 0.0053100 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
放入时:[B@29453f44
放入时:[B@5cad8086
放入时:[B@6e0be858
放入时:[B@61bbe9ba
[Full GC (Ergonomics) [PSYoungGen: 4849K->4469K(6144K)] [ParOldGen: 12312K->12288K(13824K)] 17161K->16757K(19968K), [Metaspace: 3189K->3189K(1056768K)], 0.0045321 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 4469K->4450K(6144K)] [ParOldGen: 12288K->12288K(13824K)] 16757K->16738K(19968K), [Metaspace: 3189K->3189K(1056768K)], 0.0025509 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 6144K, used 4742K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 5632K, 84% used [0x00000007bf980000,0x00000007bfe21898,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 13824K, used 12288K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
  object space 13824K, 88% used [0x00000007bec00000,0x00000007bf800058,0x00000007bf980000)
 Metaspace       used 3232K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at ReferenceType.strongReference(ReferenceType.java:22)
	at ReferenceType.main(ReferenceType.java:13)

从GC信息中可以看出,在执行过程中,JVM并没有明显回收掉堆内存空间。最后抛了OOM异常。

软引用:通过 SoftReference 类来修饰软引用对象,这些对象会在发生 内存溢出异常之前 进入 二次回收 。如果回收后依然内存不足,才会抛出OOM。

   	/**
     * 软引用
     */
    public static void softReference() {
        ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
        //软引用队列,被回收的软引用对象会存入该队列中
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();

        //往集合中放入五个软引用对象,每个对象占4M内存,同时关联软引用队列
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> softReference = new SoftReference<>(new byte[_4M], referenceQueue);
            list.add(softReference);
            System.out.println("放入时:" + softReference.get());
        }

        //遍历查看每个引用对象的内容
        for (SoftReference<byte[]> softReference : list) {
            System.out.println("最终:" + softReference.get());
        }

        //从软引用队列中读取存入该队列的软引用对象
        Reference<? extends byte[]> reference = null;
        do {
            reference = referenceQueue.poll();
            if (reference != null) {
                System.out.println("reference = " + reference + ",内容为:" + reference.get());
            }
        } while (reference != null);
    }

控制台信息如下:

放入时:[B@29453f44
放入时:[B@5cad8086
放入时:[B@6e0be858
[GC (Allocation Failure) [PSYoungGen: 1780K->512K(6144K)] 14068K->12832K(19968K), 0.0037693 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
放入时:[B@61bbe9ba
[GC (Allocation Failure) --[PSYoungGen: 4833K->4833K(6144K)] 17153K->17161K(19968K), 0.0011433 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 4833K->4457K(6144K)] [ParOldGen: 12328K->12300K(13824K)] 17161K->16758K(19968K), [Metaspace: 3201K->3201K(1056768K)], 0.0030645 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) --[PSYoungGen: 4457K->4457K(6144K)] 16758K->16766K(19968K), 0.0006077 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 4457K->0K(6144K)] [ParOldGen: 12308K->355K(8192K)] 16766K->355K(14336K), [Metaspace: 3201K->3201K(1056768K)], 0.0022575 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
放入时:[B@610455d6
最终:null
最终:null
最终:null
最终:null
最终:[B@610455d6
reference = java.lang.ref.SoftReference@511d50c0,内容为:null
reference = java.lang.ref.SoftReference@60e53b93,内容为:null
reference = java.lang.ref.SoftReference@5e2de80c,内容为:null
reference = java.lang.ref.SoftReference@1d44bcfa,内容为:null
Heap
 PSYoungGen      total 6144K, used 4431K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 5632K, 78% used [0x00000007bf980000,0x00000007bfdd3f30,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 8192K, used 355K [0x00000007bec00000, 0x00000007bf400000, 0x00000007bf980000)
  object space 8192K, 4% used [0x00000007bec00000,0x00000007bec58fb0,0x00000007bf400000)
 Metaspace       used 3213K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 356K, capacity 388K, committed 512K, reserved 1048576K

从控制台信息中可以明显看到,集合中的部分对象被置为NULL,同时也能发现在存放最后一个元素时执行了FULL GC,将新生代中的所有的对象全部清理了,所以最后只留下了最后一个还未放入的对象。

执行过程:

  1. 首先堆内存的大小被设置为20M,每个对象的大小被设置为4M;
  2. 每个对象被软引用类所包装,也就成为了软引用对象;
  3. 在对象产生过程中,堆内存的剩余空间逐渐减少,从控制台可以看出,JVM一直在发生GC;
  4. 当清理完其他可回收对象后,堆内存依然不足,此时JVM 即将 抛OOM,于是再次执行GC,这是会将堆内存中的软引用对象作为垃圾回收掉;
  5. 被垃圾回收的软引用对象被存入软引用队列中。

弱引用:只要发生垃圾回收,这些引用对象必然被回收。也就是说,它们的生命周期只维持在一次垃圾回收之前。
代码:

/**
     * 弱引用
     */
    public static void weekReference() {
        ArrayList<WeakReference<byte[]>> list = new ArrayList<>();
        //弱引用队列,被回收的弱引用对象会存入该队列中
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
        //往集合中放入五个弱引用对象,每个对象占4M内存,同时关联弱引用队列
        for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4M], referenceQueue);
            list.add(weakReference);
            System.out.println("放入时:" + weakReference.get());
        }
        //遍历查看每个引用对象的内容
        for (WeakReference<byte[]> weakReference : list) {
            System.out.println("最终:" + weakReference.get());
        }

        //从软引用队列中读取存入该队列的软引用对象
        Reference<? extends byte[]> reference = null;
        do {
            reference = referenceQueue.poll();
            if (reference != null) {
                System.out.println("reference = " + reference + ",内容为:" + reference.get());
            }
        } while (reference != null);
    }

控制台信息如下

[GC (Allocation Failure) [PSYoungGen: 5629K->512K(6144K)] 5629K->4640K(19968K), 0.0038495 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
放入时:[B@29453f44
放入时:[B@5cad8086
放入时:[B@6e0be858
放入时:[B@61bbe9ba
[Full GC (Ergonomics) [PSYoungGen: 4911K->0K(6144K)] [ParOldGen: 12320K->383K(13312K)] 17231K->383K(19456K), [Metaspace: 3179K->3179K(1056768K)], 0.0040361 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
放入时:[B@610455d6
放入时:[B@511d50c0
放入时:[B@60e53b93
放入时:[B@5e2de80c
[Full GC (Ergonomics) [PSYoungGen: 4244K->0K(6144K)] [ParOldGen: 12671K->374K(13824K)] 16916K->374K(19968K), [Metaspace: 3189K->3189K(1056768K)], 0.0027988 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
放入时:[B@1d44bcfa
放入时:[B@266474c2
最终:null
最终:null
最终:null
最终:null
最终:null
最终:null
最终:null
最终:null
最终:[B@1d44bcfa
最终:[B@266474c2
reference = java.lang.ref.WeakReference@6f94fa3e,内容为:null
reference = java.lang.ref.WeakReference@5e481248,内容为:null
reference = java.lang.ref.WeakReference@66d3c617,内容为:null
reference = java.lang.ref.WeakReference@63947c6b,内容为:null
reference = java.lang.ref.WeakReference@2b193f2d,内容为:null
reference = java.lang.ref.WeakReference@355da254,内容为:null
reference = java.lang.ref.WeakReference@4dc63996,内容为:null
reference = java.lang.ref.WeakReference@d716361,内容为:null
Heap
 PSYoungGen      total 6144K, used 4401K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 5632K, 78% used [0x00000007bf980000,0x00000007bfdcc7b0,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 13824K, used 4470K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
  object space 13824K, 32% used [0x00000007bec00000,0x00000007bf05d8e8,0x00000007bf980000)
 Metaspace       used 3199K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 354K, capacity 388K, committed 512K, reserved 1048576K

从集合中的元素中可以看到,在每一次进行FULL GC时,之前的弱引用对象都会被垃圾回收,最终只留下了最后两个元素。
注:如果只发生了YOUNG GC,弱引用对象可能依然会存在内存中,也可能被回收掉。具体证明只需减少循环次数至5次左右,防止发生FULL GC即可,控制台具体信息如下:

放入时:[B@29453f44
放入时:[B@5cad8086
放入时:[B@6e0be858
[GC (Allocation Failure) [PSYoungGen: 1783K->480K(6144K)] 14071K->12792K(19968K), 0.0025459 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
放入时:[B@61bbe9ba
[GC (Allocation Failure) [PSYoungGen: 4801K->496K(6144K)] 17113K->12816K(19968K), 0.0008608 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
放入时:[B@610455d6
最终:[B@29453f44
最终:[B@5cad8086
最终:[B@6e0be858
最终:null
最终:[B@610455d6
reference = java.lang.ref.WeakReference@511d50c0,内容为:null
Heap
 PSYoungGen      total 6144K, used 4815K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 5632K, 76% used [0x00000007bf980000,0x00000007bfdb7cc8,0x00000007bff00000)
  from space 512K, 96% used [0x00000007bff80000,0x00000007bfffc010,0x00000007c0000000)
  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
 ParOldGen       total 13824K, used 12320K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
  object space 13824K, 89% used [0x00000007bec00000,0x00000007bf808030,0x00000007bf980000)
 Metaspace       used 3213K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 356K, capacity 388K, committed 512K, reserved 1048576K

将循环次数下降至5次时,可以看到,整个程序执行过程中没有发生FULL GC,而是发生了YOUNG GC,在第一次垃圾回收时,前三个对象很幸运的被保留了下来,但在第二次垃圾回收时,第四个对象就没那么幸运了,他被回收了,所以最终结果只有第四个是NULL值。

虚引用

虚引用无法通过get()方法来拿到对象实例,因为PhantomReference类重写了get()方法,只返回NULL。

/**
 * 返回此引用对象的引用。因为幻像引用的引用总是不可访问的,所以此方法总是返回null
 * /
public T get() {
        return null;
    }

代码:

/**
     * 虚引用
     */
    public static void phantomReference() {
        //虚引用队列,被回收的虚引用对象会存入该队列中
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
        //往集合中放入三个虚引用对象,每个对象占4M内存,同时关联虚引用队列
        for (int i = 0; i < 3; i++) {
            byte[] bytes = new byte[_4M];
            PhantomReference<byte[]> phantomReference = new PhantomReference<>(bytes, referenceQueue);
            //将对象置为NULL以便垃圾回收
            bytes = null;
            //提示虚拟机执行GC操作
            System.gc();
        }
        //从虚引用队列中读取存入该队列的虚引用对象
        Reference<? extends byte[]> reference = null;
        do {
            reference = referenceQueue.poll();
            if (reference != null) {
                System.out.println("reference = " + reference);
            }
        } while (reference != null);
    }

控制台信息如下:

[GC (System.gc()) [PSYoungGen: 5629K->480K(6144K)] 5629K->4704K(19968K), 0.0047531 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 480K->0K(6144K)] [ParOldGen: 4224K->4474K(13824K)] 4704K->4474K(19968K), [Metaspace: 3097K->3097K(1056768K)], 0.0036661 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 4286K->96K(6144K)] 8761K->8666K(19968K), 0.0016526 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 96K->0K(6144K)] [ParOldGen: 8570K->8561K(13824K)] 8666K->8561K(19968K), [Metaspace: 3135K->3135K(1056768K)], 0.0038363 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 4279K->64K(6144K)] 12840K->12721K(19968K), 0.0014040 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 64K->0K(6144K)] [ParOldGen: 12657K->12657K(13824K)] 12721K->12657K(19968K), [Metaspace: 3135K->3135K(1056768K)], 0.0013712 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
reference = java.lang.ref.PhantomReference@29453f44,内容为:null
reference = java.lang.ref.PhantomReference@5cad8086,内容为:null
reference = java.lang.ref.PhantomReference@6e0be858,内容为:null
Heap
 PSYoungGen      total 6144K, used 253K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 5632K, 4% used [0x00000007bf980000,0x00000007bf9bf450,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 13824K, used 12657K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
  object space 13824K, 91% used [0x00000007bec00000,0x00000007bf85c5a8,0x00000007bf980000)
 Metaspace       used 3186K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 351K, capacity 388K, committed 512K, reserved 1048576K

从控制台可以观察出,引用队列中存放着虚引用,这就表示被虚引用关联的对象已经被垃圾回收了。这也是虚引用的价值所在:可以在对象被垃圾回收时得到一个通知。
典型案例:NIO的clean() 释放内存的方法。

注:虚引用不会对原始对象造成任何影响,可尝试增加循环次数,并去掉 bytes = null; 这行代码,保证对象不会被回收,可以发现依然会抛出OOM异常,这和我们认识的软引用和弱引用完全不同。

终结器引用暂时不谈论…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值