Java-189 Guava Cache 源码剖析:LocalCache、Segment 与 LoadingCache 工作原理全解析

TL;DR

  • 场景:线上项目广泛使用 Guava Cache,但对 LocalCache / Segment / LoadingCache 具体行为缺乏源码级认知。
  • 结论:Guava 通过 LocalCache+Segment 分段结构、引用队列和访问/写入队列,实现并发安全的本地缓存、过期与刷新策略。
  • 产出:一份围绕体系类图、put/get 流程与过期重载机制的工程化笔记,可直接支撑排查缓存不一致和过期异常问题。

版本矩阵

运行环境 已验证 说明
JDK 8 + Guava 31.1-jre 基本类结构、LocalCache/Segment/LoadingCache 行为与文中描述一致,示例代码可直接运行。
JDK 11 + Guava 32.1.x-jre API 与核心实现保持一致,主要差异集中在内部细节与警告抑制,不影响文中分析结论。
JDK 17 + Guava 33.x-jre 部分 整体结构延续 LocalCache 设计,局部实现有演进,建议排查问题时以对应版本源码为准。

原理分析

体系类图

Guava Cache的体系类图:

  • CacheBuilder:类,缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。CacheBuilder在 build方法中,会把前面设置的参数,全部传递给 LocalCache,它自己实际不参与任何计算
  • CacheLoader:抽象类,用于从数据源加载数据,定义 load、reload、loadAll 等操作。
  • Cache:接口,定义 get、put、invalidate 等操作,这里只有缓存增删改查的操作,没有数据加载的操作
  • LoadingCache:接口,继承自 Cache,定义 get、getUnchecked、getAll 等操作,这些操作基本上都会总数据源load数据
  • LocalCache:类,整个 Guava Cache 的核心类,包含了 Guava Cache 的数据结构以及基本的缓存的操作方法
  • LocalManualCache:LocalCache内部静态类,实现Cache接口,其内部的增删改查操作全部调用成员变量 LocalCache (LocalCache类型)的相应方法
  • LocalLoadingCache:LocalCache内部静态类,继承自 LocalManualCache类,实现 LoadingCache接口,其所有操作也是调用成员变量 LocalCache的相应方法

LocalCache

LoadingCache这些类表示获取Cache的方式,可以有多种方式,但是他们的方法最终调用到LocalCache的方法,LocalCache是Guava Cache的核心类。

java 复制代码
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>

LocalCache 为 Guava Cache 的核心类,LocalCache 的数据结构与 ConcurrentHashMap 很相似,都由多个 Segment 组成,且各 Segment 相互独立,互不影响,所以能够支持并行操作。

java 复制代码
// Map的数组
final Segment<K, V>[] segments;
// 并发量 即segments数组的大小
final int concurrencyLevel;
...
// 访问后的过期时间 设置了expireAfterAccess就有
final long expireAfterAccessNanos;
// 写入后的过期时间 设置了expireAfterWrite就有
final long expireAfterWriteNa就有nos;
// 刷新时间 设置了refreshAfterWrite就有
final long refreshNanos;
// removal的事件队列 缓存过期后先放到该队列
final Queue<RemovalNotification<K, V>> removalNotificationQueue;
// 设置的removalListener
final RemovalListener<K, V> removalListener;
...

每个 Segment 由一个Table和若干队列组成,缓存数据存储在Table中,其类型为 AtomicReferenceArray。

java 复制代码
static class Segment<K, V> extends ReentrantLock {
    /**
     * 所属的 LocalCache 实例。
     * Segment 通过其中的配置(过期策略、权重策略、等)来决定自身行为。
     */
    final LocalCache<K, V> map;

    /**
     * 当前 segment 中「逻辑上存活」的元素个数(未过期且未被移除)。
     */
    volatile int count;

