一、引言
Java 虚拟机在执行 Java 程序的过程中会把他所管理的内存划分为若干个不同的数据区域。其中有的区域是线程共享的有的区域是线程私有的,如下图所示。(更详细说明请看:
http://smallbug-vip.iteye.com/blog/2274277)现在要讨论的java虚拟机字节码执行引擎就是执行在虚拟机栈中(本地方法暂不考虑),它是线程私有的。
二、运行时栈帧结构
现在放大虚拟机栈结构:
局部变量表
1)变量值的存储空间,由方法参数和方法内部定义的局部变量组成,其容量用Slot1作为最小单位。在编译期间,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
2)由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
3)在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。如果是实例方法,那局部变量表第0位索引的SLot存储的是方法所属对象实例的引用,因此在方法内可以通过关键字this来访问到这个隐含的参数。其余的参数按照参数表顺序排列,参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。
4)类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
操作数栈
1)后入先出(LIFO)栈。当一个方法开始执行时,它的操作数栈是空的。在方法执行过程中,会有各种字节码指令往操作数栈写入和提取内容。
2)在概念模型中,两个栈帧是完全独立的。但大多虚拟机实现都会做优化,让两个栈帧出现一部分重叠。让下面的栈帧的部分操作数栈与上面的栈帧的部分局部变量表重叠在一起,无须进行额外的参数复制。
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class 文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
方法返回地址
当方法开始执行后,有两种方式退出。一是遇到方法返回的字节码指令;二是遇到异常并且这个异常没有在方法体内得到处理。无论哪种退出方式,方法退出之后都要返回到方法被调用的位置。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是通过异常处理器表来确定的,栈帧中一般不会保存此信息。
方法退出的过程就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,修改PC计数器的值以指向后一条指令等。
三、方法调用
方法调用的主要任务就是确定被调用方法的版本(即调用哪一个方法),该过程不涉及方法具体的运行过程。按照调用方式共分为两类:
- 解析调用:静态的过程,在编译期间就完全确定目标方法。
- 分派调用:可能是静态,也可能是动态的,根据分派标准可以分为单分派和多分派。两两组合有形成了静态单分派、静态多分派、动态单分派、动态多分派
解析(Resolution)
所有方法调用中的目标方法在Class文件里都是一个常量池中的符号引用。在解析阶段,会将其中一部分符号引用转化为直接引用:如果方法在真正运行之前就有一个可确定的调用版本,并且这个调用版本在运行期是不可改变的,那么就会被转化为直接引用。
符合这个条件的有静态方法、私有方法、实例构造器和父类方法4类。这4类方法和final方法都称为非虚方法。非虚方法在编译期间就完成了解析调用,将符号引用转变为可确定的直接引用。
分派(Dispatch)
静态分派
先看一段代码:
class Bug {
}
class Smallbug extends Bug {
}
class Bigbug extends Bug {
}
public class DispatchTest {
public void sayHello(Bug bug) {
System.out.println("bug say hello!");
}
public void sayHello(Smallbug smallbug) {
System.out.println("smallbug say hello!");
}
public void sayHello(Bigbug bug) {
System.out.println("bigbug say hello!");
}
public static void main(String[] args) {
DispatchTest assign = new DispatchTest();
Bug smallbug = new Smallbug();
Bug bigbug = new Bigbug();
assign.sayHello(smallbug);
assign.sayHello(bigbug);
}
}
运行结果是:
bug say hello!
bug say hello!
在main方法中,Bug称为变量的Static类型或Apparent类型,而Smallbug和Bigbug则为变量的实际类型。
将这段代码javap反编译之后截取面main方法局部:
Code:
stack=2, locals=4, args_size=1
0: new #7 // class DispatchTest
3: dup
▽ 4: invokespecial #8 // Method "<init>":()V
7: astore_1
8: new #9 // class Smallbug
11: dup
12: invokespecial #10 // Method Smallbug."<init>":()V
15: astore_2
16: new #11 // class Bigbug
19: dup
20: invokespecial #12 // Method Bigbug."<init>":()V
23: astore_3
24: aload_1
25: aload_2
26: invokevirtual #13 // Method sayHello:(LBug;)V
29: aload_1
30: aload_3
31: invokevirtual #13 // Method sayHello:(LBug;)V
34: return
程序调用的是参数实际类型不同的方法,但是虚拟机最终分派了相同外观类型(静态类型)的方法,这说明在重载的过程中虚拟机在运行的时候是只看参数的外观类型(静态类型)的,而这个外观类型(静态类型)是在编译的时候就已经确定的,和虚拟机没有关系。
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载(Overload)。虚拟机在重载时通过参数的静态类型而不是实际类型作为判定依据。并且静态类型是编译期可知的,因此在编译阶段,编译器会根据参数的静态类型决定使用哪个方法的重载版本
动态分派
将上面的代码修改一下:
class Bug {
public void sayHello() {
System.out.println("bug say hello!");
}
}
class Smallbug extends Bug {
public void sayHello() {
System.out.println("smallbug say hello!");
}
}
class Bigbug extends Bug {
public void sayHello() {
System.out.println("bigbug say hello!");
}
}
public class DispatchTest {
public static void main(String[] args) {
Bug smallbug = new Smallbug();
Bug bigbug = new Bigbug();
smallbug.sayHello();
bigbug.sayHello();
}
}
运行结果:
smallbug say hello!
bigbug say hello!
在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。典型应用是方法重写(Override)
将这段代码javap反编译之后截取面main方法局部:
Code:
stack=2, locals=3, args_size=1
0: new #2 // class Smallbug
3: dup
4: invokespecial #3 // Method Smallbug."<init>":()V
7: astore_1
8: new #4 // class Bigbug
11: dup
12: invokespecial #5 // Method Bigbug."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method Bug.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method Bug.sayHello:()V
24: return
程序调用的是不同实际类型的同名方法,虚拟机依据对象的实际类型去寻找是否有这个方法,如果有就执行,如果没有去父类里找,最终在实际类型里找到了这个方法,所以最终是在运行期动态分派了方法。在编译的时候我们可以看到字节码指示的方法都是一样的符号引用,但是运行期虚拟机能够根据实际类型去确定出真正需要的直接引用。这种依赖实际类型来做方法的分配叫做动态分派。
单分派与多分派
首先明白什么是宗量?
方法的接收者与方法的参数统称为方法的宗量。这是《深入理解java虚拟机第二版》的解释。根据分派基于多少种宗量,可以分为单分派和多分派。单分派根据一个宗量对目标方法进行选择,而多分派则根据多于一个宗量对目标方法进行选择。下面看一段代码:
//定义叶子
class Leaf {
}
//定义豆子
class Bean {
}
//我是虫子
class Bug {
//虫子吃叶子
public void eat(Leaf leaf) {
System.out.println("I am bug and I am going to eat leafs!");
}
//虫子吃豆子
public void eat(Bean bean) {
System.out.println("I am bug and I am going to eat beans!");
}
}
//我是小虫子
class Smallbug extends Bug {
//小虫子吃叶子
public void eat(Leaf leaf) {
System.out.println("I am smallbug and I am going to eat leafs!");
}
//小虫子吃豆子
public void eat(Bean bean) {
System.out.println("I am smallbug and I am going to eat beans!");
}
}
public class DispatchTest {
public static void main(String[] args) {
Bug smallbug = new Smallbug();
Bug bug = new Bug();
//小虫子吃叶子
smallbug.eat(new Leaf());
//虫子吃豆子
bug.eat(new Bean());
}
}
对于吃来说有两个宗量,1、是谁要吃小虫子还是虫子?2、要吃什么吃豆子还是叶子。
静态分派时考虑的问题是,谁要吃,吃什么,这是两个宗量,所以静态分派又是多分派。javap反编译这段代码:
Code:
stack=3, locals=3, args_size=1
0: new #2 // class Smallbug
3: dup
4: invokespecial #3 // Method Smallbug."<init>":()V
7: astore_1
8: new #4 // class Bug
11: dup
12: invokespecial #5 // Method Bug."<init>":()V
15: astore_2
16: aload_1
17: new #6 // class Leaf
20: dup
21: invokespecial #7 // Method Leaf."<init>":()V
24: invokevirtual #8 // Method Bug.eat:(LLeaf;)V
27: aload_2
28: new #9 // class Bean
31: dup
32: invokespecial #10 // Method Bean."<init>":()V
35: invokevirtual #11 // Method Bug.eat:(LBean;)V
重点观察24,35行,静态分派之后吃什么就一定确定下来了,24行吃叶子,35行吃豆子。那么下一个问题就是运行时该方法对应的是哪个实例。即是虫子的子类小虫子要吃叶子呢还是虫子要吃叶子呢,这是不清楚的。所以动态分派只有一个宗量。即单分派。
综上所述,可以总结:
Java是一门静态多分派,动态单分派的语言
四、虚拟机动态分派的实现
动态分派在Java中被大量使用,使用频率及其高,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率,因此JVM在类的方法区中建立虚方法表(virtual method table)来提高性能。每个类中都有一个虚方法表,表中存放着各个方法的实际入口。如果某个方法在子类中没有被重写,那子类的虚方法表中该方法的地址入口和父类该方法的地址入口一样,即子类的方法入口指向父类的方法入口。如果子类重写父类的方法,那么子类的虚方法表中该方法的实际入口将会被替换为指向子类实现版本的入口地址。
虚方法表会在类加载的连接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
- 大小: 22.8 KB
- 大小: 25.9 KB
分享到:
相关推荐
本书共分20章,第1-4章解释了java虚拟机的体系结构,包括java栈、堆、方法区、执行引擎等;第5-20章深入描述了java技术的内部细节,包括垃圾收集、java安全模型、java的连接模型和动态扩展机制、class文件、运算及...
Java虚拟机(JVM)是Java语言的重要核心之一,它是Java程序运行的基础。Java虚拟机是一种抽象的计算机,...执行引擎层则负责解释Java字节码并执行;应用层则提供了与具体应用相关的功能,如安全管理器、垃圾收集器等。
Java虚拟机的主要任务是装在class文件并且执行其中的字节码。Java虚拟机包含一个类装载器,它可以从程序和 API中装载class文件。Java API中只有程序执行时需要的那些类才会被装载。字节码由执行引擎来执行。不同的...
《Java虚拟机精讲》以极其精练的语句诠释了HotSpot VM 的方方面面,比如:字节码的编译原理、字节码的内部组成结构、通过源码的方式剖析HotSpot VM 的启动过程和初始化过程、Java 虚拟机的运行时内存、垃圾收集算法...
本书以极其精练的语句诠释了HotSpot VM 的方方面面,比如:字节码的编译原理、字节码的内部组成结构、通过源码的方式剖析HotSpot VM 的启动过程和初始化过程、Java 虚拟机的运行时内存、垃圾收集算法、垃圾收集器...
Java虚拟机(JVM)是Java Virtual Machine的缩写,...类加载器负责将字节码文件加载到内存中,运行时数据区用于存储程序执行时所需的数据,执行引擎则负责执行字节码文件,而垃圾收集器则负责回收不再使用的内存空间。
本书以极其精练的语句诠释了HotSpot VM 的方方面面,比如:字节码的编译原理、字节码的内部组成结构、通过源码的方式剖析HotSpot VM 的启动过程和初始化过程、Java 虚拟机的运行时内存、垃圾收集算法、垃圾收集器...
3.4.3 第三趟:字节码验证 3.4.4 第四趟:符号引用的验证 3.4.5 二进制兼容 3.5 Java虚拟机中内置的安全特性 3.6 安全管理器和Java API 3.7 代码签名和认证 3.8 一个代码签名示例 3.9 策略 3.10...
第三部分分析了虚拟机的执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎。第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测...
Java虚拟机(JVM)面试题(总结最全面的面试题!...能不能解释一下方法区(重点理解)什么是JVM字节码执行引擎你听过直接内存吗?知道垃圾收集系统吗?堆栈的区别是什么?深拷贝和浅拷贝Java会存在内存泄漏吗?请说 收
本书以极其精练的语句诠释了 HotSpot VM的方方面面,比如:字节码的编译原理、字节码的内部组成结构、通过源码的方式剖析 HotSpot VM 的启动过程和初始化过程、Java 虚拟机的运行时内存、垃圾收集算法、垃圾收集器...
在虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型称为各种虚拟机执行引擎的统一外观. 在不同的虚拟机实现里,执行引擎在执行 java 代码的时候,可能会解释执行和编译执行等,但是从外观上来看,...
3.4.3 第三趟:字节码验证 3.4.4 第四趟:符号引用的验证 3.4.5 二进制兼容 3.5 Java虚拟机中内置的安全特性 3.6 安全管理器和Java API 3.7 代码签名和认证 3.8 一个代码签名示例 3.9 策略 3.10 保护域 ...
视频目录 第1节说在前面的话 [免费观看] 00:05:07分钟 | 第2节整个部分要讲的内容说明 [免费观看] 00:06:58分钟 | 第3节环境搭建以及jdk,...第104节字节码执行引擎小结00:03:38分钟 | 第105节总结与回顾00:10:55分钟
JVM=类加载器classloader+执行引擎executionengine+运行时数据区域runtimedataarea首先Java源代码文件被Java编译器编译为字节码文件,然后JVM中的类加载器加载完毕之后,交由JVM执行引擎执行。在整个
3.4.3 第三趟:字节码验证 3.4.4 第四趟:符号引用的验证 3.4.5 二进制兼容 3.5 Java虚拟机中内置的安全特性 3.6 安全管理器和Java API 3.7 代码签名和认证 3.8 一个代码签名示例 3.9 策略 ...
/ 189 7.4.1 类与类加载器 / 189 7.4.2 双亲委派模型 / 191 7.4.3 破坏双亲委派模型 / 194 7.5 本章小结 / 197 第8章 虚拟机字节码执行引擎 / 198 8.1 概述 / 198 8.2 运行时栈帧结构 / 199 8.2.1 局部变量...