ehcache的clear()方法使用不当引起的gc

1. 问题:

线上的一个服务需要做缓存,并且每隔 10s 刷新一次缓存,使用了 Ehcache 框架。
为了避免缓存的并发读写问题(仔细研究 ehcache 就会发现这并不是问题),当时设计了两个 Cache 对象轮流使用的方式,即 使用(读取)缓存 A 的过程中刷新缓存 B,时间到之后使用缓存 B,使用缓存B的过程中再去刷新缓存 A,循环往复。
上线一段时间后,服务在峰值期间会在某个时间点开始 young GC 变得非常频繁,老年代大小快速增长随后引发多次 mixed GC(使用的是G1),服务峰值过后会自愈,但服务峰值期间重启无效。

2. 问题分析

使用 jmap 得到 mixed gc 发生前后的直方图,发现 mixed gc 时有大量的
org.ehcache.impl.internal.concurrent.ConcurrentHashMap$Node
,即问题出现 ehcache 缓存上。

因为是频繁的young gc 然后引发多次的 mixed gc 并且 mixed gc 能回收大量堆内存,所以肯定是因为某种原因持续不断的产生了大量对象,并且这种对象经过多次 young gc 仍然存活然后进入了老年代。


首先想到的是服务峰值期间 young gc 太频繁,导致 10s 缓存期间缓存对象的 gc 年龄达到了最大值,进入了老年代。那么观察两个指标:

  1. young gc 频率
  2. jvm 设置的老年代晋升年龄

对应如下:

  1. 10s刷新一次缓存,AB轮流,所以缓存最长生存 20s(不可能达到20s),发生问题时服务平均 30s 进行一次 young gc
  2. 使用的是默认晋升年龄15(实际上晋升年龄是动态调整的,但是这里不影响)

所以不可能通过正常的 young gc 产生这么多晋升老年代的 缓存对象。


运维通过压测得出结论,当缓存数量超过19.8万时才会出现这个问题。说明这些被缓存的Node对象正常是能够被 young GC 回收掉的,并没有进入老年代。那么为什么数据到达 19.8 万之后这些对象就没有被回收掉呢?


仔细看 gc 日志,偶然注意到发生 mixed gc之前开始出现多次:
[GC pause (G1 Humongous Allocation)
意味着发生了大对象直接分配在老年代。


在G1中,如果一个对象的大小超过分区大小的一半,该对象就被定义为大对象(Humongous Object)。大对象时直接分配到老年代分区,分配之后也不会被移动。
如果缓存作为大对象分配在老年代,那么缓存的Node因为被缓存集合对象引用,也无法回收,最终进入老年代?
G1的分区大小对照表:

最小堆大小 分区的大小
heap < 4GB 1MB
4GB <= heap < 8GB 2MB
8GB <= heap < 16GB 4MB
16GB <= heap < 32GB 8MB
32GB <= heap < 64GB 16MB
64GB <= heap 32MB

我们的服务是8G的堆,所以大于 2MB 就是大对象。
我们来算一下存放 19.8w 个 Node 的 ConcurrentHashMap 应该是多大:

HashMap 中 Node 数组大小应该是 2 的 n 次方,并且算上承载因子 0.75 后应该大于19.8万,最后计算得到应该是 262144 个。
4 byte * 262144 = 1M (HashMap保存的是Node的引用,引用压缩之后是 4 byte,压测数据还是很靠谱的)。
如果大于19.8w个,HashMap需要翻倍扩容,就大于 2M 了,这时候就是个大对象了。


刚要兴奋,找到了问题,突然一想又不对。虽然大对象 HashMap 在老年代,但是这些Node 只是在 HashMap 中有个引用,Node 本体还是在年轻代,10s后就作为垃圾回收了,并不会进入老年代。
这时候我们的任务就变成了寻找为什么Node会进入老年代。这是一个很曲折的过程,怎么发现的已经回想不清,这里只能给出结果。

3. ehcache的clear()方法的特殊之处

首先我们来看一段代码。java.util.ConcurrentHashMap 有一个 clear() 方法,用于清楚当前所持有的所有 key-value 数据:

public void clear() {
    long delta = 0L; 
    int i = 0;
    Node<K,V>[] tab = table;
    while (tab != null && i < tab.length) {
        int fh;
        Node<K,V> f = tabAt(tab, i);
        if (f == null)
            ++i;
        else if ((fh = f.hash) == MOVED) {
            tab = helpTransfer(tab, f);
            i = 0; // restart
        }
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> p = (fh >= 0 ? f :
                                   (f instanceof TreeBin) ?
                                   ((TreeBin<K,V>)f).first : null);
                    while (p != null) {
                        --delta;
                        p = p.next;
                    }
                    setTabAt(tab, i++, null);
                }
            }
        }
    }
    if (delta != 0L) {
        addCount(delta, -1);
    }
}

