四、执行引擎(☆)
1.执行引擎概述
java是半编译半解释型语言
1.作用
将字节码指令解释/编译为对应平台上的本地机器指令。(因为本地机器指令无法识别字节码指令)
2.结构
- 解释器
(java代码->java字节码)->c++代码->机器码 - 即时编译器(JIT-(Just In Time))
java字节码->机器码 - GC
3.执行过程
- 首先通过程序计数器找到执行的字节码指令
- 通过虚拟机栈的栈帧中的 局部变量中找到堆空间中的对象实例数据
- 通过对象中的类型指针找到方法区的类元信息
2.java代码的编译和执行的过程
1.java代码编译和执行的过程
- 橙色部分:前端编译,编译器将java源文件编译为字节码文件
- 绿色部分:解释器将字节码文件解释成本地的机器指令,并逐行执行
- 蓝色部分:JIT(即时编译器)将字节码指令编译为本地机器指令,但是不执行(解释热点代码,热点代码放在方法区中),内部有模板编译器。
2.为什么java是半编译半解释语言
1.原因
前端编译(javac),后端执行
编译指的是javac编译生成的字节码文件.class,但为什么是半呢,是因为生成的这个.class文件操作系统不能直接执行,需要解释器进行解释后(机器码),才可能运行,所以才把java叫做半编译半解释型语言。
2.什么是编译型语言,什么是解释型语言?
- 编译型语言:编写好程序以后,首先需要编译器进行编译,统一转化成机器码,然后这个编译完的文件,可以放在操作系统直接执行
- 解释型语言: 程序是边运行边进行机器码转化(转化完后cpu执)
3.什么是模板解释器、字节码解释器(前提都是直接读取的.class文件)
- 字节码解释器:读取字节码,转换为c++代码,再由c++代码转换为硬编码,以此类推。其字节码解释器主要的缺点是执行比较慢(Java字节码->c++代码->硬编码);
- 模板解释器 (JIT的一部分):刚开始的运行原理和字节码解释器一样,只不过模板解释器里面有个特殊的机制-即时编译(即时编译底层原理):
1、申请一块内存:可读可写可执行(mac系统不支持,所以说mac系统不支持jit)
2、将处理new字节码的硬编码拿过来
3、将处理new字节码的硬编码写入申请的内存
4、申请一个函数指针,用这个函数指针执行这块内存
5、调用的时候,直接通过这个函数指针调用就可以了(Java字节码->硬编码)
4.执行引擎执行字节码的3种方式
- -Xint 纯字节码解释器(1)
- -Xcomp 纯模板解释器(2)
- -Xmixed 字节码解释器 + 模板解释器(3)
注: VM默认是混合模式,我们可以执行下java -version查看下,可以通过java -Xint version来设置JVM的运行模式为纯字节码解释器。上面三个中执行模式中性能排比是什么呢,321或者是231,直接影响2和3的性能因素是,程序的大小。如果是大程序的话,可以直接采用混合模式,启动时间较快,编译优化器可以根据热点代码等进行优化。
5.即时编译器(JIT,即时编译器生成的代码就是给模板解释器用的)
HotSpot虚拟机内置了两个即时编译器,分别称为Client Compiler和Server Compiler,习惯上将前者称为C1,后者称为C2。
- C1
- 需要收集的较少
- 编译优化比较浅
- C2
- 触发条件比较严格,一般来说,程序运行了一段时间以后才会触发
- 优化比较深(优化汇编指令
- 编译生成的代码执行效率较C1更高
- 混合编译
- 程序运行初期触发C1编译器
- 程序运行一段时间后触发C2编译器
- Client 编译器模式下,N 默认的值 1500(N表示热点代码的次数)
- Server 编译器模式下,N 默认的值则是 10000
6.即时编译触发的条件:热点代码(存放在方法区)
在程序运行期间,根据对热点字节码的探测(运行次数超过某个阀值的代码),将这部分热点代码进行特别的优化,将其直接编译为本地机器码执行并缓存。其使用的 定期清理算法是LRU,最近最久未使用算法
7.LRU算法(缓存淘汰算法)
//TODO待补充
8.即时编译器是如何运行的呢?
- 将即时编译任务(即函数弹出栈的次数)写入一个队列中;
- VM_THREAD 读取任务,并运行
注:所以即时编译是一个异步的操作
9.基于逃逸分析,JVM开发了三种优化技术
- 栈上分配:逃逸分析如果是开启的,栈上分配就是存在的(不发生gc的情况下,查看堆上的对象个数,如果是程序中创建的个数,就存在栈上分配)
- -XX:+DoEscapeAnalysis 开启逃逸分析
- -XX:-DoEscapeAnalysis 关闭逃逸分析
- -XX:+PrintEscapeAnalysis 显示逃逸分析结果
- 标量替换:标量:不可再分,java中的基本类型就是标量
public class ScalarSubstitution {
public static void main(String[] ars) {
Point point = new Point();
System.out.println(point.x); //编译器会替换成System.out.println(0);
System.out.println(point.y); //同上
}
}
@Data
class Point {
public int x;
public int y;
}
- 锁消除:
public void test(){
synchronized (new Object()){ //编译器判定这个对象是个局部变量,是线程私有的,所以就没必要加锁,会直接把锁去掉
System.out.println("zong");
}
}
3.解释器
1. 为什么需要字节码文件
可以实现跨语言,使其他的语言生成自己码指令,JVM也能执行。
2.解释器作用:
根据程序计数器的指令地址,逐条将字节码指令“翻译”为本地机器指令,以便程序能够运行。
4.即时编译器(JIT)
将整个函数体编译成机器码(当该函数执行次数达到一定次数的时候触发,变为热点代码),每次函数执行时,只编译机器码即可,还可以将常用的机器码进行缓存在方法区(元空间)中,从而提高效率。
5.解释器与即时编译器的优缺点
1.解释器:
优点:程序一开始执行的时候,解释器就能够逐条执行,省去编译的时间。
缺点:相对于即时编译器而言,逐条进行翻译效率较低
2.即时编译器(JIT)
优点:因为是提前编译好的机器指令,因此效率更高;
缺点:程序一开始执行的时候,需要事先对字节码指令进行编译,需要一定的时间。
综上:HotSpot虚拟机采用两者兼容的方式,虚拟机开始启动后,解释器可以立即发挥作用,不需要等到即时编译器编译完后再去执行,节省不必要的编译时间,随着时间的推移,即时编译器编译完成之后,采用即时编译器效率更高。
6.StringTable(字符串)
1.String基本特性
- 存储结构的变更
jdk1.8时采用char[] 进行存储;jdk1.9之后采用byte[] 进行存储; - 不可变特性
通过字面量的方式给一个字符串赋值,此时的字符串位于堆空间的常量池中,
- 字符串常量池中不会存储相同的字符串
常量池中的字符串均是唯一的,如果两个字符串变量相等,则两个变量指向字符串常量池中的同一个地址。
String Pool(常量池) 底层是一个固定大小的HashTable,默认长度为1009,可通过-XX:StringTableSize设置
2.String内存分配
- jdk1.6时,字符串常量池位于永久代内
- jdk1.7之后,字符串常量池位于堆空间;
移动的原因/好处:
永久代几乎不进行来及回收,将其移动到堆空间后,更方便进行垃圾回收。