有业务反馈, 线上一个应用运行了一段时间之后, 在高峰期之后, 突然发现处理能力下降, 接口的响应时间变长, 但是看 Cat 上的 GC 数据, 一切都很正常.
通过跳板机上机器查看日志, 发现一段平时很少见到的日志:
其中 CodeCache is full, 说明 Code Cache 已经满了, 导致 Compiler 失效, 这是为什么?
首先, 我们得了解什么是 Code Cache.
什么是 Code Cache
Java 代码在执行次数达到一个阈值会触发 JIT 编译, 一旦代码块被编译成本地机器码, 下次执行的时候会直接运行编译后的本地机器码. 所以这本地机器码必须被缓存起来, 而缓存这个本地机器码的内存区域就是 Code Cache, 它并不属于 Java 堆的一部分, 除了 JIT 编译的代码之外, Java 所使用的本地方法代码 (JNI) 也会存在 codeCache 中.
Code Cache 调优
由于 Code Cache 是一块内存区域, 那么肯定有大小的限制, 但是不同版本的 JVM, 不同的启动方式, Code Cache 的默认大小也不同, 可通过 jinfo-flagReservedCodeCacheSize 进行查看.
服务启动之后, 随着时间的推移, 肯定会有越来越多的方法被 JIT 编译成本地机器码, 并存放到 Code Cache, 由于 Code Cache 大小是固定的, 那么就存在被用完的风险.
一旦 Code Cache 被填满, 就会出现下面情况:
. JVM 的 JIT 功能会被停止, 将不会编译任何额外的代码.
. 被编译过的代码仍然以编译方式执行, 但是尚未被编译的代码只能以解释方式执行了.
这种情况下, 如果应用中还有很多代码以解释方式执行, 其性能会大大降低. 为了避免这种情况, 就需要对 Code Cache 比较深入的理解.
JVM 启动的时候, Code Cache 所需内存会被单独初始化, 这时候 Java 堆还会被初始化, 所以 Code Cache 和 Java 堆是两块独立内存区域.
在 codeCache.cpp 的 CodeCache::initialize()方法中, 实现了 Code Cache 的初始化
Code Cache 包含了 3 种数据:
- . NonNMethodCode
- . ProfiledCode
- . NonProfiledCode
通过 SegmentedCodeCache 参数可以选择按照整体初始化, 还是分段初始化.
通过 - XX:ReservedCodeCacheSize 参数可以指定 Code Cache 的初始化大小, 这个默认值在不同的 JDK 版本也不同, 目前我这边调试的是 OpenJDK11, 默认大小是 240M, 这个已经够用了.
可以看下其它版本的默认大小:
对于那些只有 32M,48M 的就可能存在 Code Cache 不足的隐患, 增加 ReservedCodeCacheSize 可以是一个解决方案, 但这通常只是一个临时的解决方案.
幸运的是, JVM 提供了一种比较激进的 codeCache 回收方式: Speculative flushing.
在 JDK1.7.0_4 之后这种回收方式默认开启, 而之前的版本需要通过一个参数来开启:-XX:+UseCodeCacheFlushing
在 Speculative flushing 开启的情况下, 当 Code Cache 不足时:
. 最早被编译的一半方法将会被放到一个 old 列表中等待回收;
. 在一定时间间隔内, 如果 old 列表中方法没有被调用, 这个方法就会被从 Code Cache 清除;
很不幸的是, 在 JDK1.7 中, Speculative flushing 释放了一部分空间, 但是从编译日志来看, JIT 并没有恢复正常, 并且系统整体性能下降很多, 出现了大量超时.
在 Oracle 官网上, 有这样一个 Bug:http://bugs.java.com/bugdatabase/viewbug.do?bugid=8006952
由于算法问题, 当 Code Cache 不足之后会导致编译线程无法继续, 并且消耗大量 CPU, 导致系统运行变慢.
这个 bug 在 7u101 及 8 以后的版本已经得到修复.
来源: http://www.jianshu.com/p/f9bf09e6ffe6