在软件技术行业,JAVA是很主流的编程语言,知力的软件工程师分享在java技术中4种类型的引用。

介绍

在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)。

 

由于这篇文章不是关于垃圾收集器的,我不会在解释中做更深入的了解,但是FYI有四种类型的垃圾收集根源:

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

Oracle没有指定如何管理内存,因此每个JVM实现都有自己的一组算法。但是这个想法总是相同的:
– JVM使用recurent算法查找非活动对象并标记它们
– 标记对象已完成(调用finalize()方法),然后销毁
–JVM有时会将剩余对象的一部分移入为了在堆中重建大面积的免费连续空间

 

问题

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

大多数情况下,您正在使用垃圾收集根本没有意识到它。例如,假设您需要在程序的生命周期中存储一些对象(因为它们的初始化代价高昂)。您可能会使用静态colllection(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”的强引用。当垃圾收集器查找非活动对象时,它只会检查是否可以强制访问 objets,这意味着通过强引用传递链接到垃圾收集根。

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

软参考

根据java API的软参考有:

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

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

 

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

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

下面是一个具体的例子:假设堆是512 MB,并有400 MB空闲空间。

我们创建一个对象,软引用一个对象缓存,并强烈引用一个一个对象。由于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 MB(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 
线程“main”中的异常java.lang.OutOfMemoryError:超出GC开销限制

Oracle提供了一个ReferenceQueue,当被引用的对象只能轻微到达时,它将充满软引用。使用这个队列,你可以清除软引用并避免OutOfMemoryError。

使用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();

}

 

}

}

输出是:

结束,删除软引用= 4976899

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

 

弱参考

弱参考是一个比软参考更加波动的概念。根据JAVA API:

“假设垃圾收集器在某个时间点确定一个对象  很弱可达。那时它会原子地清除对该对象的所有弱引用,以及通过一系列强和软引用可访问该对象的任何其他弱可访问对象的所有弱引用。同时它将宣布所有以前弱可达的物体可以定型。在同一时间或稍后的时间,它会将那些新注册的弱引用排入参考队列。“

这意味着当垃圾收集器检查所有对象时,如果它检测到对垃圾收集根目录只有弱引用的对象(即没有链接到对象的强引用或软引用),则此对象将被标记为要移除并尽快删除。使用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;

//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的关键字是一个包含事务标识的字符串,“简单”映射是我在生命周期中需要保留的信息交易。有了这个结构,我一定会在WeakHashMap中获得我的信息,因为包含事务ID的String在事务结束之前不会被销毁,我也不必关心清理Map。

Oracle建议使用WeakHashMap作为“规范化”映射。

 

幻影参考

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

与软引用和弱引用不同,对对象使用明确的幻影引用可防止删除对象。程序员需要明确或隐含地移除幻影引用,以便最终化的对象可以被销毁。为了明确地清除幻影引用,程序员需要使用一个  ReferenceQueue  ,当一个对象完成时,该引用队列中充满了幻影引用。

幻像引用无法检索引用的对象:幻像引用的get()方法始终返回null,因此程序员无法再次强烈/轻微/弱地访问虚拟可访问对象。它是有道理的,因为幻像可达对象已经完成,因此如果例如被覆盖的finalize()函数清除了资源,它就不再工作了。

由于无法访问引用的对象,因此我没有看到幻像引用是如何有用的。一个用例可能是如果你需要在一个对象完成后做一个动作,并且你不能(或者不想出于性能的原因)在这个对象的finalize()方法中执行特定的动作。

 

结论

我希望你现在对这些参考文献有更好的了解。大多数时候,你不需要明确地使用它们(而你不应该)。但是,许多框架正在使用它们。如果你想了解东西是如何工作的,那么知道这个概念是很好的。

如果你想获取更多的学习内容,可以关注知力科技,我们会定期分享技术前沿的内容,也可以关注我们的微信公众号“知力宝来啦”。

 

Comments are closed.