四、操作数栈(Operand Stack)
1.栈 :可以使用数组或者链表来实现
2.每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以成为表达式栈
3.操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈。(如字节码指令bipush操作)
比如:执行复制、交换、求和等操作
代码举例
4.1 操作数栈特点
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了,保存在方法的code属性中,为max_stack的值。
栈中的任何一个元素都是可以任意的java数据类型
32bit的类型占用一个栈单位深度
64bit的类型占用两个栈深度单位
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈push和出栈pop操作来完成一次数据访问
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类验证阶段的数据流分析阶段要再次验证。
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
五、代码追踪
结合上图结合下面的图来看一下一个方法(栈帧)的执行过程
①15入栈;②存储15,15进入局部变量表
注意:局部变量表的0号位被构造器占用,这里的15从局部变量表1号开始
③压入8;④8出栈,存储8进入局部变量表;
⑤从局部变量表中把索引为1和2的是数据取出来,放到操作数栈;⑥iadd相加操作
⑦iadd操作结果23出栈⑧将23存储在局部变量表索引为3的位置上istore_3
六、栈顶缓存技术(Top Of Stack Cashing)
基于栈式架构的虚拟机所使用的零地址指令(即不考虑地址,单纯入栈出栈)更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率
七、动态链接(Dynamic Linking)
1.每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用
2.包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),比如:invokedynamic指令
3.在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里
4.比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
代码示例
public class DynamicLinkingTest { int num = 10; public void methodA(){ System.out.println("methodA()...."); } public void methodB(){ System.out.println("methodB()...."); methodA(); num++; } }
在字节码指令中,methodB() 方法中通过 invokevirtual #7 指令调用了方法 A
那么 #7 是个啥呢?
public void methodB(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String methodB().... 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: aload_0 9: invokevirtual #7 // Method methodA:()V 12: aload_0 13: dup 14: getfield #2 // Field num:I 17: iconst_1 18: iadd 19: putfield #2 // Field num:I 22: return LineNumberTable: line 16: 0 line 18: 8 line 20: 12 line 21: 22 LocalVariableTable: Start Length Slot Name Signature 0 23 0 this Lcom/atguigu/java1/DynamicLinkingTest;
往上面翻,找到常量池的定义:#7 = Methodref #8.#31
先找 #8 :
#8 = Class #32 :去找 #32
#32 = Utf8 com/atguigu/java1/DynamicLinkingTest
结论:通过 #8 我们找到了 DynamicLinkingTest 这个类
再来找 #31:
#31 = NameAndType #19:#13 :去找 #19 和 #13
#19 = Utf8 methodA :方法名为 methodA
#13 = Utf8 ()V :方法没有形参,返回值为 void
结论:通过 #7 我们就能找到需要调用的 methodA() 方法,并进行调用
Constant pool: #1 = Methodref #9.#23 // java/lang/Object."<init>":()V #2 = Fieldref #8.#24 // com/atguigu/java1/DynamicLinkingTest.num:I #3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream; #4 = String #27 // methodA().... #5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V #6 = String #30 // methodB().... #7 = Methodref #8.#31 // com/atguigu/java1/DynamicLinkingTest.methodA:()V #8 = Class #32 // com/atguigu/java1/DynamicLinkingTest #9 = Class #33 // java/lang/Object #10 = Utf8 num #11 = Utf8 I #12 = Utf8 <init> #13 = Utf8 ()V #14 = Utf8 Code #15 = Utf8 LineNumberTable #16 = Utf8 LocalVariableTable #17 = Utf8 this #18 = Utf8 Lcom/atguigu/java1/DynamicLinkingTest; #19 = Utf8 methodA #20 = Utf8 methodB #21 = Utf8 SourceFile #22 = Utf8 DynamicLinkingTest.java #23 = NameAndType #12:#13 // "<init>":()V #24 = NameAndType #10:#11 // num:I #25 = Class #34 // java/lang/System #26 = NameAndType #35:#36 // out:Ljava/io/PrintStream; #27 = Utf8 methodA().... #28 = Class #37 // java/io/PrintStream #29 = NameAndType #38:#39 // println:(Ljava/lang/String;)V #30 = Utf8 methodB().... #31 = NameAndType #19:#13 // methodA:()V #32 = Utf8 com/atguigu/java1/DynamicLinkingTest #33 = Utf8 java/lang/Object #34 = Utf8 java/lang/System #35 = Utf8 out #36 = Utf8 Ljava/io/PrintStream; #37 = Utf8 java/io/PrintStream #38 = Utf8 println #39 = Utf8 (Ljava/lang/String;)V
在上面,其实还有很多符号引用,比如 Object、System、PrintStream 等等
为什么要用常量池呢?
1.因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,然后记录其引用即可,节省了空间
2.常量池的作用:就是为了提供一些符号和常量,便于指令的识别
八、方法的调用:解析和分派
8.1 静态链接与动态链接
静态链接机制与动态链接机制
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期确定,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
动态链接:
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
8.2 早期绑定与晚期绑定
方法的绑定机制
静态链接和动态链接对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
早期绑定
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
代码示例
代码
/** * 说明早期绑定和晚期绑定的例子 * * @author shkstart * @create 2020 上午 11:59 */ class Animal { public void eat() { System.out.println("动物进食"); } } interface Huntable { void hunt(); } class Dog extends Animal implements Huntable { @Override public void eat() { System.out.println("狗吃骨头"); } @Override public void hunt() { System.out.println("捕食耗子,多管闲事"); } } class Cat extends Animal implements Huntable { public Cat() { super();//表现为:早期绑定 } public Cat(String name) { this();//表现为:早期绑定 } @Override public void eat() { super.eat();//表现为:早期绑定 System.out.println("猫吃鱼"); } @Override public void hunt() { System.out.println("捕食耗子,天经地义"); } } public class AnimalTest { public void showAnimal(Animal animal) { animal.eat();//表现为:晚期绑定 } public void showHunt(Huntable h) { h.hunt();//表现为:晚期绑定 } }
invokevirtual 体现为晚期绑定
invokeinterface 也体现为晚期绑定
invokespecial 体现为早期绑定
8.3 多态性与方法绑定
多态性与方法绑定机制
1.随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
2.Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
虚方法与非虚方法
虚方法与非虚方法的区别
1.如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
2.静态方法、私有方法、fina1方法、实例构造器、父类方法都是非虚方法。
3.其他方法称为虚方法。
子类对象的多态的使用前提:
1.类的继承关系
2.方法的重写
虚拟机中调用方法的指令
四条普通指令:
1.invokestatic:调用静态方法,解析阶段确定唯一方法版本
2.invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本
3.invokevirtual:调用所有虚方法
4.invokeinterface:调用接口方法
一条动态调用指令:
invokedynamic:动态解析出需要调用的方法,然后执行
区别:
1.前四条指令固化在虚拟机内部,方法的调用执行不可人为干预
2.而invokedynamic指令则支持由用户确定方法版本
3.其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(fina1修饰的除外)称为虚方法。
代码示例:
代码
/** * 解析调用中非虚方法、虚方法的测试 * * invokestatic指令和invokespecial指令调用的方法称为非虚方法 * @author shkstart * @create 2020 下午 12:07 */ class Father { public Father() { System.out.println("father的构造器"); } public static void showStatic(String str) { System.out.println("father " + str); } public final void showFinal() { System.out.println("father show final"); } public void showCommon() { System.out.println("father 普通方法"); } } public class Son extends Father { public Son() { //invokespecial super(); } public Son(int age) { //invokespecial this(); } //不是重写的父类的静态方法,因为静态方法不能被重写! public static void showStatic(String str) { System.out.println("son " + str); } private void showPrivate(String str) { System.out.println("son private" + str); } public void show() { //invokestatic showStatic("atguigu.com"); //invokestatic super.showStatic("good!"); //invokespecial showPrivate("hello!"); //invokevirtual //虽然字节码指令中显示为invokevirtual,但因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。 showFinal(); //invokespecial super.showCommon(); //invokevirtual //有可能子类会重写父类的showCommon()方法 showCommon(); info(); MethodInterface in = null; //invokeinterface in.methodA(); } public void info() { } public void display(Father f) { f.showCommon(); } public static void main(String[] args) { Son so = new Son(); so.show(); } } interface MethodInterface { void methodA(); }
Son 类中 show() 方法的字节码指令如下
关于 invokedynamic 指令
JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现【动态类型语言】支持而做的一种改进。
但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。
代码
@FunctionalInterface interface Func { public boolean func(String str); } public class Lambda { public void lambda(Func func) { return; } public static void main(String[] args) { Lambda lambda = new Lambda(); Func func = s -> { return true; }; lambda.lambda(func); lambda.lambda(s -> { return true; }); } }
字节码指令