    /**
     * 记录影响 table 结构的修改次数(结构性修改:插入、删除、rehash 等)。
     *
     * 作用:
     * 1)在 size()、containsValue() 等批量读取操作中,用于检测遍历期间是否发生结构变化,
     *    若遍历前后 modCount 不一致,则说明视图不再一致,需要重试。
     * 2)配合 count 一起使用,作为内存可见性与「弱一致快照」的辅助机制。
     *
     * 可以类比为「版本号」字段,用于感知 Segment 内部结构是否被并发修改。
     */
    int modCount;

    /**
     * 每个 Segment 自己的哈希表。
     * 使用 AtomicReferenceArray 保存 ReferenceEntry,依赖 CAS 实现无锁更新。
     *
     * 与 ConcurrentHashMap 不同:
     * - 这里存的是带引用语义的 Entry(ReferenceEntry),同时可能涉及弱引用/软引用。
     * - 直接用普通数组在并发场景下无法保证线程安全,因此采用 AtomicReferenceArray。
     */
    volatile AtomicReferenceArray<ReferenceEntry<K, V>> table;

    /**
     * 写入顺序队列:按「写入时间」排序的元素队列。
     * - 元素写入(新建或替换)时加入队尾。
     * - 主要用于基于写入时间的过期/淘汰策略。
     */
    @GuardedBy("Segment.this")
    final Queue<ReferenceEntry<K, V>> writeQueue;

    /**
     * 访问顺序队列:按「访问时间」排序的元素队列。
     * - 每次访问(包括读和写)会把元素移动到队尾。
     * - 主要用于 LRU / 基于访问时间的过期/淘汰策略。
     */
    @GuardedBy("Segment.this")
    final Queue<ReferenceEntry<K, V>> accessQueue;
}

interface ReferenceEntry<K, V> {
    /**
     * 返回该 Entry 当前持有的 ValueReference(可能是强引用、弱引用、软引用等包装)。
     */
    ValueReference<K, V> getValueReference();

    /**
     * 设置该 Entry 的 ValueReference。
     */
    void setValueReference(ValueReference<K, V> valueReference);

    /**
     * 返回哈希桶链表中的下一个 Entry。
     * 用于在同一个 bucket 内进行冲突链表遍历。
     */
    @Nullable
    ReferenceEntry<K, V> getNext();

    /**
     * 返回该 Entry 的哈希值(通常为 key 的扰动哈希)。
     */
    int getHash();

    /**
     * 返回该 Entry 的 key。
     */
    @Nullable
    K getKey();

    /*
     * 下面这些方法用于按访问顺序维护双向链表:
     * - 使用 accessTime 以及内部的 prev/next 指针,将 Entry 串成按访问时间排序的链表。
     * - 新访问的 Entry 会被移动到链表尾部,过期或被淘汰的 Entry 从链表头部移除。
     */

    /**
     * 返回该 Entry 最近一次被访问的时间(单位:纳秒)。
     */
    long getAccessTime();

    /**
     * 设置该 Entry 最近一次访问时间(单位:纳秒)。
     */
    void setAccessTime(long time);
}

CacheBuilder

缓存构建器,构建缓存的入口,指定缓存配置参数并初始化本地缓存。

主要采用builder的模式,CacheBuilder的每一个方法都返回这个 CacheBuilder。

注意build方法有重载,带有参数的为构建一个具有数据加载功能的缓存,不带参数的构建一个没有数据加载功能的缓存。

java 复制代码
LocalLoadingCache(
        CacheBuilder<? super K, ? super V> builder,
        CacheLoader<? super K, V> loader) {
    // LocalLoadingCache 作为「加载型缓存」的包装实现,内部委托一个 LocalCache 来完成实际存取逻辑
    super(new LocalCache<K, V>(builder, checkNotNull(loader)));
}

