一支菜鸟 2019-06-28
本文参考自来自周志明《深入理解Java虚拟机(第2版)》,拓展内容建议读者可以阅读下这本书。
文字版如下:
一个存储单位称为一个Slot(32位)
为了让所有数据类型的局部变量都能够存储到局部变量表中而设定了定长的Slot长度
局部变量第0位Slot
Slot重用:离开了作用域的局部变量占用的Slot可以被重用
对垃圾回收的影响:离开了作用域的局部变量所在的Slot只有在真的不存在对原有局部变量的引用时,GC才会回收相应的对象实例
局部变量和类字段的区别
生命周期不同
赋初值不同
reference类型
一个存储单位称为一个栈容量
为了让所有数据类型的局部变量都能够存储到栈中而设定了定长的栈容量
为了满足方法指令的运行时解析
退出方法的方式
退出方法执行的操作
非虚方法:在类加载的时候就可以将符号引用解析为直接引用的方法,即不存在因覆盖而产生多版本的方法
虚方法:在类加载的时候有可能无法确定符号引用可以解析为哪个直接引用的方法
invokestatic
invokespecial
invokevirtual
invokeinterface
invokedynamic
静态分派 Method Overload Resolution
典型应用是方法重载 Overload
invokevirtual 选择的方法版本的符号引用
动态分派
典型应用是方法重写 Override
invokevirtual 选择的方法版本的符号引用
类B的方法m(P)重写了B所继承的类A的方法m(P),实际类型为B,A b = new B(),即静态类型为A的对象b,发生了方法调用b.m(p)
invokevirtual A.m
运行期
在B中寻找与invokevirtual调用的方法符号引用名称和签名一致的方法
找到了的话说明实际类型B中就有了这个方法m(P),然后判定该方法是否满足访问权限
没找到
在B的父类中自下向上寻找与invokevirtual调用的方法符号引用名称和签名一致的方法
找到了的话说明实际类型B的父类中有这个方法m(P),然后判定该方法是否满足访问权限
单分派:根据单个方法宗量进行方法选择
多分派:根据多个方法宗量进行方法选择
稳定优化手段:虚方法表 提前记录避免运行时搜索的手段
非稳定的激进优化手段
源码定义了继承了Human
的Man
和Woman
,关键在于看一下编译器将test.sayHi(human);
、test.sayHi(woman);
、test.sayHi(man);
编译成了什么,也就是说静态阶段编译器重载方法的方法分派是什么样的。
class Human{ } class Man extends Human{ } class Woman extends Human{ } public class DispatchTest { public void sayHi(Human human){ System.out.println("Hi human"); } public void sayHi(Man man){ System.out.println("Hi man"); } public void sayHi(Woman woman){ System.out.println("Hi woman"); } public static void main(String[] args) { Human human = new Human(); Human man = new Man(); Human woman = new Woman(); DispatchTest test = new DispatchTest(); test.sayHi(human); test.sayHi(woman); test.sayHi(man); } }
javap获取到的class字节码解释:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=5, args_size=1 0: new #7 // class Human 3: dup 4: invokespecial #8 // Method Human."<init>":()V 7: astore_1 8: new #9 // class Man 11: dup 12: invokespecial #10 // Method Man."<init>":()V 15: astore_2 16: new #11 // class Woman 19: dup 20: invokespecial #12 // Method Woman."<init>":()V 23: astore_3 24: new #13 // class DispatchTest 27: dup 28: invokespecial #14 // Method "<init>":()V 31: astore 4 33: aload 4 35: aload_1 36: invokevirtual #15 // Method sayHi:(LHuman;)V 39: aload 4 41: aload_3 42: invokevirtual #15 // Method sayHi:(LHuman;)V 45: aload 4 47: aload_2 48: invokevirtual #15 // Method sayHi:(LHuman;)V 51: return LocalVariableTable: Start Length Slot Name Signature 0 52 0 args [Ljava/lang/String; 8 44 1 human LHuman; 16 36 2 man LHuman; 24 28 3 woman LHuman; 33 19 4 test LDispatchTest;
这段字节码的表义很清晰,就是创建了类型为Human
的Man
和Woman
的三个类的对象实例,然后在执行test.sayHi(human);
、test.sayHi(woman);
、test.sayHi(man);
的时候都将其编译成了调用方法sayHi:(LHuman;)V
,即参数类型是Human
的sayHi
方法。从这里我们可以看出来实际上静态编译重载方法的时候的只会使用方法参数的静态类型指定的方法,而实际类型由于在运行时可能会发生变化,没有办法在编译期获得其实际类型,因此采用了这种方式直接确定方法的分派结果。
源码定义了继承了Human
的Man
和Woman
,关键在于看一下编译器将man.sayHi();
、woman.sayHi();
编译成了什么,也就是说静态阶段编译器对重写方法的方法分派是什么样的。
abstract class Human{ protected abstract void sayHi(); } class Man extends Human{ @Override protected void sayHi() { System.out.println("Hi man"); } } class Woman extends Human{ @Override protected void sayHi() { System.out.println("Hi woman"); } } public class DispatchTest { public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); DispatchTest test = new DispatchTest(); man.sayHi(); woman.sayHi(); } }
javap获取到的class字节码解释:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: new #2 // class Man 3: dup 4: invokespecial #3 // Method Man."<init>":()V 7: astore_1 8: new #4 // class Woman 11: dup 12: invokespecial #5 // Method Woman."<init>":()V 15: astore_2 16: new #6 // class DispatchTest 19: dup 20: invokespecial #7 // Method "<init>":()V 23: astore_3 24: aload_1 25: invokevirtual #8 // Method Human.sayHi:()V 28: aload_2 29: invokevirtual #8 // Method Human.sayHi:()V 32: return LocalVariableTable: Start Length Slot Name Signature 0 33 0 args [Ljava/lang/String; 8 25 1 man LHuman; 16 17 2 woman LHuman; 24 9 3 test LDispatchTest;
这段字节码创建了类型为Man
和Woman
的两个类的对象实例,然后在执行man.sayHi();
、woman.sayHi();
的时候都将其编译成了调用方法Human.sayHi:()V
,即Human
类型的sayHi
方法。从这里我们可以看出来实际上静态编译重写方法的时候的只会使用实例对象的静态类型的方法,而实际类型由于在运行时可能会发生变化,没有办法在编译期获得其实际类型,因此采用了这种方式直接确定方法的分派结果。但是在运行时会去获取这个实例对象的实际类型,然后看这个实际类型是否定了名称和限定符满足的方法,如果有就会选择分派到这个方法上。这就是运行时动态分派产生的影响,这种选择在编译器是看不出来的。
程序的分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器完成。Java堆是垃圾收集器管理的内存区域,因此也被称为“GC堆”。 3.指令流中大部分指令是零地址指令,执行过程依赖于栈操作。