目录
问题描述
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的继承关系与子类的作用如下图: