CodeCache 深入了解

Posted dwtfukgv

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CodeCache 深入了解相关的知识,希望对你有一定的参考价值。

问题描述

  • 一个应用程序一直正常运行,突然某个时刻处理能力下降,但是从流量jstackgc上来看都是比较正常的。

  • 会在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编译后的代码被缓存的内存区域就是code cache,这是一块独立于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:+UseCodeCacheFlushingJIT编译器被停止了,并且不会被重新启动,此时会回归到解释执行,被编译过的代码仍然以编译方式执行,但是尚未被编译的代码就 只能以解释方式执行了。
    • 如果未开启-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

以上是关于CodeCache 深入了解的主要内容,如果未能解决你的问题,请参考以下文章

深入JVM - Code Cache内存池

CodeCache与CodeBlob

jvm——CodeCache

Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled

深入理解DOM节点类型第四篇——文档片段节点DocumentFragment

《Docker 源码分析》全球首发啦!