一、引言
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
分享到:
相关推荐
Java 虚拟机的体系结构由多个子系统组成,包括类加载器子系统、执行引擎、数据区等。类加载器子系统负责加载程序中的类型(类和接口),并赋予唯一的名字。执行引擎负责执行被加载类中包含的指令。数据区中保存了...
Java虚拟机(JVM)是Java编程语言的核心组成部分,它是一种抽象的计算设备,能够运行Java字节码。Java虚拟机规范(Java SE 7版)是定义JVM行为的官方文档,确保所有Java平台的实现遵循相同的规则,以提供跨平台的...
Java虚拟机(JVM)是Java程序运行的基础,它...本地方法栈使Java能够调用本地库,执行系统级别的操作,而执行引擎则确保字节码的有效执行,优化程序性能。两者共同协作,为Java程序提供了一个高效且灵活的运行环境。
Java虚拟机(Java Virtual Machine,简称JVM)是Java编程语言的核心组成部分,它是一个用于执行Java字节码的软件或硬件设备。Java程序在编译时并不直接转化为机器语言,而是转化为中间代码,即字节码。JVM的作用就是...
MiniJavaVM—个Java虚拟机的设计和实现 在本篇文章中,我们将详细介绍 MiniJavaVM 的设计和实现,包括其总体架构、功能、运行环境和开发工具,以及具体的实现步骤。 第一章绪论 Java 虚拟机(Java Virtual ...
4. **执行引擎**:解释执行器或即时编译器(JIT)是JVM执行字节码的关键。书中会介绍如何设计和实现这些组件,以执行字节码指令。 5. **异常处理和多线程**:JVM支持异常处理和多线程,书中会讲解如何在Go中实现...
执行引擎通过字节码验证器(Bytecode Verifier)确保字节码符合安全规范,然后通过解释器或即时编译器(Just-In-Time Compiler, JIT)将字节码转换为机器码执行。 4. **字节码验证器(Bytecode Verifier)**:在代码执行...
- **程序计数器**:记录正在执行的虚拟机字节码指令的地址。 3. **类加载机制**: - **加载**:JVM通过类加载器加载Class文件。 - **连接**:连接又分为验证、准备、解析三个阶段。 - **初始化**:类的静态变量...
执行引擎通过解释器逐条解释执行,或者使用即时编译器(JIT)将字节码转换成机器码执行。 #### 6. 字节码指令集 Java字节码指令集是一组操作码,用于指导JVM如何处理特定的任务。它包括加载和存储指令、运算指令、...
字节码执行引擎则是JVM的心脏,它通过解释器或即时编译器(JIT)将字节码转换为机器码。JIT技术能够在运行时动态优化代码,显著提升性能。此外,JVM的垃圾收集机制是其内存管理的关键,它自动回收不再使用的对象,...
3. 执行引擎:负责解释和执行字节码,包括解释器和JIT(Just-In-Time)编译器。解释器逐条执行字节码,而JIT编译器将热点代码编译成本地机器码,以提高执行效率。 4. 内存管理:主要涉及垃圾收集机制,自动回收不再...
它详细阐述了Java虚拟机(JVM)的工作原理,包括内存管理、类加载机制、字节码执行以及垃圾回收等核心概念。深入理解这些知识点对于提升程序性能、解决运行时问题以及设计高效的应用程序至关重要。 1. **JVM架构** ...
4. 执行引擎:执行引擎负责执行字节码指令。它可以通过解释执行的方式逐条将字节码指令转换成机器码执行,也可以使用即时编译器(JIT)将热点代码编译成高效的本地代码执行。JIT编译器可以在运行时优化代码的性能。 ...
Java虚拟机(JVM)是Java程序运行的核心组件,它负责执行字节码,使得Java具有跨平台的能力。...虽然它可能不包含完整的JVM特性,但对于学习JVM的内部机制和字节码执行过程而言,是一个非常有价值的实践项目。
执行引擎是JVM的核心,它根据指令集执行字节码。JVM支持约248个字节码,每个字节码对应一个特定的操作。解释器负责逐行解释执行字节码,而JIT编译器则用于优化性能,将频繁执行的代码段编译成本地机器代码。 JVM的...
本书共分20章,第1-4章解释了java虚拟机的体系结构,包括java栈、堆、方法区、执行引擎等;第5-20章深入描述了java技术的内部细节,包括垃圾收集、java安全模型、java的连接模型和动态扩展机制、class文件、运算及...
2. **执行引擎**(Execution Engine):负责解释或编译字节码为机器码并执行。它根据不同的平台可以有不同的实现方式,如解释执行、即时编译(Just-In-Time Compilation)等。 3. **运行时数据区**(Runtime Data ...
4. **字节码执行**:JVM通过解释器或即时编译器(JIT)来执行字节码。解释器逐行解释执行,而JIT将热点代码编译成机器码,提升运行效率。 5. **内存管理**:JVM自动进行垃圾收集,管理堆内存。它采用分代收集算法,...
这本书详尽地阐述了JVM的工作原理、内存管理、类加载机制、字节码执行以及性能优化等多个核心主题,对于Java开发者和系统架构师来说,是不可或缺的参考书籍。 在Java编程中,JVM起着至关重要的作用。它是Java平台的...