欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM学习笔记十 之 字节码执行引擎

程序员文章站 2022-05-02 16:03:09
...

一、概述

jvm spec只给出了执行引擎的概念模型,并没有规定具体实现细节。执行引擎在执行时候可以解释执行、编译执行或直接由嵌入芯片的指令执行。引擎的行为使用指令集来定义。

java的目标是一次编写到处运行,为了达到这个目标,jvm指令集就不能依赖于任何硬件平台的指令,jvm指令集中就只有对栈的操作,没有对特定于硬件平台的寄存器的操作。当然jvm运行期优化的时候,可以针对不同的硬件平台提供不同的优化实现,比如充分利用硬件平台的寄存器提高访问速度。既然jvm执行引擎只有对栈的操作,那么我们下边就开始了解下栈的机构。

二、栈和栈帧

栈是线程私有的内存区域,每个线程都有一个栈,线程生则栈生,线程亡则栈灭(这里有一些栈的描述)。栈又由栈帧组成,每个方法调用都生成一个栈帧,方法调用结束则弹出栈帧。

栈帧又由多个部分组成:

1、局部变量表。包含方法参数和方法内部声明的局部变量,如果是实例方法,还有当前对象的this引用。局部变量表的大小在编译期就已经确定了,Locals:2即是;局部变量表所有的值也确定了,Local variable table:即是。此处可以先看class文件中方法的属性中局部变量表信息:

类的实例方法:

public class BigObejct {
	int[] value;
	private static final int M1 = 1024 * 1024;

	public BigObejct() {
		//4 * 1m = 4m
		this.value = new int[M1];
	}
	public void setValue(int[] value){
		this.value = value;
	}
}

 setValue的本地变量表信息:

 // Method descriptor #23 ([I)V
  // Stack: 2, Locals: 2
  public void setValue(int[] value);
    0  aload_0 [this]
    1  aload_1 [value]
    2  putfield com.yymt.jvm.BigObejct.value : int[] [16]
    5  return
      Line numbers:
        [pc: 0, line: 11]
        [pc: 5, line: 12]
      Local variable table:
        [pc: 0, pc: 6] local: this index: 0 type: com.yymt.jvm.BigObejct
        [pc: 0, pc: 6] local: value index: 1 type: int[]

   运行时本地变量表怎么查看?整个栈帧内容怎么查看?在eclipse中调试时候可以Variable窗口可以看到局部变量信息,但是跟局部变量表并不是一一对应的,因为局部变量在运行期只在start_pc之后才被创建并存活到超出作用域。

 

2、操作栈。出入栈操作就是对该操作数栈的操作,操作数栈的最大栈深在运行期也已经确定,1中Stack:2,表示最大栈深为2。如果考虑上运行期优化技术里的标量替换和栈上分配对象,此处的栈是显然不够用的。后续jvm团队会如何解决呢?

3、解析相关的数据,即指向常量池的指针。在方法运行过程中,可能会用到常量池中的表项,所以需要持有一个到常量池的引用。

4、方法调用返回相关的信息,记录一些信息恢复调用者的栈帧和计数器。需要记录方法调用返回后返回到何处,调用者pc计数器指向哪条指令?方法返回有两种方式,正常的调用返回和异常返回。正常调用返回如果有返回值,则把返回值压入调用者栈中,把pc计数器指向调用者下一条指令,继续调用者的执行。如果没有返回值则只设置pc计数器。异常返回则直接弹出栈帧,同样恢复调用者的栈帧和计数器,调用者根据是否捕捉异常决定是弹出栈帧到上层还是捕捉异常处理。

5、异常相关信息。栈帧中还必须保存一个到异常表的引用,当方法抛出异常时候进行处理。

6、其他信息,如调试相关信息。

以上3、4、5、6一起也称作帧数据区。

三、方法调用

分派是指根据参数和接受者?决定方法调用的版本。

1、静态分派和动态分派

