`

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

    博客分类:
  • jvm
阅读更多

一、概述

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方法都指向方法区中方法的直接地址的,运行期这种非虚方法很容易优化,不需要动态派发。

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

四、字节码执行引擎

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

 

 

分享到:
评论
2 楼 yueyemaitian 2012-09-01  
lcfyolanda2007 写道
楼主的博文写得真是好,不仅有对概念的分析,还有对结果的印证。我想请问的是,你的method descriptor是如何看到的啊,有什么特别的工具吗?

eclipse中直接打开class文件就看到了
1 楼 lcfyolanda2007 2012-08-30  
楼主的博文写得真是好,不仅有对概念的分析,还有对结果的印证。我想请问的是,你的method descriptor是如何看到的啊,有什么特别的工具吗?

相关推荐

    jVM学习笔记.ppt

    执行阶段则是由JVM执行引擎解析字节码并执行。这一过程涉及到类加载机制,包括Bootstrap ClassLoader、Extension ClassLoader、App ClassLoader和Custom ClassLoader,它们按照特定的层次关系和加载顺序加载类,确保...

    JVM学习笔记

    ### JVM学习笔记 #### JVM内存模型 (JMM) JVM内存模型主要分为以下几个部分: - **Java堆**:这是所有线程共享的一块区域,在虚拟机启动时创建。主要用于存放对象实例,几乎所有的对象实例都在这里分配内存。 - *...

    JVM学习资料+笔记

    2. 字节码执行引擎:JVM通过解释器和即时编译器(JIT)来执行字节码,解释器用于快速启动,JIT则在运行时优化代码性能。 3. 内存模型:JVM内存分为堆、栈、方法区、本地方法栈、程序计数器等几部分。其中,堆是对象...

    JVM 学习笔记(Java虚拟机)

    **JVM学习笔记(Java虚拟机)** Java虚拟机(JVM)是Java语言的核心组成部分,它是Java程序运行的平台,负责解释和执行字节码。深入理解JVM对于优化Java应用程序性能至关重要。本笔记将从以下几个方面详细介绍JVM:...

    jvm视频及笔记

    Java虚拟机(JVM)是Java程序运行的核心组件,它负责解释和执行字节码,为开发者提供了跨平台的运行环境。"jvm视频及笔记"这个资源显然是一份全面学习JVM的材料,结合了视频教程和书面笔记,帮助学习者深入理解JVM的...

    JVM的学习笔记PDF版

    5. **字节码执行**:JVM通过解释器将字节码转换为机器码执行,为了提高性能,JVM还实现了Just-In-Time(JIT)编译器,将热点代码编译成本地机器码。 6. **内存调优**:理解和调整JVM内存参数(如-Xms, -Xmx, -Xss等...

    学习jvm笔记.zip

    JVM的执行引擎是其核心部分,负责解释和执行字节码。现代JVM采用了即时编译(JIT)技术,将热点代码编译成机器码,提高运行效率。此外,还有分代收集算法、并发标记扫描等垃圾回收策略,用于自动管理内存,避免...

    JVM笔记(阳哥).zip

    JVM主要包括以下几个组件:类加载器、运行时数据区、执行引擎、本地方法接口和本地库。理解JVM的工作原理,有助于我们更好地理解和排查程序中的问题。 二、内存管理 JVM内存主要分为堆内存和栈内存两大部分。堆...

    JVM性能学习笔记思维导图

    - **执行引擎(Execution Engine)**:解析和执行字节码,包含解释器和JIT编译器。 - **垃圾收集器(Garbage Collector)**:自动管理内存,避免内存泄露。 2. **内存区域详解** - **堆(Heap)**:所有对象实例都在堆...

    jvm原理分析课程笔记

    JVM的字节码执行引擎(也称为解释器)是Java代码运行的基础。它将字节码转换为机器码执行,同时也支持即时编译(Just-In-Time, JIT)技术,将频繁执行的热点代码编译为本地机器代码,以提升执行效率。 此外,JVM的...

    jvm-study:jvm学习笔记

    本学习笔记旨在全面解析JVM的工作原理,涵盖内存管理、类加载机制、垃圾收集、性能调优等多个关键领域,帮助读者从基础到深入地掌握JVM。 1. **JVM结构与运行过程** - JVM由类装载器、运行时数据区、执行引擎、...

    JVM思维导图,学习思维笔记

    本思维导图及学习笔记将深入探讨JVM的工作原理、内存模型、垃圾收集机制以及性能优化等方面,帮助你全面理解这个至关重要的技术。 一、JVM概述 Java虚拟机是Java平台的一部分,它负责解析字节码并执行Java程序。JVM...

    JVM成神之路.rar

    3. **执行引擎**:这是JVM的心脏,负责执行字节码。它包括解释器和即时编译器(如HotSpot JVM的C1和C2编译器),能将字节码转换为机器码以提高性能。 4. **垃圾收集器**(GC):Java的一大优势就是自动内存管理。...

    jvm-juc:jvm学习笔记

    3. 执行引擎:是JVM的心脏,负责执行字节码。主要包括解释器和JIT(Just In Time)编译器。解释器逐条执行字节码,而JIT编译器会将热点代码编译为本地机器码以提高性能。 4. 本地方法接口和本地方法库:JVM通过本地...

    JVM和性能优化学习思维笔记.rar

    《JVM与性能优化:深度探索思维导图与学习笔记》 在Java世界里,JVM(Java Virtual Machine)是程序执行的核心,它负责解析字节码并将其转化为机器指令,使得Java具备“一次编写,到处运行”的特性。而性能优化则是...

    学习笔记之对象的创建(Java)

    ### 学习笔记之对象的创建(Java) #### Java技术与Java虚拟机 Java作为一种广泛使用的编程语言,其独特之处在于“一次编写,到处运行”的理念。这一特性主要得益于Java虚拟机(JVM)的存在。Java技术主要包括以下几...

    JVM:深入理解Java虚拟机 - 学习笔记

    4. **字节码执行** JVM的执行引擎执行字节码,这包括解释器和即时编译器(JIT)。解释器逐行执行字节码,而JIT会将热点代码编译成机器码以提高性能。 5. **垃圾收集** JVM通过垃圾收集机制自动管理内存,避免内存...

    JVM-LearningAndOptimize:JVM学习笔记与调优实战

    《JVM学习笔记与调优实战》是一本深入探讨Java虚拟机(JVM)的书籍,旨在帮助读者理解和掌握JVM的工作原理以及如何进行性能优化。在这个项目中,作者不仅分享了理论知识,还提供了实战调优的经验。下面将详细讨论JVM...

Global site tag (gtag.js) - Google Analytics