Java对象的强、软、弱、虚引用

来源:互联网 发布:淘宝运营绩效考核方案 编辑:程序博客网 时间:2024/06/05 05:40

自己一直对Java对象的强、软、弱、虚四种引用的概念不是很清楚,因此最近重新学习了一下。
乘此机会顺便用本篇博客总结一下阅读的相关资料,并记录一下自己的理解。

1、强引用(StrongReference)
强引用是最普遍被使用的引用。
通常情况下,我们创建一个对象时,使用的都是强引用,例如:

class Test {    Test() {        //在栈中创建变量object,其对堆中创建出的对象,就是强引用        Object object = new Object();        ...........    }}

如果一个对象具有强引用,那垃圾回收器绝不会回收它。
当内存空间不足时,JVM宁愿抛出OOM错误使程序异常终止,
也不会靠随意回收具有强引用的对象来解决内存不足的问题。

2、软引用(SoftReference)
对于软引用,我参考的资料都是这么描述的:
“如果一个对象只具有软引用,那么在内存空间足够的时候,垃圾回收器就不会回收它;
一旦内存空间不足了,垃圾回收器就会回收这些对象的内存。
使用软引用关联的对象时,只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,
Java虚拟机就会把这个软引用加入到与之关联的引用队列中。”

然而,我自己测试了一下,发现JVM对软引用的实现并没有想象中的那么智能,还是依赖于GC机制的回收策略。
我自己写的测试代码如下:

public class Test {    //这里是上文所述的引用队列,GC回收对象后,将对应引用加入到mRQ中    private static ReferenceQueue<TestLargeObject> mRQ = new ReferenceQueue<>();    public static void main(String[] args) {        //创建一个List,以强引用的方式持有每个软引用对象        List<SoftReference<TestLargeObject>> sl = new ArrayList<>();        for (int i = 0; i < 10; ++i) {            //创建软引用,持有大内存对象,并绑定mRQ            SoftReference<TestLargeObject> tmp = new SoftReference<>(                    new TestLargeObject("Soft " + i), mRQ);            System.out.println("Just created " + tmp.get());            //将软引用加入到sl中            sl.add(tmp);            //试图主动GC一下            System.gc();            //主线程休眠一下,给GC线程一点回收内存的时间            try {                Thread.sleep(3000);            } catch (Exception e) {                //......            }            //判断是否有引用加入到mRQ中            checkQueue();        }    }    //该函数就是完成打印功能    private static void checkQueue() {        Reference<? extends TestLargeObject> rq = mRQ.poll();        if (rq != null) {            System.out.println("In queue: " + rq);        }    }    private static class TestLargeObject {        private String mId;        private double[] mLargeData;        TestLargeObject(String id) {            mId = id;            //我的对象真的很大!!!!            mLargeData = new double[50000000];        }        @Override        public String toString() {            return mId;        }        @Override        public void finalize() {            //析构时打印一下            System.out.println("Finalizing ... " + mId);            try {                mLargeData = null;                super.finalize();            } catch (Throwable t) {                //......            }        }    }}

该测试的实际运行结果如下:

Just created Soft 0Just created Soft 1Just created Soft 2Just created Soft 3Just created Soft 4Just created Soft 5Exception in thread "main" java.lang.OutOfMemoryError: Java heap space    at Test$TestLargeObject.<init>(Test.java:42)    at Test.main(Test.java:13)    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)    at java.lang.reflect.Method.invoke(Method.java:498)    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)Finalizing ... Soft 1Finalizing ... Soft 0Finalizing ... Soft 5Finalizing ... Soft 4Finalizing ... Soft 3Finalizing ... Soft 2

我的JDK是1.8.0_101,每次运行的结果可能略有差异,但无一例外会出现OOM。
如果改小TestLargeObject占用的内存,并增大创建的总数,依然会出现OOM。
有可能是自己的代码写的不太合理,不过单从这个实验现象来看,软引用或许在某些场景下也会出现问题。

3、弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用同样可以和一个引用队列联合使用,
如果弱引用所引用的对象被垃圾回收,
Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

我们将上面代码中的软引用改为弱引用(对象名称修改为Weak),去掉线程休眠相关的代码(弱引用回收快,不需要休眠),
发现运行结果如下:

Just created Weak 0Finalizing ... Weak 0Just created Weak 1In queue: java.lang.ref.WeakReference@1540e19dFinalizing ... Weak 1Just created Weak 2In queue: java.lang.ref.WeakReference@677327b6Finalizing ... Weak 2Just created Weak 3In queue: java.lang.ref.WeakReference@14ae5a5Finalizing ... Weak 3Just created Weak 4In queue: java.lang.ref.WeakReference@7f31245aFinalizing ... Weak 4Just created Weak 5In queue: java.lang.ref.WeakReference@6d6f6e28Finalizing ... Weak 5Just created Weak 6In queue: java.lang.ref.WeakReference@135fbaa4Finalizing ... Weak 6Just created Weak 7In queue: java.lang.ref.WeakReference@45ee12a7Finalizing ... Weak 7Just created Weak 8In queue: java.lang.ref.WeakReference@330bedb4Finalizing ... Weak 8Just created Weak 9In queue: java.lang.ref.WeakReference@2503dbd3Finalizing ... Weak 9

每次运行的结果可能略微不同,但基本与上面的结果类似,不会出现OOM。
从上面的结果可以看出,弱引用还是比较靠谱的,其行为符合对应的介绍。
即GC线程扫描到仅有弱引用的对象时,就会直接释放对应内存,
然后将对应的引用加入到对应的ReferenceQueue中。

4、虚引用(PhantomReference)
最后,我们看看虚引用。

与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,
在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。
虚引用与软引用、弱引用的一个区别在于:
虚引用必须和引用队列联合使用(软、弱都是可选的)。

同样使用上面的测试代码,将软引用改为虚引用,结果如下:

Just created nullFinalizing ... Phantom 0Just created nullFinalizing ... Phantom 1Just created nullFinalizing ... Phantom 2In queue: java.lang.ref.PhantomReference@1540e19dJust created nullIn queue: java.lang.ref.PhantomReference@677327b6Finalizing ... Phantom 3Just created nullIn queue: java.lang.ref.PhantomReference@14ae5a5Finalizing ... Phantom 4Just created nullIn queue: java.lang.ref.PhantomReference@7f31245aFinalizing ... Phantom 5Just created nullIn queue: java.lang.ref.PhantomReference@6d6f6e28Finalizing ... Phantom 6Just created nullIn queue: java.lang.ref.PhantomReference@135fbaa4Finalizing ... Phantom 7Just created nullIn queue: java.lang.ref.PhantomReference@45ee12a7Finalizing ... Phantom 8Just created nullIn queue: java.lang.ref.PhantomReference@330bedb4Finalizing ... Phantom 9

从上面的运行结果可以看出:
1、虚引用持有的对象,不能利用虚引用的Get来获取,将返回null。
毕竟对象仅被虚引用持有的话,随时可能被干掉,因此这么设计是合理的。

2、有些网上的资料写道,”对象被释放前,会先将引用先加入到ReferenceQueue中”。
从上面的结果来看,实际情况应该是:对象先被释放掉,才将其引用加入到ReferenceQueue中。
为了排除自己代码中System.gc和checkQueue调用顺序对结果的影响,
我把checkQueue函数移动到TestLargeObject的finalize函数中,依然发现对象是先析构,后添加引用到ReferenceQueue。
因此个人认为,”试图监控ReferenceQueue中的引用,在对象被回收前,采取必要行动”的说法不是很靠谱。

3、System.gc虽然不能保证GC线程立即执行工作,但还是有一定用处的。
如果在上述代码中移除对System.gc的调用,不论使用何种引用,无一例外全部OOM。

0 0