Java中的引用

在Java中有四种类型的引用:

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用

这些引用的区别仅在于 垃圾收集器 的处理方式。如果你从来没有听说过这些引用,说明你一直在使用强引用。了解这些区别是很有帮助的,尤其是在你需要存储临时对象同时又无法使用eHcache或者Guava等缓存库时。

由于这些引用类型都与JVM的垃圾收集器高度相关,所以先对Java中的垃圾收集器做一个简单的介绍,然后开始分析这些不同的引用类型。

垃圾回收

Java与C++的主要区别之一就在于 内存管理 。在Java中,开发人员不需要了解存储部分是如何工作的,因为JVM会解决垃圾回收的问题。

当你创建一个对象时,JVM会在 堆内存 中为其分配空间。堆内存的空间是有限的,因此JVM通常需要删除一些对象来释放空间。如果要销毁一个对象,JVM需要先知道这个对象是否有效。如果一个对象被一个 垃圾回收根节点 (传递式)引用,那么该对象仍然有效。

举例来说:

  • 如果对象C被对象B引用,对象B被对象A引用,而对象A被一个 垃圾回收根节点 引用,那么C、B、A对象都被当做有效的(情形1)。
  • 如果对象B不再被对象A引用,那么对象C和B也就变为无效对象,可以被销毁(情形2)。
Java中的引用

由于本文并不是关于垃圾回收器的,这里不再深入介绍,只列出JVM中的四类垃圾回收根节点供参考:

  1. 局部变量
  2. 活跃Java线程
  3. 静态变量
  4. JNI引用 ,即包含本地代码,存储不受JVM管理的Java对象。

Oracle没有指定如何管理内存,因此每个JVM各自都有一系列算法。但是内在的思想都是一致的:

  • JVM使用一个递归算法查找并标记无效对象

  • 被标记的对象会被析构(调用*finalize()*方法)并销毁

  • JVM有时会移动部分存活对象,以期在堆内存中构建大段的连续可用空间。

问题

既然JVM会管理内存,为什么开发者还需要关注这些?因为无法保证不会出现 内存泄漏

其实你在大多数时间都在使用垃圾回收根节点,只是你没有意识到。举例说,假设你需要在你的程序的生命周期内存储一些对象(因为这些对象的初始化代价很大),你可能会使用一个静态集合(List,Map等),方便在代码中的任何地方对这些对象进行存储或访问。

但是,如果这样做的话,将会阻止JVM销毁集合中的那些对象,甚至一时疏忽就会出现内存溢出 OutOfMemoryError 错误。举例来说:

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提供了不同类型的引用来避免内存溢出错误。对于其中一些引用类型来说,即使这些对象被程序需要,仍然允许JVM释放这些对象,相应地,处理这些情况就成为了开发者的责任。

强引用

强引用是Java中的标准引用。当使用如下方式创建一个对象obj时:

MyClass obj = new MyClass()
复制代码

实际上,你创建了一个名为“obj”的强引用,指向了新建的MyClass的实例。当垃圾回收器查找无效对象时,只会检查对象是否具有 强可达性 ,即是否能从垃圾回收根节点通过强引用传递式地链接到该对象。

使用这一类引用会强制JVM将这些对象保留在堆内存中,直到这些对象如“垃圾回收器”一节所述不再被使用时,才会被回收。

软引用

根据Java API,软引用的特点为:

“软引用对象,由垃圾收集器根据内存需求自行决定是否回收”

也就是说,你在不同的JVM(Oracle HotSpot,Oracle JRockit,IBM J9等)上运行程序时,软引用对象的运行状态可能会有所不同。

我们来看看Oracle的虚拟机HotSpot(规范且使用最多的JVM)是如何管理软引用的。根据Oracle的文档:

“默认值为每MB 1000 ms,也就是说堆内存中每多1 MB的可用空间,软引用就可以多存活1s(在对象的最后一个强引用被回收之后)”

这里有一个具体的例子:假设堆内存大小为512MB,并且有400MB空闲。新建一个对象 A ,通过软引用被对象 cache 引用,同时被对象 B 强引用。由于 A 被B强引用,因此它具有强可达性,不会被垃圾回收器删除(情形1)。

假设 B 被删除,则 A 仅被对象 cache 软引用。如果对象 A 在接下来的400秒内没有被强引用,则其将在超时之后被删除(情形2)。

Java中的引用

