shoufeng

瘦风的南墙

[转载] Java的四种引用关系

版权声明: 本文为转载文章, 转载时有适量修改. 再次转载时请附上原文出处链接和本声明.
原文链接: https://blog.csdn.net/u013256816/article/details/50907595

Java中提供了4个级别的引用: 强引用、软引用、弱引用和虚引用, 这四个引用定义在包java.lang.ref下:

Java四种引用关系的源码位置

1 强引用 (Final Reference)

强引用就是指在程序代码中普遍存在的、类似于Object obj = new Object() 的引用, 只要强引用还存在, 垃圾收集器就不会去回收这些被引用的对象.

强引用有以下三个特点:

  1. 强引用可以直接访问目标对象;
  2. 强引用锁指向的对象在任何时候都不会被系统回收 ——JVM宁愿抛出OOM异常也不回收强引用所指向的对象;
  3. 强引用可能导致内存泄露, 比如List中添加了new出来的对象, List失去被回收之后, 其内部的对象不能被访问、却又不会被回收的现象.

FinalReference类的全部定义如下:

package java.lang.ref;
/**
 * Final references, used to implement finalization
 */
class FinalReference<T> extends Reference<T> {
    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

FinalReference只有一个构造函数: 根据给定对象的引用和引用队列构造一个强引用.

2 软引用 (Soft Reference)

软引用用来描述一些还有用但并非必须的对象: 被软引用关联着的对象, 如果内存充足, 则垃圾回收器不会回收该对象;
如果内存不够用, 就会回收这些对象.

在系统将要发生OutOfMemoryError之前, JVM会把被软引用关联着的对象列进回收范围, 并进行第二次回收. 如果这次回收之后内存仍然不够用, 系统才会抛出OutOfMemoryError.

从JDK 1.2开始提供了SoftReference类来实现软引用: 与一个引用队列 (ReferenceQueue) 联合使用实现内存敏感的高速缓存 —— 如果软引用所引用的对象被垃圾回收器回收, JVM就会把这个软引用加入到与之关联的引用队列中.

2.1 案例1: 软引用的垃圾回收

package com.healchow.java.detail;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;

public class SoftRefTest {
    private static ReferenceQueue<MyObject> softQueue = new ReferenceQueue<>();

    private static 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";
        }
    }

    private static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;
        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>) softQueue.remove();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (obj != null) {
                System.out.println("Object for SoftReference is " + obj.get());
            }
        }
    }
    
    public static void main(String[] args) {
        MyObject obj = new MyObject();
        SoftReference<MyObject> softRef = new SoftReference<>(obj, softQueue);
        new Thread(new CheckRefQueue()).start();

        // 删除强引用, 否则obj不会被回收
        obj = null;
        System.gc();
        System.out.println("After GC: Soft Get = " + softRef.get());
        System.out.println("尝试分配大块内存...");
        byte[] b = new byte[5 * 1024 * 725];
        System.out.println("After new byte[]: Soft Get = " + softRef.get());
        System.gc();
    }
}

(1) 测试方法1:

在运行测试主方法时设置VM参数: -Xmx5M, 也就是指定该程序的Java Heap最大为5MB, 运行结果为:

After GC: Soft Get = I am MyObject
尝试分配大块内存...
After new byte[]: Soft Get = I am MyObject
MyObject's finalize called
Object for SoftReference is null

案例代码解释:

① 首先构造MyObject对象, 并将其赋值给object变量, 构成强引用.

② 然后使用SoftReference构造这个MyObject对象的软引用softRef, 并注册到softQueue引用队列 —— 当softRef被回收时, 会被加入softQueue队列.

③ 设置obj = null, 删除这个强引用, 这时系统内对MyObject对象的引用只剩下软引用.

④ 显示调用GC, 通过软引用的get()方法获取MyObject对象的引用, 发现对象并未被回收, 这说明GC在内存充足的情况下, 不会回收软引用对象.

