JAVA引用全解析:一篇文章带你掌握

发表时间: 2022-12-06 16:47

介绍

JAVA 中有 4 种类型的引用:
-
强引用
-
软引用
-
弱引用
-
虚引用

这些引用仅在垃圾收集器管理它们的方式上有所不同。如果您从未听说过它们,则意味着您只使用过强的。了解其中的区别会对您有所帮助,尤其是当您需要存储临时对象并且不能 使用像 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)。


由于这篇文章不是关于垃圾收集器的,所以我不会深入解释,但仅供参考,有 4 种类型的垃圾收集器根:

  1. 局部 变量
  2. 活跃的 Java 线程
  3. 静态变量
  4. JNI 引用是包含本机代码的 Java 对象,而不是由 jvm 管理的内存

Oracle 没有指定如何管理内存,因此每个 JVM 实现都有自己的一套算法。但想法总是一样的:
– JVM 使用循环算法寻找非活动对象并标记它们
– 标记的对象被终结(调用 finalize() 方法)然后被销毁
– JVM 有时会移动剩余对象的一部分为了重建堆中大面积的空闲连续空间

问题

如果 JVM 管理内存,您为什么需要关心?因为这并不意味着你不能有 内存泄漏

大多数时候,您都在不知不觉中使用垃圾收集根。例如,假设您需要在程序的生命周期内存储一些对象(因为它们的初始化成本很高)。您可能会使用静态集合(List、Map 等)在代码中的任何位置存储和检索这些对象:

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

但是,这样做可以防止 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);                }            }        }

输出是:

线程“main”中的异常
java.lang.OutOfMemoryError:Java 堆空间

Java 提供了不同类型的引用来避免 OutOfMemoryError。

有些类型允许 JVM 释放对象,即使程序仍然需要它们。处理这些情况是开发人员的责任。

强引用

强引用是标准引用。当您像这样在对象 obj 上创建时:

MyClass obj = new MyClass ();

您正在为新创建的 MyClass 实例创建一个名为“obj”的强引用。当垃圾收集器查找非活动对象时,它只检查对象是否是强可达的,这意味着通过强引用可传递地链接到垃圾收集根。

使用这种类型的引用会强制 JVM 将对象保留在堆中,直到对象不被使用,如“垃圾收集器”部分所述。

软引用

根据 java API soft reference 有:

“软引用对象,由垃圾收集器根据内存需求自行清除”

这意味着如果您在不同的 JVM(Oracle 的 Hotspot、Oracle 的 JRockit、IBM 的 J9 等)上运行您的程序,软引用的行为可能会发生变化。

让我们看一下 Oracle 的 JVM Hotspot(标准和最常用的 JVM),看看它是如何管理软引用的。根据 Oracle 文档:

“默认值为每兆字节 1000 毫秒,这意味着对于堆中每兆字节的可用空间,软引用将存活(在收集到对象的最后一个强引用之后)1 秒”

这是一个具体的例子:假设堆是 512 MB,还有 400 MB 空闲。

我们创建一个对象A,软引用到对象缓存,并强引用A到对象B。由于A被强引用到B,它是强可达的并且不会被垃圾收集器删除(案例 1)。

想象一下,现在B 被删除了,所以A只是对缓存对象的软引用。如果对象A在接下来的 400 秒内没有被强引用,它将在超时后被删除(情况 2)。


以下是如何操作软引用:

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。

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");    }}

输出代码是:

缓存大小:1
缓存大小:200001
缓存大小:400001
缓存大小:600001
线程“主”
java.lang.OutOfMemoryError 中的异常:超出 GC 开销限制

Oracle 提供了一个ReferenceQueue,当引用的对象只能软访问时,它会填充软引用。使用此队列,您可以清除软引用并避免 OutOfMemoryError。

使用 ReferenceQueue,与上面相同的代码具有相同的堆大小(64 MB)但要存储更多数据(500 万对 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();        }     }}

输出是:

结束,删除软引用=4976899

当您需要存储许多对象时,软引用很有用,如果它们被 JVM 删除,这些对象可能会(代价高昂)重新实例化。

弱引用

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

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

这意味着当垃圾收集器检查所有对象时,如果它检测到一个对象只有对垃圾收集根的弱引用(即没有强引用或软引用链接到该对象),该对象将被标记为移除并尽快删除。使用 Wea​​kReference 的方法与使用 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;        //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));        }    }}

例如,我使用 Wea​​kHashMap 来解决以下问题:存储多个交易信息。我使用了这个结构:WeakHashMap<String,Map<K,V>> 其中 WeakHashMap 的键是一个包含交易 ID 的字符串,“简单”Map 是我需要在生命周期内保留的信息交易。有了这个结构,我肯定会在 WeakHashMap 中获取我的信息,因为包含事务 ID 的字符串在事务结束之前不会被销毁,而且我不必关心清理 Map。

Oracle 建议使用 Wea​​kHashMap 作为“规范化”映射。

虚引用

在垃圾收集过程中,没有对垃圾收集根的强/软引用的对象将被删除。在被删除之前,方法 finalize() 被调用。当一个对象被终结但没有被删除(还)时,它就变成了“幻影可达”,这意味着在对象和垃圾收集根之间只有一个幻影引用。

与软引用和弱引用不同,对对象使用显式幻像引用可以防止对象被删除。程序员需要显式或隐式地移除幻影引用,以便销毁最终化的对象。要显式清除幻影引用,程序员需要使用 ReferenceQueue ,当对象完成时,它会填充幻影引用。

幻影引用无法检索被引用的对象:幻影引用的 get() 方法始终返回 null,因此程序员无法再次使幻影可达对象强/软/弱可达。这是有道理的,因为幻影可达对象已经完成,所以如果重写的 finalize() 函数已清除资源,它可能不再工作。

由于无法访问引用的对象,因此我看不出幻影引用有何用处。一个用例可能是,如果您需要在对象完成后执行操作,而您不能(或出于性能原因不想)在该对象的 finalize() 方法中执行特定操作。

结论

我希望您现在对这些参考资料有了更好的了解。大多数时候,您不需要显式使用它们(也不应该)。但是,许多框架正在使用它们。如果你想了解东西是如何工作的,那么了解这个概念是很好的。