避坑指南
如果你看懂了上文的bug原理,相信你已经知道了如何闭坑,如果没看懂也没关系, 一句话 不要使用jdk11+zgc的同时频繁使用StackWalker(比如错误使用log4j)。当然也不是完全不能使用log4j了,只要不是频繁调用StackWalker就没问题,像我们代码中的logger只需要声明成static,这样StackWalker只会在类初始化的时候调用,就不会有问题了。知道了原理,也就能解释清楚为什么我们很多其他应用用了jdk11也用了有问题的RedisClient没有出现cpu异常的现象,就是因为其他应用没有启用zgc。
当然这个bug的本质就是jdk11+zgc+StackWalker的bug,三者都是bug触发的必要条件,如果你能避免其中一条就可以完美避开这个bug了,比如升级到jdk12+,比如不用zgc……
Bugfix
对于我们应用来说,只需按照上面的避坑指南操作即可,但对于jdk团队来说,这个bug他们肯定是要修复的。
从官网bug页面可以看到这个bug在jdk13中已经修复了,我们来看看他们是如何修复的。是不是只需要在zgc合适的地方调一下SymbolTable::unlink()就行了?是的,但jdk团队做的远不止于此,除了unlink之外,他们还优化了ResolvedMethodTable的实现,支持了动态扩缩容,可以避免单链表过长的问题,具体可以看下jdk源码中src/hotspot/share/prims/resolvedMethodTable.cpp的文件。
void ResolvedMethodTable::do_concurrent_work(JavaThread* jt) { _has_work = false; double load_factor = get_load_factor(); log_debug(membername, table)("Concurrent work, live factor: %g", load_factor); // 人工load_factor大于2,并且没有达到最大限制,就执行bucket扩容,并且移除无用的entry if (load_factor > PREF_AVG_LIST_LEN && !_local_table->is_max_size_reached()) { grow(jt); } else { clean_dead_entries(jt); } } void ResolvedMethodTable::grow(JavaThread* jt) { ResolvedMethodTableHash::GrowTask gt(_local_table); if (!gt.prepare(jt)) { return; } log_trace(membername, table)("Started to grow"); { TraceTime timer("Grow", TRACETIME_LOG(Debug, membername, table, perf)); while (gt.do_task(jt)) { gt.pause(jt); { ThreadBlockInVM tbivm(jt); } gt.cont(jt); } } gt.done(jt); _current_size = table_size(); log_info(membername, table)("Grown to size:" SIZE_FORMAT, _current_size); }
总结
这个bug触发的主要原因其实还是我们自己的代码写的不够规范(logger未声明为static),而这个不规范其实也对其他没有触发这个bug的应用也产生了影响,毕竟生成logger也是会消耗性能的,我们代码fix后其他应用升级,有些服务CPU占用率降低5%+。这也充分说明代码质量的重要性,尤其是那种被广泛采用的基础代码。
另外是不是有些人还有个疑问,这个bug为什么不在jdk11后续版本中修掉,而是选择在jdk13中彻底修掉,不怕影响到使用jdk11的用户吗?对这个问题我有个想法,其实这个bug并不是很容易触发的严重bug(jdk11+zgc+log4j的频繁调用),而且即便是触发了,jdk的使用者也很容易通过修改自己的代码来规避这个bug,所以对jdk的开发者而言这不是一个重要且紧急的bug,后续修复掉就行了。
参考资料
阿里巴巴开源java问题排查工具 Arthas.
如何读懂火焰图? 阮一峰
jdk开发者关于bug讨论的邮件列表