Java虚拟机字节码执行引擎详解

8 篇文章 0 订阅

1. 栈帧结构

局部变量表、操作数栈、动态链接、方法返回地址、附加信息

2. 局部变量表

用于存放方法参数和方法内部定义的局部变量。编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

局部变量表的容量以变量槽为最小单位,局部变量表中的变量槽是可以重用的。

如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位 数据类型的变量,则说明会同时使用第N和N+1两个变量槽。对于两个相邻的共同存放一个64位数据 的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个

当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程。

3. 复用变量槽与垃圾收集

public static void main(String[] args)() {
	byte[] placeholder = new byte[64 * 1024 * 1024]; // 不会回收
    System.gc();
}
public static void main(String[] args)() { 
    {
		byte[] placeholder = new byte[64 * 1024 * 1024]; 
    } // 加上作用于仍然不回收,因为变量槽还有引用
	System.gc(); 
}
public static void main(String[] args)() { 
    {
		byte[] placeholder = new byte[64 * 1024 * 1024]; 
    } 
    int i = 0; //变量槽复用,导致GC Root不再可达大变量,回收 placeholder
	System.gc(); 
}

4. 操作数栈

操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项 之中。如在做算术运算的时候是通过 将运算涉及的操作数栈压入栈顶后调用运算指令来进行的

5. 动态链接与静态链接

5.1 静态解析

字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号 引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。

调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析

invokestaticinvokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本, Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种

方法重载

方法重载就是静态解析的一种

public class L06_StaticDisPatch {

    public void print(Animal animal){
        System.out.println("Animal");
    }

    public void print(Dog dog){
        System.out.println("Dog");
    }

    public void print(Cat cat){
        System.out.println("Cat");
    }

    public static void main(String[] args) {
        L06_StaticDisPatch test = new L06_StaticDisPatch();
        Animal dog = new Dog();
        Animal cat = new Cat();
        test.print(dog);
        test.print(cat);
    }
}

class Animal{}

class Dog extends Animal{}

class Cat extends Animal{}


/*
* 输出
* Animal
* Animal
*/

Animal dog = new Dog();

拿这一句来讲,dog变量的实际类型是Dog,静态类型为Animal

编译器就根据参数的静态类型决定了会使用哪个重载版本,因此选择了 print( Animal ) 作为调用目标,并把这个方法的符号引用写到 main()方法里的两条invokevirtual指令的参数中。

 0 new #7 <com/szu/jvm/learn03_byte_code_engine/L06_StaticDisPatch>
 3 dup
 4 invokespecial #8 <com/szu/jvm/learn03_byte_code_engine/L06_StaticDisPatch.<init>>
 7 astore_1
 8 new #9 <com/szu/jvm/learn03_byte_code_engine/Dog>
11 dup
12 invokespecial #10 <com/szu/jvm/learn03_byte_code_engine/Dog.<init>>
15 astore_2
16 new #11 <com/szu/jvm/learn03_byte_code_engine/Cat>
19 dup
20 invokespecial #12 <com/szu/jvm/learn03_byte_code_engine/Cat.<init>>
23 astore_3
24 aload_1
25 aload_2
// 没有父类,所以不会触发 invokevirtual 的确定动态类型的动态分派过程,就在本类中找到 符合静态类型的方法
26 invokevirtual #13 <com/szu/jvm/learn03_byte_code_engine/L06_StaticDisPatch.print> 
29 aload_1
30 aload_3
// 没有父类,所以不会触发 invokevirtual 的确定动态类型的动态分派过程,就在本类中找到 符合静态类型的方法
31 invokevirtual #13 <com/szu/jvm/learn03_byte_code_engine/L06_StaticDisPatch.print>
34 return

所以上述代码的输出是Animal

5.2 动态分派—多态的实现

invokevirtual指令

方法重写


public class L07_MethodOverriding {