根据接受者类型和参数类型,在编译器静态决定调用哪个方法叫做静态分派。根据接受者类型,在运行期动态决定调用哪个方法叫做动态分派。java中调用重载的方法属于静态分派,在编译器根据类型信息就决定了方法调用的版本。调用重写的方法属于动态分派,在运行期根据实际的类型信息决定调用方法的版本。

package com.yymt.jvm.method.dispatch;
public class Dispatcher {

	static class Base {
		public void printMessage() {
			System.out.println("Base Message");
		}
	}

	static class Sub extends Base {
		public void printMessage() {
			System.out.println("Sub Message");
		}
	}

	public static void accept(Base base) {
		System.out.println("Accept Base");
	}

	public static void accept(Sub sub) {
		System.out.println("Accept Sub");
	}

	public static void staticDispatch() {
		Base base = new Base();
		Base sub = new Sub();
		accept(base);
		accept(sub);
	}

	public static void main(String[] args) {
		staticDispatch();
//		System.out.println("=========");
//		dynamicDispatch();
	}
	
	public static void dynamicDispatch() {
		Base base = new Base();
		Base b2s = new Sub();
		Sub sub = new Sub();
		base.printMessage();
		b2s.printMessage();
		sub.printMessage();
	}
}
    此处读懂了静态分派和动态分派的解释,也就猜到结果了,即使之前没见到过这种经典的笔试题:
Accept Base
Accept Base

  调用accept方法的时候都是调用的accept(Base)方法,编译器已经根据参数的静态类型决定了调用的方法,从字节码(红色)就可以判定出来:

  // Method descriptor #6 ()V
  // Stack: 2, Locals: 2
  public static void staticDispatch();
     0  new com.yymt.jvm.method.dispatch.Dispatcher$Base [38]
     3  dup
     4  invokespecial com.yymt.jvm.method.dispatch.Dispatcher$Base() [40]
     7  astore_0 [base]
     8  new com.yymt.jvm.method.dispatch.Dispatcher$Sub [41]
    11  dup
    12  invokespecial com.yymt.jvm.method.dispatch.Dispatcher$Sub() [43]
    15  astore_1 [sub]
    16  aload_0 [base]
    17  invokestatic com.yymt.jvm.method.dispatch.Dispatcher.accept(com.yymt.jvm.method.dispatch.Dispatcher$Base) : void [44]
    20  aload_1 [sub]
    21  invokestatic com.yymt.jvm.method.dispatch.Dispatcher.accept(com.yymt.jvm.method.dispatch.Dispatcher$Base) : void [44]
    24  return
      Line numbers:
        [pc: 0, line: 26]
        [pc: 8, line: 27]
        [pc: 16, line: 28]
        [pc: 20, line: 29]
        [pc: 24, line: 30]
      Local variable table:
        [pc: 8, pc: 25] local: base index: 0 type: com.yymt.jvm.method.dispatch.Dispatcher.Base
        [pc: 16, pc: 25] local: sub index: 1 type: com.yymt.jvm.method.dispatch.Dispatcher.Base
虽然两处aload_1/aload_2分别从本地变量表中将base和sub压入栈中,但是因为invokestatic指令是直接根据后边常量池的CONSTANT_MethodRef_info表项指向的方法调用的,此方法的直接引用是指向对应类的方法区的字节码的指针,所以就一定调用的是accept(Base)方法。
我们再来看看动态调用:
public static void dynamicDispatch() {
	Base base = new Base();
	Base b2s = new Sub();
	Sub sub = new Sub();
	base.printMessage();
	b2s.printMessage();
	sub.printMessage();
} 

这个的输出大概都能猜出:

Base Message
Sub Message
Sub Message 

