面试经典问题之Java的不同引用类型

Java中4中引用类型

  • 强引用Strong References
  • 软引用Soft References
  • 弱引用Week References
  • 虚引用Phantom References

这些引用的区别仅在于垃圾回收时管理它们的方式不同。如果你从来没有听说过它们,意味着你只使用过强引用类型。了解它们的区别可以帮助你,尤其是你需要临时存储对象,但是不能使用Guava这种缓存库的时候。

因为这些类型和JVM垃圾回收器强相关,我简要的回顾下关于Java的垃圾回收的一些信息,然后我会演示这些不用的类型。

垃圾回收器

Java和C++最主要的区别就是内存管理。在Java中,开发者不需要知道内存是怎么工作的(但是我们应该知道),因为Java用它的垃圾回收器搞定了这部分。
当年创建一个对象时,JVM会在堆中为它分配。堆是有限的内存空间。因此,JVM经常需要删除对象来释放空间。为了销毁一个对象,JVM需要知道这个对象是活跃的还是不活跃的。一个对象如果它被垃圾回收根所引用,它仍然是使用中的。
举个例子:
如果一个C对象被对象B引用,对象B被对象A引用,而对象A又被GC Root引用,那对象C,B,A都会被认为是活跃的(Case1)。
但是,如果对象B已经不被A所引用,那C和B都不是活跃的,所以他们都可以销毁了。(Case2)
在这里插入图片描述

因为这个文章不是关于GC的,所以我不会更深入的解释,但是仅供参考,有4种GC Root

  • 本地变量
  • 活跃的Java线程
  • 静态变量
  • JNI 引用,它们是包含本机代码而不是由 jvm 管理的内存的 Java 对象

Oracle没有指定如果管理内存,所以每个JVM实现都拥有它自己的一组算法,但是思路总是类似的:

  • JVM使用循环算法查找非活跃对象,然后标记他们
  • 标记、终止(调用finalize方法)、销毁
  • JVM有时会移动一部分仍然存在的对象来在堆中重构出一个大的连续的未使用的空间(消除碎片)。

问题

如果JVM管理内存,为什么我们还需要关心?因为这不意味着你就没有内存泄露了。
大多数时候,你没有意识到你在使用GC Root。举个例子,假如你需要在你的程序的生命周期内存储一些对象(因为初始化它们开销很大)。你会更喜欢使用静态集合(比如list,map…)来存储,然后你在代码的任何地方都可以取回这些对象:

public static Map<K, V> myStoredObjects= new HashMap<>();

但是这样做,你可以防止JVM从集合中销毁这些对象,但可能会导致OOM。

public class OOM {
    public static List<Integer> myCachedObjects = new ArrayList<>();
 
    public static void main(String[] args) {
        for (int i = 0; i < 100_000_000; i++) {
            myCachedObjects.add(i);
        }
    }
}

输出是:

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

Java提供了不同的引用类型来避免OOM。
有些类型允许JVM释放对象尽管他们仍然被程序所需要。开发有责任来处理这些情况。

强引用 Strong Reference

强引用是标准引用。当你像这样创建对象的时候

MyClass obj = new MyClass ();

你创建了一个新的MyClass的叫obj的实例的强引用。当垃圾回收器寻找不活跃对象时,它只会确认这个对象是否是强可达的,这意味着通过强引用传递链接到一个GC Root。
使用这种类型的引用强制JVM保留这些对象在堆内直到这些对象不再被使用,如垃圾回收那部分的描述。

软引用 Soft Reference

根据JAVA API 软引用是:

Soft reference objects, which are cleared at the discretion of the garbage collector in response to memory demand

软引用对象,由垃圾收集器根据内存需求自行清除。
这意味着,如果你的程序运行在不同的JVM上可能行为也有所变化(Oracle’s Hotspot, Oracle’s JRockit, IBM’s J9, …)。

我们看下在Oracle的JVM Hotspot(标准,且被使用最多的JVM)是如果管理软引用的,根据Oracle的文档

The default value is 1000 ms per megabyte, which means that a soft reference will survive (after the last strong reference to the object has been collected) for 1 second for each megabyte of free space in the heap

默认值是每兆字节1000毫秒,这就意味着对于堆内每兆字节堆内的可用空间,软引用将存活(在强引用对象被回收后)1秒。

