java的4种引用类型及应用场景

Java有4种引用类型,分别是强引用、软引用、弱引用和虚引用。
主要是为了根据场景来控制不同的回收时机。

强引用

强引用(Strong Reference)是最普通的引用,以=进行赋值,例如String s="ni hao"中的s就是一个强引用。
特性: 只要有引用存在,那么堆中数据不会被回收,哪怕是OOM。
代码示例(-Xmx50m -verbose:gc):

public static void main(String[] args) throws InterruptedException {
    //首先设置-Xmx50m
    List list = new ArrayList();
    for (int i = 0; i < 10; i++) {
        list.add(new byte[1024 * 1024 * 10]);//10M
        TimeUnit.SECONDS.sleep(1);
        System.out.println("time:" + i);
    }
}

运行的结果是撑爆内存,抛出OOM异常。

软引用

软引用(SoftReference)是一种比强引用弱的引用。
特性: 在分配堆内存的时候,如果空间不足,就会将堆中的软引用的数据空间回收。
代码示例(-Xmx50m -verbose:gc):

public class SoftRefer {
    public static void main(String[] args) throws InterruptedException {
        SoftReference softReference = new SoftReference(new byte[1024 * 1024 * 30]);//30M
        System.out.println("before gc data:" + softReference.get());
        System.out.println("before gc ref:" + softReference);
        byte[] bytes = new byte[1024 * 1024 * 30];
        System.out.println("after gc data:" + softReference.get());
        System.out.println("after gc ref:" + softReference);
        TimeUnit.SECONDS.sleep(10);
    }
}

运行结果如下:

