阅读 354

【评论抽掘金限定周边】| 谈谈java中的引用类型

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

引用类型简介

java中的引用其实就像是一个对象的名字或者别名,一个对象在内存中会请求一块空间来保存数据,根据对象的大小,它可能需要占用的空间大小也不等。访问对象的时候,不会直接是访问对象在内存中的数据,而是通过引用去访问。引用也是一种数据类型,可以把它想象为类似C++语言中指针的东西,它指示了对象在内存中的地址,只不过我们不能够观察到这个地址究竟是什么。

如果我们定义了不止一个引用指向同一个对象,那么这些引用是不相同的,因为引用也是一种数据类型,需要一定的内存空间(stack,栈空间)来保存。但是它们的值是相同的,都指示同一个对象在内存(heap,堆空间)的中位置。在java中引用类型分为两类:值类型和引用类型,其中值类型就是基本数据类型,如int,double类型,而引用类型就是除了基本数据类型之外的所有类型。在JDK.1.2之后Java对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用的强度依次减弱。

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。比如:A a = new A()。强引用有一下三个特性:

强引用可以直接访问目标对象。

强引用所指向的对象在任何时候都不会被系统回收。

强引用可能导致内存泄漏。

源码如下:

/**
 * Final references, used to implement finalization
 */
class FinalReference<T> extends Reference<T> {

    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}
复制代码

对象新建后默认为强引用类型的,是普遍对象引用的类型。FinalReference的源码只有一个空实现,这也说明强引用是“默认引用类型”

GC 回收问题

对象因为Finalizer的引用而变成了一个临时的强引用,即使没有其他的强引用,还是无法立即被回收; 对象至少经历两次GC才能被回收,因为只有在FinalizerThread执行完了对象的finalize方法的情况下才有可能被下次GC回收,而有可能期间已经经历过多次GC了,但是一直还没执行对象的finalize方法; CPU资源比较稀缺的情况下FinalizerThread线程有可能因为优先级比较低而延迟执行对象的finalize方法; 因为对象的finalize方法迟迟没有执行,有可能会导致大部分f对象进入到old分代,此时容易引发old分代的GC,甚至Full GC,GC暂停时间明显变长,甚至导致OOM;

软引用

软引用是用来描述一些“还有用但是非必须”的对象。软引用的回收策略在不同的JVM实现会略有不同,JVM不仅仅只会考虑当前内存情况,还会考虑软引用所指向的referent最近的使用情况和创建时间来综合决定是否回收该referent。软引用保存了两个变量:

timestamp:每次调用get方法都会更新时间戳。JVM可以利用该字段来选择要清除的软引用,但不是必须要这样做。

clock:时间锁,由垃圾收集器更新。

因此,任何GC都可以使用这些字段并定义清除软引用的策略,例如:最后清除最近创建的或最近使用的软引用。在JDK 1.2之后,提供了SoftReference类来实现软引用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。源码如下:

/**
 * 软引用对象由垃圾收集器根据内存需要决定是否清除。软引用经常用于实现内存敏感的缓存。
 *
 * 假如垃圾收集器在某个时间确定对象是软可达的,此时它可以选择原子地清除
 * 指向该对象的所有软引用,以及从该对象通过强引用链连接的其他软可达对象的所有软引用。
 * 与时同时或者之后的某个时间,它会将注册了reference queues的新清除的软引用加入队列。
 *
 * 在虚拟机抛出OutOfMemoryError异常之前,将保证清除对软可达对象的所有软引用。
 * 不过,并没有对清除软引用的时间以及清除顺序施加强制约束。
 * 但是,鼓励虚拟机实现偏向不清除最近创建或最近使用的软引用。
 *
 * 该类的直接实例可用于实现简单的缓存。
 * 该类或其派生子类也可用于更大的数据结构以实现更复杂的高速缓存。
 * 只要软引用的引用对象还是强可达的,即还在实际使用中,软引用就不会被清除。
 * 因此,复杂的高速缓存可以通过持有对最近使用缓存对象的强引用来防止其被清除,
 * 而不常使用的剩余缓存对象由垃圾收集器决定是否清除。
 */