调用printMessage的地方,都是运行期根据方法实际类型动态决定调用哪个类的实例方法的:

  // Method descriptor #6 ()V
  // Stack: 2, Locals: 3
  public static void dynamicDispatch();
     0  new com.yymt.jvm.method.dispatch.Dispatcher$Base [38]
     3  dup
     4  invokespecial com.yymt.jvm.method.dispatch.Dispatcher$Base() [40]
     7  astore_0 [base]
     8  new com.yymt.jvm.method.dispatch.Dispatcher$Sub [41]
    11  dup
    12  invokespecial com.yymt.jvm.method.dispatch.Dispatcher$Sub() [43]
    15  astore_1 [b2s]
    16  new com.yymt.jvm.method.dispatch.Dispatcher$Sub [41]
    19  dup
    20  invokespecial com.yymt.jvm.method.dispatch.Dispatcher$Sub() [43]
    23  astore_2 [sub]
    24  aload_0 [base]
    25  invokevirtual com.yymt.jvm.method.dispatch.Dispatcher$Base.printMessage() : void [53]
    28  aload_1 [b2s]
    29  invokevirtual com.yymt.jvm.method.dispatch.Dispatcher$Base.printMessage() : void [53]
    32  aload_2 [sub]
    33  invokevirtual com.yymt.jvm.method.dispatch.Dispatcher$Sub.printMessage() : void [56]
    36  return
      Line numbers:
        [pc: 0, line: 39]
        [pc: 8, line: 40]
        [pc: 16, line: 41]
        [pc: 24, line: 42]
        [pc: 28, line: 43]
        [pc: 32, line: 44]
        [pc: 36, line: 45]
      Local variable table:
        [pc: 8, pc: 37] local: base index: 0 type: com.yymt.jvm.method.dispatch.Dispatcher.Base
        [pc: 16, pc: 37] local: b2s index: 1 type: com.yymt.jvm.method.dispatch.Dispatcher.Base
        [pc: 24, pc: 37] local: sub index: 2 type: com.yymt.jvm.method.dispatch.Dispatcher.Sub

从上边的字节码出看两处aload_0/aload_1/aload_2分别从本地变量表中将base、b2s和sub引用压入栈中,invokevirtual指令会根据引用去引用指向的对象的类的方法表中查找具有相同名称和描述符的方法,如果找到了则直接调用;如果没有找到则去其父类的方法表中找,如果找到了则调用;如果没有找到继续向继承关系上级去找,如果找不到就抛出java.lang.AbstractMethodError。问题是,invokevirtual指令后边Constant_Methodref_info的直接引用是方法表的偏移量,在子类找和在父类查找的时候,怎么确保同样的偏移量指向的是相同签名的方法?如b2s和sub实例都指向相同的方法入口。下边讲到虚拟机动态分派的时候会讲到。

此处方法调用的直接引用是特定于hotspot vm的实现的。

2、单分派和多分派

先解释个名词,总量:方法的接受者和方法的参数一起被称作宗量。分派时候根据影响方法调用的宗量个数不同,分派又分为单分派和多分派,如果方法调用只受一个宗量影响的叫单分派,受多个宗量影响的叫多分派。来这里了解更多。Java语言目前为止属于静态多分派,动态单分派,根据java语言的不断发展也许以后会支持动态多分派的。java的静态多分派是指,方法调用在编译期根据方法接受者的静态类型和参数的静态类型共同决定;动态多分派是指,在运行期,究竟调用哪个方法只由接受者的静态类型决定。此处接受者在编译期和运行期都决定了调用哪个方法,算是影响了两次?

package com.yymt.jvm.method.dispatch;

public class SinMulDispatcher {

	public static class Car {
		public void printName() {
			System.out.println("I'm a Car");
		}
	}

	public static class BYDCar extends Car {
		public void printName() {
			System.out.println("I'm a BYD Car");
		}
	}

	public static class Father {
		public void chooseCar(Car car) {
			System.out.println("Father choose Car");

		}

		public void chooseCar(BYDCar car) {
			System.out.println("Father choose BYDCar");
		}
	}

	public static class Son extends Father {
		public void chooseCar(Car car) {
			System.out.println("Son choose Car");
		}

		public void chooseCar(BYDCar car) {
			System.out.println("Son choose BYDCar");
		}
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Car car = new Car();
		Car byd = new BYDCar();
		
		Father father = new Father();
		Father son = new Son();
		
		father.chooseCar(car);
		son.chooseCar(byd);
	}

}

  输出为:

Father choose Car
Son choose Car

