You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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的监听机制:

  1. 定义关联键与监听逻辑:

    // 全局线程安全缓存,存储(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();
    }
    
  2. 获取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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 03:45:54