CodeCache 深入了解


問題描述

  • 一個應用程序一直正常運行,突然某個時刻處理能力下降,但是從流量、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中每個指針的含義如下圖:
    image

  • CodeBlob的繼承關系與子類的作用如下圖:
    image


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM