开发的Java虚拟机栈Java虚拟机以方法为基本执行单元,“栈帧(StackFrame)”是用来支持Java虚拟机进行方法调用和方法的基本数据结构执行。每个堆栈帧包含局部变量表、操作数堆栈、动态链接、方法返回地址和一些额外的附加信息(例如与调试和性能电话相关的信息)。1)局部变量表局部变量表(LocalVariablesTable)用来保存方法中的局部变量和方法参数。当Java源代码文件被编译成class文件时,局部变量表的最大容量就已经确定了。我们来看这样一段代码。publicclassLocalVaraiablesTable{privatevoidwrite(intage){Stringname="XiaoWang";}}write()方法有一个参数age和一个局部变量名。然后使用IntellijIDEA的jclasslib查看编译后的字节码文件LocalVaraiablesTable.class。可以看到在write()方法的Code属性中,Maximumlocalvariables(局部变量表的最大容量)的值是3,按理说局部变量表的最大容量应该是2,一个年龄,一个名字,为什么是3?当一个成员方法(非静态方法)被调用时,第0个变量其实就是调用该成员方法的对象引用,也就是大名鼎鼎的this。调用方法write(18)实际上调用了write(this,18)。单击Code属性并查看LocalVaraiableTable以查看详细信息。第0个是这个,类型是LocalVaraiablesTable对象;第一个是方法参数age,类型是integerint;第二个是方法内部的局部变量名,类型是String。当然,局部变量表的大小并不是方法中所有局部变量个数的总和,它与变量的类型和变量的作用域有关。当局部变量的范围结束时,它在局部变量表中的位置被下一个局部变量替换。看看下面的代码。publicstaticvoidmethod(){//①if(true){//②Stringname="SilentKingTwo";}//③if(true){//④intage=18;}//⑤}method()方法的局部变量表大小为1,因为是静态方法,所以不需要将this作为局部变量表的第一个元素;②中,局部变量有名字,局部变量表的大小变为1;in③name变量的作用域结束;④处,局部变量有年龄,局部变量表的大小为1;在⑤处,局部年龄变量的作用域结束;关于局部变量的作用域,《Effective Java》文章57建议:尽量减少局部变量的作用域,可以增强代码的可读性和可维护性,减少出错的可能性。在这里,我还有一点要提醒大家。为了尽可能节省栈帧所消耗的内存空间,局部变量表中的槽位可以被重用,如method()方法所示,这意味着合理的作用域有助于提高性能程序。局部变量表的容量是基于槽(slot)的。一个槽可以容纳一个32位的数据类型(比如int,当然《Java 虚拟机规范》没有明确表示一个槽应该占用的内存空间大小,但是我觉得这样比较容易理解),数据类型比如显式占用64位的float和double占用两个相邻的槽位。看看下面的代码。publicvoidsolt(){doubled=1.0;inti=1;}从jclasslib可以看出,solt()方法的Maximumlocalvariables的值为4,为什么等于4呢?带这个只有3?查看LocalVaraiableTable可以理解,变量i的下标为3,也就是说变量d占用了两个槽位。2)操作数栈与局部变量表相同。操作数栈(OperandStack)的最大深度也在编译时确定,写入Code属性的maximumstacksize。当一个方法第一次执行时,操作数栈是空的。在方法执行过程中,各种字节码指令会向操作数栈写入和取出数据,即push和pop操作。看看下面的代码。publicclassOperandStack{publicvoidtest(){add(1,2);}privateintadd(inta,intb){returna+b;}}OperandStack类一共有2个方法,在test()中调用methodadd()方法,传递2个参数。用jclasslib可以看到,test()方法的最大栈大小的值为3,这是因为调用成员方法时,this和所有参数都被压入栈中,而this和参数被弹出调用完成后一个一个的栈。通过“字节码”面板可以查看相应的字节码指令。aload_0用于将局部变量表中下标为0的引用类型变量即this加载到操作数栈中;iconst_1用于将整数1装入操作数栈;iconst_2用于将整数2加载到操作数栈中;invokevirtual用于调用对象的成员方法;pop用于弹出栈顶的值;return是void方法的返回指令。让我们再看看add()方法的字节码指令。iload_1用于将局部变量表中下标为1的int类型变量加载到操作数栈中(下标0就是这个);iload_2用于将局部变量表中下标为2的int类型变量加载到操作数栈上;iadd用于int类型的加法运算;ireturn是返回值为int的方法的返回指令。操作数中的数据类型必须与字节码指令匹配。以上面的iadd指令为例,该指令只能用于整型数据的加法运算。执行时,栈顶的两个数据必须是int类型的,iadd命令不能添加一个long类型和一个double类型的数据。3)动态链接每个栈帧都包含对运行时常量池中栈帧所属方法的引用。持有这个引用是为了支持方法调用期间的动态链接(DynamicLinking)。看看下面的代码。publicclassDynamicLinking{staticabstractclassHuman{protectedabstractvoidsayHello();}staticclassManextendsHuman{@OverrideprotectedvoidsayHello(){System.out.println("男人哭不是罪");}}staticclassWomanextendsHuman{@OverrideprotectedvoidsayHello(){System.out.println("下山女人为虎");}}publicstaticvoidmain(String[]args){Humanman=newMan();人类女人=newWoman();man.sayHello();女人.sayHello();男人=新女人();man.sayHello();}}如果你懂Java重写,应该能看懂这段代码的意思。Man类和Woman类继承了Human类并重写了sayHello()方法。来看看运行结果:mancry,cry,notasin.山下女子为虎。运行结果很容易理解。man的引用类型是Human,但是指向的是Man对象,woman的引用类型也是Human,但是指向的是Woman对象;稍后,man指向新的Woman对象。从面向对象编程和多态的角度,我们对运行结果很好理解,但是从Java虚拟机的角度来看,它是如何决定男人和女人应该调用哪个方法的呢?使用jclasslib查看main方法的字节码说明。第1行:new指令创建一个Man对象,并将该对象的内存地址压入堆栈。第2行:dup指令复制栈顶的值并将其压入栈顶。因为后面的invokespecial指令会消耗当前类的引用,所以需要复制一份。第3行:invokespecial指令用于调用构造函数进行初始化。第4行:astore_1,Java虚拟机从栈顶弹出Man对象的引用,然后存入下标为1的局部变量man中。第5、6、7、8行指令类似到第1、2、3和4行,但不同之处在于Woman对象。第9行:aload_1指令将第一个局部变量man压入操作数堆栈。第10行:invokevirtual指令调用对象的成员方法sayHello()。注意此时的对象类型是com/itwanger/jvm/DynamicLinking$Human。第11行:aload_2指令将第一个局部变量woman压入操作数堆栈。第12行和第10行一样,注意从字节码的角度来看,man.sayHello()(第10行)和woman.sayHello()(第12行)的字节码是一样的,但是我们都知道这两个instructions最终的执行目标方法是不一样的。发生了什么?我们要从invokevirtual指令开始,看看它是如何实现多态的。根据《Java 虚拟机规范》,invokevirtual指令在运行时的解析过程可以分为以下几个步骤:①找到操作数栈顶元素指向的对象的实际类型,记为C。②。如果在类型C中找到与常量池中的描述符相匹配的方法,则进行访问权限检查。如果通过,则返回该方法的直接引用,搜索结束;否则,返回java.lang.IllegalAccessError。③.否则,按照继承关系从下到上进行第二步C的各个父类的查找验证。④.如果找不到合适的方法,则抛出java.lang.AbstractMethodError异常。也就是说,invokevirtual指令在第一步就确定了运行时的实际类型,所以两次调用中的invokevirtual指令并没有以将常量池中方法的符号引用解析为直接引用而结束。根据方法接收者的实际类型选择方法版本的过程是Java重写的本质。我们将这种在运行时根据实际类型确定方法执行版本的过程称为动态链接。4)方法返回地址当一个方法开始执行时,方法退出只有两种方式:正常退出,可能有返回值传递给上层方法调用者,方法是否有返回值和类型方法返回值的返回值指令来决定,前面说过,ireturn用于返回int类型,return用于void方法;还有其他的,lreturn用于long类型,freturn用于float,dreturn用于double,areturn用于引用类型。异常退出。该方法在执行过程中遇到异常,没有得到妥善处理。在这种情况下,它不会向其上层调用者返回任何值。无论哪种方式退出,方法退出后,必须回到原来调用该方法的位置,程序才能继续执行。一般来说,当方法正常退出时,PC计数器的值会作为返回地址,这个计数器的值很可能会保存在栈帧中,但方法异常退出时则不会。方法退出的过程实际上相当于弹出当前栈帧,所以接下来可能的操作是:恢复上层方法的局部变量表和操作数栈,将返回值(如果有的话)压入调用者栈中帧的操作数栈,调整PC计数器的值,找到下一条要执行的指令等。