⑤ 接着请求一块大的堆空间5*1024*725(要多次调整使得垃圾回收工作能顺利进行、线程能顺利退出), 这个操作会使系统堆内存使用紧张, 从而产生新一轮的GC. 在这次GC后, softRef.get()不再返回MyObject对象, 而是返回null —— 说明在系统内存紧张的情况下, 软引用被回收. 软引用被回收时, 会被加入注册的引用队列, 此时引用队列中有了元素, 开辟的多线程中softQueue.remove()不再阻塞, 因此程序得以成功退出.

如果将上面案例中的数组再改大点, 比如5*1024*1024, 就会抛出OOM异常:

After GC: Soft Get = I am MyObject
尝试分配大块内存...
MyObject's finalize called
Exception in thread "main" Object for SoftReference is null
java.lang.OutOfMemoryError: Java heap space
	at com.healchow.java.detail.SoftRefTest.main(SoftRefTest.java:56)

(2) 测试方法2:

在运行测试主方法时设置VM参数: -Xmx5M -XX:PrintGCDetails, 打印出GC的日志信息(关于GC日志可以查看《Java堆内存http://blog.csdn.net/u013256816/article/details/50764532》), 运行结果为:

[GC (Allocation Failure) [PSYoungGen: 1024K->480K(1536K)] 1024K->480K(5632K), 0.0013139 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 1383K->480K(1536K)] 1383K->544K(5632K), 0.0011186 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 480K->0K(1536K)] [ParOldGen: 64K->504K(4096K)] 544K->504K(5632K), [Metaspace: 3316K->3316K(1056768K)], 0.0044642 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
After GC: Soft Get = I am MyObject
尝试分配大块内存...
[GC (Allocation Failure) [PSYoungGen: 38K->64K(1536K)] 542K->568K(5632K), 0.0009263 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 64K->32K(1536K)] 568K->536K(5632K), 0.0011345 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(1536K)] [ParOldGen: 504K->504K(4096K)] 536K->504K(5632K), [Metaspace: 3317K->3317K(1056768K)], 0.0038031 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 504K->504K(5632K), 0.0007999 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 504K->486K(4096K)] 504K->486K(5632K), [Metaspace: 3317K->3317K(1056768K)], 0.0037241 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
MyObject's finalize called
Object for SoftReference is null
After new byte[]: Soft Get = null
[Full GC (System.gc()) [PSYoungGen: 59K->0K(1536K)] [ParOldGen: 4086K->4083K(4096K)] 4145K->4083K(5632K), [Metaspace: 3317K->3317K(1056768K)], 0.0036086 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 1536K, used 10K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 1024K, 1% used [0x00000007bfe00000,0x00000007bfe02a68,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 4096K, used 4083K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
  object space 4096K, 99% used [0x00000007bfa00000,0x00000007bfdfcfc0,0x00000007bfe00000)
 Metaspace       used 3324K, capacity 4564K, committed 4864K, reserved 1056768K
  class space    used 366K, capacity 388K, committed 512K, reserved 1048576K

2.2 案例2: 软引用缓存的使用

public class BitMapManager {
    private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();

    // 保存Bitmap的软引用到HashMap
    public void saveBitmapToCache(String path) {
        // 强引用的Bitmap对象
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        // 软引用的Bitmap对象
        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
        // 添加该对象到Map中使其缓存
        imageCache.put(path, softBitmap);
        // 使用完后手动将位图对象置null
        bitmap = null;
    }

    public Bitmap getBitmapByPath(String path) {
        // 从缓存中取软引用的Bitmap对象
        SoftReference<Bitmap> softBitmap = imageCache.get(path);
        // 判断是否存在软引用
        if (softBitmap == null) {
            return null;
        }
        // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空
        Bitmap bitmap = softBitmap.get();
        return bitmap;
    }
}

2.3 软引用的应用场景

软引用主要用于内存敏感的高速缓存, 在Android系统中经常用到. 大多数 Android应用都会用到大量的图片. 由于读取文件需要硬件操作, 速度较慢, 所以考虑将图片缓存起来, 需要的时候直接从内存中读取.

但由于图片占用内存空间比较大, 缓存很多图片就可能容易发生OutOfMemoryError, 这时我们可以考虑使用软引用技术来避免这个问题发生.

SoftReference可以解决OOM的问题: 每一个对象通过软引用进行实例化, 这个对象就以cache的形式保存起来, 再次调用这个对象时, 就可以直接通过软引用中的get()方法得到对象中的资源数据. 当内存将要发生OOM的时候, GC会迅速把所有的软引用清除, 防止OOM发生.

3 弱引用 (Weak Reference)

弱引用用来描述非必须的对象, 它的强度比软引用更弱, 被弱引用关联的对象只能生存到下一次垃圾收集发生之前. 当垃圾收集器工作时, 无论当前内存是否足够, 都会回收掉只被弱引用关联的对象. 一旦一个弱引用对象被垃圾回收器回收, 便会加入到一个注册引用队列中.

我们略微修改一下上一节案例1的代码:

package com.healchow.java.detail;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

public class WeakRefTest {
    private static ReferenceQueue<MyObject> weakQueue = new ReferenceQueue<>();

    private static 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";
        }
    }

    private static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;
        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>)weakQueue.remove();
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(obj != null) {
                System.out.println("删除的弱引用为: " + obj);
                System.out.println("获取弱引用的对象 obj.get() 为: " + obj.get());
            }
        }
    }

    public static void main(String[] args) {
        MyObject object = new MyObject();
        Reference<MyObject> weakRef = new WeakReference<>(object,weakQueue);
        System.out.println("创建的弱引用为: " + weakRef);
        new Thread(new CheckRefQueue()).start();

        object = null;
        System.out.println("Before GC: Weak Get = " + weakRef.get());
        System.gc();
        System.out.println("After GC: Weak Get = " + weakRef.get());
    }
}

