CodeCache 深入了解
Posted dwtfukgv
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
编译后的代码被缓存的内存区域就是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:+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
的继承关系与子类的作用如下图:
以上是关于CodeCache 深入了解的主要内容,如果未能解决你的问题,请参考以下文章
Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled