一、问题描述
在开发中遇到一个困境,需要在某个类(如 ValueHolder
)中设计一个 Map
其中 Key 是另外一个类型 (如Source
)。Source
有自己的生命周期,由于ValueHolder
的生命周期较长,在 Source
生命周期结束后就应该回收,但由于被 ValueHolder
所持有导致无法被回收,从而导致内存泄露。
判断 Java 对象是否可以被回收,有两种常见的方法:
可达性分析可以有效地解决循环引用的问题,即便两个或多个对象相互引用,只要从GC Roots出发无法到达它们,那么它们就都是可回收对象。 Java虚拟机并不使用引用计数法来判断对象是否可以被回收,因为这种方法无法解决循环引用的问题。Java虚拟机主要使用可达性分析法来进行垃圾回收。
因此我们可以采用弱引用这个知识点来解决这个问题。
WeakHashMap
是一种基于弱引用的动态散列表,它可以实现“自动清理”的内存缓存。当它的键对象没有被其他强引用引用时,垃圾回收器会回收它和对应的值对象,从而避免内存泄漏或浪费。
WeakHashMap
的使用场景有以下几种:
WeakHashMap
可以作为二级缓存,存放过期或低频数据,当内存不足时,可以自动释放这些数据。WeakHashMap
可以避免因为监听器或回调函数的强引用导致被监听或回调的对象无法被回收。WeakHashMap
可以作为线程局部变量的容器,当线程结束时,可以自动清理线程局部变量。本场景就是需要没有其他强引用时,自动回收,避免内存泄露。
但是 WeakHashMap
也存在一些缺点:
WeakHashMap
的行为取决于垃圾回收器的运行时机,这是不可预测的。因此,您不能确定 WeakHashMap
中的元素何时被移除。WeakHashMap
不是线程安全,如果多个线程同时访问或修改它,可能会导致不一致或并发异常。需要使用同步机制来保证线程安全。WeakHashMap
的迭代器(Iterator
)不支持快速失败(fail-fast)机制,也就是说,在迭代过程中如果有其他线程修改了 WeakHashMap,迭代器不会抛出 ConcurrentModificationException
异常。WeakHashMap
的性能可能不如 HashMap
,因为它需要额外的工作来处理弱引用和垃圾回收。采用这种方案的好处是不需要手动处理 Key 的释放,但是多线程场景下,需要额外做同步。
WeakReference
是一种弱引用,它可以用来描述非必须存在的对象,当它指向的对象没有被其他强引用引用时,垃圾回收器会回收它。
因此,可以采用 WeakReference
包装 Key ,这样 Source
没有其他强引用时就可以被回收。
当然WeakReference
也存在一些缺点:
WeakReference
不能保证对象的存活时间,当对象只被 WeakReference
引用时,它随时可能被垃圾回收器回收,这可能导致一些意外的情况或者数据丢失。WeakReference
需要额外的内存空间和时间来维护引用队列和弱引用对象,这可能影响程序的性能和效率。WeakReference
不能防止内存泄漏,如果弱引用对象本身没有被及时清理或者释放,它仍然会占用内存空间。WeakReference
不能单独使用,它需要配合其他强引用或者软引用来实现缓存或者监听等功能。采用这种方案,好处是可以和线程安全的 Map
结合,更容易做到线程安全,但需要自己去合适的时机清理。
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Source {
private String id;
}
import lombok.Data;
import java.util.Map;
import java.util.WeakHashMap;
@Data
public class ValueHolder {
private Map<Source, String> map = new HashMap<>(8);
public void putValue(Source source, String value) {
map.put(source, value);
}
public void print() {
System.out.println(map);
}
}
测试代码:
package org.example.demo.weak;
import java.util.concurrent.TimeUnit;
public class WeakHashMapDemo {
private static final ValueHolder valueHolder = new ValueHolder();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
test("index" + i);
if (i % 10 == 0) {
System.gc();
TimeUnit.MILLISECONDS.sleep(30);
valueHolder.print();
}
}
}
private static void test(String id) {
Source source = new Source(id);
String value = "test";
valueHolder.putValue(source, value);
}
}
根据可达性分析可知, valueHolder
在 main 方法执行完毕前都会被 GCRoot (valueHolder) 引用,由于 Source
被 ValueHoder
中的 Map
所持有,在 test 执行完毕后就无法被释放。
那么,本文要实现的效果就是在 test 方法执行完毕后,就允许 Source
被回收。
可以将 HashMap
替换成 WeakHashMap
即可。
import lombok.Data;
import java.util.Map;
import java.util.WeakHashMap;
@Data
public class ValueHolder {
private Map<Source, String> map = new WeakHashMap<>(8);
public void putValue(Source source, String value) {
map.put(source, value);
}
public void print() {
System.out.println(map);
}
}
如果想保证线程安全,可以使用 Collections.synchronizedMap
进行包装。
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
@Data
public class ValueHolder {
private Map<Source, String> map = Collections.synchronizedMap(new WeakHashMap<>(8));
public void putValue(Source source, String value) {
synchronized (map) {
map.put(source, value);
}
}
public void print() {
synchronized (map) {
System.out.println(map);
}
}
}
测试代码:
package org.example.demo.weak;
import java.util.concurrent.TimeUnit;
public class WeakHashMapDemo {
private static final ValueHolder valueHolder = new ValueHolder();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
test("index" + i);
if (i % 10 == 0) {
System.gc();
TimeUnit.MILLISECONDS.sleep(30);
valueHolder.print();
}
}
}
private static void test(String id) {
Source source = new Source(id);
String value = "test";
valueHolder.putValue(source, value);
}
}
可以使用 WeakReference
对 Key 进行封装,这样 Source
没有其他强引用时会释放。
import lombok.Data;
import java.util.Map;
import java.util.WeakHashMap;
import lombok.Data;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
@Data
public class ValueHolder {
private Map<WeakReference<Source>, String> map = new ConcurrentHashMap<>(8);
public void putValue(Source source, String value) {
map.put(new WeakReference<>(source), value);
}
public void print() {
System.out.println("mapSize:" + map.size());
for (Map.Entry<WeakReference<Source>, String> entry : map.entrySet()) {
System.out.println("element:" + entry.getKey().get());
}
}
}
存在一个问题, Source
可以被回收,但是 WeakReference
不会被回收。
(1) 可以设计一个 clear
方法,去将已经回收的 Source
对应的数据给清除掉。
package org.example.demo.weak;
import lombok.Data;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Data
public class ValueHolder {
private Map<WeakReference<Source>, String> map = new ConcurrentHashMap<>(8);
public void putValue(Source source, String value) {
map.put(new WeakReference<>(source), value);
}
public void print() {
System.out.println("mapSize:" + map.size());
clear();
for (Map.Entry<WeakReference<Source>, String> entry : map.entrySet()) {
System.out.println("element:" + entry.getKey().get());
}
}
public void clear() {
Iterator<Map.Entry<WeakReference<Source>, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<WeakReference<Source>, String> entry = iterator.next();
WeakReference<Source> key = entry.getKey();
if (key.get() == null) {
iterator.remove();
}
}
}
}
(2) 还可以在构造 WeakReferece
时传入队列,回收的 Source
会自动放到队列中,定时清理即可。
import lombok.Data;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Data
public class ValueHolder {
private Map<WeakReference<Source>, String> map = new ConcurrentHashMap<>(8);
private ReferenceQueue<Source> queue = new ReferenceQueue<>();
public void putValue(Source source, String value) {
map.put(new WeakReference<>(source, queue), value);
}
public void print() {
System.out.println("mapSize:" + map.size());
clear();
for (Map.Entry<WeakReference<Source>, String> entry : map.entrySet()) {
System.out.println("element:" + entry.getKey().get());
}
}
public void clear() {
WeakReference<Source> ref;
while ((ref = (WeakReference<Source>) queue.poll()) != null) {
map.remove(ref);
}
}
}
测试代码:
package org.example.demo.weak;
import java.util.concurrent.TimeUnit;
public class WeakHashMapDemo {
private static final ValueHolder valueHolder = new ValueHolder();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
test("index" + i);
if (i % 10 == 0) {
System.gc();
TimeUnit.MILLISECONDS.sleep(30);
valueHolder.clear();
valueHolder.print();
}
}
}
private static void test(String id) {
Source source = new Source(id);
String value = "test";
valueHolder.putValue(source, value);
}
}
虽然很多人调侃,“面试造轮子,进去拧螺丝”然而当你真正面临复杂问题时,面试中常问的知识点还是非常重要的。扎实的专业基础能够帮助你快速寻找到解决问题的思路。 另外,解决问题的方法不止有一种,需要对比利弊综合分析,选择一个更适合的方案。 此外,现在人工智能的时代已经来临,大家可以尝试使用 AI 来寻找解决问题思路、甚至可以使用 AI 来帮我完成一些基础的代码。