0 問題描述
一個應用在運行一段時間后,隨著訪問量不斷增加,突然處理能力下降。但是從流量,jstack,gc上看基本正常。感覺好像突然從 “健康狀態” 進入了 “虛弱狀態”。
1 排查問題
-
在JVM日志里,可以發現如下log:
1234Java 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已經滿了。而且導致這個時候JIT就會停止,JIT一旦停止,就不會再起來了,可以想象一下,如果很多代碼沒有辦法去JIT的話,性能就會比較差。
-
使用如下命令檢查一下Code Cache的值:
1jinfo -flag ReservedCodeCacheSize
2 解決問題
-
一個可行的方法,就是擴大Code Cache空間:
使用 -XX:ReservedCodeCacheSize= 指定一個更大的空間,來支持更多的JIT編譯;
-
此外,另一個可行的方法,啟用Code Cache的回收機制:
通過在啟動參數上增加:-XX:+UseCodeCacheFlushing 來啟用;
打開這個選項,在JIT被關閉之前,也就是CodeCache裝滿之前,會在JIT關閉前做一次清理,刪除一些CodeCache的代碼;
如果清理后還是沒有空間,那么JIT依然會關閉。這個選項默認是關閉的;
3 背景知識
3.1 JIT即時編譯
在Java中提到“編譯”,自然很容易想到 javac 編譯器將.java文件編譯成為.class文件的過程,這里的 javac 編譯器稱為前端編譯器,其他的前端編譯器還有諸如Eclipse,JDT中的增量式編譯器ECJ等。相對應的還有 后端編譯器,它在程序運行期間將字節碼轉變成機器碼(現在的Java程序在運行時基本都是 解釋執行加編譯執行),如HotSpot虛擬機自帶的JIT(Just In Time Compiler)編譯器(分Client端和Server端)。
Java程序最初是僅僅通過解釋器解釋執行的,即對字節碼逐條解釋執行,這種方式的執行速度相對會比較慢,尤其當某個方法或代碼塊運行的特別頻繁時,這種方式的執行效率就顯得很低。于是后來 在虛擬機中引入了JIT編譯器(即時編譯器),當虛擬機發現某個方法或代碼塊運行特別頻繁時,達到某個閾值,就會把這些代碼認定為“Hot Spot Code”(熱點代碼),為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,并進行各層次的優化,完成這項任務的正是JIT編譯器。
現在主流的商用虛擬機(如:Sun HotSpot、IBM J9)中幾乎 都同時包含解釋器和編譯器,三大商用虛擬機之一的JRockit是個例外,它內部沒有解釋器,因此會有啟動相應時間長之類的缺點,但它主要是面向服務端的應用,這類應用一般不會重點關注啟動時間。
解釋器與編輯器二者各有優勢:
- 當程序需要迅速啟動和執行時,解釋器可以首先發揮作用,省去編譯的時間,立即執行;
- 當程序運行后,隨著時間的推移,編譯器逐漸會發揮作用,把越來越多的代碼編譯成本地代碼后,可以獲取更高的執行效率;
- 解釋執行可以節約內存,而編譯執行可以提升效率;
運行過程中會被即時編譯器編譯的“熱點代碼”有兩類:
- 被多次調用的方法;
- 被多次調用的循環體;
3.2 Code Cache
Java代碼在執行時一旦被編譯器編譯為機器碼,下一次執行的時候就會直接執行編譯后的代碼,也就是說,編譯后的代碼被緩存了起來。緩存編譯后的機器碼的內存區域就是codeCache。這是一塊獨立于Java堆之外的內存區域。除了JIT編譯的代碼之外,Java所使用的本地方法代碼(JNI)也會存在codeCache中。