public class SoftReference&lt;T&gt; extends Reference&lt;T&gt; {	
    static private long clock;
    private long timestamp;
	
	// 返回对象的引用。如果该引用对象已经被程序或者垃圾收集器清除,则返回null。
	// 把最近一次垃圾回收时间赋值给timestamp
    public T get() {
        T o = super.get();
        if (o != null &amp;&amp; this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }

}

复制代码

下面看一个例子:

/**
 * 软引用在pending状态时,referent就已经是null了。
 *
 * 启动参数:-Xmx5m
 *
 */
public class SoftReference {

    private static ReferenceQueue<MyObject> queue = new ReferenceQueue<>();

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(3000);
        MyObject object = new MyObject();
        SoftReference<MyObject> softRef = new SoftReference(object, queue);
        new Thread(new CheckRefQueue()).start();

        object = null;
        System.gc();
        System.out.println("After GC : Soft Get = " + softRef.get());
        System.out.println("分配大块内存");

        /**
         * 总共触发了 3 次 full gc。第一次有System.gc();触发;第二次在在分配new byte[5*1024*740]时触发,然后发现内存不够,于是将softRef列入回收返回,接着进行了第三次full gc。
         */
        //byte[] b = new byte[5*1024*740];

        /**
         * 也是触发了 3 次 full gc。第一次有System.gc();触发;第二次在在分配new byte[5*1024*740]时触发,然后发现内存不够,于是将softRef列入回收返回,接着进行了第三次full gc。当第三次 full gc 后发现内存依旧不够用于分配new byte[5*1024*740],则就抛出了OutOfMemoryError异常。
         */
        byte[] b = new byte[5*1024*790];

        System.out.println("After new byte[] : Soft Get = " + softRef.get());
    }

    public static class CheckRefQueue implements Runnable {

        Reference<MyObject> obj = null;

        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>) queue.remove();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (obj != null) {
                System.out.println("Object for softReference is " + obj.get());
            }

        }
    }

    public static class MyObject {

        @Override
        protected void finalize() throws Throwable {
            System.out.println("MyObject's finalize called");
            super.finalize();
        }

        @Override
        public String toString() {
            return "I am MyObject.";
        }
    }
}
复制代码

弱引用

用来描述非必须的对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发送之前。开始回收是,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java垃圾回收器准备对WeakReference所指向的对象进行回收时,调用对象的finalize()方法之前,WeakReference对象自身会被加入到这个ReferenceQueue对象中,此时可以通过ReferenceQueue的poll()方法取到它们。源码如下:

/**
 * 弱引用对象不能阻止自身的引用被回收。
 * 弱引用常用于实现规范化映射(对象实例可以在程序的多个地方同时使用)。
 *
 * 假如垃圾收集器在某个时间点确定对象是弱可达的。那时它将原子地清除对该对象的所有弱引用
 * 以及该引用通过强引用或者软引用连接的所有其他弱可达对象的所有弱引用。
 * 同时,它将表明前面所指的所有弱可达对象都可以执行finalize方法。
 * 与此同时或之后某一个时间,它将注册了reference queues的那些新清除弱引用加入队列。
 */
public class WeakReference&lt;T&gt; extends Reference&lt;T&gt; {

	// 创建没有注册ReferenceQueue的弱引用
    public WeakReference(T referent) {
        super(referent);
    }

	// 创建注册了ReferenceQueue的弱引用
    public WeakReference(T referent, ReferenceQueue&lt;? super T&gt; q) {
        super(referent, q);
    }
}

复制代码

虚引用

