有業務反饋,線上一個應用運行了一段時間之后,在高峰期之后,突然發現處理能力下降,接口的響應時間變長,但是看Cat上的GC數據,一切都很正常。
通過跳板機上機器查看日志,發現一段平時很少見到的日志:
Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled.
Java HotSpot(TM) 64-Bit Server VM warning: Try increasing the code cache size using -XX:ReservedCodeCacheSize=.
...
“CompilerThread0” java.lang.OutOfMemoryError: requested 2854248 bytes for Chunk::new. Out of swap space?
其中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 -flag ReservedCodeCacheSize
進行查看。
服務啟動之后,隨著時間的推移,肯定會有越來越多的方法被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/view_bug.do?bug_id=8006952 由于算法問題,當Code Cache不足之后會導致編譯線程無法繼續,并且消耗大量CPU,導致系統運行變慢。