We create an object A, softly referenced to an object cache and strongly referenced A to an object B. Since A is strongly referenced to B, it’s strongly reachable and won’t be deleted by the garbage collector (case 1).

这是一个具体的例子:我们假设堆是512兆字节,还剩下400M字节的可用空间。
我们创建一个对象A,被缓存对象软引用,被对象B强引用。因为A被B强引用,所以它是强可达的,它不会被GC回收,(Case1)

想象一下,现在B被销毁了,所以A只是软引用到缓存对象,如果对象A在接下来的400秒内没有被强引用,它就会在超时后被删除了。(Case2)
在这里插入图片描述

操作软引用的方法:

public class ExampleSoftRef {
    public static class A{
 
    }
    public static class B{
        private A strongRef;
 
        public void setStrongRef(A ref) {
            this.strongRef = ref;
        }
    }
    public static SoftReference<A> cache;
 
    public static void main(String[] args) throws InterruptedException{
        //initialisation of the cache with a soft reference of instanceA
        ExampleSoftRef.A instanceA = new ExampleSoftRef.A();
        cache = new SoftReference<ExampleSoftRef.A>(instanceA);
        instanceA=null;
        // instanceA  is now only soft reachable and can be deleted by the garbage collector after some time
        Thread.sleep(5000);
 
        ...
        ExampleSoftRef.B instanceB = new ExampleSoftRef.B();
        //since cache has a SoftReference of instance A, we can't be sure that instanceA still exists
        //we need to check and recreate an instanceA if needed
        instanceA=cache.get();
        if (instanceA ==null){
            instanceA = new ExampleSoftRef.A();
            cache = new SoftReference<ExampleSoftRef.A>(instanceA);
        }
        instanceB.setStrongRef(instanceA);
        instanceA=null;
        // instanceA a is now only softly referenced by cache and strongly referenced by B so it cannot be cleared by the garbage collector
 
        ...
    }
}

例如,对于像 64 Mbytes (Xmx64m) 这样的低堆大小,尽管使用了软引用,但以下代码给出了 OutOfMemoryException。

但即使软引用对象是被GC自动删除,软引用也不会删除,所以你仍然需要手动清理他们。举个例子,一个只有64M字节的堆,尽管使用了软引用,但还是OOM了

public class TestSoftReference1 {
 
    public static class MyBigObject{
        //each instance has 128 bytes of data
        int[] data = new int[128];
    }
    public static int CACHE_INITIAL_CAPACITY = 1_000_000;
    public static Set<SoftReference<MyBigObject>> cache = new HashSet<>(CACHE_INITIAL_CAPACITY);
 
    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000; i++) {
            MyBigObject obj = new MyBigObject();
            cache.add(new SoftReference<>(obj));
            if (i%200_000 == 0){
                System.out.println("size of cache:" + cache.size());
            }
        }
        System.out.println("End");
    }
}

输出:

size of cache:1
size of cache:200001
size of cache:400001
size of cache:600001
Exception in thread “main” java.lang.OutOfMemoryError: GC overhead limit exceeded

Oracle提供了一个引用队列ReferenceQueue,当被引用的对象仅软可达时,它会可以填充软引用。当使用这个队列时,你可以清理软引用并避免内存溢出。
使用 ReferenceQueue,与上面代码一样,具有相同的堆大小(64 MB),但要存储更多数据(500 万 vs 100 万):

public class TestSoftReference2 {
    public static int removedSoftRefs = 0;
 
    public static class MyBigObject {
        //each instance has 128 bytes of data
        int[] data = new int[128];
    }
 
    public static int CACHE_INITIAL_CAPACITY = 1_000_000;
    public static Set<SoftReference<MyBigObject>> cache = new HashSet<>(
            CACHE_INITIAL_CAPACITY);
    public static ReferenceQueue<MyBigObject> unusedRefToDelete = new ReferenceQueue<>();
 
    public static void main(String[] args) {
        for (int i = 0; i < 5_000_000; i++) {
            MyBigObject obj = new MyBigObject();
            cache.add(new SoftReference<>(obj, unusedRefToDelete));
            clearUselessReferences();
        }
        System.out.println("End, removed soft references=" + removedSoftRefs);
    }
 