在编译期,根据接受者静态类型Father和参数静态类型Car,一同决定了调用Father.chooseCar(Car),而不是Object.chooseCar:

  // Method descriptor #15 ([Ljava/lang/String;)V
  // Stack: 2, Locals: 5
  public static void main(java.lang.String[] args);
     0  new com.yymt.jvm.method.dispatch.SinMulDispatcher$Car [16]
     3  dup
     4  invokespecial com.yymt.jvm.method.dispatch.SinMulDispatcher$Car() [18]
     7  astore_1 [car]
     8  new com.yymt.jvm.method.dispatch.SinMulDispatcher$BYDCar [19]
    11  dup
    12  invokespecial com.yymt.jvm.method.dispatch.SinMulDispatcher$BYDCar() [21]
    15  astore_2 [byd]
    16  new com.yymt.jvm.method.dispatch.SinMulDispatcher$Father [22]
    19  dup
    20  invokespecial com.yymt.jvm.method.dispatch.SinMulDispatcher$Father() [24]
    23  astore_3 [father]
    24  new com.yymt.jvm.method.dispatch.SinMulDispatcher$Son [25]
    27  dup
    28  invokespecial com.yymt.jvm.method.dispatch.SinMulDispatcher$Son() [27]
    31  astore 4 [son]
    33  aload_3 [father]
    34  aload_1 [car]
    35  invokevirtual com.yymt.jvm.method.dispatch.SinMulDispatcher$Father.chooseCar(com.yymt.jvm.method.dispatch.SinMulDispatcher$Car) : void [28]
    38  aload 4 [son]
    40  aload_2 [byd]
    41  invokevirtual com.yymt.jvm.method.dispatch.SinMulDispatcher$Father.chooseCar(com.yymt.jvm.method.dispatch.SinMulDispatcher$Car) : void [28]
    44  return
      Line numbers:
        [pc: 0, line: 42]
        [pc: 8, line: 43]
        [pc: 16, line: 45]
        [pc: 24, line: 46]
        [pc: 33, line: 48]
        [pc: 38, line: 49]
        [pc: 44, line: 50]
      Local variable table:
        [pc: 0, pc: 45] local: args index: 0 type: java.lang.String[]
        [pc: 8, pc: 45] local: car index: 1 type: com.yymt.jvm.method.dispatch.SinMulDispatcher.Car
        [pc: 16, pc: 45] local: byd index: 2 type: com.yymt.jvm.method.dispatch.SinMulDispatcher.Car
        [pc: 24, pc: 45] local: father index: 3 type: com.yymt.jvm.method.dispatch.SinMulDispatcher.Father
        [pc: 33, pc: 45] local: son index: 4 type: com.yymt.jvm.method.dispatch.SinMulDispatcher.Father

  在运行期,invokevirtual指令根据前边压入的调用者类型,动态决定分别调用了Father.chooseCar(Car)和Son.chooseCar(Car)。

3、虚拟机动态分派的实现

HotSpot VM虚拟机动态分派是通过方法表实现的。在jvm装载完类型后连接阶段的准备子阶段,会在方法区为类变量分配内存,同时会为别的结构分配内存,如方法表。而对象在内存中会有一个指向方法区的指针,可以通过对象来找到对象的方法表,进而找到方法。方法表里只有虚方法,即非静态、非私有、非初始化、非final的实例方法,也成为虚方法。常量池解析的时候,对于虚方法,直接引用会是方法表的偏移量。私有、静态、初始化、final方法都指向方法区中方法的直接地址的,运行期这种非虚方法很容易优化,不需要动态派发。

每个类型的方法表,都会包含超类的方法。超类方法在方法表中的顺序跟超类方法表顺序一致,这样就可以实现子类方法表索引跟父类方法表索引相同时候,指向的方法也相同。

JVM学习笔记十 之 字节码执行引擎
            
    
    博客分类: jvm 动态分派静态分派单分派多分派栈帧 

四、字节码执行引擎

字节码执行是基于对操作数的出栈和入栈操作进行的,相对比较简单,没有寄存器。当然运行期优化时候把字节码编译为本地代码的时候,会充分利用机器的寄存器的。