(1) 演示方法 —— 不修改JVM参数:

运行结果为:

创建的弱引用为: java.lang.ref.WeakReference@6f94fa3e
Before GC: Weak Get = I am MyObject
After GC: Weak Get = null
删除的弱引用 obj 为: java.lang.ref.WeakReference@6f94fa3e
但是获取弱引用的对象 obj.get() 为: null
MyObject's finalize called

可以看到:

在GC之前, 弱引用对象并未被垃圾回收器发现, 因此通过 weakRef.get()可以获取对应的对象引用.

但是只要进行垃圾回收, 弱引用就会被发现, 并立即被回收, 并加入注册引用队列中. 此时再试图通过weakRef.get()获取对象的引用就会失败.

(2) 弱引用的使用场景:

弱引用的使用场景可参考java.util.WeakHashMap.

软引用、弱引用都非常适合来保存那些可有可无的缓存数据. 系统内存不足时, 这些缓存数据会被回收, 就不会导致内存溢出. 而当内存资源充足时, 这些缓存数据又可以存在相当长的时间, 从而提高系统的响应速度用.

4 虚引用 (Phantom Reference)

虚引用也称为幽灵引用或者幻影引用, 它是最弱的一种引用关系. 一个持有虚引用的对象, 和没有引用几乎是一样的 —— 随时都有可能被垃圾回收器回收.

当试图通过虚引用的get()方法取得强引用时, 总是会失败.

并且虚引用必须和引用队列一起使用, 它的作用在于跟踪垃圾回收过程.

虚引用中get()方法永远返回null, 其实现如下:

public T get() {
    return null;
}

我们再来修改第2节案例1的代码:

package com.healchow.java.detail;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.TimeUnit;

public class PhantomRefTest {
    private static ReferenceQueue<MyObject> phantomQueue = new ReferenceQueue<>();