// 构造 LocalCache:真正持有数据、分段表和策略配置的核心实现
LocalCache(
        CacheBuilder<? super K, ? super V> builder,
        @Nullable CacheLoader<? super K, V> loader) {

    // Segment 并发级别,取 builder 配置与 MAX_SEGMENTS 的较小值(默认并发级别通常为 4)
    concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);

    // key 的引用强度(强引用/弱引用等),由 CacheBuilder 决定
    keyStrength = builder.getKeyStrength();

    // value 的引用强度(强引用/弱引用/软引用等)
    valueStrength = builder.getValueStrength();

    // key 的等价关系(比较器),用于哈希与相等性判断
    keyEquivalence = builder.getKeyEquivalence();

    // value 的等价关系(比较器),用于统计、比较等场景
    valueEquivalence = builder.getValueEquivalence();

    // 整个缓存允许的最大权重(可以是条目数,也可以是自定义权重总和)
    maxWeight = builder.getMaximumWeight();

    // 权重计算器:支持按自定义权重进行淘汰控制(非简单按条目数)
    weigher = builder.getWeigher();

    // 访问后过期时间(纳秒):读/写访问都会刷新该时间,用于「expireAfterAccess」策略
    expireAfterAccessNanos = builder.getExpireAfterAccessNanos();

    // 写入后过期时间(纳秒):仅写入操作刷新,用于「expireAfterWrite」策略
    expireAfterWriteNanos = builder.getExpireAfterWriteNanos();

    // 自动刷新时间(纳秒):超过该时间后,下次访问会触发异步/同步刷新
    refreshNanos = builder.getRefreshNanos();

    // 移除监听器:元素被显式删除、覆盖、过期、容量淘汰或 GC 回收(弱/软引用)的回调
    removalListener = builder.getRemovalListener();

    // 移除通知队列:若为 NullListener,则使用丢弃队列;否则用无界并发队列缓存通知
    removalNotificationQueue =
            (removalListener == NullListener.INSTANCE)
                    ? LocalCache.<RemovalNotification<K, V>>discardingQueue()
                    : new ConcurrentLinkedQueue<RemovalNotification<K, V>>();

    // 计时器:用于过期与统计,recordsTime() 决定是否需要真实时间来源
    ticker = builder.getTicker(recordsTime());

    // Entry 工厂:根据 key 引用强度以及是否使用访问/写入队列,选择具体 Entry 实现
    entryFactory = EntryFactory.getFactory(
            keyStrength,
            usesAccessEntries(),
            usesWriteEntries()
    );

    // 全局统计计数器:记录命中率、加载时间、淘汰次数等
    globalStatsCounter = builder.getStatsCounterSupplier().get();

    // 默认缓存加载器:用于在缺失时加载 value
    defaultLoader = loader;

    // 初始容量:取 builder 初始容量与最大容量上限的较小值
    int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);

    // 若按权重淘汰且使用默认 weigher(权重≈条目数),则再用 maxWeight 裁剪一次初始容量
    // 避免一开始就分配过大的 table
    if (evictsBySize() && !customWeigher()) {
        initialCapacity = Math.min(initialCapacity, (int) maxWeight);
    }
}

Put

这里进行 put 流程的分析:

● 上锁

● 清理队列元素:清理的是 keyReferenceQueue 和 valueReferenceQueue 两个队列,这两个队列是引用队列,如果发现key或者value被GC了,那么在put的时候触发清理

● setValue方法做的是将value写入到Entry