下面是使用软引用的对应代码:

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{
        //初始化cache,通过软引用指向实体A
        ExampleSoftRef.A instanceA = new ExampleSoftRef.A();
        cache = new SoftReference<ExampleSoftRef.A>(instanceA);
        instanceA=null;
        // 实体A现在只有软可达性,可以被垃圾回收器删除
        Thread.sleep(5000);
 
        ...
        ExampleSoftRef.B instanceB = new ExampleSoftRef.B();
        //由于cache仅有指向instanceA的软引用,我们无法断定instanceA是否仍然存在
        //我们需要进行判断,如果需要还得新建一个instanceA
        instanceA=cache.get();
        if (instanceA ==null){
            instanceA = new ExampleSoftRef.A();
            cache = new SoftReference<ExampleSoftRef.A>(instanceA);
        }
        instanceB.setStrongRef(instanceA);
        instanceA=null;
        //instanceA目前被cache软引用,同时被instanceB强引用,因此不会再被垃圾回收器清除
 
        ...
    }
}
复制代码

即使软引用指向的对象被垃圾回收器自动删除, 软引用(引用本身也是对象)并没有被删除 。因此,你还是需要清除它们。举例来说,假如堆内存容量较小比如64MB(Xmx64m),下面的代码会由于软引用的使用发生内存溢出异常。

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,上面同样大小的堆内存(64MB)同样的代码却可以支持更多的数据运行(5000000 vs 1000000):

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删除,也可以(花费一些代价)重新实例化,那么软引用还是比较有用的。

弱引用

弱引用是比软引用更不稳定的一个概念。根据Java API的描述:

“假设垃圾收集器在某个时间点确定对象是弱可达的。 那时它将自动清除指向该对象的所有弱引用,对于通过强引用或弱引用与该对象关联的其它弱可达对象,也会清除指向这些对象的所有弱引用,通过一系列强引用和软引用可以从该对象到达该对象。 同时,它将声明之前所有弱可达的对象为可终结的(finalizable)。 在同一时间或稍后,垃圾回收器会将新清除的弱引用放入创建弱引用对象时所指定的引用队列”

也就是说,当垃圾回收器检查所有对象时,如果发现某个对象仅通过弱引用与垃圾回收根节点关联(即没有强引用或者软引用链接到该对象),该对象会被立即标记为可清除的。使用WeakReference的方法与使用SoftReference的方法是相同的,可以直接参考“软引用”一节的示例。

Oracle提供了一个基于弱引用的类: WeakHashMap ,在这个类中可以使用弱引用的键值。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;
        // {5,"five"} 会一直存活到下次垃圾回收
 
        Integer i2 = 2;
        // {2,"two"} 会在Map中存活到,i2不再具有强可达性
        cache.put(i2, "two");
 
        //这里不会出现OutOfMemoryError
        // 因为Map会清除其中的项
        for (int i = 6; i < 100_000_000; i++) {
            cache.put(i,String.valueOf(i));
        }
    }
}
复制代码

举例说明,我们使用WeakHashMap存储多笔交易信息,使用的结构如下:WeakHashMap<String,Map<K,V>>,其中WeakHashMap的键值为包含交易ID的字符串,对应的Map中保存的是交易期间所有需要保存的信息。使用该结构的优势在于,我们一定可以从WeakHashMap中获取到需要的信息,因为键值字符串中包含的交易ID直到交易之后才会被销毁,同时我们也无需考虑清除Map结构。

Oracle建议将WeakHashMap作为规范映射结构使用。

虚引用

在垃圾回收器的处理过程中,与垃圾回收根节点之间没有强/软引用连接的对象将会被清除。在这些对象被清除之前,会调用其对应的finalize()方法。当一个对象被析构且还未被清除时,它就变为具有“虚可达性”,也即是说,在该对象与垃圾回收根节点之间只有虚引用。

与弱引用及软引用不同,显式使用虚引用可以防止对象被清除。程序员需要显式或隐式地移除虚引用,如何其指向的可终结的对象才会被销毁。如果要隐式地清除一个虚引用,一般都需要使用 ReferenceQueue ,当一个对象被析构,该队列中就会插入一个虚引用。

通过虚引用无法获得其指向的对象:虚引用的 get() 方法通常会返回 null ,因此编程人员无法将一个虚可达的对象重新置为强/软/弱可达。这是有道理的,因为虚可达对象已经被析构,所以如果覆盖 finalize() 函数已经清除了对象所需资源,该对象可能不再起作用。

虚对象几乎没有什么用途,因为其指向的对象无法访问。一个可能的使用场景就是能在对象被GC时收到系统通知。

总结

大多数情况下,我们并不会显示地使用这些引用,但是很多框架内部都会使用它们,如果你想理解框架的工作原理,那么了解这些知识就是比较有用的。

原文 

https://juejin.im/post/5c047cc76fb9a049d974f40c

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » Java中的引用

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址