Jvm内存中栈分两种:本地方法栈 和 虚拟机栈。两者没有本质上的区别,区别在于服务的对象不同。本地方法栈服务于JVM虚拟机的native方法,如JDK安装目录下很多C的文件实现的方法,这些就是native方法。虚拟机栈服务于虚拟机执行的Java方法。
栈帧其实就是栈里面的元素,用于支持Java虚拟机进行方法调用和方法执行背后的数据结构。了解它就可以更好地理解Java虚拟机执行引擎是如何运行的。
虚拟机栈和栈帧的结构如图:
栈帧的部分信息,如局部变量表、操作数栈、运行时常量池可以通过JVM字节码查看:
javap -v 类名
public class ShowJOL {
public static volatile int a = 3;
public static void main(String[] args) {
a = 5;
}
}
// 执行javap -p ShowJOL
Classfile /Users/shaotuo/java/java/target/classes/edward/com/ShowJOL.class
Last modified 2022-3-15; size 474 bytes
MD5 checksum 19d688ea832ca46ef13267208f9d22e1
Compiled from "ShowJOL.java"
public class edward.com.ShowJOL
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#21 // java/lang/Object."":()V
#2 = Fieldref #3.#22 // edward/com/ShowJOL.a:I
#3 = Class #23 // edward/com/ShowJOL
#4 = Class #24 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ledward/com/ShowJOL;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8
#19 = Utf8 SourceFile
#20 = Utf8 ShowJOL.java
#21 = NameAndType #7:#8 // "":()V
#22 = NameAndType #5:#6 // a:I
#23 = Utf8 edward/com/ShowJOL
#24 = Utf8 java/lang/Object
{
public static volatile int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
public edward.com.ShowJOL();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ledward/com/ShowJOL;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iconst_5
1: putstatic #2 // Field a:I
4: return
LineNumberTable:
line 7: 0
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 args [Ljava/lang/String;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_3
1: putstatic #2 // Field a:I
4: return
LineNumberTable:
line 4: 0
}
SourceFile: "ShowJOL.java"
局部变量表也叫本地变量表,用于存放方法参数和方法内部定义的局部变量的数据结构。
局部变量表的存储
局部变量表一片逻辑连续的内存空间,最小单位是变量槽(Variable Slot),每个槽的大小为32位。
局部变量表的使用
虚拟机通过索引定位对应数据,索引值范围是从0到局部变量表最大槽数量-1。
局部变量表的Reference
对象的使用就涉及到了对象的引用,在Java程序操作堆上对象需要通过Java栈上的reference数据;而reference类型在Java虚拟机规范里面只规定了是一个指向对象的引用,并没有定义这个引用应该通过什么种方式去定位、访问到堆中的对象的具体位置。所以对象访问方式也是取决于虚拟机实现而定的,主流的访问方式有使用句柄和直接指针两种。
1、使用句柄访问对象
该方式中,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。如图
好处:reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
2、使用直接指针访问对象
Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如图:
好处:速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可观的执行成本。就虚拟机HotSpot而言,它是使用直接指针方式进行对象访问。
供方法执行中操作数进行运算的临时存储空间,为栈结构。
栈桢和栈桢是完全独立的吗?
栈桢作为虚拟机栈的一个单元,应该是栈桢之间完全独立的。
但是虚拟机进行了一些优化:为了节省方法内调用方法间参数、返回值的复制传递等一些操作,就让一部分数据进行栈桢间共享。如下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,共用数据,节约空间,减少操作如下图:
在已编译Class类文件中,就已经计算好操作数栈所需要分配的内存,存储在类文件中方法Code属性的stack数据项中。
在方法执行期间,将常量池中的符号引用动态的转化为直接引用(实际的内存地址)。
Class类文件的常量池中存有大量的符号引用,如一个方法内调用另一个方法this.testMethod(),或一个类使用另一个类的成员变量b.testVar,其中testMethod/testVar就是指符号,在运行时会将这些符号转化为对应变量或方法存储在元数据空间的内存地址或入口,简单理解就是动态链接存储的就是这些地址。而这些符号的解析有两种场景:
JVM将符号引用转换为调用方法的直接引用与方法的绑定机制相关,绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,分为早期绑定(Early Binding)和晚期绑定(Late Binding)。
/*
* 说明早期绑定和晚期绑定的例子
*/
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(); // 表现为:晚期绑定
}
}
晚期绑定:
1、调用父类方法,invokevirtual:调用实例方法
public void showAnimal(Animal animal) {
animal.eat(); // 表现为:晚期绑定
}
// 对应字节码
invokevirtual #2
2、调用接口方法,invokeinterface:调用接口方法
public void showHunt(Huntable h) {
h.hunt(); // 表现为:晚期绑定
}
// 对应字节码
invokeinterface #3 count 1
早期绑定:
invokespecial,在下面三种情况下使用:
动态链接的前提?
每个栈帧都包含一个指向运行时常量池(位于方法区)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
方法出口保存方法调用的返回信息在栈帧中,如方法中掉其他方法,用来于恢复调用者(调用其他方法的当前方法)的执行状态。
方法退出时,可能恢复调用者的那些执行状态?