before gc data:[B@4554617c
before gc ref:java.lang.ref.SoftReference@74a14482
[GC (Allocation Failure) 33091K->31568K(49152K), 0.0010995 secs]
[Full GC (Ergonomics) 31568K->31358K(49152K), 0.0080070 secs]
[GC (Allocation Failure) 31358K->31358K(49152K), 0.0007548 secs]
[Full GC (Allocation Failure) 31358K->620K(37376K), 0.0082833 secs]
after gc data:null
after gc ref:java.lang.ref.SoftReference@74a14482

当第二次申请内存的时候,发现内存不够,则会检查内存中是否有软引用,有的话就会回收。
如果是第二次申请的仍然是软引用的数据,结果是一样的,将会回收旧的空间。
那么什么时候会发生回收的事件呢?每次gc就会回收吗?
将代码修改一下,增加一个gc的操作,如下:

public class SoftRefer {
    public static void main(String[] args) throws InterruptedException {
        SoftReference softReference = new SoftReference(new byte[1024 * 1024 * 30]);//30M
        System.out.println("before gc data:" + softReference.get());
        System.out.println("before gc ref:" + softReference);
        System.gc();
        System.out.println("after gc data:" + softReference.get());
        System.out.println("after gc ref:" + softReference);
        TimeUnit.SECONDS.sleep(10);
    }
}

执行结果如下:

before gc data:[B@4554617c
before gc ref:java.lang.ref.SoftReference@74a14482
[GC (System.gc()) 33091K->31536K(49152K), 0.0011518 secs]
[Full GC (System.gc()) 31536K->31358K(49152K), 0.0082917 secs]
after gc data:[B@4554617c
after gc ref:java.lang.ref.SoftReference@74a14482

可以看到,即便是发生了Full gc,也没有回收掉上面的软引用。那OOM会不会回收呢?应该会的,上面的第一个例子里面,即便是没有发生OOM,也是会回收的,因为分配空间不足了,不过还是试一试吧。
代码如下(-Xmx50m -verbose:gc):

    public static void main(String[] args) throws InterruptedException {
        //首先设置-Xmx50m
        SoftReference softReference = new SoftReference(new byte[1024 * 1024 * 30]);//30M
        System.out.println(softReference.get());
        System.out.println(softReference);
        List list = new ArrayList();
        while (true) {
            list.add(new byte[1024 * 1024 * 5]);
            System.out.println(softReference.get());
        }
    }

结果如料想的那样,到不了OOM的时候就会被回收了。jdk8的SoftReference有如下的注释:

Soft reference objects, which are cleared at the discretion of the
garbage collector in response to memory demand. Soft references are
most often used to implement memory-sensitive caches. Suppose that the
garbage collector determines at a certain point in time that an object
is softly reachable. At that time it may choose to clear atomically
all soft references to that object and all soft references to any
other softly-reachable objects from which that object is reachable
through a chain of strong references. At the same time or at some
later time it will enqueue those newly-cleared soft references that
are registered with reference queues.

All soft references to softly-reachable objects are guaranteed to have
been cleared before the virtual machine throws an OutOfMemoryError.
Otherwise no constraints are placed upon the time at which a soft
reference will be cleared or the order in which a set of such
references to different objects will be cleared. Virtual machine
implementations are, however, encouraged to bias against clearing
recently-created or recently-used soft references.

Direct instances of this class may be used to implement simple caches;
this class or derived subclasses may also be used in larger data
structures to implement more sophisticated caches. As long as the
referent of a soft reference is strongly reachable, that is, is
actually in use, the soft reference will not be cleared. Thus a
sophisticated cache can, for example, prevent its most recently used
entries from being discarded by keeping strong referents to those
entries, leaving the remaining entries to be discarded at the
discretion of the garbage collector.

所以,回收时机可以这样描述:

  1. 当发生GC时,虚拟机可能会回收SoftReference对象所指向的软引用,如果空间足够,就不会回收,还要取决于该软引用是否是新创建或近期使用过。
  2. 在虚拟机抛出OutOfMemoryError之前,所有软引用对象都会被回收。

使用场景
可以是在排除过期数据的情况下。JDK中有个类叫ResourceBundle,内部会使用ConcurrentMap缓存ResourceBundle对象,这里就是使用的软引用机制。

弱引用

弱引用(WeakReference)是比软引用更弱的一种引用。
特性: 只要触发了gc(包括Allocation Failure类型的gc),无论内存空间是否充足,都会将堆中的弱引用的数据空间回收。
示例代码(-Xmx50m -verbose:gc):

    public static void main(String[] args) throws InterruptedException {
        //首先设置-Xmx50m
        WeakReference weakReference = new WeakReference(new byte[1024 * 1024 * 5]);//5M
        System.out.println(weakReference.get());

        List list = new ArrayList();
        for (int i = 0; i < 10; i++) {
            list.add(new byte[1024 * 1024 * 5]);
            System.out.println("time:" + i + "==" + weakReference.get());
        }

        TimeUnit.SECONDS.sleep(5);
    }

执行结果如下:

[B@4554617c
time:0–[B@4554617c
[GC (Allocation Failure) 12611K->5968K(49152K), 0.0042473 secs]
time:1–null
time:2–null
…(略)

可以从上面看到,哪怕是在年轻代空间不足的时候,也会把弱引用回收掉。
使用场景
个人理解,是在较为复杂的数据结构中,为了避免内存泄露而使用的一种引用方式。
下面说一下ThreadLocal中对弱引用的使用。

ThreadLocal中的弱引用

ThreadLocal的存在是提供了一种线程内全局的上下文容器,类似Spring中的Context,只不过使用范围是在当前线程内。
它可以简化同一个线程内多个方法直接参数的重复传递,隔离其他线程的干扰。
原理是把ThreadLocal变量以弱key的形式存放在java.lang.ThreadLocal.ThreadLocalMap中。
java.lang.ThreadLocal.ThreadLocalMap.Entry就是继承了WeakReference,把ThreadLocal作为一种弱引用存在于map中的key中,一旦ThreadLocal变量的引用被回收或者被置为null值的时候,JVM就会将数据中的key也回收掉并置为null值。
可以写个例子来证明上面的结论。
首先为了看到回收结果,我继承了一下ThreadLocal,打了个日志,代码如下:

public class WeakRefer {
    public static void main(String[] args) throws InterruptedException {
        saveSomething("something");
        System.gc();
        TimeUnit.SECONDS.sleep(5);
    }

    private static void saveSomething(String str) {
        MyThreadLocal<String> threadLocal = new MyThreadLocal<>();
        threadLocal.set(str);
    }
}

class MyThreadLocal<T> extends ThreadLocal<T> {

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("============finalize");
    }
}

执行结果如下:

[GC (System.gc()) 2115K->848K(49152K), 0.0014758 secs]
[Full GC (System.gc()) 848K->612K(49152K), 0.0076629 secs]
============finalize

可以看到,当threadLocal引用消失的时候,数组中的弱引用也被回收了。
如果不使用弱引用,那么threadLocal变量就会一直存在于java.lang.ThreadLocal.ThreadLocalMap中,直到线程停止运行。
这样的话,就有可能会有内存泄露的风险。

虚引用

虚引用是使用PhantomReference创建的引用,虚引用也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个。
一个对象是否有虚引用的存在,完全不会对其生命周期构成影响,也无法通过虚引用获得一个对象实例。
虚引用只有一个含有队列的构造函数,也就是说,虚引用必须和队列同时使用,换句话说,虚引用是通过队列来实现它的价值的。
当虚引用所指向的那块内存被回收之后,JVM就会把那个虚引用的变量放到队列中,表示对象被回收。
画个图,如下:
在这里插入图片描述
当上面的data数据块被回收之后,JVM就会把PhantomRef这个引用放入到queue中。
简单写个demo,如下所示(-verbose:gc):

public class PhantomRefer {
    private static final ReferenceQueue<MyUsefulObj> QUEUE = new ReferenceQueue<>();

    public static void main(String[] args) {
        MyUsefulObj obj = new MyUsefulObj();
        PhantomReference<MyUsefulObj> reference = new PhantomReference<>(obj, QUEUE);
        monitorQueue();
        obj = null;
        System.gc();
    }

    private static void monitorQueue() {
        // 这个线程不断读取引用队列,当弱引用指向的对象呗回收时,该引用就会被加入到引用队列中
        new Thread(() -> {
            while (true) {
                Reference<? extends MyUsefulObj> poll = QUEUE.poll();
                if (poll != null) {
                    System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
                }
            }
        }).start();
    }

}

class MyUsefulObj {
    public void doSomething() {
        //一些重要的事情
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("============finalize");
    }
}

我所期望的结果是打印出“虚引用对象被jvm回收了”,但是实际上并没有,输出如下:

[GC (System.gc()) 6506K->1024K(249344K), 0.0014907 secs]
[Full GC (System.gc()) 1024K->838K(249344K), 0.0090643 secs]
============finalize

明明执行了finalize()方法,那为什么队列中没有取到值呢?是对象实际上没有被回收吗?还是因为已经回收了但是JVM没有把引用放到队列中?
这个时候就涉及finalize方法的特性了。
finalize()是在回收之前被JVM调用的一个方法,但并不意味着调用了该方法之后就一定会被回收,也可能被救赎。
在上面这个例子当中,主要是因为复写了finalize()方法,导致了MyUsefulObj对象被延迟回收了。
当我把复写finalize的方法删掉之后,再次执行,结果就是预期的那样了,如下:

[GC (System.gc()) 6506K->1024K(249344K), 0.0010478 secs]
[Full GC (System.gc()) 1024K->838K(249344K), 0.0061029 secs]
— 虚引用对象被jvm回收了 ---- java.lang.ref.PhantomReference@6018d1f6

使用场景
使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。这个虚引用对于对象而言完全是无感知的,有没有完全一样,但是对于虚引用的使用者而言,就像是待观察的对象的把脉线,可以通过它来观察对象是否已经被回收,从而进行相应的处理。
事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放,DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章