Java引用类型:强引用,软引用,弱引用,虚引用

 2022-09-17
原文地址:https://blog.51cto.com/u_15742657/5546739

在Java中提供了4个级别的引用:强引用,软引用,弱引用,虚引用。在这4个引用级别中,只有强引用FinalReference类是包内可见,其他3中引用类型均为public,可以在应用程序中直接使用。

强引用

Java中的引用,有点像C++的指针,通过引用,可以对堆中的对象进行操作。
在我们的代码生涯中,大部分使用的都是强引用,所谓强引入,都是形如Object o = new Object()的操作。
强引用具备一下特点:

  • 强引用可以直接访问目标对象
  • 强引用所指向的对象在任何时候不会被系统回收,JVM宁愿抛出OOM异常,也不回收强引用所指向的对象
  • 强引用可能导致内存泄漏

所以当我们在使用强引用创建对象时,如果下面不使用这个对象了,一定要显式地使用o = null操作来辅助垃圾回收器进行gc操作。一旦我们没有进行置null操作,就会造成内存泄漏,再使用MAT等工具进行复杂操作,浪费了大量的时间。
在jdk代码中也有许多显式置null的操作。对于ArrayList,相信了解过java的都知道,ArrayList底层使用的数组实现的,在我们进行clear操作时,就会对数组进行置null操作。

    public void clear() {
            modCount++;
    
            // clear to let GC do its work
            for (int i = 0; i < size; i++)
                elementData[i] = null;
    
            size = 0;
        }

其实如果我们的对象Object o = new Object()是在方法内创建的,那么局部变量o将被分配在栈上,而对象Object实例被分配在堆上,局部变量o指向Object实例所在的对空间,通过o可以操作该实例,那么o就是Object的引用。这时候显式置null的作用不大,只要在我们的方法退出,即该栈桢从Java虚拟机栈弹出时,o指向Object的引用就断开了,此时Object在堆上分配的内存在GC时就能被回收。

软引用

软引用是除强引用外,最强的引用类型。可以通过java.lang.ref.SoftReference使用软引用,一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收,当堆使用率临近阈值时,才会去回收软引用对象。只要有足够的内存,软引用便可能在内存中存活相当长一段时间。因此,软引用可以用于实现对内存敏感的Cache。
在java doc中,软引用是这样描述的

虚拟机在抛出 OutOfMemoryError 之前会保证所有的软引用对象已被清除。此外,没有任何约束保证软引用将在某个特定的时间点被清除,或者确定一组不同的软引用对象被清除的顺序。不过,虚拟机的具体实现会倾向于不清除最近创建或最近使用过的软引用。

举个例子

    /**
     *
     * @author xiaosuda
     * @date 2018/10/23
     */
    public class ReferenceTest {
    
    
        public static void main(String[] args) {
    
            //强引用
            MyObject object = new MyObject();
            //创建引用队列
            ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
            //创建软引用
            SoftReference<MyObject> softRef = new SoftReference<>(object, queue);
            //检查引用队列,监控对象回收情况
            new Thread(new CheckRefQueue(queue)).start();
            //删除强引用
            object = null;
            //手动GC
            System.gc();
            System.out.println("After GC:Soft Get= " + softRef.get());
            System.out.println("分配大内存:" + Runtime.getRuntime().maxMemory());
            try {
                //分配大内容
                  byte[] maxObject = new byte[(int) Runtime.getRuntime().maxMemory()];
            } catch (Throwable e) {
                System.out.println(e.getMessage());
            }
            System.out.println("After new byte[]:Soft Get= " + softRef.get());
        }
    
    }
    
    class CheckRefQueue implements Runnable {
    
        ReferenceQueue queue;
    
        public CheckRefQueue(ReferenceQueue queue) {
            this.queue = queue;
        }
    
        @Override
        public void run() {
            Reference myObj = null;
            try {
                myObj = queue.remove();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (myObj != null) {
                System.out.println("Object for SoftReference is " + myObj.get());
            }
        }
    }
    
    class MyObject {
    
    
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            //被回收时输出
            System.out.println("MyObject's finalize called");
        }
    
        @Override
        public String toString() {
            return "I'am MyObject";
        }
    }