java 复制代码
V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 加锁,保证当前 Segment 内 put 操作的线程安全
    lock();
    try {
        // 当前时间戳(用于写入、过期判断等)
        long now = map.ticker.read();
        // 写入前的清理:处理过期、回收引用、队列清理等
        preWriteCleanup(now);
        ...
        // 当前 Segment 的哈希表
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        // 根据 hash 计算桶下标(length 为 2 的幂,通过位与加速取模)
        int index = hash & (table.length() - 1);
        // 桶中链表的头结点
        ReferenceEntry<K, V> first = table.get(index);

        // 在该桶的链表中查找是否已存在相同 key
        // Look for an existing entry.
        for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
            K entryKey = e.getKey();
            if (e.getHash() == hash
                && entryKey != null
                && map.keyEquivalence.equivalent(key, entryKey)) {
                // 找到已存在的 entry
                ValueReference<K, V> valueReference = e.getValueReference();
                // 取出旧 value
                V entryValue = valueReference.get();

                // 旧 value 为 null(可能已被 GC 回收,仅剩下 key/Entry 外壳)
                if (entryValue == null) {
                    ++modCount;
                    if (valueReference.isActive()) {
                        // 已处于激活状态但 value 被回收,发送回收通知,尽量缩短持锁时间
                        enqueueNotification(
                            key,
                            hash,
                            entryValue,
                            valueReference.getWeight(),
                            RemovalCause.COLLECTED
                        );
                        // 复用原有 Entry,写入新 value,并写入写队列/访问队列
                        setValue(e, key, value, now);
                        // 逻辑上是"覆盖",count 不变
                        newCount = this.count;
                    } else {
                        // 非激活引用,视作新插入:写入新 value,并加入写队列/访问队列
                        setValue(e, key, value, now);
                        // 元素数增加 1
                        newCount = this.count + 1;
                    }
                    // 写 volatile,保证 count 的内存可见性
                    this.count = newCount;
                    // 按权重/容量策略执行逐出
                    evictEntries(e);
                    return null;
                } else if (onlyIfAbsent) {
                    // 已存在指定 key,并且配置为 onlyIfAbsent:
                    // 本次只做"读 + 访问更新",不覆盖旧值,但需要更新访问队列
                    .......
                    setValue(e, key, value, now); // 存储数据,并写入写队列/访问队列(根据策略)
                    // 根据容量/权重淘汰数据
                    evictEntries(e);
                    return entryValue;
                }
            }
        }

        // 链表中不存在目标 key,新建 Entry
        // Create a new entry.
        ++modCount;
        ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);
        // 绑定 value,并写入写队列/访问队列
        setValue(newEntry, key, value, now);
        ......
    } finally {
        // 释放锁
        unlock();
        // 写入后的清理:处理通知回调、延迟删除等
        postWriteCleanup();
    }
}

Get

Get流程分析:

● 获取对象引用(引用可能是非alive的,比如是需要失效的,比如是loading的)

● 判断对象引用是否是alive的(如果entry是非法的、部分回收的、loading状态、需要失效的,则认为不是alive)

● 如果对象是alive的,如果设置 refresh,则异步刷新查询value,然后等待返回最新的value

● 针对不是alive的,但却是在loading的,等待loading完成(阻塞等待)

● 这里如果value还没有拿到,则查询loader方法获取对应的值(阻塞获取)

java 复制代码
// LoadingCache methods

// LocalCache 的对外入口:通过 LoadingCache 语义对外提供 get 能力,内部委派到 Segment
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
    int hash = hash(checkNotNull(key)); // 对 key 做 hash -> rehash
    return segmentFor(hash).get(key, hash, loader);
}

// loading
// 对指定 key 执行加载型读取:优先无锁读,读不到或失效时再进入加锁加载流程
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
    ....
    try {
        if (count != 0) {
            // 先做一次 volatile 读:
            // - 非 0 说明当前段中「很可能」已有元素
            // - 作为 volatile 读,可以刷新本段的可见性缓存
            // 若为 0,可直接跳过查找,走加载路径

            // 不使用 getLiveEntry,因为其中会忽略「正在加载中的值」
            ReferenceEntry<K, V> e = getEntry(key, hash);

            // 命中 Entry:说明 key 仍在表中(但值可能已过期)
            if (e != null) {
                long now = map.ticker.read();
                // 当前时间戳,用于结合过期策略判断 value 是否仍然"活着"

                V value = getLiveValue(e, now);
                // 懒失效:在每次 get 时才检查是否过期,过期则视为未命中并回退到加载路径
                ......
            }
        }

        // 运行到这里时,说明:
        // - 段中没有元素(count == 0),或者
        // - 未找到对应 Entry,或者
        // - 找到了但已过期
        // 统一进入加锁的加载路径
        // at this point e is either null or expired
        return lockedGetOrLoad(key, hash, loader);
    } catch (ExecutionException ee) {
        // 将底层异常按约定包装/转换后抛出
        Throwable cause = ee.getCause();
        if (cause instanceof Error) {
            throw new ExecutionError((Error) cause);
        } else if (cause instanceof RuntimeException) {
            throw new UncheckedExecutionException(cause);
        }
        throw ee;
    } finally {
        // 每次 get / put 后都做一次读后清理:
        // - 清理过期条目
        // - 回收无效引用
        // - 维护队列/统计的一致性
        postReadCleanup();
    }
}

