文章目录
执行引擎是什么?
执行引擎是JVM运行java程序的一套子系统。
执行引擎执行的就是字节码文件。
语言的深度:Java->C++ -> 硬编码(010101 CPU指令)
一、两种解释器
1.1、字节码解释器
将Java字节码-> 解释成C++代码 -> 运行硬编码
早期的时候只有字节码解释器,但是由于字节码解释器性能比较低,才出现了模板解释器。 因为需要将java代码先转化成C++代码,再将C++代码转换成硬编码执行,比价耗费时间。而C++代码可以直接转化成硬编码,所以效率比较好。
new #2 <com/jihu/test/jvm/Test>
3 dup
4 invokespecial #3 <com/jihu/test/jvm/Test.<init>>
7 astore_1
8 getstatic #4 <java/lang/System.out>
11 aload_1
12 invokevirtual #5 <com/jihu/test/jvm/Test.add>
15 invokevirtual #6 <java/io/PrintStream.println>
18 return
上面的字节码,如果是用字节码解释器通用框架如下:
while (true) {
chat code = X;
switch (code) {
case NEW:
...
break';
CASE DUP:
...
break;
}
}
会根据switch,将字节码转化成C++代码,CPU执行C++编译后的硬编码。
1.2、模板解释器
Java字节码 -> 硬编码
看一个C程序:
1、申请一块内存,可读可写可执行
2、将处理new字节码的硬编码拿过来(硬编码怎么拿到)
3、将处理new字节码的硬编码写入申请的内存
4、申请一个函数指针,用这块函数指针指向这块内存
5、调用的时候,直接通过这个函数指针调用就可以了
如果是字节码解释器,需要先将字节码解析成C++,然后再解释成硬编码执行;
如果是模板解释器,因为开启了及时编译,所以字节码已经被解释成了硬编码,直接执行即可。下一次执行的时候,模板解释器会直接执行硬编码,所以说模板解释器效率比字节码解释器高。
二、三种运行模式
1、-Xint 纯字节码解释器
2、-Xcomp 纯模板解释器: 会先将代码编译成硬编码,之后再运行程序
3、-Xmixed 混合模式:字节码解释器 + 模板解释器
我们使用java -version来查看JVM的运行模式:
D:\Tool_Workspace\IDEA\JDK-Test-01\src\com\jihu\test\byte_code>java -version
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)
64位机下,JVM只有server模式,32位机可以设置为client模式运行。
我们可以看到,此时运行模式是mixed,即混合模式。
java -Xint -version
D:\Tool_Workspace\IDEA\JDK-Test-01\src\com\jihu\test\byte_code>java -Xint -version
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, interpreted mode)
此时为字节码解释器模式
java -Xcomp -version
D:\Tool_Workspace\IDEA\JDK-Test-01\src\com\jihu\test\byte_code>java -Xcomp -version
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, compiled mode)
此时为编译模式
2.1、比较三种运行模式的性能
从我们的测试的值,性能从高到低是:模板解释器、混合模式解释器、字节码解释器。
我们思考一下,为什么此时JVM默认还是使用混合模式呢?而不是纯模板解释器呢?
纯模板解释器 会先将代码编译成硬编码,之后再运行程序。而如果一个程序比较大,那么在运行初期,并不会执行,但是会耗费较多时间。
所以说,混合模式还是模板模式哪个效率更高,和程序的大小有关。如果程序很小,纯模板解释器效率更好。反之,混合模式更好。
注意: 模板解释器运行的是编译好的硬编码。
现在有两个问题?
1、谁来编译?
2、编译后生成的热点代码(热点代码就是编译好的硬编码)保存在哪里?
编译优化,找找感觉。
正常的(非裸函数)C++函数生成的硬编码都有堆栈操作。
三、及时编译器
字节码解释器和及时编译器没有关系!字节码解释器是解释执行,和编译器没有关系。
只有模板解释器才和及时编译器有关系。模板解释器运行的是热点代码,即硬编码,是及时编译器编译后产生的。
3.1、C1编译器、
c1编译器是client模式下的及时编译器。
1、触发的条件相对C2比较宽松:需要收集的数据较少。
2、编译的优化比较浅:基本运算在编译的时候运算掉了。比如final修饰的拼接字符串。
3、c1编译器编译生成的代码执行效率较c2低。
3.2、C2编译器
C2编译器是server模式下的及时编译器。
1、触发的条件比较严格,一般来说,是程序运行了一段时间以后才会触发;
2、优化比较深;
3、编译生成的代码执行效率较C1更高。
3.3、混合编译
现在一般是混合编译。
程序运行初期触发c1编译器
程序运行一段时间后触发c2编译器
及时编译器生成的代码就是给模板解释器用的。
四、触发条件
及时编译的最小单位不是一个方法,而是一个代码块(for、while循环)。
Client 编译器模式下,N 默认的值 1500
Server编译器模式下,N 默认的值 10000
4.1、热度衰减
10000次就会触发及时编译
比如调用一个new方法,之前已经调用了7000次了。然后接下来的一段时间中没有继续调用,热度会2倍速往下掉 3500。
如果没有热度衰减,我们只需要调用3001次就会触发及时编译,但是现在热度调到3500,需要调用6501次才能触发及时编译。
4.2、分享阿里早年的一个故障
热机切冷机故障
冷机:刚运行程序不久;
热机:运行程序一段时间了;
比如现在有10台节点,随着业务的增长,需要加新的节点。而我们添加节点的时候,一般都是添加相同配置的节点。
因为我们知道之前的10台机器每天的压力是多少,所以我们打算将这些压力往新的节点上分摊一下,即修改负载均衡。但是现在的问题是,发现一切冷机之后,冷机会立马挂掉。
因为热机上对于热点代码已经进行了缓存,所以热机抗的并发更大。所以一旦将压力切到新的冷机上,冷机的并发能力不如热机,而且冷机刚启动,一遍运行一遍还在做及时编译。所以说CPU扛不住。
正确的做法是,将一部分流量先切到冷机上,一段时间后出发及时编译后再切全部的流量。
五、热点代码缓存区
热点代码存储在方法区。
参数名是CodeCache。
热点代码缓存区也是调优中的一种,server编译器模式下缓存大小起始于 2496k。
client编译器模式下热点代码缓存大小起始于 160k。
调优参数:
InitialCodeCacheSize
ReservedCodeCacheSize
一般将这两个值调成一样大。
热点代码会根据LRU自动清理,有点像Redis的热点数据缓存。
六、及时编译时如何运行的
GC,,及时编译等都是通过VM_THREAD执行的。
VM_THREAD就像一个队列一样,有线程不断的将任务放到这个队列中。
1、将这个及时编译任务放入到队列;
2、VM_THREAD从这个队列中读取任务并运行。
VM_THREAD是异步运行的。
那么执行及时编译的线程有多少呢?如何调优?
可以通过命令:
java -XX:+PrintFlagsFinal
然后找到CICompilerCount参数,发现是3
问题:如何理解java是半编译半解释型语言
1、javac编译,java运行;
2、字节码解释器解释执行,模板解释器编译执行。
思路理解:
硬编码在JVM角度叫热点代码
热点代码存在哪里? 热点代码存在缓冲区
热点代码是如何生成的?
及时编译器
及时编译器的触发条件
热度衰减
阿里的热机切冷机故障
字节码解释器需要将java字节码一句句的先解释成C++,然后由C++再转化成硬编码执行,整个过程比较耗时;
而模板解释器会直接将java字节码解释成硬编码执行。但是如果是非常大的程序,整个解释的过程是比较长的,所以说只有程勋运行一段时间后,才会使用模板解释器。
七、逃逸分析
7.1、逃逸
逃逸是一种现象,逃逸指的是对象逃逸。
逃逸可以这么理解,对象的作用域不仅仅是当前线程。对象逃逸到方法外或者线程外。或者说,非局部变量就是逃逸。
如果对象的作用域是局部变量,就不会逃逸。共享变量、返回值、参数都不会逃逸
package com.jihu.test.escape;
public class EscapeAnalysis {
public static Object globaVariableObject;
public Object instanceObject;
// 静态变量,外部线程可见,发生逃逸
public void globaVariableEscape() {
globaVariableObject = new Object();
}
// 返回实例,外部线程可见,发生逃逸
public void instanceObjectEscape() {
instanceObject = new Object();
}
// 返回实例,外部线程可见,发生逃逸
public Object returnObject() {return new Object();}
// 仅创建线程可见,对象未逃逸
public void noEscape() {
synchronized (new Object()) {
System.out.println("hello");
}
}
// 仅创建线程可见,对象无逃逸
public void noEscape2() {
Object obj = new Object();
}
public static void main(String[] args) {
EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
escapeAnalysis.globaVariableEscape();
}
}
7.2、分析
分析是一种手段。
为什么要对对象的逃逸进行分析?
为什么呢?因为如果对象发生了逃逸,那情况就会变的非常复杂,优化无法实施。
基于逃逸分析,JVM开发了三种优化技术。所以当JVM在判断对象不会发生逃逸的时候,就会尝试使用这三种优化技术进行优化!
7.2.1、栈上分配
逃逸分析如果是开启的,栈上分配就是存在的。
JDK8中,逃逸分许默认开启。
我们传统的观念是对象是在堆中创建的。
而栈上分配指的是,对象在虚拟机栈上分配。
jdk 8中栈上分配存在吗?如何证明?
我们此时new一个对象一百万次,然后看堆中是否有一百万个,如果没有,就证明存在栈上分配(加一个限制条件,不发生gc)。
package com.jihu.test.runengine;
public class StackAlloc {
public static void main(String[] args) {
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 stackAlloc = new StackAlloc();
}
}
我们使用HSDB来分析,启动命令:
java -classpath C:\Program Files\Java\jdk1.8.0_201\lib\sa-jdi.jar sun.jvm.hotspot.HSDB
然后看我们刚才测试类的进程号:
连接HSDB:
然后选择:
开启逃逸分析,耗时6ms. 可以看到,我们刚才创建的对象StackAlloc在堆中大概有十一万多个。所以说栈上分配是存在的。
我们再来看栈上的对象:
关闭逃逸分析:
-XX:+DoEscapeAnalysis 开启逃逸分析(JDK1.8默认开启)
-XX:-DoEscapeAnalysis 关闭逃逸分析
-XX:+printEscapeAnalysis 查看逃逸分析结果
关闭逃逸分析后的运行时间是19ms. 而且从HSDB中我们也能看到有一百万个对象。
所以说,如果我们同时创建很多个不会逃逸的对象的时候,JVM会尝试栈上分配,即在栈上创建对象。而不是全部在堆中创建对象!因为此时对象不会逃逸,意味着线程私有,根本不需要去共享空间堆中去创建。而且栈上创建的对象易于销毁,堆中创建的对象更难回收!
7.2.2、标量替换
标量:标量不可能再分,java中的基本数据类型就是标量
聚合量:可再分,对象就是聚合量
package com.jihu.test.runengine;
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(1);
System.out.println(position.y); // 替换成: System.out.println(2);
System.out.println(position.z); // 替换成: System.out.println(3);
}
}
class Position {
int x;
int y;
int z;
public Position(int i, int i1, int i2) {
this.x = x;
this.y = y;
this.z = z;
}
}
这里,positon.x,positon.y,positon.z 结果都是int,都是标量。
这里的position是一个局部变量,不会发生逃逸,所以当前这个position对象的x, y, z属性是不可能被修改的。JVM在做逃逸分析的时候发现了这种情况,就会进行标量替换,将positon.x,positon.y,positon.z替换成表象1, 2,3. 这样传输效率就会更高。如果这里不做标量替换的话, 就需要根据position对象的属性去常量池中找值。
7.3.3、锁消除
我们加锁的目的是为了保证同步,而下面的代码中加锁是没有意义的,因为这里new出来的对象是一个方法内的局部变量,其他线程是不可见的。
// 仅创建线程可见,对象未逃逸
public void noEscape() {
synchronized (new Object()) {
System.out.println("hello");
}
}
JVM分析的时候如果发现了这种情况,就会优化代码,优化后的代码如下:
public void noEscape() {
System.out.println("hello");
}
问题:如何判断栈上分配对象的阈值是多少?
java中的对象是8字节对齐的。