    public static void main(String[] args) {
        L07_MethodOverriding test = new L07_MethodOverriding();
        NewAnimal dog = new NewDog();
        NewAnimal cat = new NewCat();
        dog.print(dog);
        cat.print(cat);
    }

}
class NewAnimal{
    public void print(NewAnimal animal){
        System.out.println("New Animal");
    }
}
class NewDog extends NewAnimal{
    public void print(NewAnimal Dog){
        System.out.println("New Dog");
    }
}
class NewCat extends NewAnimal{
    public void print(NewAnimal Cat){
        System.out.println("New Cat");
    }
}
/*
* New Dog
* New Cat
*/
 0 new #2 <com/szu/jvm/learn03_byte_code_engine/L07_MethodOverriding>
 3 dup
 4 invokespecial #3 <com/szu/jvm/learn03_byte_code_engine/L07_MethodOverriding.<init>>
 7 astore_1
 8 new #4 <com/szu/jvm/learn03_byte_code_engine/L07_MethodOverriding$NewDog>
11 dup
12 invokespecial #5 <com/szu/jvm/learn03_byte_code_engine/L07_MethodOverriding$NewDog.<init>>
15 astore_2
16 new #6 <com/szu/jvm/learn03_byte_code_engine/L07_MethodOverriding$NewCat>
19 dup
20 invokespecial #7 <com/szu/jvm/learn03_byte_code_engine/L07_MethodOverriding$NewCat.<init>>
23 astore_3
24 aload_2
25 aload_2    // load指令把新创建的对象压栈
26 invokevirtual #8 <com/szu/jvm/learn03_byte_code_engine/L07_MethodOverriding$NewAnimal.print>  // 但是还是指向 NewAnimal 
29 aload_3
30 aload_3    // load指令把新创建的对象压栈
31 invokevirtual #8 <com/szu/jvm/learn03_byte_code_engine/L07_MethodOverriding$NewAnimal.print>  // 但是还是指向 NewAnimal 
34 return

load指令分别把创建的两个对象的引用压到栈顶,这两个对象是将要执行的print()方法

两个invokevirtual的参数都是常量池中第8项的常量,注释显示了这个常量是NewAnimal.print()的符号引用都完全一样

那是怎样实现调用的方法确实不是同一个的呢?

invokevirtual指令的运行时解析两个对象的实际类型

  • 找到操作数栈顶的第一个元素所指向的对象的实际类型(对象组成的第二个部分classPointer)
  • 如果在class对象指向MetaSpace的内存地址中找到与常量中的描述符和简单名称都相符的方法,进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
  • 否则,按照继承关系从下往上依次对各个父类进行第二步的搜索和验证过程。
  • 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

Field不参与多态

而是按照静态解析的方式找到静态类型中的变量

public class L08_FieldHaveNoOverride {
    public static void main(String[] args) {
        Father guy = new Son();
        // 此时的guy.money 会找到Father类,按照静态解析使用
        System.out.println("guy  "+guy.money);
    }

}
class Father{
    public int money = 1;

    public Father(){
        money = 2;
        showMeTheMoney();
    }

    private void showMeTheMoney() {
        System.out.println("Father  " + money);
    }
}

class Son extends Father{
    public int money = 3;

    public Son(){
        money = 4;
        showMeTheMoney();
    }

    private void showMeTheMoney() {
        System.out.println("Son  " + money);
    }

}

/*
Father  2  // 创建子类导致父类被创建
Son  4
guy  2	   // 此时的guy.money 会找到父类,按照静态解析使用	
*/

5.3 动态分派优化

动态分派是执行非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在接收者类型的 方法元数据中搜索合适的目标方法,因此,Java虚拟机实现基于执行性能的考虑,真正运行时一般不 会如此频繁地去反复搜索。

优化1. 虚方法表

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方 法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。

优化2. 方法内联和其他

即时编译使用的优化方式

主要目的有两个:一是去除方法调用的成本(如查找方法版本、建立栈帧等); 二是为其他优化建立良好的基础。

优化前的原始代码
static class B {
	int value;
	final int get() { 
        return value;
	} 
}