    private static 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";
        }
    }

    private static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;
        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>) phantomQueue.remove();
                System.out.println("删除的虚引用 obj 为: " + obj);
                System.out.println("但是获取虚引用的对象 obj.get() 为: " + obj.get());
                System.exit(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyObject object = new MyObject();
        Reference<MyObject> phantomRef = new PhantomReference<>(object, phantomQueue);
        System.out.println("创建的虚引用为:" + phantomRef);
        new Thread(new CheckRefQueue()).start();

        object = null;
        TimeUnit.SECONDS.sleep(1);
        int i = 1;
        while (true) {
            System.out.println("第" + i++ + "次gc");
            System.gc();
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

(1) 演示方法 —— 不修改JVM参数:

运行结果为:

创建的虚引用为:java.lang.ref.PhantomReference@6f94fa3e
第1次gc
MyObject's finalize called
第2次gc
删除的虚引用 obj 为: java.lang.ref.PhantomReference@6f94fa3e
但是获取虚引用的对象 obj.get() 为: null

可以看到:

在经过一次GC之后, 系统找到了垃圾对象, 并调用finalize()方法回收内存, 但没有立即将虚引用对象加入回收队列.

第二次GC时, 该对象真正被GC清除, 此时虚引用对象被加入虚引用队列.

(2) 虚引用的使用场景:

虚引用的最大作用在于跟踪对象回收, 清理被销毁对象的相关资源.

通常当对象不被使用时, 重载该对象的类的finalize()方法可以回收对象的资源. 但是如果使用不慎, 会使得对象复活. 比如这样重写finalize()方法:

public class Test {
    private static Test obj;
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        obj = this;
    }
}

创建Test对象: obj = new Test();, 然后令obj = null;, 之后调用System.gc()企图销毁该对象.

但是很抱歉, 不管你调用多少次System.gc()都没有用, 除非再次obj = null;才能回收对象.

原因: JVM对每一个对象最多只执行一次被重写的finalize()方法, 示例代码中, 在super.finalize()之后又对obj进行了赋值, 使得obj又复活了, 它重写的finalize()方法不会被调用第二次.

(3) 通过虚引用清理对象

上面的小片段说明重写finalize()方法并不是很靠谱, 我们可以使用虚引用来清理对象所占用的资源. 修改代码如下:

package com.healchow.java.detail;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class PhantomRefTest2 {
    private static ReferenceQueue<MyObject> phantomQueue = new ReferenceQueue<>();
    private static Map<Reference<MyObject>, String> resourceMap = new HashMap<>();

    private static 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";
        }
    }

    private static class CheckRefQueue implements Runnable {
        Reference<MyObject> refObj = null;

        @Override
        public void run() {
            try {
                refObj = (Reference<MyObject>) phantomQueue.remove();
                // 从资源Map中移除弱引用对象, 即手动释放资源
                Object value = resourceMap.get(refObj);
                System.out.println("clean resource: " + value);
                resourceMap.remove(refObj);

                System.out.println("删除的虚引用为: " + refObj);
                System.out.println("获取虚引用的对象 obj.get() 为: " + refObj.get());
                System.exit(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyObject object = new MyObject();
        Reference<MyObject> phantomRef = new PhantomReference<>(object, phantomQueue);
        System.out.println("创建的虚引用为: " + phantomRef);
        new Thread(new CheckRefQueue()).start();
        // 将创建的虚引用对象存入资源Map
        resourceMap.put(phantomRef, "Some Resources");

        object = null;
        TimeUnit.SECONDS.sleep(1);
        int i = 1;
        while (true) {
            System.out.println("第" + i++ + "次gc");
            System.gc();
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

运行结果:

创建的虚引用为: java.lang.ref.PhantomReference@6f94fa3e
第1次gc
MyObject's finalize called
第2次gc
clean resource: Some Resources
删除的虚引用 obj 为: java.lang.ref.PhantomReference@6f94fa3e
但是获取虚引用的对象 obj.get() 为: null

参考资料

《Java程序性能优化——让你的Java程序更快、更稳定》葛一鸣 等编著。

《Java堆内存http://blog.csdn.net/u013256816/article/details/50764532


版权声明

本文版权归原作者所有,如有侵权,请联系博主,定当立即删除。
感谢阅读,公众号 「瘦风的南墙」 ,手机端阅读更佳,还有其他福利和心得输出,欢迎扫码关注🤝

posted @ 2019-09-09 14:07  瘦风  阅读(1175)  评论(0编辑  收藏  举报