总结Java中的reference类型与四种引用类型

总结Java中的reference类型与四种引用类型

本文通过分析源码和实验测试总结了Java中的reference类型、Reference类以及四种引用类型的基础知识。
仅做学习记录目的,有误的欢迎指出!

一、什么是reference类型

Java数据类型分为两大类:

基本类型 (primitive type)

8种基本类型 byte, short, int, long, float, double, char , boolean

引用类型(reference)

《Java虚拟机规范》中写道:

Java虚拟机中有三种引用类型:类类型(class type)、数组类型(array type)和接口类型(interface type)。这些引用类型的值分别指向动态创建的类实例、数组示例和实现了某个接口的类示例或数组示例。

可见,引用类型的值其实就是实例在堆内存上的地址,可以把引用近似理解为指针。

引用中的null值:当一个引用不指向任何对象的时候,它的值就用null来表示。引用的默认值就是null。

JVM应能通过引用实现两点:

  1. 从该引用直接或间接地查找到对象在堆中的数据存放的起止地址索引。
  2. 从该引用中直接或间接地查到对象所属类在方法区中存储的类型信息。

这是很容易理解的,比如下面的代码:

User ref = new User();
ref.getUsername();	// 通过引用获取该类的实例的数据
ref.getClass();		// 通过引用获取该类的类型信息 (Class对象)

实际上,在HotSpot的实现中,reference的值并不直接指向实例,而是指向一个句柄,由句柄再指向实际的实例。这样的好处时,在对象实例数据在内存中的位置被移动时(比如GC时),不需要修改栈上所有相关的reference的值,只需要修改句柄的值(只需要修改一次),代价是多一次的寻址。

二、什么是Reference类

Reference类是Java.lang.ref包里的一个抽象类,源码中对其的描述是:

Abstract base class for reference objects. This class defines the operations common to all reference objects.

我把这个Reference类理解为 (也许不准确):描述reference类型的类,这个类定义了reference类型的行为,提供了reference类型的基本功能。就像Integer类之于int类型。

Reference对象可以“注册”相关的引用对象,并通过内部的reference队列提供外部程序监控对象被GC的能力。

部分源码:

/**
* 被注册的引用对象
*/
private T referent;         /* Treated specially by GC */
/**
* 当一个Reference对象绑定的对象被GC回收时,JVM会将该引用对象被绑定到的reference对象(this)推入此队列。
* 其他程序可以通过轮询此队列,来获得该注册对象被GC的的“通知”,并完成一些工作
* 如WeakHashMap可以"知道"被GC的Entry并将其从Map中移除
* 实际只是逻辑上的一个标志,标志该对象是否加入到了队列。
* 队列里的Reference对象是通过next属性组成链式循环队列
*/
volatile ReferenceQueue<? super T> queue;
volatile Reference next;