过期重载

数据过期不会自动重载,而是通过get操作时执行过期重载,具体就是 CacheBuilder构造的LocalLoadingCache。

java 复制代码
/**
 * 支持自动加载(Loading)的本地缓存实现。
 *
 * 在 LocalManualCache 基础上增加了 CacheLoader:
 * - 手动 put/get 仍然可用;
 * - 未命中时可以通过 CacheLoader 自动加载。
 */
static class LocalLoadingCache<K, V> extends LocalManualCache<K, V>
        implements LoadingCache<K, V> {

    LocalLoadingCache(
            CacheBuilder<? super K, ? super V> builder,
            CacheLoader<? super K, V> loader) {
        // LocalCache 内部持有 CacheLoader,用于 getOrLoad / getAll 时的自动加载
        super(new LocalCache<K, V>(builder, checkNotNull(loader)));
    }

    // LoadingCache methods

    /**
     * 返回给定 key 对应的值;如果不存在则通过 CacheLoader 加载。
     *
     * 若加载过程中抛出异常,会被封装为 ExecutionException 抛出。
     */
    @Override
    public V get(K key) throws ExecutionException {
        return localCache.getOrLoad(key);
    }

    /**
     * 与 get(key) 类似,但不会声明受检异常:
     * - 若加载抛出异常,会被包装为 UncheckedExecutionException(运行时异常)。
     */
    @Override
    public V getUnchecked(K key) {
        try {
            return get(key);
        } catch (ExecutionException e) {
            throw new UncheckedExecutionException(e.getCause());
        }
    }

    /**
     * 批量加载:对一组 key 执行 get/自动加载,并返回不可变 Map。
     *
     * 具体加载策略由 LocalCache + CacheLoader 决定(可能合并批量加载)。
     */
    @Override
    public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
        return localCache.getAll(keys);
    }

    /**
     * 主动刷新指定 key:
     * - 通常会异步触发 CacheLoader 重新加载;
     * - 刷新期间旧值在一定时间窗口内仍可被读取。
     */
    @Override
    public void refresh(K key) {
        localCache.refresh(key);
    }

    /**
     * 使 LocalLoadingCache 可以作为 Function<K, V> 使用:
     * - 语义等价于 getUnchecked(key)。
     */
    @Override
    public final V apply(K key) {
        return getUnchecked(key);
    }

    // Serialization Support

    private static final long serialVersionUID = 1L;

    /**
     * 自定义序列化代理:
     * - 通过 LoadingSerializationProxy 进行序列化,
     *   避免直接序列化 LocalLoadingCache 的内部结构。
     */
    @Override
    Object writeReplace() {
        return new LoadingSerializationProxy<K, V>(localCache);
    }
}

错误速查

