Code Cache滿致使應用性能下降

0 問題描述

一個應用在運行一段時間後,隨着訪問量不斷增長,忽然處理能力降低。可是從流量,jstack,gc上看基本正常。感受好像忽然從 「健康狀態」 進入了 「虛弱狀態」前端

1 排查問題

  1. 在JVM日誌裏,能夠發現以下log:java

    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已經滿了。並且致使這個時候JIT就會中止,JIT一旦中止,就不會再起來了,能夠想象一下,若是不少代碼沒有辦法去JIT的話,性能就會比較差算法

  2. 使用以下命令檢查一下Code Cache的值:後端

    jinfo -flag ReservedCodeCacheSize
    複製代碼

2 解決問題

  1. 一個可行的方法,就是擴大Code Cache空間:緩存

    使用 -XX:ReservedCodeCacheSize= 指定一個更大的空間,來支持更多的JIT編譯;bash

  2. 此外,另外一個可行的方法,啓用Code Cache的回收機制:jvm

    經過在啓動參數上增長:-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是個例外,它內部沒有解釋器,所以會有啓動相應時間長之類的缺點,但它主要是面向服務端的應用,這類應用通常不會重點關注啓動時間。

解釋器與編輯器兩者各有優點:

  1. 當程序須要迅速啓動和執行時,解釋器能夠首先發揮做用,省去編譯的時間,當即執行;
  2. 當程序運行後,隨着時間的推移,編譯器逐漸會發揮做用,把愈來愈多的代碼編譯成本地代碼後,能夠獲取更高的執行效率;
  3. 解釋執行能夠節約內存,而編譯執行能夠提高效率

運行過程當中會被即時編譯器編譯的「熱點代碼」有兩類:

  1. 被屢次調用的方法
  2. 被屢次調用的循環體

3.2 Code Cache

Java代碼在執行時一旦被編譯器編譯爲機器碼,下一次執行的時候就會直接執行編譯後的代碼,也就是說,編譯後的代碼被緩存了起來。緩存編譯後的機器碼的內存區域就是codeCache。這是一塊獨立於Java堆以外的內存區域。除了JIT編譯的代碼以外,Java所使用的本地方法代碼(JNI)也會存在codeCache中

JVM運行時內存區域,堆與非堆劃分

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編譯器

  1. C2編譯器在將代碼編譯成機器碼以前,須要收集大量的統計信息以便在編譯的時候作優化,所以編譯後的代碼執行效率也高,代價是程序啓動速度慢,而且須要比較長的執行時間才能達到最高性能;

  2. C1編譯器的目標在於使程序儘快進入編譯執行階段所以編譯前須要收集的統計信息比C2少不少,編譯速度也快很多。代價是編譯出的目標代碼比C2編譯的執行效率要低。

儘管如此,C1編譯的執行效率也比解釋執行有巨大的優點。分層編譯方式是一種折衷方式,在系統啓動之初執行頻率比較高的代碼將先被C1編譯器編譯,以便儘快進入編譯執行。隨着時間推動,一些執行頻率高的代碼會被C2編譯器再次編譯,從而達到更高的性能。

經過如下JVM參數開啓分層編譯模式:

-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?
複製代碼
  1. JIT編譯器被中止了,而且不會被從新啓動,此時會迴歸到解釋執行

  2. 被編譯過的代碼仍然以編譯方式執行,可是還沒有被編譯的代碼就 只能以解釋方式執行了

針對這種狀況,JVM提供了一種比較激進的codeCache回收方式:Speculative flushing

在JDK1.7.0_4以後這種回收方式默認開啓,而以前的版本須要經過一個啓動參數來開啓:-XX:+UseCodeCacheFlushing

在Speculative flushing開啓的狀況下,當codeCache將要耗盡時:

  1. 最先被編譯的一半方法將會被放到一個old列表中等待回收

  2. 在必定時間間隔內,若是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會致使問題,以下

  1. Code Cache滿了時緊急進行清掃工做,它會丟棄一半老的編譯代碼
  2. Code Cache空間降了一半,方法編譯工做仍然可能不會重啓
  3. flushing可能致使高的cpu使用,從而影響性能降低;

3.6 Code Cache 調優

以client模式或者是分層編譯模式運行的應用,因爲須要編譯的類更多(C1編譯器編譯閾值低,更容易達到編譯標準),因此更容易耗盡codeCache。當發現codeCache有不夠用的跡象(經過上一節提到的監控方式)時,能夠經過啓動參數來調整codeCache的大小。

-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.7 問題解決

問題的來龍去脈都弄清楚了,也就好解決了。上面提到過純編譯方式和分層編譯方式均可以解決或緩解啓動後負載太高的問題,那麼咱們就有2種選擇:

  1. 採用分層編譯方式,並修改codeCache的大小爲256M;

  2. 採用純編譯方式,並修改codeCache的大小爲256M;

通過一段時間運行發現,在啓動後負載控制方面,純編譯方式要好一些,啓動以後負載幾乎不上升,而 分層編譯方式啓動後負載會有所上升,可是不會很高,也會在較短期內降下來。可是啓動時間方面,分層編譯比原來的默認啓動方式縮短了大概10秒(原來啓動須要110-130秒),而純編譯方式啓動時間比原來多了一倍,達到了250秒甚至更高。因此看起來分層編譯方式是更好的選擇。

然而JDK 7在codeCache的回收方面作的很很差。即便咱們將codeCache設置爲256M,線上仍是輕易達到了設置的報警閾值200M。並且一旦codeCache滿了以後又會致使系統運行變慢的問題。因此咱們的目標指向了JDK 8

測試代表,JDK 8對codeCache的回收有了很明顯的改善。不只codeCache的增加比較平緩,並且當使用量達到75%時,回收力度明顯加大,codeCache使用量在這個值上下浮動,並緩慢增加。最重要的是,JIT編譯還在正常執行,系統運行速度也沒有收到影響

3.8 運行時查看Code Cache

若是想在運行時查看code cache的大小,須要寫段代碼,目前只能經過JMX來獲取到Code Cache區域的使用情況,代碼相似以下:

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。

相關文章
相關標籤/搜索