Java是半编译半解释型语言
- 编译型语言:程序在执行之前需要一个专门的编译过程,把程序编译成 为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。
- 解释型语言:程序不需要编译,程序在运行时才翻译成机器语言,每执 行一次都要翻译一次。
两种解释器的底层实现
字节码解释器
做的事情是:java字节码->c++代码->硬编码
原理:
- 通过while(true) 或者 for 将读取的字节码进行一个个编译
源码:
CASE(_new): {
u2 index = Bytes::get_Java_u2(pc+1);
ConstantPool* constants = istate->method()->constants();
if (!constants->tag_at(index).is_unresolved_klass()) {
// Make sure klass is initialized and doesn't have a finalizer
Klass* entry = constants->slot_at(index).get_klass();
assert(entry->is_klass(), "Should be resolved klass");
Klass* k_entry = (Klass*) entry;
assert(k_entry->oop_is_instance(), "Should be InstanceKlass");
InstanceKlass* ik = (InstanceKlass*) k_entry;
if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
……
模板解释器
做的事情:java字节码->硬编码
原理:
- 申请一块内存:可读可写可执行
- 将处理new字节码的硬编码拿过来(硬编码怎么拿到?) lldb 解析可执行文件
- 将处理new字节码的硬编码写入申请的内存
- 申请一个函数指针,用这个函数指针执行这块内存
- 调用的时候,直接通过这个函数指针调用就可以了
源码:
void TemplateTable::_new() {
transition(vtos, atos);
Label slow_case;
Label done;
Label initialize_header;
Label initialize_object; // including clearing the fields
Register RallocatedObject = Otos_i;
Register RinstanceKlass = O1;
Register Roffset = O3;
Register Rscratch = O4;
__ get_2_byte_integer_at_bcp(1, Rscratch, Roffset, InterpreterMacroAssembler::Unsigned);
__ get_cpool_and_tags(Rscratch, G3_scratch);
// make sure the class we're about to instantiate has been resolved
// This is done before loading InstanceKlass to be consistent with the order
// how Constant Pool is updated (see ConstantPool::klass_at_put)
__ add(G3_scratch, Array<u1>::base_offset_in_bytes(), G3_scratch);
__ ldub(G3_scratch, Roffset, G3_scratch);
__ cmp(G3_scratch, JVM_CONSTANT_Class);
__ br(Assembler::notEqual, false, Assembler::pn, slow_case);
……
三种运行模式
-Xint:纯字节码解释器模式
-Xcomp:纯模板解释器模式
-Xmixed:字节码解释器+模板解释器模式(默认)
验证效率:
public class IntCompMixed {
public static void main(String[] args) {
long star = System.currentTimeMillis();
test1(100000);
long end = System.currentTimeMillis();
System.out.println(end - star + " ms");
// while (true);
}
public static void test1(int n){
int num=0;
boolean sign;
for(int i=2;i<n;i++){
if(i % 2 == 0 && i != 2 ) continue; //偶数和1排除
sign=true;
for (int j=2;j<i;j++){
if(i%j==0){
sign=false;
break;
}
}
if (sign){
num++;
/* System.out.println(""+i);*/
}
}
}
}
分别设置三种模式
-Xint 5247 ms
-Xcomp 2564 ms
-Xmixed 1981 ms
即时编译器(编译完模板解释器执行硬编码)
- jdk6以前是没有混合编译的,后来根据两种编译器的使用场景组合起来使用进一步提升性能
- 在部分的商用虚拟机中(Sun HotSpot、IBM J9)中,Java
程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码” (Hot SpotCode)。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,简称 JIT 编译器)。
C1编译器
-client模式启动,默认启动的是C1编译器。有哪些特点呢?
1、需要收集的数据较少,即达到触发即时编译的条件较宽松
2、自带的编译优化的点较少
3、编译时较C2,没那么耗CPU,带来的结果是编译后生成的代码执行效率较C2低
C2编译器
-server模式启动。有哪些特点呢?
1、需要收集的数据较多
2、编译时很耗CPU
3、编译优化的点较多
4、编译生成的代码执行效率高
混合编译
目前的-server模式启动,已经不是纯粹只使用C2。程序运行初期因为产生的数据较少,这时候执行C1编译,程序执行一段时间后,收集到足够的数据,执行C2编译器
即时编译触发条件
判断一段代码是否是热点代码,是不是需要出发即时编译,这样的行为称为热点探测(Hot Spot Detection),探测算法有两种,分别如下:
- 基于采样的热点探测(Sample Based Hot Spot Detection):
虚拟机会周期的对各个线程栈顶进行检查,如果某些方法经常出现在栈顶,这个方法就是“热点方法”。好处是实现简单、高效,很容易获取方法调用关系。缺点是很难确认方法的
reduce,容易受到线程阻塞或其他外因扰乱。 - 基于计数器的热点探测(Counter Based Hot Spot Detection):
为每个方法(甚至是代码块)建立计数器,执行次数超过阈值就认为是“热点方法”。优点是统计结果精确严谨。缺点是实现麻烦,不能直接获取方法的调用关系。
HotSpot 使用的是第二种——基于计数器的热点探测,并且有两类计数器:方法调用计数器(Invocation Counter )和回边计数器(Back Edge Counter )。
这两个计数器都有一个确定的阈值,超过后便会触发 JIT 编译。
- 首先是方法调用计数器。Client 模式下默认阈值是 1500 次,在 Server 模式下是 10000次,这个阈值可以通过 -XX:CompileThreadhold 来人为设定。如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内的方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那么这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就成为此方法的统计的半衰周期( Counter Half Life Time)。进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:CounterHalfLifeTime 参数设置半衰周期的时间,单位是秒。整个 JIT 编译的交互过程如下图
- 第二个回边计数器,作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”( Back Edge )。显然,建立回边计数器统计的目的就是为了触发 OSR 编译。关于这个计数器的阈值, HotSpot 提供了
-XX:BackEdgeThreshold 供用户设置,但是当前的虚拟机实际上使用了 -XX:OnStackReplacePercentage 来简介调整阈值,计算公式如下: - 在 Client 模式下, 公式为 方法调用计数器阈值(CompileThreshold)X OSR
比率(OnStackReplacePercentage)/ 100 。其中 OSR 比率默认为 933,那么,回边计数器的阈值为
13995。 - 在 Server 模式下,公式为 方法调用计数器阈值(Compile Threashold)X (OSR
比率(OnStackReplacePercentage) -
解释器监控比率(InterpreterProfilePercent))/100。其中 onStackReplacePercentage 默认值为 140,InterpreterProfilePercentage 默认值为 33,如果都取默认值,那么 Server 模式虚拟机回边计数器阈值为 10700 。
整个执行过程,如下图:
解释执行和 C1 代码中增加循环回边计数器的位置并不相同,但这并不会对程序造成影响。
实际上,Java 虚拟机并不会对这些计数器进行同步操作,因此收集而来的执行次数也并非精确值。不管如何,即时编译的触发并不需要非常精确的数值。只要该数值足够大,就能说明对应的方法包含热点代码。
具体来说,在不启用分层编译的情况下,当方法的调用次数和循环回边的次数的和,超过由参数 -XX:CompileThreshold 指定的阈值时(使用 C1 时,该值为 1500;使用 C2 时,该值为 10000),便会触发即时编译。
当启用分层编译时,Java 虚拟机将不再采用由参数 -XX:CompileThreshold 指定的阈值(该参数失效),而是使用另一套阈值系统。在这套系统中,阈值的大小是动态调整的。
所谓的动态调整其实并不复杂:在比较阈值时,Java 虚拟机会将阈值与某个系数 s 相乘。该系数与当前待编译的方法数目成正相关,与编译线程的数目成负相关。
系数的计算方法为:
s = queue_size_X / (TierXLoadFeedback * compiler_count_X) + 1
其中X是执行层次,可取3或者4;
queue_size_X是执行层次为X的待编译方法的数目;
TierXLoadFeedback是预设好的参数,其中Tier3LoadFeedback为5,Tier4LoadFeedback为3;
compiler_count_X是层次X的编译线程数目。
在 64 位 Java 虚拟机中,默认情况下编译线程的总数目是根据处理器数量来调整的(对应参数 -XX:+CICompilerCountPerCPU,默认为 true;当通过参数 -XX:+CICompilerCount=N 强制设定总编译线程数目时,CICompilerCountPerCPU 将被设置为 false)。
Java 虚拟机会将这些编译线程按照 1:2 的比例分配给 C1 和 C2(至少各为 1 个)。举个例子,对于一个四核机器来说,总的编译线程数目为 3,其中包含一个 C1 编译线程和两个 C2 编译线程。
对于四核及以上的机器,总的编译线程的数目为:n = log2(N) * log2(log2(N)) * 3 / 2
其中N为CPU核心数目。
当启用分层编译时,即时编译具体的触发条件如下。
当方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阈值乘以系数,或者当方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时,便会触发X层即时编译。
触发条件为:i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s && i + b > TierXCompileThreshold * s)
其中 i 为调用次数,b 为循环回边次数。
热点代码缓存区
- 热点代码缓存是保存在方法区的,这块也是调优需要调的地方
- server 编译器模式下代码缓存大小则起始于 2496KB
- client 编译器模式下代码缓存大小起始于 160KB
- java -XX:+PrintFlagsFinal -version | grep InitialCodeCacheSize
逃逸分析
逃逸分析是目前Java虚拟机中比较前沿的优化技术,它并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术。
逃逸分析获取对象的引用,分析对象动态作用域,分析其会不会逃逸到方法或者线程之外。
- 当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。
- 若是被外部线程所访问,譬如赋值给类变量或可以在其他线程中访问实例变量,称为线程逃逸。
栈上分配
栈上分配。对象在栈上分配内存,对象占用空间随着栈帧出栈而销毁,降低了逃逸的可能性,就能节省GC过程中分析对象的消耗
验证:
- 关闭gc
- 查看对象个数
public class StackAlloc {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println((end - start) + " ms");
while (true);
}
public static void alloc() {
StackAlloc obj = new StackAlloc();
}
}
锁消除
同步消除。线程同步本身是一个相对耗时的过程,如果逃逸分析确认对象不会逃逸出线程,不会被其他线程访问,则不会有竞争,那么可以考虑去掉同步措施提高效率。
标量替换
标量替换。标量是指一个数据已经不能再分解成更小的数据来表示了,原始数据类型和reference引用类型都是。而对象则是聚合量,它可以继续分解。如果逃逸分析证明该对象不会逃逸到对象之外,那么它可以分解为方法、数据类型等标量,之后需要使用时可直接用标量替代,而不去创建整个对象,以节省资源。