CaffeineCache 慎用weakKeys

前两天在一个Spring项目里使用了Caffine缓存,在application.yml中的配置如下:

  cache:
    type: CAFFEINE
    caffeine:
      spec: initialCapacity=1048576,maximumSize=1073741824,weakKeys,weakValues,expireAfterAccess=10m

为了避免缓存占用过多内存导致频繁GC,使用了weakKeys和weakValues选项。
不过测试时发现缓存不能命中,仍然会查询数据库。
通过debug发现,caffine使用WeakKeyReference将缓存的key做了封装。WeakKeyReference的结构如下:

static class WeakKeyReference 
    extends WeakReference implements InternalReference {
    
    private final int hashCode;
 
    public WeakKeyReference(@Nullable K key, @Nullable ReferenceQueue queue) {
      super(key, queue);
      hashCode = System.identityHashCode(key);
    }
 
    @Override
    public Object getKeyReference() {
      return this;
    }
 
    @Override
    public boolean equals(Object object) {
      return referenceEquals(object);
    }
 
    @Override
    public int hashCode() {
      return hashCode;
    }
  }

需要注意这里的equals()和hashCode()方法。
equals()调用的referenceEquals()方法是接口InternalReference的default方法,具体为:

    default boolean referenceEquals(@Nullable Object object) {
      if (object == this) {
        return true;
      } else if (object instanceof InternalReference) {
        InternalReference referent = (InternalReference) object;
        return (get() == referent.get());
      }
      return false;
    }

referenceEquals()方法中调用的get()方法在WeakKeyReference类中获取的是key的原始值。在方法中对两个key是否一致的判定使用的是

==


,而非是equals()。也就是说需要两个key指向同一个对象才能被认为是一致的。

hashCode()的实现也与equals()方法呼应。生成hashCode使用的是

System
.
identityHashCode
(
)


。identityHashCode方法是jre的一个native方法,这个方法的注释如下:

    /**
     * Returns the same hash code for the given object as
     * would be returned by the default method hashCode(),
     * whether or not the given object's class overrides
     * hashCode().
     * The hash code for the null reference is zero.
     */

注释说明这个方法对于指定的对象会返回相同的hashCode。即这个方法是针对对象进行操作的,比如两个字符串对象,即使其字符序列相同,通过identityHashCode方法生成的hashCode也不会相同。 看一个示例程序:

    public static void main(String[] args) throws IOException {
        System.out.println(System.identityHashCode(new String("zhyea")));
        System.out.println(System.identityHashCode(new String("zhyea")));
    }

示例程序输出了相通字符序列“zhyea”的两个字符串对象的identityHashCode执行结果,结果为:
可以看到最终结果是不同的。
到现在缓存不能命中的原因应该是找到了:因为使用了weakKeys选项,caffine使用WeakKeyReference封装了缓存key,导致相同字符序列的不同String对象的key被视为是不同的缓存主键。
果然在去掉weakKeys和weakValues配置项后,测试发现缓存能够命中了。

后来在 Caffeine的文档
中找到了如下说明:



Caffeine
.
weakKeys
(
)


stores keys using weak references. This allows entries to be garbage-collected if there are no other strong references to the keys. Since garbage collection depends only on identity equality, this causes the whole cache to use identity (==) equality to compare keys, instead of

equals
(
)


.
文档中提到因为GC的限制,需要对weakKey使用“==”替换equals()。
原因算是找到了,不过回过头来想想,在Spring中Caffeine的weakKeys选项确实有些鸡肋:Spring的CacheKey生成方式导致weakKey必然指向不同的对象,结果就是缓存注定不能命中,并且每次调用都会在缓存中插入一条新的记录。这样尽管使用weakKey不会造成内存泄漏,可是也会增加GC负担。因此在SpringBoot中使用Caffeine时需要慎用weakKeys。