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
引用被设置为null
。imageNameSecond
引用保持不变。在垃圾回收后,映射中只包含一个条目——imageNameSecond
。
4. 结论
在这篇文章中,我们深入研究了Java中的引用类型,以便完全理解java.util.WeakHashMap
的工作原理。我们创建了一个简单的缓存,并测试了它是否按预期工作。
所有这些示例和代码片段的实现可以在GitHub项目中找到——这是一个Maven项目,可以直接导入并运行,无需额外配置。