执行引擎就是 JVM运行Java程序的一套子系统
Java是半编译半解释型语言
如果面试官问你这个问题,要分成两个角度来讲解
1、javac编译,java运行
2、运行期即时编译+解释执行(字节码解释器解释执行,模板解释器编译执行)
两种解释器的底层实现
JVM中目前来说有两种解释器
具体细节见课堂上操作实战
1、字节码解释器
做的事情是:java字节码->c++代码->硬编码
根据不同的字节码指令,执行不同的操作。比如下面代码
0 new #4 <com/luban/test/Test_4>
3 dup
4 invokespecial #5 <com/luban/test/Test_4.<init>>
7 astore_1
8 goto 8 (0)
执行的伪代码如下
while(true) {
for() {
char code =
switch(code) {
case NEW:
break;
case DUP:
break;
}
}
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() ) {
……
2、模板解释器
做的事情:java字节码->硬编码
先看一个C程序:模拟的就是模板解释器的底层实现
1、申请一块内存:可读可写可执行
JIT在Mac是无法运行的, Mac无法申请可执行的内存块
2、将处理new字节码的硬编码拿过来(硬编码怎么拿到?) lldb 解析可执行文件
3、将处理new字节码的硬编码写入申请的内存
4、申请一个函数指针,用这个函数指针执行这块内存
5、调用的时候,直接通过这个函数指针调用就可以了
![image-20201212173500496](https://gitee.com/mx0425/images/raw/master/img/20201212173500.png)
有对硬编码的生成规则感兴趣的同学吗?
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);
……
字节码解释器是解释执行的,是一步一步执行的,比如执行了new,执行硬编码,执行到dup,再执行硬编码。模版解释器前面已经触发了即时编译,把字节码对应的c++代码已经全部编译生成硬编码,所以他直接执行硬编码,所以它的执行效率比字节码解释器高。
三种运行模式
JIT为什么能提升性能呢?原因是运行期的热点代码编译与缓存
JVM中有两种即时编译器,就诞生了三种运行模式
1、-Xint:纯字节码解释器模式
2、-Xcomp:纯模板解释器模式
3、-Xmixed:字节码解释器+模板解释器模式(默认)
java默认的是混合模式,可以通过 java -Xit -version修改对应的模式。
这三种模式,哪种效率最高呢?jvm默认为什么要混合模式
首先要知道,如果一个程序很大,如果纯模版解释器(运行的就是编译好的硬编码)的话,那么运行初期就要编译很长时间,这段时间程序时不运行的。所以如果程序很小的话,那么第二种比较合适。反之采用第三种。
两种即时编译器
jdk6以前是没有混合编译的,后来根据两种编译器的使用场景组合起来使用进一步提升性能
1、C1编译器
-client模式启动,默认启动的是C1编译器。有哪些特点呢?
-
需要收集的数据较少,即达到触发即时编译的条件相对C2比较宽松
-
自带的编译优化的点较少(编译的优化比较浅,基本运算在编译的时候运算掉了,比如final)
-
编译时较C2,没那么耗CPU,带来的结果是编译后生成的代码执行效率较C2低
2、C2编译器
-server模式启动。有哪些特点呢?
-
触发的条件比较严格,一般来说,程序运行了一段时间以后才会触发。需要收集的数据较多
-
编译时很耗CPU
-
编译优化的点较多
-
编译生成的代码执行效率较C1更高
Server模式和client模式
在64位机上只有Server模式,在32位机上可以java -client -version指定成client模式。
3、混合编译
目前的-server模式启动,已经不是纯粹只使用C2。程序运行初期因为产生的数据较少,这时候执行C1编译,程序执行一段时间后,收集到足够的数据,执行C2编译器
Mac中是无法使用JIT的!因为Mac无法申请一块可读可写可执行的内存块
字节码解释器是解释执行的,跟即时编译器无关。模板解释器执行的硬编码就是即时编译器给编译的。即时编译器有C1,C2。
即时编译触发条件
目前的64bit机器上只有server模式。大家现在谈执行引擎,说的都是server模式启动的JVM中的执行引擎
触发即时编译的最小单位是代码段(for,while…),最大单位是方法,比如循环个数N:
Client 编译器模式下,N 默认的值 1500,即达到1500时才触发
Server 编译器模式下,N 默认的值则是 10000
java -client -XX:+PrintFlagsFinal -version | grep CompileThreshold
热度衰减
比如现在一个线程调用某一个方法,已经调用了7000次了,随后很长一段时间又没调用了,这时候就会2倍数往下掉,比如掉到3500,原来的话我需要再执行3001次就可以触发即时编译,但是现在我就需要6501次了。
编译器编译后就生成了硬编码,在JVM中也叫热点代码。
热点代码缓存区
热点代码缓存是保存在方法区的,这块也是调优需要调的地方
server 编译器模式下代码缓存大小则起始于 2496KB
client 编译器模式下代码缓存大小起始于 160KB
java -XX:+PrintFlagsFinal -version | grep CodeCache
调优的话一般将InitialCodeCacheSize,ReservedCodeCacheSize这俩调成一样大。
即时编译器时如何运行的
其实在JVM中很多系统性的操作,像GC,即时编译都是通过VM_THREAD出发的,可以把它理解成一个队列,当达到出发条件,有线程向这个队列里面推送任务,然后其他线程异步去执行任务。
比如System.gc
1、将这个即时编译任务写入队列QUEUE
2、VM_THREAD从这个队列中读取任务,并运行
执行即时编译的线程有多少,以及如何调优
java -client -XX:+PrintFlagsFinal -version | grep CICompilerCount
-XX:CICompilerCount=N //调优参数
逃逸分析
理解含义
逃逸分析这个词可要拆成两个词来理解:逃逸、分析;逃逸是一种现象,分析是一种技术手段。
逃逸
外部能访问到就叫逃逸,比如共享变量,返回值,参数。。。
如果对象的作用域不是局部的,也就是逃逸。
逃到哪里呢,可以理解为逃到方法外,线程外。
不逃逸
对象的作用域是局部变量
分析
先想想:为什么要做逃逸分析?
如果对象发生了逃逸,那情况就会变的非常复杂,优化无法实施。基于逃逸分析,JVM开发了三种优化技术(不逃逸的情况)。
以下优化技术只有不逃逸的情况下才能做。
1、栈上分配
逃逸分析默认是开启的,栈上分配就是存在的,对象在虚拟机栈上分配
如何证明栈上分配的存在?
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();
}
}
可以这样证明,生成了一个对象100w次,不发生gc的情况下,利用HSDB看堆区是不是有100w个,如果没有,就存在栈上分配。如下图:
![image-20201212222024578](https://gitee.com/mx0425/images/raw/master/img/20201212222024.png)
可以看到远远不够100w。然后在启动参数加上-XX:+/-DoEscapeAnalysis(-关闭,+开启),进行开启和关闭关闭栈上分配后如下图:
![image-20201212222313196](https://gitee.com/mx0425/images/raw/master/img/20201212222313.png)
2、标量替换
标量:不可再分,java中的基本数据类型就是标量
聚合量:可再分,对象
看一个例子
public class ScalarReplace {
public static void main(String[] args) {
}
public static void test() {
Position position = new Position(1, 2, 3);
System.out.println(position.x);
System.out.println(position.y);
System.out.println(position.z);
}
}
class Position {
int x;
int y;
int z;
public Position(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
}
Position对象的x,y,z就是一个标量,所以postion就是有效的局部变量此时jvm在做逃逸分析的时候就回把输出代码替换为
System.out.println(1);
System.out.println(2);
System.out.println(3);
这就是标量替换。
3、锁消除
举个例子
public void noEscape1(){
synchronized (new Object()){
System.out.println("hello");
}
}
此时在局部变量中加锁,jvm在做逃逸分析后就会变为这样
public void noEscape1(){
System.out.println("hello");
}