Code Cache是JVM用于存儲經過JIT C1/C2編譯優化后的代碼。因為是存在內存中的,所以肯定得限制大小,Code Cache的最大大小可通過 jinfo -flag ReservedCodeCacheSize 來獲取,通常在64 bit機器上默認是48m。
不同版本的JVM、不同的啟動方式codeCache的默認大小也不同:
JVM 版本和啟動方式 | 默認 codeCache大小 |
---|---|
32-bit client, Java 8 | 32 MB |
32-bit server, Java 8 | 48M |
32-bit server with Tiered Compilation, Java 8 | 240 MB |
64-bit server, Java 8 | 48M |
64-bit server with Tiered Compilation, Java 8 | 240 MB |
32-bit client, Java 7 | 32 MB |
32-bit server, Java 7 | 48 MB |
32-bit server with Tiered Compilation, Java 7 | 96 MB |
64-bit server, Java 7 | 48 MB |
64-bit server with Tiered Compilation, Java 7 | 96 MB |
3.3 分層編譯
JVM提供了一個參數-Xcomp,可以使JVM運行在純編譯模式下,所有方法在第一次被調用的時候就會被編譯成機器代碼。加上這個參數之后,隨之而來的問題是啟動時間變得很長,差不多是原來的2倍還多。
除了純編譯方式和默認的mixed之外,從JDK6u25開始引入了一種分層編譯的方式。
Hotspot JVM內置了2種編譯器,分別是 client方式啟動時用的C1編譯器 和 server方式啟動時用的C2編譯器 。
C2編譯器在將代碼編譯成機器碼之前,需要收集大量的統計信息以便在編譯的時候做優化,因此編譯后的代碼執行效率也高,代價是程序啟動速度慢,并且需要比較長的執行時間才能達到最高性能;
C1編譯器的目標在于使程序盡快進入編譯執行階段,因此編譯前需要收集的統計信息比C2少很多,編譯速度也快不少。代價是編譯出的目標代碼比C2編譯的執行效率要低。
盡管如此,C1編譯的執行效率也比解釋執行有巨大的優勢。分層編譯方式是一種折衷方式,在系統啟動之初執行頻率比較高的代碼將先被C1編譯器編譯,以便盡快進入編譯執行。隨著時間推進,一些執行頻率高的代碼會被C2編譯器再次編譯,從而達到更高的性能。
通過以下JVM參數開啟分層編譯模式:
1
|
-XX:+TieredCompilation |
在JDK8中,當以server模式啟動時,分層編譯默認開啟。需要注意的是,分層編譯方式只能用于server模式中,如果需要關閉分層編譯,需要加上啟動參數 -XX:-TieredCompilation;如果以client模式啟動,-XX:+TieredCompilation 參數將會被忽略。
3.4 Code Cache 滿了怎么辦
隨著時間推移,會有越來越多的方法被編譯,codeCache使用量會逐漸增加,直至耗盡。當Code Cache用滿了后,會打印下面的日志:
在JDK1.7.0_4之前,你會在jvm的日志里看到這樣的輸出:
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?
JIT編譯器被停止了,并且不會被重新啟動,此時會回歸到解釋執行;
被編譯過的代碼仍然以編譯方式執行,但是尚未被編譯的代碼就 只能以解釋方式執行了。
針對這種情況,JVM提供了一種比較激進的codeCache回收方式:Speculative flushing。
在JDK1.7.0_4之后這種回收方式默認開啟,而之前的版本需要通過一個啟動參數來開啟:-XX:+UseCodeCacheFlushing。
在Speculative flushing開啟的情況下,當codeCache將要耗盡時:
最早被編譯的一半方法將會被放到一個old列表中等待回收;
在一定時間間隔內,如果old列表中方法沒有被調用,這個方法就會被從codeCache充清除;
很不幸的是,在JDK1.7中,當codeCache耗盡時,Speculative flushing釋放了一部分空間,但是從編譯日志來看,JIT編譯并沒有恢復正常,并且系統整體性能下降很多,出現大量超時。
在Oracle官網上看到這樣一個Bug:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8006952 由于codeCache回收算法的問題,當codeCache滿了之后會導致編譯線程無法繼續,并且消耗大量CPU導致系統運行變慢。Bug里影響版本是JDK8,但是從網上其他地方的信息看,JDK7應該也存在相同的問題,并且沒有被修復。
所以,目前來看,開啟UseCodeCacheFlushing會導致問題,如下:
- Code Cache滿了時緊急進行清掃工作,它會丟棄一半老的編譯代碼;
- Code Cache空間降了一半,方法編譯工作仍然可能不會重啟;
- flushing可能導致高的cpu使用,從而影響性能下降;
3.5 Code Cache 調優
以client模式或者是分層編譯模式運行的應用,由于需要編譯的類更多(C1編譯器編譯閾值低,更容易達到編譯標準),所以更容易耗盡codeCache。當發現codeCache有不夠用的跡象(通過上一節提到的監控方式)時,可以通過啟動參數來調整codeCache的大小。
1
|
-XX:ReservedCodeCacheSize=256M |
那具體應該設置為多大合適,根據監控數據估算,例如單位時間增長量、系統最長連續運行時間等。如果沒有相關統計數據,一種推薦的設置思路是設置為當前值(或者默認值)的2倍。
需要注意的是,這個codeCache的值不是越大越好。對于32位JVM,能夠使用的最大內存空間為4g。這個4g的內存空間不僅包括了java堆內存,還包括JVM本身占用的內存、程序中使用的native內存(比如directBuffer)以及codeCache。如果將codeCache設置的過大,即使沒有用到那么多,JVM也會為其保留這些內存空間,導致應用本身可以使用的內存減少。對于64位JVM,由于內存空間足夠大,codeCache設置的過大不會對應用產生明顯影響。
在JDK 8中,提供了一個啟動參數 -XX:+PrintCodeCache 在JVM停止的時候打印出codeCache的使用情況。其中max_used就是在整個運行過程中codeCache的最大使用量。可以通過這個值來設置一個合理的codeCache大小,在保證應用正常運行的情況下減少內存使用。
3.6 問題解決
問題的前因后果都弄清楚了,也就好解決了。上面提到過純編譯方式和分層編譯方式都可以解決或緩解啟動后負載過高的問題,那么我們就有2種選擇:
采用分層編譯方式,并修改codeCache的大小為256M;
采用純編譯方式,并修改codeCache的大小為256M;
經過一段時間運行發現,在啟動后負載控制方面,純編譯方式要好一些,啟動之后負載幾乎不上升,而 分層編譯方式啟動后負載會有所上升,但是不會很高,也會在較短時間內降下來。但是啟動時間方面,分層編譯比原來的默認啟動方式縮短了大概10秒(原來啟動需要110-130秒),而純編譯方式啟動時間比原來多了一倍,達到了250秒甚至更高。所以看起來分層編譯方式是更好的選擇。
然而JDK 7在codeCache的回收方面做的很不好。即使我們將codeCache設置為256M,線上還是輕易達到了設置的報警閾值200M。而且一旦codeCache滿了之后又會導致系統運行變慢的問題。所以我們的目標指向了JDK 8。
測試表明,JDK 8對codeCache的回收有了很明顯的改善。不僅codeCache的增長比較平緩,而且當使用量達到75%時,回收力度明顯加大,codeCache使用量在這個值上下浮動,并緩慢增長。最重要的是,JIT編譯還在正常執行,系統運行速度也沒有收到影響。
3.7 運行時查看Code Cache
如果想在運行時查看code cache的大小,需要寫段代碼,目前只能通過JMX來獲取到Code Cache區域的使用狀況,代碼類似如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
import java.io.File; import javax.management.MBeanServerConnection; import javax.management.ObjectName; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import com.sun.tools.attach.VirtualMachine; public class CodeCacheUsage { private static final String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress" ; public static void main(String[] args) throws Exception { if (args.length != 1 ) { System.err.println( "Must enter one arg: pid" ); System.exit( 0 ); } VirtualMachine vm = VirtualMachine.attach(args[ 0 ]); JMXConnector connector = null ; try { String connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS); if (connectorAddress == null ) { String agent = vm.getSystemProperties().getProperty( "java.home" ) + File.separator + "lib" + File.separator + "management-agent.jar" ; vm.loadAgent(agent); connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS); } JMXServiceURL url = new JMXServiceURL(connectorAddress); connector = JMXConnectorFactory.connect(url); MBeanServerConnection mbeanConn = connector.getMBeanServerConnection(); ObjectName name = new ObjectName( "java.lang:type=MemoryPool,name=Code Cache" ); System.out.println(mbeanConn.getAttribute(name, "Usage" )); } finally { if (connector != null ) connector.close(); vm.detach(); } } } |
傳入pid,執行上面的代碼后,會輸出類似下面的信息:
javax.management.openmbean.CompositeDataSupport(compositeType=javax.management.openmbean.CompositeType(name=java.lang.management.MemoryUsage,items=
(
(itemName=committed,itemType=javax.management.openmbean.SimpleType(name=java.lang.Long)),
(itemName=init,itemType=javax.management.openmbean.SimpleType(name=java.lang.Long)),
(itemName=max,itemType=javax.management.openmbean.SimpleType(name=java.lang.Long)),
(itemName=used,itemType=javax.management.openmbean.SimpleType(name=java.lang.Long))
)),
contents={committed=50331648, init=2555904, max=50331648, used=48281152})
上面的信息顯示Code Cache區域初始化的時候為2555904,最大為50331648,已占用了50331648,使用了48281152。
到此這篇關于Java Code Cache滿導致應用性能降低問題解決的文章就介紹到這了,更多相關Java Code Cache滿導致應用性能降低內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://juejin.cn/post/6844903601786060808#heading-8