探索JVM底层之执行引擎、逃逸分析、标量替换、锁消除

执行引擎是什么?

执行引擎是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字节对齐的。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值