Reference(T referent) {
    this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

/**
* 返回注册的引用对象,若对象已被GC,返回null
*/
public T get() {
    return this.referent;
}
/**
* 清除注册到该对象的引用对象。但是并不会加入referenceQueue
*/
public void clear() {
    this.referent = null;
}
/**
*将"注册"的对象,加入referenceQueue。
*/
public boolean enqueue() {
    this.referent = null;
    return this.queue.enqueue(this);
}

三、四种引用类型

JVM把引用类型分为四种类型:强引用、软引用、弱引用、虚引用,引用的类型可以描述它所指向的实例的可达性,进而供垃圾回收器根据不同类型做出不同的处理的能力,同时也提供了编程者跟踪对象生命周期的功能。

描述不同的引用类型,由Reference类的子类来实现:

  • FinalReference(强引用)
  • SoftReference (软引用)
  • WeakReference (弱引用)
  • PhantomReference (虚引用)

1、 FinalReference 强引用

强引用是指创建一个对象并它赋值给一个引用,引用是存在JVM中的栈(还有方法区)中的。具有强引用的对象,垃圾回收器绝对不会去回收它,直到内存不足以分配时,抛出OOM。

大多数情况,我们new一个对象,并把它赋值给一个变量,这个变量就是强引用。

class TestA {
    // 方法区中的类静态属性引用的对象
    private static Object finalRet2 = new Object();
    // 方法区中的常量引用的对象
    private static final Object finalRet3 = new Object();
    
    void methodA {
        // 栈上的局部变量引用的对象
		Object finalRet1 = new Object();
    }
    
    native void methodB {
        // JNI中引用的对象
		// ......
    }
}

以上指向的实例对象,是可达的。

FinalReference 类只用于实现Finalize功能,非public类,用户是不可用的

2、SoftReference 软引用

软引用描述一些还有用但非必需的对象

具有软引用关联的对象,内存空间足够时,垃圾回收器不会回收它。当内存不足时(接近OOM),垃圾回收器才会去决定是否回收它。

软引用一般用来实现简单的内存缓存。

我们通过以下测试代码来验证它的特性:

public class ReferenceTest {

    class User {
        // 模拟内存占用3M,以更好观察gc前后的内存变化
        private byte[] memory = new byte[3*1024*1024];
    }
    /**
     * 测试弱引用在内存足够时不会被GC,在内存不足时才会被GC的特性
     * JVM参数 -Xms20m -Xmx20m -Xlog:gc  将内存大小限制在20M,并打印出GC日志
     */
    public void testSoftReference(){

        // 当仅使用强引用,脱离GC Root后将会被回收 (可以通过查看gc日志来确认该对象确实被回收)
        // 这是对照组
        User retA = new User();
        retA = null;
        System.gc();
        System.out.println("对照组GC后:" + retA);


        User retB = new User();
        // 创建弱引用类,将该引用绑定到弱引用对象上
        SoftReference<User> sortRet = new SoftReference<>(retB);
        retB = null;
        // 此时并不会被GC
        System.gc();
        retB = sortRet.get();
        System.out.println("GC后通过软引用重新获取了对象:" + retB);

        retB = null;

        // 模拟内存不足,即将发生OOM
        List<User> manyUsers = new ArrayList<>();
        for(int i = 1; i < 100000; i++){
            System.out.println("将要创建第" + i + "个对象");
            manyUsers.add(new User());
            System.out.println("创建第" + i + "个对象后, 软引用对象:" + sortRet.get());
        }
    }


    public static void main(String[] args) {
        ReferenceTest referenceTest = new ReferenceTest();
        referenceTest.testSoftReference();
    }
}

执行结果如下:

3、WeakReference 弱引用

弱引用描述非必需对象,但它的强度比软引用更弱一些。

WeakReference对其引用的对象并无保护作用,当垃圾回收器进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。弱引用一般用于实现canonicalizing mappings (正规化映射),典型的应用是WeakHashMap。

我们通过以下代码来验证它的特性:

/**
* 测试弱引用无论内存是否足够都会被GC的特性
*/
public void testWeakReference(){
    User user = new User();
    WeakReference<User> ret = new WeakReference<>(user);
    System.out.println("GC前: " + ret.get());
    user = null;
    System.gc();
    System.out.println("GC后: " + ret.get());
}

执行结果:

[0.014s][info][gc] Using G1
[0.033s][info][gc] Periodic GC disabled
GC前: memory.ReferenceTest$User@b4c966a
[0.098s][info][gc] GC(0) Pause Full (System.gc()) 6M->0M(20M) 2.815ms
GC后: null

4、PhantomReference 虚引用

虚引用也被称为幽灵引用或幻引用,它是最弱的一种引用关系。

虚引用并不会影响对象的GC,而且并不可以通过PhantomReference对象取得一个引用的对象。

虚引用唯一的作用则是利用其必须和ReferenceQueue关联使用的特性,当其绑定的对象被GC回收后会被推入ReferenceQueue,外部程序可以通过对此队列轮询来获得一个通知,以完成一些目标对象被GC后的清理工作。

PhantomReference 的构造方法,与SoftReference和WeakReference不同,他的构造必须传入一个ReferenceQueue

public PhantomReference(T referent, ReferenceQueue<? super T> q) {    super(referent, q);}

四、应用

1、软引用实现内存缓存

上文提到,软引用关联的对象在内存足够时不会被GC清理,在内存不足时才会被GC清,结合我们可以通过ReferenceQueue获取一个被GC的对象的Reference引用对象的能力,我们可以实现一个简单的内存缓存,该缓存在JVM内存不足时能够自动清理,在内存充足时可以自动装入。

实现代码:

/**
 * @ClassName SoftRefCache
 * @Description 软引用实现的内存缓存(仅做学习目的,实际项目当然是用造好的轮子,memcached、redis等)
 */
public class SoftRefCache<K, V> {

    // 实际装载缓存的数据结构,采用Hashtable可以保证线程安全
    private final Hashtable<K, ValueRef> cache;
    // 此队列用来接收被GC的引用对象,来完成清理工作
    private final ReferenceQueue<V> queue;
    // 当被缓存对象不存在缓存中时,调用该接口来查询此对象,以装入缓存
    private final QueryForCache<K,V> queryForCache;

    public SoftRefCache(QueryForCache<K,V> queryForCache) {
        this.cache = new Hashtable<>();
        this.queue = new ReferenceQueue<>();
        this.queryForCache = queryForCache;
    }

    /**
     * 对value的包装,使用软引用来关联value对象,使其具有软引用的对象特性,并保存该value对象的key,以便于完成清理工作
     */
    private class ValueRef extends SoftReference<V> {

        private final K key;

        public ValueRef(K key, V referent, ReferenceQueue<? super V> q) {
            super(referent, q);
            this.key = key;
        }

        public K getKey() {
            return key;
        }
    }

    /**
     * 由Key获取一个对象,若已被缓存,则直接返回,若未被缓存,则将其缓存
     * @param key 要获取的对象的eky
     * @return 要获取的对象
     */
    public V get(K key) {
        V val = null;
        if (cache.containsKey(key)) {
            ValueRef valueRef = cache.get(key);
            val = valueRef != null ? valueRef.get() : null;
        }
        // cache中没有该key对应的对象实例
        if (val == null) {
            // 到数据库或硬盘查询该对象,并加入到cache中
            val = this.queryForCache.query(key);
            addToCache(key, val);
        }
        return val;
    }

    /**
     * 获取缓存内key--value对的数量
     */
    public int size(){
        this.clearCache();
        return cache.size();
    }
    
    /**
     * 清除缓存
     */
    public void clearAllCache(){
        clearCache();
        cache.clear();
        // 可以根据实际情况决定是否要GC
        System.gc();

    }

    /**
     * 将对象加入缓存
     */
    private void addToCache(K key, V val){
        // 清除垃圾引用
        clearCache();
        // 加入到缓存
        ValueRef valueRef = new ValueRef(key, val, queue);
        this.cache.put(key, valueRef);
    }

    /**
     * 清除缓存中已被GC的Value对象。
     * 具体是通过对ReferenceQueue轮询来实现的
     */
    private void clearCache(){
        ValueRef valueRef = null;
        while((valueRef = (ValueRef) queue.poll()) != null){
            cache.remove(valueRef.getKey());
        }
    }

}

/**
 * 该接口定义了一个需要缓存的对象不在缓存时,应该通过怎样的方式获取
 * @param <K> key的类型
 * @param <V> value的类型
 */
@FunctionalInterface
interface QueryForCache<K,V> {
    V query(K key);

测试代码:

/**
 * @ClassName SortRefCacheTest
 * @Description 测试自己实现的软引用缓存,JVM参数:-Xms20m -Xmx20m -Xlog:gc
 */
public class SortRefCacheTest {

    public static void main(String[] args) {
        // 这个接口实际应该实现为到数据库或硬盘查询实际的数据,这里就简单模拟,直接new
        QueryForCache<Integer, MyImage> queryForCache = key -> new MyImage(key, new byte[2*1024*1024]);
        // 创建缓存
        SoftRefCache<Integer, MyImage> softRefCache = new SoftRefCache<>(queryForCache);
        // 此处模拟不断对缓存进行装入,观察内存和gc情况
        for(int i=1; i < 100; i++){
            MyImage value = softRefCache.get(i);
            System.out.println("从缓存中获取到第" + value.getId() + "个MyImage");
        }
    }

}
class MyImage {
    private Integer id;
    private byte[] data; // 模拟较大的内存占用,以更好观察gc前后的内存变化

    public MyImage(Integer id, byte[] data) {
        this.id = id;
        this.data = data;
    }

    public Integer getId() {
        return id;
    }
}

执行结果(部分):

执行到最后,并没有抛出OOM

如果使用普通的HashMap等容器,结果就是OOM,这里就不验证了

参考文献

  1. 《深入理解Java虚拟机》 第二版 周志明著

  2. JAVA中reference类型简述 https://www.iteye.com/blog/shift-alt-ctrl-1839163

  3. JAVA四种引用方式 https://blog.csdn.net/u014086926/article/details/52106589#

posted @ 2020-05-16 22:23  Allen_0x4bb  阅读(1423)  评论(0编辑  收藏  举报