虚拟机栈(线程私有)
由于跨平台性的设计,Java 的指令都是根据栈来设计的。不同平台 CPU 架构不同,所以不能设计为基于寄存器的。
一、基本概念
1、概述
栈 vs 堆:
栈
:解决的是程序运行
的问题,即程序如何执行,或者说如何处理数据。堆
:解决的是数据存储
的问题,即数据怎么放,放哪里
虚拟机栈的生命周期:
- 和线程的生命周期一致
虚拟机栈的作用:
- 描述 Java方法 的内存模型,保存方法的局部变量、部分结果,并参与方法的调用和返回。
栈的特点:
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
- 对于栈来说不存在垃圾回收问题(但是栈存在SOF和OOM的问题)
- 栈的大小直接决定了方法调用的最大可达深度
2、设置栈内存大小
# 栈
-Xss128k 设置每个线程的栈大小(包括初始大小以及栈的动态扩展行为)
-XX:ThreadStackSize=128k 设置每个线程的初始栈大小(只影响每个线程的初始栈大小)
3、栈相关的异常
在不同的 Java 虚拟机实现中,对于栈的大小可以有不同的处理方式,可以是 动态扩展的 或 固定不变的:
-
固定不变的 Java 虚拟机栈
在虚拟机启动时就确定了。在这种情况下,虚拟机会为每个线程分配固定大小的栈空间,并且不会随着程序的运行而动态调整。这样的实现可能会更简单、更高效,但是如果栈空间不足,就有可能导致栈溢出异常(
StackOverflowError
)。 -
动态扩展的 Java 虚拟机栈
虚拟机栈的大小可以根据程序的需求动态增长。这种情况下,虚拟机会根据需要动态地调整栈的大小,以满足程序运行时的需求。这样可以在一定程度上避免因为栈空间不足而导致的栈溢出问题。不过,动态扩展也可能带来一些性能开销。
对于 HotSpot VM,一般来说,虚拟机栈的大小是固定的,这个固定的大小由 -Xss
参数来设置。
1)StackOverflowError
如果 线程请求分配的栈容量 超过 虚拟机栈允许的最大容量,就会抛出StackOverflowError
异常。(常见于递归)
package stack;
/**
* 栈超出最大深度:StackOverflowError
* - 默认情况下:count:31857
* - VM options 设置栈的大小:-Xss512k count:4924
*/
public class StackSOF {
private int stackLength = 1;
// 递归
public void recursion() {
stackLength++;
recursion();
}
public static void main(String[] args) {
StackSOF stackSOF = new StackSOF();
try {
stackSOF.recursion();
} catch (Throwable e) {
System.out.println("当前栈深度:" + stackSOF.stackLength);
e.printStackTrace();
}
}
}
2)OutOfMemoryError
-
在尝试扩展的时候无法申请到足够的内存,就会抛出
OutOfMemoryError
异常。 -
在创建新线程的时候没有足够的内存去创建对应的虚拟机栈,也会抛出
OutOfMemoryError
异常。
以下代码示例谨慎使用,可能会引起电脑卡死
package stack;
/**
* 栈内存溢出: OOM
* VM options 设置栈的大小:-Xss2m
**/
public class StackOOM {
public static void main(String[] args) {
StackOOM stackOOM = new StackOOM();
stackOOM.stackLeakByThread();
}
// 不断创建线程 -> 不断创建Java虚拟机栈 -> 不断申请内存 -> 内存溢出
public void stackLeakByThread() {
while (true) {
Thread t = new Thread(new Runnable() {
public void run() {
dontStop();
}
});
t.start();
}
}
private void dontStop() {
while (true) {
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:719)
at stack.StackOOM.stackLeakByThread(StackOOM.java:22)
at stack.StackOOM.main(StackOOM.java:11)
二、栈的运行原理
每个线程在创建时,都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用
1、栈的存储单位 - 栈帧
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
2、当前栈帧
一个线程,一个时间点上,只会有一个活动的栈帧,这个栈帧被称为 当前栈帧
(Current Frame)
- 与
当前栈帧
相对应的方法就是当前方法
(Current Method) - 定义这个方法的类就是
当前类
(Current Class)
执行引擎 运行的所有字节码指令 只针对 当前栈帧 进行操作。
3、压栈 & 出栈
JVM 直接对 Java 栈的操作只有两个,就是对 栈帧的压栈和出栈,遵循「先进后出」「后进先出」的 原则。
- 压栈:
- 每当方法被调用时,对应的栈帧会被创建出来,压入栈顶。(栈顶的栈帧 即
当前栈帧
) - 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,压入栈顶,成为
新的当前栈帧
。
- 每当方法被调用时,对应的栈帧会被创建出来,压入栈顶。(栈顶的栈帧 即
- 出栈:
- 正常的方法返回(使用 return 指令) 和 抛出异常,都会导致栈帧被弹出。
- 方法返回时,当前栈帧将当前方法的执行结果传给前一个栈帧,接着,当前栈帧出栈,前一个栈帧成为当前栈帧。
注意:不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
4、方法的执行过程
- 方法调用前:创建栈帧
- 方法执行时:栈帧入栈
- 方法执行后:栈帧出栈
三、栈帧的内部结构
栈帧是用来存储数据和部分过程结果的数据结构,每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
- 一些附加信息
有些地方将 动态链接、方法返回地址、附加信息 统称为 帧数据区。
每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要是由 局部变量表 和 操作数栈 决定的。
1、局部变量表(Local Variables)
局部变量表
用于存储方法中的局部变量,包括方法参数
以及在方法中定义的局部变量
。
- 基本数据类型(byte、short、int、long、float、double、char、boolean)
- 对象引用(对象实例存在堆中)
- returnAddress类型(指向了一条字节码地址)
1)特点
- 局部变量表存储在栈帧中,是
线程私有
的,因此 不存在数据安全问题。 - 局部变量表的大小是在编译时确定的,在方法运行期间不会改变。
- 局部变量表中的变量只在
当前方法
的调用中有效。方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
2)Slot 变量槽
局部变量表是一个数组结构,每个元素都存储一个局部变量的值。最基本的存储单元是 Slot(变量槽)
-
32 位以内的类型只占用一个 Slot(包括 returnAddress 类型),64 位的类型占用两个 Slot。
-
引用数据类型(32位),占用一个 Slot
-
int类型(32位),占用一个 Slot
byte、short、char 在存储前被转换为 int,boolean 也被转换为 int,0 表示 false,非 0 表示 true。
-
long类型 和 double类型(64位),占用两个 Slot
-
-
JVM 会为局部变量表中的每一个 Slot 都分配一个
访问索引
,通过这个索引即可成功访问到指定的局部变量值。 -
当方法被调用时,
方法参数
和方法中定义的局部变量
会 按照顺序 被复制到局部变量表中的每一个 Slot 上。
如果是
构造方法
或实例方法
,Index=0 的 Slot 存放的是对象引用this
,其余的参数 按照顺序 继续排列。
3)Slot 的复用
栈帧中的局部变量表中的槽位是可以复用的,如果一个局部变量过了其作用域,那么在其作用域之后声明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
public class SlotReusingTest {
public void localVar1() {
int a = 0;
int b = 0;
}
public void localVar2() {
{
int a = 0;
}
int b = 0; // 此时的b就会复用a的槽位
}
}
4)成员变量 vs 局部变量
根据变量在类中声明的位置,可以分为成员变量
和局部变量
:
成员变量
:在使用前,都经历过默认初始化赋值。- 静态变量:在
链接-准备
阶段,会给静态变量设置默认初始值(在初始化
阶段显示赋值) - 实例变量:随着对象的创建,在堆空间中分配实例变量空间,并进行默认赋值
- 静态变量:在
局部变量
:在使用前,必须进行显示赋值,否则,编译不通过!
public class LocalVariablesTest {
static int a;
public static void main(String[] args) {
System.out.println(a); // 默认值0
}
public void test() {
int a;
System.out.println(a); // 编译报错
}
}
5)补充说明
在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
2、操作数栈(Operand Stack)
1)基本概念
操作数栈就是 JVM 执行引擎的一个工作区。我们说 Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
操作数栈 主要用于存放方法执行过程中的 操作数
和 中间结果
- 在方法开始执行时,一个新的栈帧也会随之被创建出来
- 此时这个方法的操作数栈是空的,但是栈深度已经确定了
- 在方法执行过程中,根据字节码指令,往操作数栈中 写入数据 或 提取数据,即入栈(push)和 出栈(pop)
- 一个完整的方法执行,往往包含多个这样出栈/入栈的过程。
- 如果被调用的方法带有返回值,其返回值将会被压入当前栈帧的操作数栈中,并更新 PC 寄存器中下一条需要执行的字节码指令。
注意事项:
操作数栈并非采用访问索引的方式来进行数据访问,而是只能通过标准的入栈和出栈操作来完成一次数据访问。
每个操作数栈都有一个明确的栈深度用于存储数值,最大深度 max_stack 在编译期就定义好了,保存在方法的 Code 属性中。
- 32bit 的类型占用一个栈单位深度
- 64bit 的类型占用两个栈单位深度
操作数栈中的任何一个元素都可以是任意的 Java 数据类型,但是元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在
编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
2)案例分析
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
0: bipush 15
2: istore_1
3: bipush 8
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
3)栈顶缓存技术
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数 和 内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。
为了解决这个问题,HotSpot JVM 的设计者们提出了栈顶缓存技术(Top Of Stack Cashing)
- 将栈顶元素全部缓存在物理 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
寄存器指令更少,执行速度快。
3、动态链接(Dynamic Linking)
1)基本概念
动态链接主要服务一个方法需要调用其他方法的场景。
Java源文件编译成class文件时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池中。
- 其中一部分会在 类加载阶段 或 第一次使用时 就直接转化为直接引用,这类转化称为静态解析(Resolve)。
- 另一部分将在每次运行期间转化为直接引用,这类转化称为动态链接(Dynamic Linking)。
Java虚拟机栈中,每一个栈帧内部都包含一个 指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的,就是为了支持方法调用过程中的动态链接(Dynamic Linking)。比如:invokedynamic指令
当一个方法要调用其他方法时,就是通过常量池中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
2)案例演示
public class DynamicLinkingTest {
int num = 10;
public void methodA(){
System.out.println("methodA()....");
}
public void methodB(){
System.out.println("methodB()....");
methodA();
num++;
}
}
$ javap -v DynamicLinkingTest.class
...
Constant pool:
#1 = Methodref #9.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #8.#24 // stack/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 // stack/DynamicLinkingTest.methodA:()V
#8 = Class #32 // stack/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 Lstack/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 stack/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
...
public void methodB();
descriptor: ()V
flags: (0x0001) 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
由反编译后的字节码指令可以看出:
-
methodB 方法中通过
invokevirtual #7
指令调用了 methodA -
往上面翻,找到常量池中的定义:
#7 = Methodref #8.#31 // stack/DynamicLinkingTest.methodA:()V
#8 = Class #32 // stack/DynamicLinkingTest
#32 = Utf8 stack/DynamicLinkingTest
#31 = NameAndType #19:#13 // methodA:()V
#19 = Utf8 methodA
#13 = Utf8 ()V
结论:通过 invokevirtual #7
指令找到需要调用的 DynamicLinkingTest 中的 methodA 方法,并进行调用,返回值为void
3)为什么需要运行时常量池
- 常量池的作用:就是为了提供一些符号和常量,便于指令的识别
- 在不同的方法,都可能调用常量或者方法,只需要存储一份,然后记录其引用即可,节省了空间。
4、方法返回地址(Return Address)
1)基本概念
方法返回地址:存放调用该方法的 pc 寄存器的值(当前栈帧的方法执行结束后,要执行的下一条指令)
当一个方法开始执行后,只有两种方式可以退出这个方法:
-
正常退出:
-
执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;
-
一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。
在字节码指令中,返回指令包含:
ireturn
:当返回值是boolean,byte,char,short和int类型时使用lreturn
:long类型freturn
:float类型dreturn
:double类型areturn
:引用类型return
:返回值类型为void的方法、实例初始化方法,类和接口的初始化方法使用。
-
-
异常退出:
- 方法执行过程中,抛出异常时的异常处理,存储在一个
异常表
,方便在发生异常的时候处理对应类型异常的代码。 - 在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。
- 方法执行过程中,抛出异常时的异常处理,存储在一个
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。
- 方法正常退出时,调用者的 pc 寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
- 方法异常退出的,返回地址要通过异常表来确定,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置 PC 寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
2)案例分析 - 返回指令
public class ReturnCommandTest {
public byte methodByte() {return 0;}
public short methodShort() {return 0;}
public int methodInt() {return 0;}
public long methodLong() {return 0L;}
public float methodFloat() {return 0.0f;}
public double methodDouble() {return 0.0;}
public char methodChar() {return 'a';}
public boolean methodBoolean() {return true;}
public String methodString() {return null;}
public void methodVoid() {}
static {System.out.println("666");}
}
这里可以自行通过Jclasslib查看,比较简单就不展示了。
3)案例分析 - 异常退出
package stack;
import java.io.FileReader;
import java.io.IOException;
public class ExceptionExitTest {
public void method1() {
try {
method2(true);
} catch (IOException e) {
e.printStackTrace();
}
}
public void method2(boolean flag) throws IOException {
FileReader fileReader = new FileReader("");
fileReader.close();
}
}
public void method1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: iconst_1
2: invokevirtual #2 // Method method2:(Z)V
5: goto 13
8: astore_1
9: aload_1
10: invokevirtual #4 // Method java/io/IOException.printStackTrace:()V
13: return
Exception table:
from to target type
0 5 8 Class java/io/IOException
LineNumberTable:
line 10: 0
line 13: 5
line 11: 8
line 12: 9
line 14: 13
LocalVariableTable:
Start Length Slot Name Signature
9 4 1 e Ljava/io/IOException;
0 14 0 this Lstack/ExceptionExitTest;
StackMapTable: number_of_entries = 2
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/io/IOException ]
frame_type = 4 /* same */
public void method2(boolean) throws java.io.IOException;
descriptor: (Z)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=3, args_size=2
0: new #6 // class java/io/FileReader
3: dup
4: ldc #7 // String
6: invokespecial #8 // Method java/io/FileReader."<init>":(Ljava/lang/String;)V
9: astore_2
10: aload_2
11: invokevirtual #9 // Method java/io/FileReader.close:()V
14: return
LineNumberTable:
line 17: 0
line 18: 10
line 19: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 this Lstack/ExceptionExitTest;
0 15 1 flag Z
10 5 2 fileReader Ljava/io/FileReader;
Exceptions:
throws java.io.IOException
可以看到其中 Exception table 部分:
Exception table:
from to target type
0 5 8 Class java/io/IOException
含义:如果字节码指令0~5行出现异常,且异常类型为Class java/io/IOException,则执行第8行的字节码指令
5、一些附加信息
栈帧可能还包括一些额外的信息,如异常处理信息、synchronized 同步块信息等,这些信息的具体内容取决于方法的执行情况和 Java 虚拟机的实现。
四、方法的调用
在JVM中,将符号引用转换为调用方法的直接引用,与方法的绑定机制相关
1、静态链接 & 动态链接
-
静态链接:
被调用的目标方法在编译期确定,且运行期保持不变。
这种情况下将 调用方法的符号引用 转换为 直接引用 的过程称之为 静态链接。 -
动态链接:
被调用的目标方法在编译期无法确定,只能在程序运行期将 调用方法的符号引用 转换为 直接引用,
由于这种引用转换过程具备动态性,因此也被称之为动态链接。
2、早期绑定 & 晚期绑定
绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,只会发生一次。
-
早期绑定(Early Binding)
被调用的目标方法在编译期确定,且运行期保持不变,可直接将这个方法与所属的类型进行绑定。
-
晚期绑定(Late Binding)
被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法。
3、多态 & 绑定
面向对象的语言都具备多态特性,那么自然也就具备 早期绑定 和 晚期绑定 两种绑定方式。
这里回顾一下多态的前提:类的继承关系 + 方法的重写
class Animal {
public void eat() {
System.out.println("动物进食");
}
}
interface Huntable {
void hunt();
}
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(); // 晚期绑定
}
}
4、虚方法 & 非虚方法
- 非虚方法:方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。
static方法
、final方法
、private方法
、构造方法
、super.父类方法
- 虚方法:除了非虚方法以外的其他方法
实例方法
、没有加super.的父类方法
、接口方法
5、动态类型语言 & 静态类型语言
动态类型语言 和 静态类型语言 的区别:
- 在于对类型的检查是在编译期还是在运行期,在编译期就是静态类型语言(Java),在运行期则是动态类型语言(JS)。
说的再直白一点就是:
- 静态类型语言是判断变量自身的类型信息;
- 动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息。
Java属于静态类型语言,Java7中增加了一个invokedynamic
指令,这是Java为了实现【动态类型语言】支持而做的一种改进。
Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改。增加了虚拟机中的方法调用,
最直接的受益者就是运行在Java平台的动态语言的编译器。
6、方法调用指令 invoke…
1)基本概念
-
普通调用指令
invokestatic
:调用static方法
,解析阶段确定唯一方法版本(非虚方法)invokespecial
:调用构造方法
、private方法
、super.父类方法
,解析阶段确定唯一方法版本(非虚方法)invokevirtual
:调用final方法
、实例方法
、没有加super.的父类方法
(除了final方法都是虚方法)invokeinterface
:调用接口方法
(虚方法)
-
动态调用指令
invokedynamic
:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预。而invokedynamic
指令则支持由用户确定方法版本。
2)案例分析 - 普通调用指令
class Father {
public static void showStatic(String str) {}
public final void showFinal() {}
public void showCommon() {}
}
interface MethodInterface {
void methodA();
}
public class Son extends Father {
public Son() {
// 父类构造器:invokespecial
super();
}
public Son(int age) {
// 子类构造器:invokespecial
this();
}
// 不是重写的父类的静态方法,因为静态方法不能被重写!
public static void showStatic() {}
private void showPrivate() {}
public void info() {}
// 非虚方法
public void nonVirtualMethod() {
// 父类的static方法:invokestatic
super.showStatic();
// 子类的static方法:invokestatic
showStatic();
// 父类的实例方法:invokespecial
super.showCommon();
// 子类的private方法:invokespecial
showPrivate();
// 父类的final方法:invokevirtual(由于final修饰不能被子类重写,虽然这里是invokevirtual,也认为是非虚方法)
showFinal();
// 加上super.之后:invokespecial(明确是父类方法了)
super.showFinal();
// 子类的final方法:invokevirtual
sonFinal();
}
// 虚方法
public void virtualMethod() {
// 没有加super.的父类方法:invokevirtual
// 没有显示的加super.,编译器认为可能会调用子类的showCommon方法(即使这里son子类没有重写,也会认为)
// 如果显示加上super.,就是invokespecial了
showCommon();
// 子类的普通方法:invokevirtual
info();
// 接口的方法:invokeinterface(编译器认为会调用实现类的方法,因此是虚方法)
MethodInterface in = null;
in.methodA();
}
}
3)案例分析 - 动态调用指令
在Java7中并没有提供直接生成invokedynamic
指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic
指令。直到Java8的Lambda表达式的出现,invokedynamic
指令 在Java中才有了直接的生成方式。
@FunctionalInterface
interface Func {
boolean func(String str);
}
public class Lambda {
public void useLambda(Func func) {}
public static void main(String[] args) {
Lambda lambda = new Lambda();
Func func = s -> true;
lambda.useLambda(func);
lambda.useLambda(v -> false);
}
}
7、方法重写的本质(动态分派)
动态分派(Dynamic Dispatch)是面向对象编程中多态性的一种实现方式,也被称为运行时多态。它指的是在程序运行时根据对象的实际类型来确定调用哪个版本的方法。(通常与继承和重写相关联)
动态分派使得程序能够根据对象的实际类型来决定方法的调用,而不是根据引用变量的类型。
在动态分派中,调用方法的选择是基于对象的运行时类型而不是编译时类型。这意味着在编译时无法确定调用哪个方法,而是在运行时根据对象的实际类型动态决定。
具体流程如下:
- 找到操作数栈栈顶的第一个元素所执行的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验。
- 如果通过,则返回这个方法的直接引用,查找过程结束
- 如果不通过,则返回
java.lang.IllegalAccessError
异常
- 否则(即没有找到相符的方法),按照继承关系从下往上,依次对C的各个父类进行第2步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出
java.lang.AbstractMethodError
异常。
IllegalAccessError
-
程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。
一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
-
比如,你把应该有的jar包放从工程中拿走了,或者Maven中存在jar包冲突
8、虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标,就可能影响到执行效率。(上面动态分派的过程,我们可以看到如果子类找不到,还要从下往上找其父类,非常耗时)
因此,为了提高性能,JVM在类的方法区建立一个虚方法表(virtual method table),使用索引表来代替查找(非虚方法不在表中)
- 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
那么,虚方法表是什么时候被创建的呢?
- 虚方法表会在 类加载的 Linking阶段 的 Resolve阶段 被创建并开始初始化。
- 类的变量初始值准备完成之后,JVM会把该类的虚方法表也初始化完毕。
【例1】
【例2】
interface Friendly {
void sayHello();
void sayGoodbye();
}
class Dog {
public void sayHello() {}
public String toString() { return "Dog"; }
}
class CockerSpaniel extends Dog implements Friendly {
public void sayHello() { super.sayHello(); }
public void sayGoodbye() {}
}
CockerSpaniel 的 虚方法表如下所示:主要注意一下 toString()
方法(本身没重写,父类重写了,所以没有指向Object)