public void foo() {
	y = b.get();
	// ...do stuff...
	z = b.get(); sum = y + z;
}
内联后的代码
public void foo() {
	y = b.value;
	// ...do stuff...
	z = b.value; 
	sum = y + z;
}
进行冗余访问消除
public  void foo() {
	y = b.value;
	// ...do stuff...
	z = y;
	sum = y + z; 
}
复写传播
public  void foo() { 
    y = b.value;
	// ...do stuff... 
	y = y;
	sum = y + y; 
}
无用代码消除
public  void foo() { 
    y = b.value;
	// ...do stuff... 
    sum = y + y;
}

优化3. 虚方法的内联

类型继承关系分析的技术

用于确定在目前已加载的类中,某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息。编译器在进行内联时就会分不同情况采取不同的处理:

  • 如果是非虚方法,那么直接进行内联就可以了,这种的内联是有百分百安全保障的;
  • 如果遇到虚方法,则会向类型继承关系分析查询此方法在当前程序状态下是否真的有多个目标版本可供选择,如果查询到只有一个版本,那就可以假设“应用程 的全貌就是现在运行的这个样子”来进行内联,这种内联被称为守护内联。

不过由 于Java程序是动态连接的,说不准什么时候就会加载到新的类型从而改变继承关系,因此这种内联属于激进预测性优化,必须预留好“逃生门”,即当假设条件不成立时的“退路”

虚拟机一直没有加载到会令这个方法的接收者的继承关系发生变化的类,那这 个内联优化的代码就可以一直使用下去。如果加载了导致继承关系发生变化的新类,那么就必须抛弃 已经编译的代码,退回到解释状态进行执行,或者重新进行编译。

内联缓存

如果该方法确实有多个版本的目标方法可供选择,使用内联缓存的方式来缩减方法调用的开销。这种状态下方法调用是真正发生了的,但是比起直接查虚方法表还是要快一些。

内联缓存是一个建立在目标方法正常入口之前的缓存,它的工作原理大致为:在未发生方法调用之前,内联缓存状态为空,当第一次调用发生 后,缓存记录下方法接收者的版本信息,并且每次进行方法调用时都比较调用对象是否是同一个类型的对象。

  • 是,通过该缓存来调用,仅多了一次类型判断的开销而已。

  • 不是,再去查虚方法表,至于会不会缓存多个类型的多个方法入口,没有搞清楚,开销相当于真正查找虚方法表来进行方法分派

优化4. 逃逸分析

栈上分配

Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引 用,就可以访问到堆中存储的对象数据。虚拟机的垃圾收集子系统会回收堆中不再使用的对象,但回 收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。如果确定一个对 象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意

对象所占用的内存 空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所 占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收 集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸。

标量替换

假如逃逸分析能够证明一个对象不会被方法外部 访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创 建它的若干个被这个方法使用的成员变量来代替。

同步消除

如果逃逸分析 能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉。

6. 方法返回地址

第一种方式是执行引擎遇到任意一个方法 返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者

另外一种退出方式是在方法执行的过程中遇到了异常

恢复上层方法的 局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值 以指向方法调用指令后面的一条指令等

7. 语法糖

7.1 泛型擦除

**Map<String, String>**带泛型的Map

public static void main(String[] args) {
	Map<String, String> map = new HashMap<String, String>(); 
    map.put("hello", "你好");
	map.put("how are you?", "吃了没?");
	System.out.println(map.get("hello")); 
    System.out.println(map.get("how are you?"));
}

经过反编译之后只剩下 Map,并在输出的时候加上了String的类型强转

public static void main(String[] args) {
    Map map = new HashMap();  // 经过反编译之后只剩下 Map
	map.put("hello", "你好");
	map.put("how are you?", "吃了没?");
	System.out.println((String) map.get("hello"));   // 加上了String的类型强转
    System.out.println((String) map.get("how are you?"));
}

因为不 支持int、long与Object之间的强制转型,既然没法转换 那就索性别支持原生类型的泛型,遇到原生类型时把装箱、拆箱也自动做了得了。这个导致了无数构造包装类和装箱、拆箱的开销,成为Java泛型慢的重要原因。

