1. 概述

在这篇文章中,我们将探讨java.util包中的WeakHashMap数据结构。我们将使用它来实现一个简单的缓存,但请记住,这主要是为了理解映射的工作原理,自己创建缓存实现通常不是一个好主意。

简单来说,WeakHashMap是基于哈希表的Map接口实现,其中键是WeakReference类型的对象。

WeakHashMap中的条目会在其键不再正常使用时自动删除,这意味着没有一个Reference指向该键。当垃圾回收(GC)过程丢弃一个键时,相应的条目就会从映射中有效移除,因此这个类的行为与其他Map实现有所不同。

2. 强引用、软引用与弱引用

为了理解WeakHashMap的工作原理,我们需要先了解WeakReference类——这是WeakHashMap实现中键的基本构造。在Java中,我们主要有三种类型的引用,我们将在接下来的章节中进行解释。

2.1 强引用

在日常编程中最常见的引用类型是强引用

Integer prime = 1;

变量prime对一个值为1的Integer对象有强引用。任何有一个强引用指向的对象都不符合垃圾回收条件。

2.2 软引用

简而言之,如果一个对象有一个软引用指向它,只有在JVM确实需要内存时才会被垃圾回收。

让我们看看如何在Java中创建软引用:

Integer prime = 1;  
SoftReference<Integer> soft = new SoftReference<Integer>(prime); 
prime = null;

prime对象有一个强引用指向它。

接着,我们将prime的强引用包裹在一个软引用中。当将强引用设置为null后,prime对象可以被垃圾回收,但只有在JVM确实需要内存时才会被收集。

2.3 弱引用

仅由弱引用引用的对象会被尽早地垃圾回收;在这种情况下,GC不会等待直到需要内存。

我们可以使用以下方式在Java中创建一个弱引用:

Integer prime = 1;  
WeakReference<Integer> soft = new WeakReference<Integer>(prime); 
prime = null;

当我们把prime引用设置为null时,由于没有其他强引用指向prime对象,它将在下一次垃圾回收周期中被清除。

弱引用类型的引用用作WeakHashMap中的键。

3. WeakHashMap作为高效内存缓存

假设我们想构建一个缓存,以大图像对象作为值,图像名称作为键。我们需要选择一个合适的映射实现来解决这个问题。

直接使用简单的HashMap不是一个好选择,因为值对象可能会占用大量内存。更重要的是,即使在我们的应用程序中不再使用这些大图像对象,它们也不会被垃圾回收过程从缓存中移除。

理想情况下,我们希望有一个Map实现,它允许GC自动删除未使用的对象。当应用程序中某个地方不再使用某个大图像对象的键时,映射中的相应条目将从内存中删除。

幸运的是,WeakHashMap正好具有这些特性。让我们测试一下WeakHashMap,看看它的表现如何:

WeakHashMap<UniqueImageName, BigImage> map = new WeakHashMap<>();
BigImage bigImage = new BigImage("image_id");
UniqueImageName imageName = new UniqueImageName("name_of_big_image");

map.put(imageName, bigImage);
assertTrue(map.containsKey(imageName));

imageName = null;
System.gc();

await().atMost(10, TimeUnit.SECONDS).until(map::isEmpty);

我们创建了一个WeakHashMap实例,用于存储我们的BigImage对象。我们将一个BigImage对象作为值,将图像名称对象的引用作为键。图像名称将以WeakReference类型存储在映射中。

接下来,我们将图像名称引用设置为null,因此没有其他引用指向bigImage对象。WeakHashMap的默认行为是在下次GC时回收没有引用的条目,所以这个条目将在下一次垃圾回收过程中从内存中删除。

我们调用System.gc()强制JVM触发垃圾回收过程。在垃圾回收周期结束后,我们的WeakHashMap将变得空:

WeakHashMap<UniqueImageName, BigImage> map = new WeakHashMap<>();
BigImage bigImageFirst = new BigImage("foo");
UniqueImageName imageNameFirst = new UniqueImageName("name_of_big_image");

BigImage bigImageSecond = new BigImage("foo_2");
UniqueImageName imageNameSecond = new UniqueImageName("name_of_big_image_2");

map.put(imageNameFirst, bigImageFirst);
map.put(imageNameSecond, bigImageSecond);
 
assertTrue(map.containsKey(imageNameFirst));
assertTrue(map.containsKey(imageNameSecond));

imageNameFirst = null;
System.gc();

await().atMost(10, TimeUnit.SECONDS)
  .until(() -> map.size() == 1);
await().atMost(10, TimeUnit.SECONDS)
  .until(() -> map.containsKey(imageNameSecond));

请注意,只有imageNameFirst引用被设置为nullimageNameSecond引用保持不变。在垃圾回收后,映射中只包含一个条目——imageNameSecond

4. 结论

在这篇文章中,我们深入研究了Java中的引用类型,以便完全理解java.util.WeakHashMap的工作原理。我们创建了一个简单的缓存,并测试了它是否按预期工作。

所有这些示例和代码片段的实现可以在GitHub项目中找到——这是一个Maven项目,可以直接导入并运行,无需额外配置。