JVM参数为:-Xmx5m -XX:+PrintGCDetails -Xmn2m
执行结果如下:

    [GC (System.gc()) [PSYoungGen: 891K->496K(1536K)] 891K->520K(5632K), 0.0015474 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [Full GC (System.gc()) [PSYoungGen: 496K->0K(1536K)] [ParOldGen: 24K->425K(4096K)] 520K->425K(5632K), [Metaspace: 2687K->2687K(1056768K)], 0.0050401 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
    After GC:Soft Get= I'am MyObject
    分配大内存:5767168
    [GC (Allocation Failure) [PSYoungGen: 20K->64K(1536K)] 445K->489K(5632K), 0.0002611 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [PSYoungGen: 64K->96K(1536K)] 489K->521K(5632K), 0.0002294 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [Full GC (Allocation Failure) [PSYoungGen: 96K->0K(1536K)] [ParOldGen: 425K->414K(4096K)] 521K->414K(5632K), [Metaspace: 2688K->2688K(1056768K)], 0.0037219 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 414K->414K(5632K), 0.0007248 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 414K->402K(4096K)] 414K->402K(5632K), [Metaspace: 2688K->2688K(1056768K)], 0.0047190 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
    异常:Java heap space
    MyObject's finalize called
    Object for SoftReference is null
    After new byte[]:Soft Get= null
    Heap
     PSYoungGen      total 1536K, used 81K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
      eden space 1024K, 7% used [0x00000007bfe00000,0x00000007bfe14760,0x00000007bff00000)
      from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
      to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
     ParOldGen       total 4096K, used 402K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
      object space 4096K, 9% used [0x00000007bfa00000,0x00000007bfa649d8,0x00000007bfe00000)
     Metaspace       used 2694K, capacity 4490K, committed 4864K, reserved 1056768K
      class space    used 290K, capacity 386K, committed 512K, reserved 1048576K
    
    Process finished with exit code 0

在这个例子中,首先构造MyObject对象,并将其赋值给obj变量,构成强引用。然后使用弱引用构造这个MyObject对象的软引用,并注册导softQueue队列里面。当softRef被回收时,会被softQueue队列,设置obj=null,删除这个强引用。因此,系统内对MyObject对象的引用只剩下软引用。此时显示调用GC,通过软引用的get方法,取得myObject对象实例的强引用。法相对象未被回收。说明在GC充足情况下不会回收软引用对象。
接着申请一块堆大小的的堆空间,并catch异常,从执行结果发现,在这次GC后,softRef.get()不再返回MyObject对象,而是返回null。说明,在系统内存紧张的情况下,软引用被回收并且加入注册的引用队列
软引用在我们的日常开发中使用的场景很多,比如商城中商品的信息。某个商品可能会被多人访问,此时我们可以把该商品的信息使用软引用保存。当系统内存足够时,可以实现高速查找,当系统内存不足又会被回收,避免OOM的风险。

TIPS: 尽管软引用会在OOM之前被清理,但是,这并不表示full gc 会清理软引用对象。在经过full gc后我们的软引用对象都放入了old区,由于full gc的存在,程序大多数强框下并不会OOM。由于软引用对象占据了老年代的空间,full gc将执行的更为频繁。所以还是建议使用弱引用

当然了,上面的例子是OOM之前回收软引用。怎么才能full gc就回收软引用对象呢?

    -XX:SoftRefLRUPolicyMSPerMB // FullGC 保留的 SoftReference 数量,参数值越大,GC 后保留的软引用对象就越多。

当我们设置这个参数值为0时,full gc就会回收我们的软引用对象了

修改main方法内容

    public static void main(String[] args) {
    
            //强引用
            MyObject object = new MyObject();
            //创建引用队列
            ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
            //创建软引用
            SoftReference<MyObject> softRef = new SoftReference<>(object, queue);
            //检查引用队列,监控对象回收情况
            new Thread(new CheckRefQueue(queue)).start();
            //删除强引用
            object = null;
            //手动GC
            System.gc();
            System.out.println("After new byte[]:Soft Get= " + softRef.get());
        }

JVM参数-Xmx5m -XX:+PrintGCDetails -Xmn2m -XX:SoftRefLRUPolicyMSPerMB=0 执行结果

    [GC (System.gc()) [PSYoungGen: 890K->496K(1536K)] 890K->520K(5632K), 0.0021185 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
    [Full GC (System.gc()) [PSYoungGen: 496K->0K(1536K)] [ParOldGen: 24K->413K(4096K)] 520K->413K(5632K), [Metaspace: 2685K->2685K(1056768K)], 0.0071067 secs] [Times: user=0.01 sys=0.01, real=0.01 secs] 
    After new byte[]:Soft Get= null
    Object for SoftReference is null
    MyObject's finalize called
    Heap
     PSYoungGen      total 1536K, used 71K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
      eden space 1024K, 6% used [0x00000007bfe00000,0x00000007bfe11e68,0x00000007bff00000)
      from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
      to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
     ParOldGen       total 4096K, used 413K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
      object space 4096K, 10% used [0x00000007bfa00000,0x00000007bfa67608,0x00000007bfe00000)
     Metaspace       used 2693K, capacity 4490K, committed 4864K, reserved 1056768K
      class space    used 290K, capacity 386K, committed 512K, reserved 1048576K
    
    Process finished with exit code 0

可以发现在手动GC后,软引用对象已经被回收。此时的软引用已经与弱引用效果一样了。下面看弱引用

弱引用

弱引用时一种比软引用较弱的引用类型。在系统GC时,只要发现弱引用,不管系统对空间是否足够,都会对对象进行回收。但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长时间。一旦一个弱引用对象被垃圾收集器回收,便会加入导一个注册引用队列中
修改软引用例子中的main方法内容为

    public static void main(String[] args) {
    
           //强引用
            MyObject object = new MyObject();
            //创建引用队列
            ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
            WeakReference<MyObject> weakRef = new WeakReference<>(object, queue);
            new Thread(new CheckRefQueue(queue)).start();
            object = null;
            System.out.println("After new byte[]:Soft Get= " + weakRef.get());
            System.gc();
            System.out.println("After new byte[]:Soft Get= " + weakRef.get());
       }

JVM参数:-Xmx5m -XX:+PrintGCDetails -Xmn2m 执行结果如下

    After new byte[]:Soft Get= I'am MyObject
    [GC (System.gc()) [PSYoungGen: 891K->496K(1536K)] 891K->520K(5632K), 0.0016576 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [Full GC (System.gc()) [PSYoungGen: 496K->0K(1536K)] [ParOldGen: 24K->426K(4096K)] 520K->426K(5632K), [Metaspace: 2685K->2685K(1056768K)], 0.0124398 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
    MyObject's finalize called
    Object for SoftReference is null
    After new byte[]:Soft Get= null
    Heap
     PSYoungGen      total 1536K, used 71K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
      eden space 1024K, 6% used [0x00000007bfe00000,0x00000007bfe11e60,0x00000007bff00000)
      from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
      to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
     ParOldGen       total 4096K, used 426K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
      object space 4096K, 10% used [0x00000007bfa00000,0x00000007bfa6a988,0x00000007bfe00000)
     Metaspace       used 2692K, capacity 4490K, committed 4864K, reserved 1056768K
      class space    used 290K, capacity 386K, committed 512K, reserved 1048576K
    
    Process finished with exit code 0

通过结果可以看到,在GC之前,弱引用对象并未被垃圾回收器发现,因此通过weakRef.get()方法可以获得对应的强引用,但是只要进行垃圾回收,弱引用对象一旦被发现,便会立刻被回收,并加入注册的引用队列中。此时,再通过weakRef.get()方法取得强引用就会失败

虚引用在我们的日常代码中也经常用到,比如ThreadLocal的内部实现就是一个ThreadLocalMap,该mapEntrykeyThreadLocal本身,value为我们向ThreadLocal对象set的值,其中的key就是弱引用对象。又比如
集合WeakHashMap,都是使用了弱引用实现的,想更加深了解的可以区看看相关源码的实现。

虚引用

虚引用时所有引用类型中最弱的一个,一个持有弱引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。当试图通过虚引用的get()方法取得强引用时,总会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。当垃圾回收器准备回收一个对象时,如果发现他还有虚引用,就会在垃圾回收后销毁这个对象,将这个对象加入引用队列。虚引用主要用于检测对象是否已经从内存中删除。