症状 根因定位 修复
缓存偶发读到"看起来已经过期"的旧数据 仅配置 expireAfterWrite/expireAfterAccess,依赖懒失效;无 refreshAfterWrite 或手动 refresh 打印命中/未命中与时间戳,结合 getLiveValue 行为分析;对热点 key 做压测复现
对一致性要求高的 key 配置 refreshAfterWrite 或显式 refresh;必要时在业务层加版本校验
size() 在高并发下明显跳动,无法当作"真实条目数"使用 LocalCache.Segment 的 count + modCount 设计为弱一致快照,非强一致计数 在压测/生产中循环打印 size(),与实际存取操作对比,观察抖动模式
避免用 size() 做容量/限流判断,仅用于监控;容量控制依赖 maximumSize / maximumWeight 配置
QPS 突增时偶发长尾请求,堆栈显示卡在 CacheLoader / get() CacheLoader 执行时间长,且多个线程对同一 key 发生串行等待(lockedGetOrLoad) 采样堆栈与加载时间,统计 loader 调用耗时分布;关注热点 key 的 get() 调用链
优化数据源查询或引入本地合并加载;对超时场景增加兜底;对热点 key 适当预热以减少同步加载
JVM 堆占用持续升高,怀疑 Guava Cache 导致"伪 OOM" 未设置 maximumSize/maximumWeight 或 weigher 配置不合理,访问/写入队列清理不及时 通过 heap dump 统计 LocalCache.Segment / ReferenceEntry 数量与引用链长度
明确设置容量与权重策略;结合业务 TTL 选择 expireAfterWrite/expireAfterAccess,并配合定期访问
以为 refreshAfterWrite 会"强制返回新值",实际仍返回旧值一段时间 误解 LocalLoadingCache.refresh 语义:刷新可异步执行,刷新期间旧值仍可能被读到 在 CacheLoader 和 refresh 路径打日志,观察同一 key 在刷新前后返回值与时间线
对强一致读路径不要依赖刷新语义;需要"强制新值"时显式 get+重载或引入版本号控制
代码阅读中误把 Segment.count / modCount 理解为"强一致版本号" 这两个字段仅用于辅助弱一致遍历与快速变化检测,不保证读操作期间绝对不变 对比 size()/containsValue 实现与 CHM 设计,理解"多次尝试+不一致即放弃"的策略
在设计依赖缓存的业务逻辑时只假定"最终一致",不要基于这些统计字段做强一致流程控制

其他系列

🚀 AI篇持续更新中(长期更新)

AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有部署 测试上手 架构研究 ,持续打造实用AI工具指南!
AI研究-132 Java 生态前沿 2025:Spring、Quarkus、GraalVM、CRaC 与云原生落地
🔗 AI模块直达链接

💻 Java篇持续更新中(长期更新)

Java-180 Java 接入 FastDFS:自编译客户端与 Maven/Spring Boot 实战

MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务已完结,Dubbo已完结,MySQL已完结,MongoDB已完结,Neo4j已完结,FastDFS 已完结,OSS正在更新... 深入浅出助你打牢基础!
🔗 Java模块直达链接

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!
大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解
🔗 大数据模块直达链接

相关推荐
程序员根根2 小时前
JavaSE 进阶:代理设计模式核心知识点(静态代理 + 动态代理 + 反射实现 + 实战案例)
java
踏浪无痕2 小时前
彻底搞懂微服务 TraceId 传递:ThreadLocal、TTL 与全链路日志追踪实战
后端·微服务·面试
程序员小假2 小时前
我们来说一说 Redis 主从复制的原理及作用
java·后端
木鹅.2 小时前
聊天记忆
java
海上彼尚2 小时前
Go之路 - 1.gomod指令
开发语言·后端·golang
我命由我123452 小时前
Java 开发使用 MyBatis PostgreSQL 问题:使用了特殊字符而没有正确转义
java·开发语言·数据库·postgresql·java-ee·mybatis·学习方法
总会落叶2 小时前
🧪 JUnit单元测试完全指南:从入门到企业级应用
后端
源码获取_wx:Fegn08952 小时前
基于springboot + vue图书商城系统
java·vue.js·spring boot·后端·spring·课程设计
未秃头的程序猿2 小时前
解决ShardingSphere分片算法在Devtools热重启后SpringUtil.getBean()空指针问题
java·后端