虚引用是所有引用类型中最弱的一种。一个对象是否关联到虚引用,完全不会影响该对象的生命周期,也无法通过虚引用来获取一个对象的实例。为对象设置一个虚引用的唯一目的是:能在此对象被垃圾收集器回收的时候收到一个系统通知,它就是利用ReferenceQueue实现的。当referent被gc回收时,JVM自动把虚引用对象本身加入到ReferenceQueue中,表明该reference指向的referent被回收。然后可以通过去queue中取到reference,可以通过这个来做额外的清理工作。可以用虚引用代替对象finalize方法来实现资源释放,这样更加灵活和安全。

PhantomReference只有当Java垃圾回收器对其所指向的对象真正进行回收时,会将其加入到这个ReferenceQueue对象中,这样就可以追踪对象的销毁情况。这里referent对象的finalize()方法已经调用过了。 所以具体用法和之前两个有所不同,它必须传入一个ReferenceQueue对象。当虚引用所引用对象准备被垃圾回收时,虚引用会被添加到这个队列中。源码如下:

/**
 * 虚引用对象在被垃圾收集器检查到后加入reference queues队列,否则会被回收。
 * 虚引用最常用于实现比Java finalization机制更灵活的安排额外的清理工作。
 *
 * 如果垃圾收集器在某个时间点确定虚引用对象是虚可达的,那么在那个时间或之后某个时间它会将引用加入reference queues队列。
 *
 * 为了确保可回收对象保持不变,虚引用的引用无法使用:虚引用对象的get方法始终返回null。
 *
 * 与软引用和弱引用不同,当虚引用加入reference queues队列后垃圾收集器不会被自动清除。
 * 只通过虚引用可达的对象将保持不变,直到所有此类引用都被清除或自已变为不可达。
 */
public class PhantomReference&lt;T&gt; extends Reference&lt;T&gt; {

    // 由于不能通过虚引用访问对象,因此此方法始终返回null。
    public T get() {
        return null;
    }

    // 使用空ReferenceQueue队列创建一个虚引用没有意义:它的get方法总是返回null,
	// 并且由于它没有注册队列,所以也不会被加入队列有任何清理前的预处理操作。
    public PhantomReference(T referent, ReferenceQueue&lt;? super T&gt; q) {
        super(referent, q);
    }
}
复制代码

下面看一个例子:

public class PhantomReferenceTest {

    private static ReferenceQueue<MyObject> queue = new ReferenceQueue<>();

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

        object = null;

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

        /**
         * 在经过一次GC之后,系统找到了垃圾对象,并调用finalize()方法回收内存,但没有立即加入PhantomReference Queue中。因为MyObject对象重写了finalize()方法,并且该方法是一个非空实现,所以这里MyObject也是一个Final Reference。所以第一次GC完成的是Final Reference的事情。
         * 第二次GC时,该对象(即,MyObject)对象会真正被垃圾回收器进行回收,此时,将PhantomReference加入虚引用队列( PhantomReference Queue )。
         * 而且每次gc之间需要停顿一些时间,已给JVM足够的处理时间;如果这里没有TimeUnit.SECONDS.sleep(1); 可能需要gc到第5、6次才会成功。
         */

    }

    public static class MyObject {

        @Override
        protected void finalize() throws Throwable {
            System.out.println("MyObject's finalize called");
            super.finalize();
        }

        @Override
        public String toString() {
            return "I am MyObject";
        }
    }

    public static  class CheckRefQueue implements Runnable {

        Reference<MyObject> obj = null;

        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>)queue.remove();
                System.out.println("删除的虚引用: " + obj + " , 虚引用的对象: " + obj.get());
                System.exit(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
复制代码

总结

Java的4种引用的级别由高到低依次为:

强引用 > 软引用 > 弱引用 > 虚引用。

引用.PNG

抽奖说明

1.本活动由掘金官方支持 详情可见juejin.cn/post/701221…

2.通过评论和文章有关的内容即可参加,要和文章内容有关哦!

3.本月的文章都会参与抽奖活动,欢迎大家多多互动!

4.除掘金官方抽奖外本人也将送出周边礼物(马克杯一个和掘金徽章若干,马克杯将送给走心评论,徽章随机抽取,数量视评论人数增加)。

文章分类
后端
文章标签