    public static void clearUselessReferences() {
        Reference<? extends MyBigObject> ref = unusedRefToDelete.poll();
        while (ref != null) {
            if (cache.remove(ref)) {
                removedSoftRefs++;
            }
            ref = unusedRefToDelete.poll();
        }
 
    }
}

输出是:

End, removed soft references=4976899

当你需要存储很多对象时,但如果它们被JVM删除,这些对象可以重新实例化,软引用非常引用。(我理解还是缓存)

弱引用 Weak Reference

弱引用是一个比软引用更容易变的概念,根据JAVA API:

假设垃圾回收器在某个时间点确定某个对象是弱可达的,那时它将原子地清除该对象的所用弱引用,以及对任何其他弱可达对象的所有弱引用,但这个对象通过强引用和软引用链是可达。
The weak reference is a concept even more volatile than soft references. According to the JAVA API:

假设垃圾收集器在某个时间点确定某个对象是弱可达的。 那时它将原子地清除对该对象的所有弱引用,以及对任何其他弱可达对象的所有弱引用,通过强引用和软引用链可以从中访问该对象。 同时它将声明所有以前弱可达的对象是可终结的。 同时或稍后它会将那些新清除的已注册到引用队列的弱引用加入队列。

这意味着当垃圾回收器检查所有对象时,如果它检测到一个对象对GC Root只有弱引用(没有强引用或者软引用链接到这个对象),这个对象就会被标记为可移除的,并且会被尽快的移除。使用一个弱引用的方式和软引用是一样的,所以可以看下下软引用那部分的示例。

Oracle提供了一个非常有趣的基于弱引用的类:WeakHashMap。这个map具有对弱引键的特殊性,WeakHashMap可以被当做为一个标准的Map使用, 唯一的区别是它会在键从堆中销毁后自动清除:

public class ExampleWeakHashMap {
    public static Map<Integer,String> cache = new WeakHashMap<Integer, String>();
 
    public static void main(String[] args) {
        Integer i5 = new Integer(5);
        cache.put(i5, "five");
        i5=null;
        //the entry {5,"five"} will stay in the Map until the next garbage collector call
 
        Integer i2 = 2;
        //the entry {2,"two"} will stay  in the Map until i2 is no more strongly referenced
        cache.put(i2, "two");
 
        //remebmber the OutOfMemoryError at the chapter "problem", this time it won't happen
        // because the Map will clear its entries.
        for (int i = 6; i < 100_000_000; i++) {
            cache.put(i,String.valueOf(i));
        }
    }
}

举个例子,我使用WeakHashMap解决以下问题:存储多个交易信息。
我使用这个结构:WeakHashMap<String,Map<K,V>> ,其中WeakHashMap的键是一个包含交易ID的字符串,Map<K,V>是我在整个交易的整个生命周期都需要保留的信息。我肯定会在WeakHashMap中获取我的信息,因为交易ID直到交易结束之前都不会被销毁,而且我不必关心清理这个Map。

虚引用 Phantom Reference

在垃圾收集过程中,没有被GC root 强/软引用 的对象会被删除。在被删除之前,finalize() 被调用。当一个对象是终止的,但是还没被删除时,是因为“虚可达”,这意味着在GC root和这个对象之前仅有一个虚引用。

不像软引用或者弱引用,对一个对象明确的使用虚引用可以防止这个对象被删除。程序员需要
显式的或者隐式的移除这个虚引用来让这个终止的对象被销毁。为了明确的清理虚引用,程序要需要使用ReferenceQueue,当一个对象终止时会填充一个虚引用。

一个虚引用不能取回引用的对象,需引用的get方法会返回null,所以程序员不能使虚可达的对象变成强\软\弱可达。这是合理的,因为虚可达的对象已经被终止了,所以它可能不能再工作了。比如重写了finalize()方法已经释放了资源。
因为无法访问被引用的对象,我不知道虚引用有什么用处。一个可能的用例是:如果你需要在对象终止后执行一些操作没,但是你不能(或者因为性能原因)不想在这个对象的finalize()方法中执行这个特定的操作。

结论:
我希望你现在关于这些引用有了更好的了解,大多数时候,你不需要显示的使用它们(并且也不应该)。但是,很多框架都在使用他们。所以如果你想了解这些东西的原理,了解这些概念是非常好的。

原文链接:http://coding-geek.com/java-the-different-reference-types/

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值