問題描述
-
一個應用程序一直正常運行,突然某個時刻處理能力下降,但是從流量、jstack、gc上來看都是比較正常的。
-
會在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?
-
這說明Code Cache已經滿了。會導致這個時候JIT就會停止,JIT一旦停止,就不會再起來了,如果很多代碼沒有辦法去JIT的話,性能就會比較差。
-
可能通過以下命令來查看JVM的參數值:
jinfo -flag <param> <PID>
-
可以查看Code Cache的最大值是多少:
jinfo -flag ReservedCodeCacheSize <PID>
JIT 即時編譯器
- JIT(Just In Time Compiler)編譯器(分Client端和Server端)。Java程序一開始是只是通過解釋器解釋執行的,即對字節碼逐條解釋執行,這樣執行速度會比較慢,尤其是當某個方法或者代碼塊運行的特別頻繁時。后來就有了JIT即時編譯器,當虛擬機發現某個方法或代碼塊運行特別頻繁時,就為了提高代碼執行效率,JIT會把這些代碼編譯成與本地平台相關的機器碼,下一次執行就會直接執行編譯后的機器碼,並進行各個層次的優化。這樣的代碼一般包括兩類:一類是頻繁調用的方法,另一個類是多執行的循環體。
- 經過JIT編譯后的代碼被緩存的內存區域就是CodeCache,這是一塊獨立於java堆之外的內存區域,並且java的本地方法代碼JNI也存儲在該區域。
分層編譯
- JVM提供了一個參數
-Xcomp
,可以使JVM運行在純編譯模式下,所有方法在第一次被調用的時候就會被編譯成機器代碼。加上這個參數之后,應用的啟動時間會變得的特別長。 - 除了純編譯方式和默認的
mixed
之外,從JDK6u25開始引入了一種分層編譯的方式。 - Hotspot JVM內置了2種編譯器,分別是 client方式啟動時用的C1編譯器和 server方式啟動時用的C2編譯器 。
- C2編譯器在將代碼編譯成機器碼之前,需要收集大量的統計信息以便在編譯的時候做優化,因此編譯后的代碼執行效率也高,代價是程序啟動速度慢,並且需要比較長的執行時間才能達到最高性能。
- C1編譯器的目標在於使程序盡快進入編譯執行階段,因此編譯前需要收集的統計信息比C2少很多,編譯速度也快不少。代價是編譯出的目標代碼比C2編譯的執行效率要低,但是這也要比解釋執行快很多。
- 分層編譯方式是一種折衷方式,在系統啟動之初執行頻率比較高的代碼將先被C1編譯器編譯,以便盡快進入編譯執行。隨着時間推進,一些執行頻率高的代碼會被C2編譯器再次編譯,從而達到更高的性能。
- 可以通過
-XX:+TieredCompilation
來開啟分層編譯。 - 在JDK8中,當以server模式啟動時,分層編譯默認開啟。需要注意的是,分層編譯方式只能用於server模式中,如果需要關閉分層編譯,需要加上啟動參數
-XX:-TieredCompilation
。
CodeCache 相關參數
-
CodeCache的內存大小相關參數:
-XX:InitialCodeCacheSize # 用於設置初始CodeCache大小 -XX:ReservedCodeCacheSize # 用於設置CodeCache的最大大小,通常默認是240M -XX:CodeCacheExpansionSize # 用於設置CodeCache的擴展大小,通常默認是64K
-
CodeCache刷新相關參數:
-XX:+UseCodeCacheFlushing # 是否在code cache滿的時候先嘗試清理一下,如果還是不夠用再關閉編譯,默認在JDK1.7.0_4后開啟
-
CodeCache編譯策略相關參數:
-XX:CompileThreshold # 方法觸發編譯時的調用次數,默認是10000 -XX:OnStackReplacePercentage # 方法中循環執行部分代碼的執行次數觸發OSR編譯時的閾值,默認是140
-
CodeCache編譯限制相關參數:
-XX:MaxInlineLevel # 針對嵌套調用的最大內聯深度,默認為9 -XX:MaxInlineSize # 方法可以被內聯的最大bytecode大小,默認為35 -XX:MinInliningThreshold # 方法可以被內聯的最小調用次數,默認為250 -XX:+InlineSynchronizedMethods # 是否允許內聯synchronized methods,默認為true
-
CodeCache輸出參數的相關參數:
-XX:+PrintCodeCache # 在JVM停止的時候打印出codeCache的使用情況,其中max_used就是在整個運行過程中codeCache的最大使用量 -XX:+PrintCodeCacheOnCompilation # 用於在方法每次被編譯時輸出CodeCache的使用情況
CodeCache 滿了的情況
- 當CodeCache滿了,會出現的情況:
- 如果未開啟
-XX:+UseCodeCacheFlushing
,JIT編譯器被停止了,並且不會被重新啟動,此時會回歸到解釋執行,被編譯過的代碼仍然以編譯方式執行,但是尚未被編譯的代碼就只能以解釋方式執行了。 - 如果未開啟
-XX:+UseCodeCacheFlushing
,最早被編譯的一半方法將會被放到一個old列表中等待回收,在一定時間間隔內,如果old列表中方法沒有被調用,這個方法就會被從CodeCache清除。
- 如果未開啟
- 開啟
-XX:+UseCodeCacheFlushing
可能會導致的問題:- CodeCache滿了時緊急進行清掃工作,它會丟棄一半老的編譯代碼
- CodeCache空間降了一半,方法編譯工作仍然可能不會重啟
- flushing可能導致高的CPU使用,從而影響性能下降
源碼介紹
-
CodeCache就是用於緩存不同類型的生成的匯編代碼,如熱點方法編譯后的代碼。所有的匯編代碼在CodeCache中都是以CodeBlob及其子類的形式存在的。
class CodeCache : AllStatic { friend class VMStructs; private: static CodeHeap * _heap; // 實際負責內存管理 // 各種類型的計數 static int _number_of_blobs; static int _number_of_adapters; static int _number_of_nmethods; static int _number_of_nmethods_with_dependencies; static bool _needs_cache_clean; static nmethod* _scavenge_root_nmethods; // gc時遍歷nmethod public: static void initialize(); // 初始化,像上面的參數,都是在這里面初始化 static void report_codemem_full(); // 報告內存滿了 static CodeBlob* allocate(int size, bool is_critical = false); // 申請內存 static void commit(CodeBlob* cb); // 當codeblob滿了時會調用該方法 static void free(CodeBlob* cb); // 釋放CodeBlob }
-
CodeCache只是CodeHeap的一層包裝而已,核心實現都在CodeHeap中。
-
CodeHeap就是實際管理匯編代碼內存分配的實現,在HotSpot VM中,除了模板解釋器外,有很多地方也會用到運行時機器代碼生成技術,如的C1編譯器產出、C2編譯器產出、C2I/I2C適配器代碼片段、解釋器到JNI適配器的代碼片段等。為了統一管理這些運行時生成的機器代碼,HotSpot VM抽象出一個CodeBlob體系,由CodeBlob作為基類表示所有運行時生成的機器代碼:
class CodeHeap : public CHeapObj<mtCode> { friend class VMStructs; private: VirtualSpace _memory; // 用於描述CodeHeap對應的一段連續的內存空間 block VirtualSpace _segmap; // 用於保存所有的segment的起始地址,記錄這些segment的使用情況 size_t _number_of_committed_segments; // 已分配內存的segments的數量 size_t _number_of_reserved_segments; // 剩余的未分配內存的保留的segments的數量 size_t _segment_size; // 一個segment的大小 -XX:CodeCacheSegmentSize每次擴展的大小 int _log2_segment_size; // segment的大小取log2,用於計算根據內存地址計算所屬的segment的序號 size_t _next_segment; // 下一待分配給Block的segment的序號 // 一個segment可以理解為一個內存頁,是操作系統分配內存的最小粒度,為了避免內存碎片,任意一個Block的大小都必須是segment的整數倍,即任意一個Block會對應N個segment。 FreeBlock* _freelist; // 可用的HeapBlock 鏈表,所有的Block按照地址依次增加的順序排序,即_freelist是內存地址最小的一個Block size_t _freelist_segments; // 可用的segments的個數,也就是freeLists的長度 // Helper functions size_t size_to_segments(size_t size) const { return (size + _segment_size - 1) >> _log2_segment_size; } // 計算size包含多少個segment size_t segments_to_size(size_t number_of_segments) const { return number_of_segments << _log2_segment_size; } // size_t segment_for(void* p) const { return ((char*)p - _memory.low()) >> _log2_segment_size; } // 地址p在第幾個segment HeapBlock* block_at(size_t i) const { return (HeapBlock*)(_memory.low() + (i << _log2_segment_size)); } // 第i個heapblock塊地址 void mark_segmap_as_free(size_t beg, size_t end); // 標記為未分配給Block void mark_segmap_as_used(size_t beg, size_t end); // 記為已分配給Block // Linux的內存映射相關操作 void on_code_mapping(char* base, size_t size); public: CodeHeap(); // 方法主要是對codeHeap中定義的_memory與_segmap屬性進行初始化,CodeCache初始化時調用此方法 // -XX:ReservedCodeCacheSize:設置代碼緩存的大小 // -XX:InitialCodeCacheSize:設置代碼緩存的初始大小, // -XX:CodeCacheSegmentSize:每次存儲請求都會分配一定大小的空間 bool reserve(size_t reserved_size, size_t committed_size, size_t segment_size); void release(); // 釋放所有 bool expand_by(size_t size); // 擴展 commited void shrink_by(size_t size); // 收縮 commited memory void clear(); // 清空所有 // Memory allocation void* allocate (size_t size, bool is_critical); // 申請一個size大小的block void deallocate(void* p); // 釋放 // Attributes char* low_boundary() const { return _memory.low_boundary (); } char* high() const { return _memory.high(); } char* high_boundary() const { return _memory.high_boundary(); } };
-
VirtualSpace是與ReservedSpace配合使用的,ReservedSpace是預先分配一段連續的內存空間,VirtualSpace負責在這段內存空間內實際申請內存。
// VirtualSpace是與ReservedSpace配合使用的,ReservedSpace是預先分配一段連續的內存空間,VirtualSpace負責在這段內存空間內實際申請內存。 class VirtualSpace VALUE_OBJ_CLASS_SPEC { friend class VMStructs; private: // Reserved area 通過ReservedSpace分配的地址空間范圍 char* _low_boundary; char* _high_boundary; // Committed area 通過VirtualSpace實際申請並使用的內存區域 char* _low; char* _high; // os::commit_memory() or os::uncommit_memory(). bool _special; // bool _executable; // 中間分配給大內存頁,兩邊默認內存頁 char* _lower_high; char* _middle_high; char* _upper_high; char* _lower_high_boundary; char* _middle_high_boundary; char* _upper_high_boundary; size_t _lower_alignment; size_t _middle_alignment; size_t _upper_alignment; public: VirtualSpace(); // 初始化 bool initialize_with_granularity(ReservedSpace rs, size_t committed_byte_size, size_t max_commit_ganularity); bool initialize(ReservedSpace rs, size_t committed_byte_size); size_t reserved_size() const; size_t actual_committed_size() const; // 使用的 size_t committed_size() const; // 未使用的 size_t uncommitted_size() const; bool contains(const void* p) const; bool expand_by(size_t bytes, bool pre_touch = false); void shrink_by(size_t bytes); void release(); }
-
ReservedSpace用來分配一段地址連續的內存空間,底層通過mmap實現,注意此時未實際分配內存。
// ReservedSpace用來分配一段地址連續的內存空間,底層通過mmap實現,注意此時未實際分配內存 class ReservedSpace VALUE_OBJ_CLASS_SPEC { friend class VMStructs; private: char* _base; // 這段連續內存空間的基地址 size_t _size; // 內存大小 size_t _noaccess_prefix; size_t _alignment; bool _special; // 是否走特殊方法分配 bool _executable; // 這段內存存儲的數據是否是可執行的 // ReservedSpace ReservedSpace(char* base, size_t size, size_t alignment, bool special, bool executable); void initialize(size_t size, size_t alignment, bool large, char* requested_address, const size_t noaccess_prefix, bool executable); }
-
VirtualSpace中每個指針的含義如下圖:
-
CodeBlob的繼承關系與子類的作用如下圖: