JVM 四虚拟机栈
虚拟机栈出现的背景
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
内存中的栈与堆
有不少Java开发人员一提到Java内存结构,就会非常粗粒度地将JVM中的内存区理解为仅有Java堆(heap)和Java栈(stack),他们的关系是,栈是运行时的单位,而堆是存储的单位
● 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
● 堆解决的是数据存储的问题,即数据怎么放,放哪里

Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,是线程私有的。它的生命周期和线程一致(上一章已经说过的了,栈是线程独有的)
作用
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
栈的特点
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
JVM直接对Java栈的操作只有两个:
● 每个方法执行,伴随着进栈(入栈、压栈)
● 执行结束后的出栈工作
对于栈来说不存在垃圾回收问题(栈存在溢出的情况)

栈中可能出现的异常
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的
● 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError 异常。
● 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutOfMemoryError 异常。
案例(我们仅演示第一点,因为第二个问题需要在我们虚拟机内存不够的时候才会报)

/*** 测试结果,main递归执行11417次栈溢出** 设置栈的大小为:-Xss256k** 修改后只递归2000次便溢出,修改生效***/
public class StackTest {public static int count;public static void main(String[] args) {System.out.println(count);count++;main(args);}
}
栈中存储什么?
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
/*** 测试手段:三个方法嵌套运行debug* 打印如下:* main方法开始执行* method1 begin* method2 begin* method3 begin* method3 end* method2 will end(wait return)* method1 end* main方法正常结束*/
public class StackWorkTest {public static void main(String[] args) throws Exception{System.out.println("main方法开始执行");StackWorkTest stackWorkTest = new StackWorkTest();try {stackWorkTest.method01();}catch (Exception e) {e.printStackTrace();}System.out.println("main方法正常结束");}//整体是嵌套关系public void method01(){System.out.println("method1 begin");//执行到这里时method1是当前栈帧method02();System.out.println("method1 end");//执行到这里(回到method1时)method1是当前栈帧
// int i = 10 / 0;return; //这里写和不写都是一样的,void方法最终都会有默认的return;当然我们可以通过return;使得方法提前结束}public int method02(){System.out.println("method2 begin");int i = 10;int v = (int) method03();System.out.println("method2 will end(wait return)");return v;}public double method03(){System.out.println("method3 begin");double i = 20.0;System.out.println("method3 end");return i;}
}
结束方式:return or exception without try-catch,上面是方法正常结束的例子
package com.sobot.net.jvm;/*** 异常时打印如下** main方法开始执行* method1 begin* method2 begin* method3 begin* method3 end* method2 will end(wait return)* method1 end* Exception in thread "main" java.lang.ArithmeticException: / by zero* at com.sobot.net.jvm.StackWorkTest.method01(StackWorkTest.java:39)* at com.sobot.net.jvm.StackWorkTest.main(StackWorkTest.java:30)*/
public class StackWorkTest {public static void main(String[] args) throws Exception{System.out.println("main方法开始执行");StackWorkTest stackWorkTest = new StackWorkTest();
// try {
// stackWorkTest.method01();
// }catch (Exception e) {
// e.printStackTrace();
// }stackWorkTest.method01();System.out.println("main方法正常结束");}//整体是嵌套关系public void method01(){System.out.println("method1 begin");//执行到这里时method1是当前栈帧method02();System.out.println("method1 end");//执行到这里(回到method1时)method1是当前栈帧int i = 10 / 0;return;}public int method02(){System.out.println("method2 begin");int i = 10;int v = (int) method03();System.out.println("method2 will end(wait return)");return v;}public double method03(){System.out.println("method3 begin");double i = 20.0;System.out.println("method3 end");return i;}
}
栈帧的内部结构
每个栈帧中存储着:
● 局部变量表(Local Variables)
● 操作数栈(operand Stack)(或表达式栈)
● 动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
● 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
● 一些附加信息

并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的,而栈中栈帧的数目又受到栈帧大小的影响

局部变量表(Local Variables)
● 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
● 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
● 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
● 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。 参考上一个案例,默认main方法可以嵌套执行11400次,但我们改了栈的大小为256k时仅能嵌套2000多次
● 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
案例
package com.sobot.net.jvm;import java.util.Date;//局部变量表的大小在编译时就已经确定下来了
//普通方法入栈后栈中局部变量表都默认会有this,静态方法局部变量表没有this
public class LocalVariableTest {public LocalVariableTest() {}public LocalVariableTest(int count) {this.count = count;}private int count = 0;public static void main(String[] args) {LocalVariableTest localVariableTest = new LocalVariableTest();int num = 10;localVariableTest.test();}public void test(){Date date = new Date();int b = 10;double a = 10.0;String name = "atgwqqqqqqw";String weight = "asau";//如果不声明test变量,即这行代码变为test(date, name);时,局部变量表就没有test这个变量了String test = test(date, name);System.out.println(date + name);}public static void staticTest(){LocalVariableTest localVariableTest = new LocalVariableTest();Date date = new Date();int count = 10;System.out.println(count);//不能使用this,因为this变量不在当前方法的局部变量表里
// System.out.println(this.count);}public String test(Date date,String str) {return date + str;}public void test3(Date date,String str) {this.count++;}public void test4() {int a = 0;{int b = 0;b = a + 1;}int c = a + 1;}
}
main方法中的局部变量如下

起始pc指局部变量从什么时候生效的,数字代表的是字节码的行号,参考字节码如下

首先args参数从最开始,也就是字节码第0行就开始生效了,而localVariableTest在字节码第8行生效,num参数在第11行生效,长度即代表生效范围

计算公式为字节码长度减去起始PC
关于Slot的理解
● 局部变量表,最基本的存储单元是Slot(变量槽)
● 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
● 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
● 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
● byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
● JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
● 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
● 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或doub1e类型变量)
● 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。

关于占用曹的大小,我们以上个案例中的test方法演示,如下

int型占用一个槽(43-42),同理String类型的double类型都是占用两个槽
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。如上述案例中的test4方法,如下

我们可以看出方法test4中的变量b在出了括号后就已经无效了,所以b所占有的槽(index=2)就会重新分配给变量c,毕竟局部变量表的本质是一个数组结构,大小不会随笔变动,所以重复利用是最好的版本
总结
* 变量的分类* 按数据类型分类:基本数据类型和引用数据类型* 按照类中声明位置* 成员变量* * 类变量(静态成员变量),被类加载器加载后,在link的prepare阶段默认赋值,initalize阶段显示复杂(直接在声明类变量时赋值或者在静态代码快赋值)* * 实例变量,随着对象的创建在堆中进行赋值,如果没有进行显示赋值(比如构造方法为空或者压根都没有重写构造方法)那就默认赋值* 局部变量:使用前必须进行显示赋值,大家一试便知
补充说明
- 在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。因为局部变量表有很多引用类型的局部变量,这本质上是引用,执行了堆中真实存储的对象,所以这个地方如果处理不当也是容易引发OOM的;
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
4.4. 操作数栈(Operand Stack)
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
● 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈
● 比如:执行复制、交换、求和等操作

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。栈中的任何一个元素都是可以任意的Java数据类型
● 32bit的类型占用一个栈单位深度
● 64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
案例代码
public void testAddOperation() {//byte,short,boolen,short都是按int类型进行保存byte i = 15;int j = 8;int k = i + j;}
反编译其字节码文件如下
0 bipush 15 2 istore_13 bipush 85 istore_26 iload_17 iload_28 iadd9 istore_3
10 return
执行的第一步bipush命令:bipush命令时把int型变量(byte,short,boolen,short都是按int类型进行保存)入栈,然后pc寄存器保存当前命令行即为0

istore_1 是把栈顶的操作数保存到局部变量表中索引位置为1的地方,然后pc寄存器保存当前命令行即为2,后续同理,不再对pc寄存器做说明了

第3,4行命令是继续把j=8这个变量入栈然后存储到到局部变量表中索引位置为2的地方


第5,6行iload命令则是分别在局部变量表中索引为1,2的位置分别取出15和8入栈


在栈中做运算,执行iadd命令,iadd命令的执行需要依赖于执行引擎调用cpu来运算,然后再把结果入栈

再把栈中变量值为23的变量k存储到局部变量吧第3个位置上

然后就是return方法结束
补充:这里是void方法,执行到最后就通过return指令方法就正常结束了,如果是带返回值的类型,那么就里的return xxx除了起到结束方法(结束方法也意味着该方法对应栈帧的局部变量表和操作数栈的清空)的作用(同return)还会把返回值xxx返回给他的上一个调用方法的栈中,也是先入栈,随后通常会再存到局部变量表,这里也要注意
public int testAddOperation() {byte i = 15;int j = 8;return 10;}public int testAddOperation2() {byte i = 15;int j = 8;return 10;}public void test(){int i = testAddOperation();testAddOperation2();}
test()是int型方法,第一行我们定义了int i = testAddOperation(),这样会存入局部变量表的第一个位置里,第二行testAddOperation2();由于没有定义局部变量来接收返回值,所以不会存入局部变量表,如下test方法
0 aload_0 #刚开始时就把上一个栈帧中方法testAddOperation的返回值加载到当前栈帧(test方法)操作数栈中1 invokevirtual #2 <com/sobot/net/jvm/StackTest.testAddOperation : ()I>4 istore_1 #把变量i存入局部变量表的第一个位置里5 aload_0 #把上一个栈帧中方法testAddOperation2的返回值加载到当前栈帧(test方法)操作数栈中6 invokevirtual #3 <com/sobot/net/jvm/StackTest.testAddOperation2 : ()I>9 pop
10 return
前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

为什么需要运行时常量池呢?
常量池的作用:就是为了提供一些符号和常量,便于指令的识别
案例
public class DynamicLinkingTest {int num = 10;public void methodA() {System.out.println("MethodA");}public void methodB() {System.out.println("MethodB");methodA();num++;}
}
javap -v命令编译后
public class DynamicLinkingTestminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #9.#20 // java/lang/Object."<init>":()V#2 = Fieldref #8.#21 // DynamicLinkingTest.num:I#3 = Fieldref #22.#23 // java/lang/System.out:Ljava/io/PrintStream;#4 = String #24 // MethodA#5 = Methodref #25.#26 // java/io/PrintStream.println:(Ljava/lang/String;)V#6 = String #27 // MethodB#7 = Methodref #8.#28 // DynamicLinkingTest.methodA:()V#8 = Class #29 // DynamicLinkingTest#9 = Class #30 // java/lang/Object#10 = Utf8 num#11 = Utf8 I#12 = Utf8 <init>#13 = Utf8 ()V#14 = Utf8 Code#15 = Utf8 LineNumberTable#16 = Utf8 methodA#17 = Utf8 methodB#18 = Utf8 SourceFile#19 = Utf8 DynamicLinkingTest.java#20 = NameAndType #12:#13 // "<init>":()V#21 = NameAndType #10:#11 // num:I#22 = Class #31 // java/lang/System#23 = NameAndType #32:#33 // out:Ljava/io/PrintStream;#24 = Utf8 MethodA#25 = Class #34 // java/io/PrintStream#26 = NameAndType #35:#36 // println:(Ljava/lang/String;)V#27 = Utf8 MethodB#28 = NameAndType #16:#13 // methodA:()V#29 = Utf8 DynamicLinkingTest#30 = Utf8 java/lang/Object#31 = Utf8 java/lang/System#32 = Utf8 out#33 = Utf8 Ljava/io/PrintStream;#34 = Utf8 java/io/PrintStream#35 = Utf8 println#36 = Utf8 (Ljava/lang/String;)V
{int num;descriptor: Iflags:public DynamicLinkingTest();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: aload_05: bipush 107: putfield #2 // Field num:I10: returnLineNumberTable:line 3: 0line 4: 4public void methodA();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String MethodA5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 6: 0line 7: 8public void methodB();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=1, args_size=10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #6 // String MethodB5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: aload_09: invokevirtual #7 // Method methodA:()V12: aload_013: dup14: getfield #2 // Field num:I17: iconst_118: iadd19: putfield #2 // Field num:I22: returnLineNumberTable:line 10: 0line 11: 8line 12: 12line 13: 22
}
}
我们来分析methodA的调用,首先是调用了命令
9: invokevirtual #7 // Method methodA:()V
我们看 #7这个符号引用类似是方法引用 Methodref类型,又引用了 #8和#28
#7 = Methodref #8.#28 // DynamicLinkingTest.methodA:()V
#8是引用的class类型,即相当于引用当前的类的一个指针
#8 = Class #29 // DynamicLinkingTest
#29这个引用 只能在代表当前类
#29 = Utf8 DynamicLinkingTest
回到#28,即方法名与类型
#28 = NameAndType #16:#13 // methodA:()V
#16和#13分别如下,含义很直白标识方法名和类型
#13 = Utf8 ()V#16 = Utf8 methodA
这就是方法A的完整流程,
总之,类在加载的时候会在编译时把所需要的常量加载到运行时常量池里,然后方法在当前栈帧执行时就会通过一系列的符号引用指向运行时常量池
比如着一些最常见的
#29 = Utf8 DynamicLinkingTest#30 = Utf8 java/lang/Object#31 = Utf8 java/lang/System#32 = Utf8 out#33 = Utf8 Ljava/io/PrintStream;#34 = Utf8 java/io/PrintStream#35 = Utf8 println#36 = Utf8 (Ljava/lang/String;)V
这样最主要的时提高资源的复用性,因为栈是线程共享的,高并发环境下如果每个栈都要把这些资源加载一份那是不可能做到的,所以通过一个小小的引用地址(起到指针作用)来引用这些资源是很好理解的,运行时常量池对于一个类中的各线程来说时需要共享的,那就比如会存储在一个从类角度上时共享的区域即方法区中
动态链接、方法返回地址、附加信息 : 有些地方被称为帧数据区,每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:字节码文件中描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

常量池的作用:可以总结为就是为了提供一些符号和常量,便于指令的识别
方法的调用:解析与分配
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接
动态链接
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
静态链接和动态链接不是名词,而是动词,这是理解的关键。
早期绑定
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
4.8.4. 晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
‘’
public class Aniaml {public void eat(){System.out.println("动物进食");}
}interface Huntable {void hunt();
}class Dog extends Aniaml implements Huntable{@Overridepublic void hunt() {System.out.println("多管闲事");}public void eat(){System.out.println("够吃骨头");}}class Cat extends Aniaml implements Huntable{public Cat(String name) {this();//典型的早期绑定}public Cat() {super();//典型的早期绑定}@Overridepublic void hunt() {System.out.println("天经地义");}public void eat(){super.eat(); //典型的早期绑定System.out.println("猫吃耗子");}}class AniamlTest {public void showAnimal(Aniaml aniaml){aniaml.eat();//表现为晚期绑定}public void showHunt(Huntable hunt){hunt.hunt();//接口那更是晚期绑定}
}
对 AniamlTest进行编译,关注里面的两个方法
showAnimal
0 aload_1
1 invokevirtual #2 <com/sobot/net/jvm/Aniaml.eat : ()V>
4 return
showHunt
0 aload_1
1 invokeinterface #3 <com/sobot/net/jvm/Huntable.hunt : ()V> count 1
6 return
可以看出invokevirtual和 invokeinterface都是典型的虚方法调用,与之相对的是,我们关注Cat类中
两个init方法分别如下
0 aload_0
1 invokespecial #1 <com/sobot/net/jvm/Cat.<init> : ()V>
4 return
0 aload_0
1 invokespecial #2 <com/sobot/net/jvm/Aniaml.<init> : ()V>
4 return
eat方法(主要关注里面对父类方法的调用super.eat();)
0 aload_01 invokespecial #6 <com/sobot/net/jvm/Aniaml.eat : ()V>4 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>7 ldc #7 <猫吃耗子>9 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>
12 return
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特悄,那么自然也就具备早期绑定和晚期绑定两种绑定方式。Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C语言中的虚函数(C中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
虚方法和非虚方法
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。反之即为非虚方法
总结
动态连接 => 晚期绑定 =>虚方法 => 调用虚方法invokeVirtual(子类重写了父类的方法,final方法),invokeinterface(接口方法),这里注意一个坑,fianl方法是非虚方法但字节码中显示依然是使用的invokeVirtual,
静态连接 => 晚期绑定 =>非虚方法 => 调用非虚方法invokestatic(调用静态方法),invokespecial(实例构造方法,私有方法,父类方法),fianl方法(对应invokeVirtual),都是非虚方法都是非虚方法
动态调用指令:invokedynamic:动态解析出需要调用的方法,然后执行
● JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现「动态类型语言」支持而做的一种改进。
● 但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
● Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。
动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征
我们分别以java,python,js来举例
String str = "abc";
int a = 10;
float f = 10.0f;
long l = 10l;
...............
我们可以看出java定义变量必须指明变量类型,而js则不需要指明变量类型,只需要用var来定义一个变量
var a = 10;
var str = ‘b’;
................
而python则是更绝
info = 100.0;
案例代码
interface Func{public boolean func(String str);
}public class Lamda{public void method(Func func) {return;}public static void main(String[] args) {Lamda lamda = new Lamda();//invokespecial//创建func的实例传入methodFunc func = str -> {return true;};lamda.method(func);//直接以匿名的方式传入进来lamda.method(str -> {return true;});}
}

这里可能有些难理解,但结合动态语言的本质特点是编译时不定死类型而是在运行时才考虑类型,在回到我们代码
Lamda lamda = new Lamda();//invokespecial//创建func的实例传入methodFunc func = str -> {return true;};lamda.method(func);//直接以匿名的方式传入进来lamda.method(str -> {return true;});
编译时我们根本就无法确定等号右边部分(匿名函数表达式)对象的类型,只有在运行时才能获取,这就已经是符合动态语言特点了,所以对应的func这个方法的调用类型为invokedynamic,主要还是因为对调用这个方法的引用func完全无法在编译期间确认下来类型
注意:可能有人感觉虚方法和动态调用比较像,它们间的确有共同点就是,总结为一个字就是晚,即编译期间无法下定论,运行时才见真章,但虚方法是站在方法调用角度的而动态调用是站在对象创建角度来说的,这是本质区别
方法返回地址(return address)
存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:
● 正常执行完成
● 出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
当一个方法开始执行后,只有两种方式可以退出这个方法:
- 执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口; 一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,short和int类型时使用),lreturn(Long类型),freturn(Float类型),dreturn(Double类型),areturn。另外还有一个return指令声明为void的方法,实例初始化方法,类和接口的初始化方法使用。
- 在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。
方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码
案例代码
public void method1() {vretrun();try {method2();} catch (Exception e) {e.printStackTrace();}}private void method2() throws IOException {FileReader fileReader = new FileReader("d://test.txt");char[] buffer = new char[1024];int len = 0;while((len = fileReader.read(buffer)) != -1) {String s = new String(buffer, 0, len);System.out.println(s);}fileReader.close();}
查看方法1中的异常表,注意方法2是把异常给抛出去了,所以没有异常表,方法2把异常抛给了方法1,方法1没有继续抛给它的上一个调用方法而是通过try-catch进线处理,所以方法1有异常表,如下

含义是字节码4到8行范围内有捕获到异常那就调整到第11行,然后我们参考

字节码和行号对应关系发现

其实含义就是try-catch包裹的代码块中的代码出现问题后直接跳转到catch块内进行处理
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
相关文章:
JVM 四虚拟机栈
虚拟机栈出现的背景 由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多…...
【R语言】获取数据
R语言自带2种数据存储格式:*.RData和*.rds。 这两者的区别是:前者既可以存储数据,也可以存储当前工作空间中的所有变量,属于非标准化存储;后者仅用于存储单个R对象,且存储时可以创建标准化档案,…...
Java BIO详解
一、简介 1.1 BIO概述 BIO(Blocking I/O),即同步阻塞IO(传统IO)。 BIO 全称是 Blocking IO,同步阻塞式IO,是JDK1.4之前的传统IO模型,就是传统的 java.io 包下面的代码实现。 服务…...
统计满足条件的4位数(信息学奥赛一本通-1077)
【题目描述】 给定若干个四位数,求出其中满足以下条件的数的个数:个位数上的数字减去千位数上的数字,再减去百位数上的数字,再减去十位数上的数字的结果大于零。 【输入】 输入为两行,第一行为四位数的个数n࿰…...
北京门头沟区房屋轮廓shp的arcgis数据建筑物轮廓无偏移坐标测评
在IT行业中,地理信息系统(GIS)是用于处理、分析和展示地理空间数据的重要工具,而ArcGIS则是GIS领域中的一款知名软件。本文将详细解析标题和描述中提及的知识点,并结合“门头沟区建筑物数据”这一标签,深入…...
Spring 面试题【每日20道】【其三】
1、Spring 中的 Profile 注解的作用是什么? 中等 Profile 注解在Spring框架中用于根据不同的环境配置文件(profiles)来激活或忽略某些Bean的注册。它允许开发者定义逻辑以区分不同环境下的bean定义,例如开发、测试和生产环境。 …...
FFmpeg(7.1版本)在Ubuntu18.04上的编译
一、从官网上下载FFmpeg源码 官网地址:Download FFmpeg 点击Download Source Code 下载源码到本地电脑上 二、解压包 tar -xvf ffmpeg-7.1.tar.xz 三、配置configure 1.准备工作 安装编译支持的软件 ① sudo apt-get install nasm //常用的汇编器,用于编译某些需要汇编…...
Apache Hudi数据湖技术应用在网络打车系统中的系统架构设计、软硬件配置、软件技术栈、具体实现流程和关键代码
网络打车系统利用Hudi数据湖技术成功地解决了其大规模数据处理和分析的难题,提高了数据处理效率和准确性,为公司的业务发展提供了有力的支持。 Apache Hudi数据湖技术的一个典型应用案例是网络打车系统的数据处理场景,具体如下: 大…...
安全策略配置
需求: 1、VLAN 2属于办公区;VLAN 3属于生产区 2、办公区PC在工作日时间(周一至周五,早8到晚6)可以正常访问0A Server,其他时间不允许 3、办公区PC可以在任意时刻访问web server 4、生产区PC可以在任意时刻访问0A Server,但是不能访问Web serv…...
c++ stl 遍历算法和查找算法
概述: 算法主要由头文件<algorithm> <functional> <numeric> 提供 <algorithm> 是所有 STL 头文件中最大的一个,提供了超过 90 个支持各种各样算法的函数,包括排序、合并、搜索、去重、分解、遍历、数值交换、拷贝和…...
【Envi遥感图像处理】008:波段(批量)分离与波段合成
文章目录 一、波段分离提取1. 提取单个波段2. 批量提取单个波段二、波段合成相关阅读:【ArcGIS微课1000例】0058:波段合成(CompositeBands)工具的使用 一、波段分离提取 1. 提取单个波段...
线程创建与管理 - 创建线程、线程同步(C++)
前言 在现代软件开发中,线程的创建和管理是并发编程的核心内容之一。通过合理地创建和管理线程,可以有效提高程序的响应速度和资源利用率。本文将详细讲解如何在C中创建线程,并探讨几种常见的线程同步机制。我们假设读者具备一定的C基础&…...
【C语言篇】“三子棋”
一、游戏介绍 三子棋,英文名为 Tic - Tac - Toe,是一款简单而经典的棋类游戏。游戏在一个 33 的棋盘上进行,两名玩家轮流在棋盘的空位上放置自己的棋子(通常用 * 和 # 表示),率先在横、竖或斜方向上连成三个…...
安培定律应用于 BH 曲线上的工作点
在本篇博文中,我将展示如何应用安培定律来确定磁芯包裹的导体必须承载多少电流才能从 BH 值工作点获得 B 值,该工作点对应于磁芯材料中的最大 B 值。我在 BH 曲线上使用两个工作点,一个在线性区域,另一个在饱和区域。 安培定律 H…...
深度求索DeepSeek横空出世
真正的强者从来不是无所不能,而是尽我所能。多少有关输赢胜负的缠斗,都是直面本心的搏击。所有令人骄傲振奋的突破和成就,看似云淡风轻寥寥数语,背后都是数不尽的焚膏继晷、汗流浃背。每一次何去何从的困惑,都可能通向…...
【CSS】什么是响应式设计?响应式设计的基本原理,怎么做
在当今多设备、多屏幕尺寸的时代,网页设计面临着前所未有的挑战。传统的固定布局已无法满足用户在不同设备上浏览网页的需求,响应式设计(Responsive Web Design)应运而生,成为网页设计的趋势和标准。本文将深入探讨响应…...
后盾人JS--继承
继承是原型的继承 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </hea…...
提升开发效率:IDE使用技巧与插件推荐
在软件开发过程中,选择一个合适的集成开发环境(IDE)并掌握其使用技巧,可以显著提高开发效率。本文将分享一些常用的IDE使用技巧,并推荐几款实用的插件,帮助开发者更好地利用IDE进行开发。 一、IDE使用技巧…...
开源模型应用落地-DeepSeek-R1-Distill-Qwen-7B与vllm实现推理加速的正确姿势(一)
一、前言 在当今人工智能技术迅猛发展的时代,各类人工智能模型如雨后春笋般不断涌现,其性能的优劣直接影响着应用的广度与深度。从自然语言处理到计算机视觉,从智能安防到医疗诊断,AI 模型广泛应用于各个领域,人们对其准确性、稳定性和高效性的期望也与日俱增。 在此背景下…...
小书包:让阅读更美的二次开发之作
小书包是在一款知名阅读软件的基础上进行二次开发的产品。在保留原有软件的基本功能和用户体验的同时,对其界面和视觉效果进行了精心美化,让阅读体验更加舒适和愉悦。 内置了171条书源,虽然数量不算多,但都是作者精挑细选出来的&a…...
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...
Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战
在现代战争中,电磁频谱已成为继陆、海、空、天之后的 “第五维战场”,雷达作为电磁频谱领域的关键装备,其干扰与抗干扰能力的较量,直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器,凭借数字射…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