即使你看不懂这段代码也没关系,你应该能看出来这段代码在把当前 map 中的数据去掉。这个操作很符合我们对于 clear() 的认知,事实上 jdk 中集合类的 clear() 方法也基本是这个效果。
我们仔细去debug ehcache 的 clear() 方法(一定要debug,不然接口有多个实现类根本不知道是哪个):
Ehcache.clear() -> EhcacheBase.clear() -> OnHeapStore.clear() -> SimpleBackend.clear()
SimpleBackend.clear() 就是最终操作,它做了什么呢?

    public void clear() {
        // 如果你去下载源码,就可以看到下面的注释,"这比清理map快"
        // This is faster than performing a clear on the underlying map
        this.realMap = (EvictingConcurrentMap)this.realMapSupplier.get();
    }

每次 clear() 都是创建了一个新的 EvictingConcurrentMap 对象,让 SimpleBackend 的属性 realMap 指向新对象,即使用这个新对象来存储缓存数据。旧对象呢?旧对象不再被引用,变成垃圾。如果频繁使用 clear() 方法,就会产生大量的等待回收的 EvictingConcurrentMap 对象。
在我们这里,这些 clear() 后产生的 HashMap 都是大对象,那么发生 mixed gc 之前就不会被回收,被引用的Node也就不能被回收,从而经过多次 young gc 之后进入老年代,导致老年代迅速增长:

  1. 每10s增加一个HashMap对象2M
  2. HashMap的key-value分别为 Long 和 CopiedOnHeapValueHolder,所以持有的对象数组是 Node<Long, CopiedOnHeapValueHolder<Object>>[], 其中 Object 就是缓存的数据,一个Node 大概 136 byte,20w 个就是 27M
    。定时任务每10s一次,每次29M,半小时 27M * 6 * 30 = 5.22G,不到半小时就要产生一次 mixed gc。

4. clear() 方法创建 EvictingConcurrentMap 对象的解释

这里使用了函数式接口,如果不了解函数式接口可能会看不懂为什么 clear() 方法会新建一个 EvictingConcurrentMap 对象。
realMapSupplier 是 OnHeapStore 类的属性,它的类型是 Supplier<EvictingConcurrentMap<K, OnHeapValueHolder<V>>>, 而 Supplier 是一个带泛型的函数式接口:

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

任意一个类如果实现了 get() 方法,就是实现了这个函数式接口,而 java 类的构造函数恰好可以算作实现了 get() 方法。
realMapSupplier 的初始化在 OnHeapStore 的构造函数中,OnHeapStore 的构造函数的调用在自己的子类 Provider 中:

OnHeapStore<K, V> onHeapStore = new OnHeapStore(storeConfig, timeSource, keyCopier, valueCopier, >sizeOfEngine, eventDispatcher, ConcurrentHashMap::new);

所以 realMapSupplier 的实现就是 ConcurrentHashMap::new 即 ConcurrentHashMap 的构造函数, 每次调用 realMapSupplier.get() 就会得到一个 ConcurrentHashMap 对象。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容