如何在Query与Document均存活时保留Index,任一销毁则触发垃圾回收?
优化Query-Document-Index的内存回收策略
问题根源
你当前的方案中,Query和Document仅通过WeakReference指向Index,这导致Index没有任何强引用持有者——只要GC触发,哪怕Query和Document都存活,Index也可能被回收,因为GC只判断对象是否有强引用可达。这就是Index过早被回收的核心原因。
你的需求本质是:Index的存活周期必须严格绑定Query和Document的交集——仅当两者都存活时Index保留,任一被销毁时Index必须被回收。要实现这一点,需要给Index添加一个强引用持有者,且该持有者的存活依赖于Query和Document同时可达。
可行优化方案
方案一:基于Guava Cache的双层弱引用缓存
利用Guava Cache的内置弱引用机制,自动管理Index的生命周期,无需手动维护监听线程:
// 外层缓存:Key为Query(弱引用),Value为内层缓存 LoadingCache<Query, LoadingCache<Document, Index>> globalCache = CacheBuilder.newBuilder() .weakKeys() // 当Query被回收时,外层缓存自动移除对应的内层缓存条目 .concurrencyLevel(Runtime.getRuntime().availableProcessors()) .build(new CacheLoader<Query, LoadingCache<Document, Index>>() { @Override public LoadingCache<Document, Index> load(Query query) { // 内层缓存:Key为Document(弱引用),Value为Index return CacheBuilder.newBuilder() .weakKeys() // 当Document被回收时,内层缓存自动移除对应的Index条目 .build(new CacheLoader<Document, Index>() { @Override public Index load(Document document) { // 此处实现Index的重建逻辑 return buildIndex(query, document); } }); } }); // 获取Index的方法 public Index getIndex(Query query, Document document) throws ExecutionException { return globalCache.get(query).get(document); }
工作原理:
- 当Query和Document都存活时,外层缓存持有内层缓存的强引用,内层缓存持有Index的强引用,因此Index不会被GC回收。
- 当Query被回收,Guava的weakKeys机制会自动移除外层缓存中对应的条目,内层缓存失去强引用,随之被回收,Index也会被GC回收。
- 当Document被回收,内层缓存的weakKeys机制会自动移除对应的Index条目,Index失去强引用被回收。
- 当需要获取Index时,缓存会自动返回已存在的实例,不存在则触发重建,完全满足“尽量避免重建”的需求。
方案二:自定义ReferenceQueue+全局缓存(无第三方依赖)
如果不能引入Guava等第三方库,可以手动实现基于ReferenceQueue的监听机制:
定义关联键与监听逻辑:
// 全局线程安全缓存,存储(Query, Document)到Index的映射 private static final ConcurrentHashMap<Key, Index> INDEX_CACHE = new ConcurrentHashMap<>(); // 监听Query和Document回收的ReferenceQueue private static final ReferenceQueue<Object> REF_QUEUE = new ReferenceQueue<>(); // 自定义Key类,持有Query和Document的WeakReference private static class Key { private final WeakReference<Query> queryRef; private final WeakReference<Document> docRef; private final int hashCode; public Key(Query query, Document doc) { this.queryRef = new WeakReference<>(query, REF_QUEUE); this.docRef = new WeakReference<>(doc, REF_QUEUE); // 提前计算哈希值,避免对象被回收后无法计算 this.hashCode = Objects.hash(query, doc); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Key key = (Key) o; // 比较实际对象是否相等(若已被回收则不匹配) Query q1 = queryRef.get(); Query q2 = key.queryRef.get(); Document d1 = docRef.get(); Document d2 = key.docRef.get(); return Objects.equals(q1, q2) && Objects.equals(d1, d2); } @Override public int hashCode() { return hashCode; } } // 后台线程,监听ReferenceQueue,清理失效的Index static { Thread cleanupThread = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { try { // 等待收到回收通知 Reference<?> ref = REF_QUEUE.remove(); // 遍历缓存,移除所有关联该回收对象的Key INDEX_CACHE.entrySet().removeIf(entry -> { Key key = entry.getKey(); return key.queryRef.get() == null || key.docRef.get() == null; }); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }); cleanupThread.setDaemon(true); // 设置为守护线程,不影响应用退出 cleanupThread.start(); }获取Index的方法:
public Index getIndex(Query query, Document document) { Key key = new Key(query, document); // 尝试从缓存获取,不存在则重建并存入 return INDEX_CACHE.computeIfAbsent(key, k -> buildIndex(query, document)); }
工作原理:
- 每个Key持有Query和Document的WeakReference,并关联到ReferenceQueue。
- 当Query或Document被回收时,对应的WeakReference会被放入ReferenceQueue,后台线程收到通知后,清理缓存中所有包含已回收对象的Key,Index失去强引用后被GC回收。
- 当Query和Document都存活时,Key在缓存中有效,Index被强引用持有,不会被过早回收。
注意事项
- 自定义Key类时,必须提前计算哈希值,否则当Query/Document被回收后,无法通过
get()获取对象,导致哈希计算错误,缓存无法命中。 - 后台清理线程需设置为守护线程,避免阻止应用正常退出。
- 若使用Guava Cache,可根据业务需求添加
maximumSize等配置,防止缓存无限膨胀。
内容的提问来源于stack exchange,提问作者Michael Kay
相关产品推荐
相关产品推荐

