CodeCache 深入了解

目录

问题描述
JIT 即时编译器
分层编译
CodeCache 相关参数
CodeCache 满了的情况
源码介绍

作者:@dwtfukgv

本文为作者原创,转载请注明出处:https://www.cnblogs.com/dwtfukgv/p/14766317.html


问题描述


  • 一个应用程序一直正常运行,突然某个时刻处理能力下降,但是从流量、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:CompileThreshold  # 方法触发编译时的调用次数,默认是10000
-XX:OnStackReplacePercentage  # 方法中循环执行部分代码的执行次数触发OSR编译时的阈值,默认是140
  • 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的继承关系与子类的作用如下图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值