7.2 当泛型遇到重载

public class GenericTypes {
	public static void method(List<String> list) {
		System.out.println("invoke method(List<String> list)"); 
    }
	public static void method(List<Integer> list) {
		System.out.println("invoke method(List<Integer> list)"); 
    }
}

由于泛型擦除的存在,导致此重载失败。

周志明老师在深入理解Java虚拟机中在此两个方法后边加了返回值之后,说是可以重载成功,但是本人的实验表明JDK11 + JDK8时周老师的结论是不正确的。

import java.util.ArrayList;
import java.util.List;

public class GenericTypes {

    public static String method(List<String> list) {
        System.out.println("invoke method(List<String> list)");
        return ""; // 加上返回值
    }

    public static int method(List<Integer> list) {
        System.out.println("invoke method(List<Integer> list)");
        return 1; // 加上返回值
    }

    public static void main(String[] args) {
        method(new ArrayList<String>());
        method(new ArrayList<Integer>());
    }
}

编译报错:

java: 名称冲突: method(java.util.List<java.lang.Integer>)和method(java.util.List<java.lang.String>)具有相同疑符

周老师的结论是这样给出解释:

这次能编译和执行成功,是因为两 个method()方法加入了不同的返回值后才能共存在一个Class文件之中。方法重载要求方法具备不同的特征签名,返回值并不包含 在方法的特征签名中,所以返回值不参与重载选择,但是在Class文件格式之中,只要描述符不是完全 一致的两个方法就可以共存。也就是说两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个Class文件中的。

实验环境

Idea 2020.3 JDK 1.8 + JDK 11 Windows 10教育版 64位操作系统

7.3 自动拆装箱与遍历

public static void main(String[] args) {
	List<Integer> list = Arrays.asList(1, 2, 3, 4); int sum = 0;
	for (int i : list) { sum += i;
	}
	System.out.println(sum); 
}
public static void main(String[] args) {
	List list = Arrays.asList( new Integer[] {
	Integer.valueOf(1),
	Integer.valueOf(2),
	Integer.valueOf(3),
	Integer.valueOf(4) });
	int sum = 0;
	for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) { 
        int i = ((Integer)localIterator.next()).intValue();
		sum += i;
    }
    System.out.println(sum); 
}

8. java.lang.invoke

8.1 MethodHandle

相当于指向方法的指针

模拟了invokevirtural的执行过程,这个方法的返回值可以相当于指向方法的指针

public class L09_MethodHandleTest {

    static class ClassA {
        public void println(String s) {
            System.out.println(s);
        }
    }


    public static void main(String[] args) throws Throwable {
        // 无论obj最终是哪个实现类,下面这句都能正确调用到println方法。
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
        getPrintlnMH(obj).invokeExact("icyfenix");
    }

    private static MethodHandle getPrintlnMH(Object receiver) throws Throwable {
        // MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和
        //具体参数(methodType()第二个及以后的参数)。
        MethodType mt = MethodType.methodType(void.class, String.class);
        // lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法 名称、方法类型,并且符合调用权限的方法句柄。
        // 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接收者,
        // 也即this指向的对象,这个参数以前是放在参数列表中进行传递,现在提供了bindTo() 方法来完成这件事情。
        return lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
    }
}

8.2 与反射的区别

  • 反射是在模拟Java代码层次 的方法调用,而MethodHandle是在模拟字节码层次的方法调用。

  • findStatic()、findVirtual()、findSpecial()分别对应invokestaticinvokevirtualinvokespecial

  • java.lang.reflect.Method对象远比MethodHandle对象所包含的信息来得多。前者包含了方法的签名、描述符以及方法属性表中各种属性、执行权限。

    后者仅包含执行该方法的相关信息。

  • 反射去调用方法则不能直接去实施各类调用点优化措施。如方法内联等

9. invokedynamic指令

MethodHandle机制的作用是一样的,只是一个用上层代码和API来实现, 另一个用字节码和Class中其他属性、常量来完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值