方法栈
JVM内存模型中,有一个栈结构,就是java方法栈,栈里面存放的一个个实体类称为栈帧。
每个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。
局部变量表
局部变量表用于存放方法参数和方法内部定义的局部变量,局部变量表的容量一般以32位为最小单位。
如果方法是非static方法,那局部变量表中第0位所以一般是实例对象的引用this。
操作数栈
JVM解析执行字节码是基于栈结构的,做算数运算时是通过操作数栈来进行的,在调用其他方法时是通过操作数栈来进行参数的传递。
方法调用过程
每一次方法调用指令之前,JVM先把方法被调用的对象引用压入操作数栈中,除了对象的引用之外,JVM还会把方法的参数依次压入操作数栈
在执行方法调用指令时,JVM会将函数参数和对象引用依次从操作数栈弹出,并新建一个栈帧,把对象引用和函数参数分别放入新栈帧的局部变量表
jvm把新栈帧push入虚拟机方法栈,并把pc指向函数的第一条待执行的指令。
方法调用的字节码指令
JVM提供了四种方法调用字节码指令
invokestatic:调用静态方法
invokespecial:调用实例构造器方法,私有方法,实例构造器,父类方法
invokevirtual:调用所有的虚方法
invokeinterface:调用接口方法,会在运行时期在确定一个实现此接口的对象
1 2 3 4 5 6 7 8 9 10 11 12
| public class Test { private void run() { List<String> list = new ArrayList<>(); // invokespecial 构造器调用 list.add("a"); // invokeinterface 接口调用 ArrayList<String> arrayList = new ArrayList<>(); // invokespecial 构造器调用 arrayList.add("b"); // invokevirtual 虚函数调用 } public static void main(String[] args) { Test test = new Test(); // invokespecial 构造器调用 test.run(); // invokespecial 私有函数调用 } }
|
字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return
private void run(); Code: 0: new #2 // class java/util/ArrayList 3: dup 4: invokespecial #3 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #4 // String a 11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 16: pop 17: new #2 // class java/util/ArrayList 20: dup 21: invokespecial #3 // Method java/util/ArrayList."<init>":()V 24: astore_2 25: aload_2 26: ldc #6 // String b 28: invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 31: pop 32: return
public static void main(java.lang.String[]); Code: 0: new #8 // class Test 3: dup 4: invokespecial #9 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokespecial #10 // Method run:()V 12: return }
|
动态分派
当JVM遇到invokevirtual或者invokeinterface时,需要运行时根据方法的符号引用查到方法地址,步骤如下
- 在方法调用指令之前,需要将对象的引用压入操作数栈
- 在执行方法调用时,找到操作数栈顶的第一个元素所指向的对象实际类型,记做C
- 在类型C中找到与常量池中的描述符和方法名称都相符的方法,并校验访问权限,如果找到该方法并通过校验,则返回这个方法的引用。
- 否则,按照继承关系往上查找方法并校验访问权限
- 如果始终没找到方法,则抛出AbstractMethodError异常
虚函数表
java通过虚函数表实现多态。
方法表中包含了所有方法的入口地址,继承父类的方法在最前面,之后是接口方法,最后是自定义方法。
如果子类重写了父类的方法,那么地址会是子类方法的地址。否则则会指向父类的地址。
invokevirtual 和 invokeinterface的区别
由于虚函数在编译时就可以确定offset,而实现了接口类型的类,直接使用接口方法的话,由于此时不确定其类型,会重新找一遍虚函数表,速度会降低。
因此在使用接口方法的时候,尽可能直接使用原有的类,而非使用接口类去转型。
不过个人认为在考虑架构的时候往往不可能照顾的那么仔细,此条理解即可。