内存是非常重要的系统资源,是硬盘和CPU的中间仓库以及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请,分配、管理的策略,保证了jvm的高效稳定运行。不同的jvm对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨一下经典的JVM内存布局。
运行时数据区
Java虚拟机定义了若干程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另一些则是与线程一一对应的,这些与线程对应的数据区会随着线程的开始和结束而创建和销毁。
从图中可以看出:程序计数器(PC)、本地方法栈(NMS)、虚拟机栈(VMS)是线程私有的。表明每个线程一份,相互不影响。
灰色的为单独线程私有的,橙色的为多个线程共享的,即方法区和堆区。
线程
- 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
- 在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。当一个java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。java线程执行终止后,本地线程也会回收。
- 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法。
JVM系统线程
- 使用jconsole或是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public stati void main(Srting[])的main线程以及所有这个main线程自己创建的线程。
- Hotspot JVM里主要的几个后台线程:
- 虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括“stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
- 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
- GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
- 编译线程:这种线程在运行时会将字节码编译成本地代码。
- 信号调度线程: 这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。
PC程序计数器
JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能运行。
这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加更加贴切(也称为程序钩子),并且也不容器引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
PC寄存器
- 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域。
- 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的的java方法的jvm指令地址;或者,如果是在执行native方法,则是未指定值(underfined)。
- 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
举例
public class PCRegister {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
}
}
执行命令:
javap -c PCRegister.class
得到如下结果:
Compiled from "PCRegister.java"
public class PCRegister {
public PCRegister();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
}
指令地址,是存储在PC寄存器中的。当执行引擎需要执行操作指令的时候,会从PC寄存器中取出指令地址,然后根据指令地址找到相应的操作指令,接着执行指令。
问题
- 为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪里开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。 - PC寄存器为啥会设定了线程私有的?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
CPU时间片
CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
在宏观上,我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
在微观上,由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复,如何保证分毫无差呢? 每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
并行:并行,对应的时串行。并行,就是一个cpu,多个核心,同时执行多个线程。
并发:就是一个cpu的一个核心,快速的在线程间间切换执行。感觉线程在并行执行,其实某一个时刻,只有一个线程在执行。但是,由于cpu执行速度非常快,感觉起来像是在并行执行,其实时并发。
虚拟机栈
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点是跨平台,指令集小,编译容易实现,缺点是性能下降,实现同样的功能需要更多指令。
虚拟机栈的概述
JAVA虚拟机是什么?
JAVA虚拟机栈,早期也叫JAVA栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的方法调用。是线程私有的。一个栈帧对应的一个方法调用
作用
主管JAVA程序的运行,它保存方法的局部变量,部分结果,并参与方法的调用和返回。
栈的特点
- 栈是一种快速有效的分配存储方式,访问速度仅仅次于程序计数器。
- JVM直接对JAVA栈的操作只有两个:a.进栈(每个方法的执行),b.出栈(方法执行完)。
- 不存在垃圾回收。
栈中可能出现的异常
- java虚拟机规范允许java栈的大小是动态的或者是固定不变的。因此
-
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个StackOverflowError异常。
-
如果java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个 OutOfMemoryError异常。
-
- 如何设置栈空间大小?
我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。例如:-Xss256k 或 -Xss1m
栈的存储单位
栈中存储什么?
- 每个线程都有自己的栈,栈中的数据都是栈帧(Stack Frame)的格式存在。
- 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame) 。
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
栈运行原理
- JVM直接对JAVA栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”。
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧。与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类。
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
- 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
- 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
- JAVA方法有两种返回函数的方式:1种是正常的函数返回,使用return指令;2另一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈帧的内部结构
局部变量表、操作数栈、动态链接、方法返回地址。
局部变量表(本地变量表)
局部变量表也称为局部变量数组或本地变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用,以及returnAddress类型。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性maximun local variables 数据项中。在方法运行期间是不会改变局部变量表的大小。
import java.util.Date;
public class LocalVariablesTest {
public static void main(String[] args) {
LocalVariablesTest test = new LocalVariablesTest();
int num = 10;
test.test1();
}
public void test1() {
Date date = new Date();
String name = "zhangsan";
String info = test2(date, name);
System.out.println(date + name);
}
private String test2(Date date, String name) {
date = null;
name = "Matrin";
double weight = 120.0;
char gender = '女';
return date + name;
}
}
编译以后执行 javap -v LocalVariablesTest.class 命令,查看
Classfile /home/mall/work/gitrepository/jvm/out/production/jvm/LocalVariablesTest.class
Last modified Oct 1, 2021; size 1330 bytes
MD5 checksum b5f9bf0e1ae61567d83004f3d13c525f
Compiled from "LocalVariablesTest.java"
public class LocalVariablesTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #19.#48 // java/lang/Object."<init>":()V
#2 = Class #49 // LocalVariablesTest
#3 = Methodref #2.#48 // LocalVariablesTest."<init>":()V
#4 = Methodref #2.#50 // LocalVariablesTest.test1:()V
#5 = Class #51 // java/util/Date
#6 = Methodref #5.#48 // java/util/Date."<init>":()V
#7 = String #52 // zhangsan
#8 = Methodref #2.#53 // LocalVariablesTest.test2:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
#9 = Fieldref #54.#55 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Class #56 // java/lang/StringBuilder
#11 = Methodref #10.#48 // java/lang/StringBuilder."<init>":()V
#12 = Methodref #10.#57 // java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
#13 = Methodref #10.#58 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#14 = Methodref #10.#59 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#15 = Methodref #60.#61 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = String #62 // Matrin
#17 = Double 120.0d
#19 = Class #63 // java/lang/Object
#20 = Utf8 <init>
#21 = Utf8 ()V
#22 = Utf8 Code
#23 = Utf8 LineNumberTable
#24 = Utf8 LocalVariableTable
#25 = Utf8 this
#26 = Utf8 LLocalVariablesTest;
#27 = Utf8 main
#28 = Utf8 ([Ljava/lang/String;)V
#29 = Utf8 args
#30 = Utf8 [Ljava/lang/String;
#31 = Utf8 test
#32 = Utf8 num
#33 = Utf8 I
#34 = Utf8 test1
#35 = Utf8 date
#36 = Utf8 Ljava/util/Date;
#37 = Utf8 name
#38 = Utf8 Ljava/lang/String;
#39 = Utf8 info
#40 = Utf8 test2
#41 = Utf8 (Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
#42 = Utf8 weight
#43 = Utf8 D
#44 = Utf8 gender
#45 = Utf8 C
#46 = Utf8 SourceFile
#47 = Utf8 LocalVariablesTest.java
#48 = NameAndType #20:#21 // "<init>":()V
#49 = Utf8 LocalVariablesTest
#50 = NameAndType #34:#21 // test1:()V
#51 = Utf8 java/util/Date
#52 = Utf8 zhangsan
#53 = NameAndType #40:#41 // test2:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
#54 = Class #64 // java/lang/System
#55 = NameAndType #65:#66 // out:Ljava/io/PrintStream;
#56 = Utf8 java/lang/StringBuilder
#57 = NameAndType #67:#68 // append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
#58 = NameAndType #67:#69 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#59 = NameAndType #70:#71 // toString:()Ljava/lang/String;
#60 = Class #72 // java/io/PrintStream
#61 = NameAndType #73:#74 // println:(Ljava/lang/String;)V
#62 = Utf8 Matrin
#63 = Utf8 java/lang/Object
#64 = Utf8 java/lang/System
#65 = Utf8 out
#66 = Utf8 Ljava/io/PrintStream;
#67 = Utf8 append
#68 = Utf8 (Ljava/lang/Object;)Ljava/lang/StringBuilder;
#69 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#70 = Utf8 toString
#71 = Utf8 ()Ljava/lang/String;
#72 = Utf8 java/io/PrintStream
#73 = Utf8 println
#74 = Utf8 (Ljava/lang/String;)V
{
public LocalVariablesTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LLocalVariablesTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class LocalVariablesTest
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: bipush 10
10: istore_2
11: aload_1
12: invokevirtual #4 // Method test1:()V
15: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 11
line 10: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
8 8 1 test LLocalVariablesTest;
11 5 2 num I
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=1
0: new #5 // class java/util/Date
3: dup
4: invokespecial #6 // Method java/util/Date."<init>":()V
7: astore_1
8: ldc #7 // String zhangsan
10: astore_2
11: aload_0
12: aload_1
13: aload_2
14: invokespecial #8 // Method test2:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
17: astore_3
18: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
21: new #10 // class java/lang/StringBuilder
24: dup
25: invokespecial #11 // Method java/lang/StringBuilder."<init>":()V
28: aload_1
29: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
32: aload_2
33: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
39: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
42: return
LineNumberTable:
line 13: 0
line 14: 8
line 15: 11
line 16: 18
line 17: 42
LocalVariableTable:
Start Length Slot Name Signature
0 43 0 this LLocalVariablesTest;
8 35 1 date Ljava/util/Date;
11 32 2 name Ljava/lang/String;
18 25 3 info Ljava/lang/String;
}
SourceFile: "LocalVariablesTest.java"
mall@ubuntu:~/work/gitrepository/jvm/out/production/jvm$ javap -c LocalVariablesTest.class
Compiled from "LocalVariablesTest.java"
public class LocalVariablesTest {
public LocalVariablesTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class LocalVariablesTest
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: bipush 10
10: istore_2
11: aload_1
12: invokevirtual #4 // Method test1:()V
15: return
public void test1();
Code:
0: new #5 // class java/util/Date
3: dup
4: invokespecial #6 // Method java/util/Date."<init>":()V
7: astore_1
8: ldc #7 // String zhangsan
10: astore_2
11: aload_0
12: aload_1
13: aload_2
14: invokespecial #8 // Method test2:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
17: astore_3
18: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
21: new #10 // class java/lang/StringBuilder
24: dup
25: invokespecial #11 // Method java/lang/StringBuilder."<init>":()V
28: aload_1
29: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
32: aload_2
33: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
39: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
42: return
}
可以看到,每个方法都有局部变量表LocalVariableTable。并且列出了局部变量表中的内容。
关于slot 的理解
- 参数值的存放总是从局部变量数组的index0开始,到数组长度-1的索引结束。
- 局部变量表,最基本的存储单元是Slot(变量槽)。
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型,returnAddress类型。
- 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
- JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引可以成功访问到局部变量表中指定的局部变量值。
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序复制到局部变量表中的每一个slot上。
- 如果需要访问局部变量表中的一个64bit的局部变量时,只需要使用前一个索引即可。
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数的顺序继续排列。
slot 重复利用
栈帧中的局部变量表中的槽位是可以重复的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期的局部变量的槽位,从而达到节省资源的目的。
public void test() {
int a = 10;
{
int b = 0;
b = a + 10;
}
// 这里c会用到b的槽位,局部变量表中槽位,
// 因为槽位可以重复利用,而b出了大括号后,就会被回收
int c = 20;
}
从局部变量表中,原来index为2的位置,存放的是变量b的值,后面被变量c用了。因此,同一个方法内部的局部变量对应的slot,是有可能被重复利用的。从而达到节省空间的目的。
静态变量与局部变量
变量按照在类中的位置,分为成员变量(类变量、实例变量)和局部变量。成员变量在使用之前都经过过默认初始化阶段。局部变量必须要显式赋值,才能够使用;否则编译不通过。
补充说明
- 栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈
- 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈、出栈。某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验的数据流分析阶段要再次验证。
- JAVA 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
- 定义:
- 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 操作数栈就是 JVM 执行引擎的一个工作区。当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。
- 栈中的任何一个元素都可以是任意的 JAVA 数据类型。32bit的类型占用一个栈单位的深度,64bit的类型占用两个栈单位的深度。
- 操作数栈并非采用访问索引的方式来进行数据访问,而是只能通过标准的入栈和出栈操作来完成一次数据访问。
栈顶缓存技术
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味这将需要更多的指令分派次数和内存读、写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读、写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。
动态链接
- 每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
- 在JAVA源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如,描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法返回地址
- 存放调用该方法的pc寄存器的值。
- 一个方法的结束有两种方式:正常退出和异常退出。无论通过那种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
- 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器值等,让调用者方法继续执行下去。
方法的调用
早期绑定和晚期绑定
- 早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟时是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
- 如果被调用的方法在编译期间无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
public class AnimalTest {
public void showAnimal(Animal animal) {
animal.eat();
}
public void showHunt(Huntable h) {
h.hunt();
}
}
class Animal {
public void eat() {
System.out.println("动物进食");
}
}
interface Huntable {
void hunt();
}
class Dog extends Animal implements Huntable {
public Dog() {
super();
}
@Override
public void eat() {
System.out.println("狗吃骨头");
}
@Override
public void hunt() {
System.out.println("捕食耗子,多管闲事");
}
}
首先用 jclasslib 工具看 AnimalTest 类中编译成的字节码:
showAnimal 方法的字节码:
0 aload_1
1 invokevirtual #2 <Animal.eat : ()V>
4 return
showHunt 方法的字节码:
0 aload_1
1 invokeinterface #3 <Huntable.hunt : ()V> count 1
6 return
看到两个指令: invokevirtual 和 invokeinterface ,表明方法运行是晚期绑定。就是在编译期间,无法确定具体的对象。到运行期间才可以。
接着用 jclasslib 工具看 Dog 类中编译成的字节码:
查看 构造器 Dog() 方法的字节码:
0 aload_0
1 invokespecial #1 <Animal.<init> : ()V>
4 return
这里用到了指令 invokespecial ,表明这里是早期绑定。也就是说,在编译器期间,调用super()方法,明确可以直到调用的是父类的构造器方法,而不用等待运行期间再确定。
相关面试题目
- 举例栈溢出的情况?
- 调整栈大小,就能保证不出现溢出吗?
- 分配的栈内存越大越好吗?
- 垃圾回收是否会涉及到虚拟机栈?
- 方法中定义的局部变量是否线程安全?
解答
- StackOverflowError。
- 不能。
- 栈和线程相对应,栈越大,能创建的线程就会少。
- 不会。
- 看情况。如果是基本类型的变量,则是线程安全的。如果是引用类型的变量,看引用指向的内存区域是否是共享的内存区域。如果线程独享,则是;否则不是。