虚拟机是相对于物理机的概念。 物理机的执行引擎是直接创建在处理器,缓存,指令集合操做系统底层上。 虚拟机的执行引擎是创建在软件之上,不受物理条件限制,定制指令集与执行引擎。 虚拟机实现中,执行过程能够是解释执行和编译执行,能够单独选择,或者混合使用。 但全部虚拟机引擎从统一外观(Facade)来讲,都是输入字节码二进制流,字节码解析执行,输出执行结果。html
本章从概念角度讲解虚拟机的方法调用和字节码执行。java
Java虚拟机以方法做为最基本的执行单元。每一个方法在执行时,都会有一个对应的栈帧(Stack Frame) .栈帧同时也是虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操做数栈、动态链接和方法返回地址等信息。 一个栈帧须要多大的局部变量表,须要多深的操做数栈,早在编译成字节码时就写到了方发表的Code属性中。 Code: stack=2, locals=1, args_size=1
所以一个栈帧须要分配多少内存,在运行前就已肯定,取决于源码和虚拟机自身实现。 缓存
局部变量表容量最小单位为变量槽(Variable Slot), 《Java虚拟机规范》规定一个变量槽能够存放一个boolean,byte,char,init,float,reference或returnAddress类型的数据。32位系统能够是32位,64位系统能够是64位去实现一个变量槽。对于64位的数据类型(long和double),以高位对齐的方式分配两个连续的变量槽。
复制代码
因为是线程私有,不管两个连续变量槽的读写是否为原子操做,都不会有线程安全问题。安全
当一个方法被调用时,会把参数值放到局部变量表中。类方法参数Slot从0开始。实例方法参数Slot从1开始,Slot0给了this,指向实例。
复制代码
咱们比较类方法和实例方法的字节码。markdown
public static int add(int a, int b) {return a + b;}
public int remove(int a, int b) {return a - b;}
复制代码
public static int add(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 a I
0 4 1 b I public int remove(int, int);
flags: ACC_PUBLIC
Code:
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Lexecute/Reerer;
0 4 1 a I
0 4 2 b I
复制代码
当变量的做用域小于整个方法体时,变量槽能够复用,为了节约栈内存空间。好比 {},if(){}等代码块内。变量槽复用会存在“轻微反作用”,内存回收问题。app
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
//执行结果
[GC (System.gc()) 69468K->66040K(251392K), 0.0007701 secs]
[Full GC (System.gc()) 66040K->65934K(251392K), 0.0040938 secs] //解释: 虽然placeholder的做用域被限制,但gc时,局部变量表仍然引用placeholder,没法被回收。 复制代码
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int i=0;
System.gc();
}
//执行结果
[GC (System.gc()) 69468K->66040K(251392K), 0.0007556 secs]
[Full GC (System.gc()) 66040K->398K(251392K), 0.0044978 secs] //解释: 虽然placeholder的做用域被限制,int i=0复用了slot0,切断了局部变量表的引用placeholder。 复制代码
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
placeholder=null;
}
System.gc();
}
//执行结果
[GC (System.gc()) 69468K->66088K(251392K), 0.0022762 secs]
[Full GC (System.gc()) 66088K->398K(251392K), 0.0050265 secs] //解释 主动释放placeholder 复制代码
类变量在准备阶段,会被赋默认零值。而局部变量没有准备阶段。因此下面代码是编译不经过的,即使编译经过,在检验阶段,也会被发现,致使类加载失败。ide
public static void fun4(){
int a;
//编译失败,Variable ‘a’ might not have been initialized
System.out.println(a);
}
复制代码
操做数栈(Operand Stack) 字节码指令读取和写入操做数栈。操做数栈中元素的数据类型必须与指令序列严格匹配。编译阶段和类检验阶段都会去保证这个。 在大多数虚拟机实现中,上面栈帧的操做数栈与下面栈帧的局部变量会有一部分重叠,这样不只节约了空间,重要的是在方法调用时直接公用数据,无须而外的参数复制。this
在类加载过程当中,会把符号引用解析为直接引用。方法调用指令以常量池中的符号引用为参数。这些方法符号引用一部分在类加载或者第一次使用时转化为直接引用,这种转化被称为静态解析。另一部分则须要在每次运行期间转化为直接引用,这部分称之为动态链接。spa
正常调用完成和异常调用完成。 恢复主调方法的执行状态。操作系统
Java虚拟机中的5条方法调用指令:
方法按照类加载阶段是否能转化成直接引用分类,能够分为:
非虚方法的调用称之为解析(Resolution),"编译器可知,运行期不可变",即类加载阶段把符号引用转化为直接引用。 而另一个方法调用的方式称之为分派(Dispatch)。
分派(Dispatch)是静态或者动态的,又或者是单分派或者多分派。重载或者重写会出现同名方法。同名方法的选择,我能够称之为分派
Method Overload Resolution, 这部份内容实际上叫作方法重载解析。静态分派发生在编译阶段。 先来看一段代码,sayHello方法重载。
//方法静态分派
public class StaticDispatch {
static abstract class Human{}
static class Man extends Human{}
static class Woman extends Human{}
public static void sayHello(Man man){System.out.println("hello,gentleman!"); }
public static void sayHello(Human guy){ System.out.println("hello,guy!");}
public static void sayHello(Woman women){System.out.println("hello,lady!");}
public static void main(String[] args) {
Human man=new Man();
Human woman=new Woman();
StaticDispatch dispatch=new StaticDispatch();
dispatch.sayHello(man);
dispatch.sayHello(woman);
}
}
//执行结果:
hello,guy!
hello,guy!
复制代码
对应Class字节码
public static void main(java.lang.String[]);
Code:
stack=2, locals=3, args_size=1
0: new #7 // class execute/StaticDispatch$Man
3: dup
4: invokespecial #8 // Method execute/StaticDispatch$Man."<init>":()V
7: astore_1
8: new #9 // class execute/StaticDispatch$Woman
11: dup
12: invokespecial #10 // Method execute/StaticDispatch$Woman."<init>":()V
15: astore_2
16: new #11 // class execute/StaticDispatch
19: dup
20: invokespecial #12 // Method "<init>":()V
23: astore_3
24: aload_3
25: aload_1
26: invokevirtual #13 // Method sayHello:(Lexecute/StaticDispatch$Human;)V
29: aload_3
30: aload_2
31: invokevirtual #13 // Method sayHello:(Lexecute/StaticDispatch$Human;)V
34: return
复制代码
第0~15行,咱们构建了Man对象和Woman对象,并放入了局部变量表中。 第26行,执行方法Method sayHello:(Lexecute/StaticDispatch Human;)V, 实际执行的都是sayHello(Human)。而不是sayHello(Man)或者sayHello(Woman)。 这里涉及到两个类型:
编译期并不知道对象的实际类型,因此按照对象的静态类型去分派方法。
与重写(Override)密切关联。动态分派发生在运行期间。在运行时,肯定方法的接收者(方法所属对象)
//方法动态分派
public class DynamicDispatch {
static abstract class Human {
public void sayHello() {System.out.println("hello,guy!");}
}
static class Man extends Human {
public void sayHello() {System.out.println("hello,gentleman!"); }
}
static class Woman extends Human {
public void sayHello() {System.out.println("hello,lady!");}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();//hello,gentleman!
woman.sayHello();//hello,lady!
man = new Woman();
man.sayHello();//hello,lady!
}
}
//执行结果:
hello,gentleman!
hello,lady!
hello,lady!
复制代码
对应字节码
0: new #2 // class execute/DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method execute/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4 // class execute/DynamicDispatch$Woman
11: dup
12: invokespecial #5 // Method execute/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method execute/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method execute/DynamicDispatch$Human.sayHello:()V
24: new #4 // class execute/DynamicDispatch$Woman
27: dup
28: invokespecial #5 // Method execute/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method execute/DynamicDispatch$Human.sayHello:()V
36: return
复制代码
第7行,astore_1存储了Man对象 第15行,astore_2存储了Woman对象 第16,17行,aload_1,invokevirtual.实际调用的是Man.sayHello()方法 第20,21行,aload_2,invokevirtual.实际调用的是Woman.sayHello()方法 第31行,astore_1存储了Woman对象 第32,33行,aload_1,invokevirtual.实际调用的是Woman.sayHello()方法
运行期间,选择是根据man和woman对象的实际类型分派方法。
小知识:字段永远不参与多态,方法中访问的属性名始终是当前类的属性。子类会遮蔽父类的同名字段
方法的宗量:方法的接收者与方法的参数 单分派:基于一种宗量分派 多分派:基于多种宗量分派。 当前Java语言是一门静态多分派,动态单分派的语言。编译期根据方法接收者和参数肯定方法的符号引用。运行期根据方法的接收者,解析和执行符号引用。
考虑下面一段代码
public class Dispatch {
static class Father{
public void f() {System.out.println("father f void");}
public void f(int value) {System.out.println("father f int");}
}
static class Son extends Father{
public void f(int value) {System.out.println("Son f int"); }
public void f(char value) { System.out.println("Son f char");}
}
public static void main(String[] args) {
Father son=new Son();
son.f('a');
}
}
//执行结果: Son f int
复制代码
字节码
0: new #2 // class execute/Dispatch$Son
3: dup
4: invokespecial #3 // Method execute/Dispatch$Son."<init>":()V
7: astore_1
8: aload_1
9: bipush 97
11: invokevirtual #4 // Method execute/Dispatch$Father.f:(I)V
复制代码
首先是编译期的静态分派,先选择静态类型Father,因为Father中没有f(char),则会选择最合适的f(int),肯定方法为Father.f:(I)V。其次是运行期,接收者为Son,Son中有重写的f:(I)V。因此最终执行的是Son.f:(I)V
虚方法表,接口方法表,类型继承分析,守护内联,内联缓存
动态类型语言的关键特征:类型检查的主体过程是在运行期而不是编译器,好比说Groovy、JavaScript、Lisp、Lua、Python。 静态类型语言:编译器就进行类型检查,好比C++,Java。
Java虚拟机须要支持动态类型语言,因而在JDK7发布 invokedynamic指令。
略
略
略
略
public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
复制代码
stack=2, locals=4, args_size=1
0: bipush 100 //常量100压入操做数栈顶
2: istore_1 //栈顶元素(100)存入变量槽1,同时消费掉栈顶元素
3: sipush 200 //常量200压入操做数栈顶
6: istore_2 //栈顶元素(200)存入变量槽2,同时消费掉栈顶元素
7: sipush 300 //常量300压入操做数栈顶
10: istore_3 //栈顶元素(300)存入变量槽3,同时消费掉栈顶元素
11: iload_1 //将局部变量slot1值100压入操做数栈顶
12: iload_2 //将局部变量slot2值200压入操做数栈顶
13: iadd //消费栈顶100和200,获得300,并压入栈顶
14: iload_3 //将局部变量slot3值300压入操做数栈顶
15: imul //消费栈顶300和300,获得90000,并压入栈顶
16: ireturn //消费栈顶90000,整型结果返回给方法调用者
复制代码