目录
1.class文件结构
2.常用字节码指令
3.运行时栈帧结构(局部变量表,操作数栈,动态链接, 方法返回地址,附加信息)
4.方法调用(解析,分派,动态语言支持)
1.class 文件结构
魔数,版本,常量池,访问符,类、超类、接口,字段,方法,属性
自己写了一个字节码解析器,直观体验下:
public static String format(byte[] bt) {
int line = 0;
StringBuilder buf = new StringBuilder();
// 跳过魔数,版本号
int constNum = getConstPoolNum(bt);
String[] constant = new String[constNum]; // 常量池表项
int[] constLen = new int[constNum]; // 对应长度
int finishedVisitAllConsts = initConstPool(constant, constLen, bt);// 最后一次循环走完
// 此时line为Class的访问标记
int AccessFlag = 2; // 访问符
int index = finishedVisitAllConsts + AccessFlag;// 到class info
index = dealWithClassInfo(bt, index, constant); // 到接口的位置
index = dealWithInterfaceInfo(bt, index, constant); // 到Field的位置
index = dealWithFieldInfo(bt, index, constant);// 到METHOD位置
index = dealWithMethodInfo(bt, index, constant);// 到最后的ATTRIBUTE位置
index = dealWithEndInfo(bt, index, constant);
System.out.println();
System.out.println("检查遍历完成:" + index + "==" + (bt.length - 1));
line = 0;
for (byte d : bt) {
if (line % 16 == 0)
buf.append(String.format("%05x: ", line));
buf.append(String.format("%02x ", d));
line++;
if (line % 16 == 0)
buf.append("\n");
}
buf.append("\n");
return buf.toString();
}
这篇文章解释的很清楚
读上面这篇文章要有耐心,读懂之后,可以结合我下面的程序,进行自己调试。让你们更好的理解CLASS文件结构;我这个程序没有写全,有些属性表的东西,没有做很好的解析,你们可以自行补充。
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public class ClassReader {
static int[] tag = { 1, 3, 7, 8, 9, 10, 11, 12 };// 1 UTF8,7 CLASS,9
// FIELDREF,10
// MethodRef,12 Name and
// Type
static int[] tagLen = { 2, 4, 2, 2, 4, 4, 4, 4 };
static Map map = new HashMap();
static String[] accessMethodFlags = { "public", "private", "protected",
"static", "final", "synchronized", "bridge", "varargs", "native",
"abstract", "strictfp", "synthetic" };
static String[] accessFieldFlags = { "public", "private", "protected",
"static", "final", "", "volatile", "transient", "synthetic", "enum" };
static {
for (int i = 0; i < tag.length; i++)
// 添加各tag长度
map.put(tag[i], tagLen[i]);
}
private static int getConstPoolNum(byte[] bt) {
int constNum = 0; // 常量池表项的个数
int i = 0;
for (byte d : bt) {
if (i == 8)
constNum = d; // 第9、10个字节为常量池大小
if (i == 9)
constNum += d - 1;// 化为10进制-1为常量池表项的个数
i++;
}
System.out.println("常量池个数:" + constNum);
return constNum;
}
public static String format(byte[] bt) {
int line = 0;
StringBuilder buf = new StringBuilder();
// 跳过魔数,版本号
int constNum = getConstPoolNum(bt);
String[] constant = new String[constNum]; // 常量池表项
int[] constLen = new int[constNum]; // 对应长度
int finishedVisitAllConsts = initConstPool(constant, constLen, bt);// 最后一次循环走完
// 此时line为Class的访问标记
int AccessFlag = 2; // 访问符
int index = finishedVisitAllConsts + AccessFlag;// 到class info
index = dealWithClassInfo(bt, index, constant); // 到接口的位置
index = dealWithInterfaceInfo(bt, index, constant); // 到Field的位置
index = dealWithFieldInfo(bt, index, constant);// 到METHOD位置
index = dealWithMethodInfo(bt, index, constant);// 到最后的ATTRIBUTE位置
index = dealWithEndInfo(bt, index, constant);
System.out.println();
System.out.println("检查遍历完成:" + index + "==" + (bt.length - 1));
line = 0;
for (byte d : bt) {
if (line % 16 == 0)
buf.append(String.format("%05x: ", line));
buf.append(String.format("%02x ", d));
line++;
if (line % 16 == 0)
buf.append("\n");
}
buf.append("\n");
return buf.toString();
}
private static int dealWithEndInfo(byte[] bt, int index, String[] constant) {
int cnt = bt[index + 2];
index += 2;
System.out.println("剩余属性数量:" + cnt);
for (int i = 0; i < cnt; i++) {
String name = constant[bt[index + 2] - 1];
System.out.print("名字:" + name);
index += 2;
System.out.print(" ,长度:" + bt[index + 4]);
index += 4;
if ("SourceFile".equals(name)) {
System.out
.print(" ,内容:"
+ constant[Integer
.parseInt(constant[bt[index] - 1]) - 1]);
}
index += bt[index];
System.out.println();
}
return index;
}
private static int dealWithMethodInfo(byte[] bt, int index,
String[] constant) {
int numOfMethod = bt[index];
numOfMethod += bt[++index];
// 打印方法
if (numOfMethod == 0) {
System.out.println(" - - - 无方法 - - - ");
} else {
System.out.println("方法数为:" + numOfMethod);
// 每个方法 前2个字节为 flag 之后2个字节为 name 之后2个字节为描述
// 还有2个字节描述其他属性的个数
for (int noM = 0; noM < numOfMethod; noM++) {
System.out.print("方法作用域为: "
+ getAccessFlag(bt[++index] + bt[++index],
accessMethodFlags));
System.out.print("方法名字为: "
+ constant[bt[++index] + bt[++index] - 1] + " 方法描述为 : "
+ constant[bt[++index] + bt[++index] - 1]);
int numOfAttrOfMethod = (int) (bt[++index] + bt[++index]); // 属性的数量
System.out.println(" 属性数量为 : " + numOfAttrOfMethod);
// 对于属性应该再进行判断 大部分名字都是Code
for (int noAOM = 0; noAOM < numOfAttrOfMethod; noAOM++) {
String methodAttribute = constant[bt[++index] + bt[++index]
- 1];
System.out.print("方法属性分别为: " + methodAttribute);
int attributr_length = (int) (bt[++index] + bt[++index]
+ bt[++index] + bt[++index]);// 取得属性长度
System.out.print(" 属性长度:" + attributr_length);
if ("Code".equals(methodAttribute)) {
index = dealWithCodeInfo(bt, index, constant, index
+ attributr_length);
} else if ("Exceptions".equals(methodAttribute)) {
int exception_numbers = (bt[++index] + bt[++index]);
System.out.print(" 异常个数:" + exception_numbers);
for (int i = 0; i < exception_numbers; i++)
System.out.println(" 异常为:"
+ constant[Integer
.parseInt(constant[bt[++index]
+ bt[++index] - 1]) - 1]);
} else {
index += attributr_length;
}
System.out.println();
}
System.out.println();
}
}
return index;
}
private static int dealWithCodeInfo(byte[] bt, int index,
String[] constant, int end) {
// index += 6;
int maxstacks = bt[++index];
maxstacks += bt[++index];
System.out.print(" MAX STACK:" + maxstacks);
int maxlocals = bt[++index];
maxlocals += bt[++index];
System.out.print(" MAX Locals:" + maxlocals);
int code_length = (int) (bt[++index] + bt[++index] + bt[++index] + bt[++index]);
System.out.println(" Code 长度:" + code_length);
index += code_length;
int exception_length = bt[++index];
exception_length += bt[++index];
System.out.println(" 异常个数:" + exception_length);
String[] c = new String[] { "start_pc", "end_pc", "handler_pc",
"catch_type" };
for (int i = 0; i < exception_length; i++) {
for (int j = 0; j < 3; j++) {
index += 2;
System.out.print(" " + c[j] + ":" + bt[index]);
}
index += 2;
System.out.println(" " + c[3] + ":"
+ constant[Integer.parseInt(constant[bt[index] - 1]) - 1]);
}
int attribute_length = bt[++index];
attribute_length += bt[++index];
System.out.println(" 属性个数:" + attribute_length);
for (int i = 0; i < attribute_length; i++) {
String attribute_name = constant[bt[++index] + bt[++index] - 1];
System.out.print("名字:" + attribute_name);
int a_length = (int) bt[index + 4];
index += 4;
index += a_length;
// System.out.println();
}
// System.out.println(index + "," + end);
return index;
}
private static int dealWithFieldInfo(byte[] bt, int index, String[] constant) {
int numOfField = bt[index];
numOfField += bt[++index];
// 打印字段
if (numOfField == 0) {
System.out.println(" - - - 无字段 - - -");
} else {
// 每个字段 前2个字节为 flag 之后2个字节为 name 之后2个字节为描述
// 还有2个字节描述其他属性的个数
System.out.println("字段数为: " + numOfField);
for (int noF = 0; noF < numOfField; noF++) {
System.out.print("字段作用域为: "
+ getAccessFlag(bt[++index] + bt[++index],
accessFieldFlags));
System.out.print("字段名字为: "
+ constant[bt[++index] + bt[++index] - 1] + " 字段描述为 : "
+ constant[bt[++index] + bt[++index] - 1]);
int numOfAttrOfFiled = (int) (bt[++index] + bt[++index]); // 属性的数量
System.out.println(" 属性数量为 : " + numOfAttrOfFiled);
// 每个属性占有8个字节
for (int noAoF = 0; noAoF < numOfAttrOfFiled; noAoF++) {
System.out.print("字段属性分别为: "
+ constant[bt[++index] + bt[++index] - 1]);
int attributr_length = (int) (bt[++index] + bt[++index]
+ bt[++index] + bt[++index]);// 取得属性长度
System.out.print(" 属性长度:" + attributr_length);
int value = 0;
for (int i = 0; i < attributr_length; i++) {
value = value + bt[++index];
}
if (value < 100 && value > 0)
System.out.print(" 属性内容为: " + constant[value - 1]);
System.out.println();
}
System.out.println();
}
}
return index + 1;
}
private static String getAccessFlag(int i, String[] accessFlags) {
StringBuilder sb = new StringBuilder();
int k = 0;
while (i != 0) {
int res = i & 1;
if (res == 1)
sb.append(accessFlags[k] + " ");
i = i >> 1;
k++;
}
return sb.toString();
}
private static int dealWithInterfaceInfo(byte[] bt, int index,
String[] constant) {
int numOfInterface = bt[index];
numOfInterface += bt[++index];
// 打印接口
if (numOfInterface == 0) {
System.out.println(" - - - 无接口 - - -");
} else {
// 每个接口占有2个字节
System.out.println("接口数为: " + numOfInterface);
for (int noI = 0; noI < numOfInterface; noI++) {
System.out.println("接口分别为: "
+ constant[bt[++index] + bt[++index] - 1]);
}
}
return index + 1;
}
private static int dealWithClassInfo(byte[] bt, int index, String[] constant) {
System.out.println((int) bt[index - 1]);
System.out.println("当前类为 : "
+ constant[Integer.parseInt(constant[bt[index] + bt[++index]
- 1]) - 1]);
System.out.println(" 超 类 为 : "
+ constant[Integer.parseInt(constant[bt[++index] + bt[++index]
- 1]) - 1]);
return index + 1;
}
private static int initConstPool(String[] constant, int[] constLen,
byte[] bt) {
int constNum = constant.length;
int lastConst = 10; // 第一个表项的位置
boolean isUTF8 = false; // 是否为utf8
int line = 0;
for (int i = 0; i < constNum; i++) {
if (i > 0) {
if (isUTF8)
lastConst += constLen[i - 1] + 3; // utf8 有两个字节记录长度
else
lastConst += constLen[i - 1] + 1; // 之后每一个表项的起始位置
}
line = 0; // 记录长度
isUTF8 = recordConstLen(i, constLen, bt, lastConst);
line = recordConsts(constant, lastConst, constLen, bt, isUTF8, i);
}
return line;
}
private static int recordConsts(String[] constant, int lastConst,
int[] constLen, byte[] bt, boolean isUTF8, int k) {
int i;
if (isUTF8) {
byte[] tmp = new byte[constLen[k]];
for (i = lastConst + 3; i < lastConst + constLen[k] + 3; i++) {
tmp[i - lastConst - 3] = bt[i];
}
constant[k] = new String(tmp);
return i;
}
int temp = 0;
for (i = lastConst + 1; i < lastConst + constLen[k] + 1; i++) {
temp += (int) bt[i];
}
constant[k] = "" + temp;
return i;
}
private static boolean recordConstLen(int i, int[] constLen, byte[] bt,
int lastConst) {
if ((int) bt[lastConst] != 1) {
constLen[i] = map.get((int) bt[lastConst]);
return false;
} else {
constLen[i] = ((int) bt[lastConst + 1]) + bt[lastConst + 2];
return true;
}
}
public static byte[] readFile(String file) throws IOException {
InputStream is = new FileInputStream(file);
int length = is.available();
byte bt[] = new byte[length];
is.read(bt);
return bt;
}
public static void main(String[] agrs) throws IOException {
String path = "F:/JVM/SimpleUser.class";
byte[] bt = ClassReader.readFile(path);
String hexData = ClassReader.format(bt);
System.out.println(hexData);
}
}
2.常用字节码指令
知道了CODE里面的字节码之后,就可以通过查表的方式,把字节码指令翻译出来,javap -verbose xxx.class 就有翻译好的字节码。
常用字节码指令集
下图是一个简单的字节码执行过程
3.运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作,在概念模型上,典型的栈帧结构如图所示。
接下来详细讲解一下栈帧中的局部变量表、操作数栈、动态连接、方法返回地址等各个部分的作用和数据结构。
局部变量表
局部变量表(Local Variable Table) 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java 程序编译为 Class 文件时,就在方法的 Code 属性的 max_locals 数据项中确定了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot,下称 Slot)为最小单位,虚拟机规范中并没有明确指明一个 Slot 应占用的内存空间大小,只是很有导向性地说到每个 Slot 都应该能存放一个 boolean、byte、char、short、int、float、reference (注:Java 虚拟机规范中没有明确规定 reference 类型的长度,它的长度与实际使用 32 还是 64 位虚拟机有关,如果是 64 位虚拟机,还与是否开启某些对象指针压缩的优化有关,这里暂且只取 32 位虚拟机的 reference 长度)或 returnAddress 类型的数据,这 8 种数据类型,都可以使用 32 位或更小的物理内存来存放,但这种描述与明确指出 “每个 Slot 占用 32 位长度的内存空间” 是有一些差别的,它允许 Slot 的长度可以随着处理器、操作系统或虚拟机的不同而发送变化。只要保证即使在 64 位虚拟机中使用了 64 位的物理内存空间去实现一个 Slot,虚拟机仍要使用对齐和补白的手段让 Slot 在外观上看起来与 32 位虚拟机中的一致。
既然前面提到了 Java 虚拟机的数据类型,在此再简单介绍一下它们。一个 Slot 可以存放一个 32 位以内的数据类型,Java 中占用 32 位以内的数据类型有 boolean、byte、char、short、int、float、reference 和 returnAddress 8 种类型。前面 6 种不需要多加解释,读者可以按照 Java 语言中对应数据类型的概念去理解它们(仅是这样理解而已,Java 语言与 Java 虚拟机中的基本数据类型是存在本质差别的),而第 7 种 reference 类型表示对一个对象实例的引用,虚拟机规范既没有说明他的长度,也没有明确指出这种引用应有怎样的结构。但一般来说,虚拟机实现至少都应当能通过这个引用做到两点,一是从此引用中直接或间接地查找到对象在 Java 堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现 Java 语言规范中定义的语法约束约束。第 8 种即 returnAddress 类型目前已经很少见了,它是为字节码指令 jsr、jsr_w 和 ret 服务的,指向了一条字节码指令的地址,很古老的 Java 虚拟机曾经使用这几条指令来实现异常处理,现在已经由异常表代替。
对于 64 位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的 Slot 空间。Java 语言中明确的(reference 类型则可能是 32 位也可能是 64 位)64 位的数据类型只有 long 和 double 两种。值得一提的是,这里把 long 和 double 数据类型分割存储的做法与 “long 和 double 非原子性协定” 中把一次 long 和 double 数据类型读写分割为两次 32 位读写的做法有些类似,读者阅读到 Java 内存模型时可以互相对比一下。不过,由于局部变量建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的 Slot 是否为原子操作,都不会引起数据安全问题。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从 0 开始至局部变量表最大的 Slot 数量。如果访问的是 32 位数据类型的变量,索引 n 就代表了使用第 n 个 Slot,如果是 64 位数据类型的变量,则说明会同时使用 n 和 n+1 两个 Slot。对于两个相邻的共同存放一个 64 位数据的两个 Slot,不允许采用任何方式单独访问其中的某一个,Java 虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非 static 的方法),那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字 “this” 来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。
为了尽可能节省栈帧空间,局部变量中的 Slot 是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码 PC 计数器的值已经超出了某个变量的作用域,那这个变量对应的 Slot 就可以交给其他变量使用。不过,这样的设计除了节省栈帧空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot 的复用会直接影响到系统的垃圾收集行为,请看代码清单 8-1 ~ 代码清单 8-3 的 3 个演示。
代码清单 8-1 局部变量表 Slot 复用对垃圾收集的影响之一
public static void main(String[] args) {
{ byte[] placeholder = new byte[64 * 1024 * 1024]; }
System.gc();
}
- [GC 66867K->66104K(124416K), 0.0010904 secs]
- [Full GC 66104K->66007K(124416K), 0.0089807 secs]
placeholder 能否被回收的根本原因是:局部变量中的 Slot 是否还存在关于 placeholder 数组对象的引用。第一次修改中,代码虽然已经离开了 placeholder 的作用域,但在此之后,没有任何局部变量表的读写操作,placeholder 原本占用的 Slot 还没有被其他变量所复用,所以作为 GC Roots 一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断,在绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量的内存、实际上已经不会再使用的变量,手动将其设置为 null 值(用来代替那句 int a=0,把变量对应的局部变量表 Slot 清空)便不见得是一个绝对无意义的操作,这种操作可以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到 JIT 的编译条件)下的 “奇技” 来使用。Java 语言的一本著名书籍《Practical Java》中把 “不使用的对象应手动赋值为 null” 作为一条推荐的编码规则。
关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那样存在 “准备阶段”。通过之前的讲解,我们已经知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始化;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为 Java 中任何情况下都存在诸如整型变量默认为 0,布尔型变量默认为 false 等这样的默认值。
操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到 Code 属性的 max_stacks 数据项中。操作数栈的每一个元素可以是任意的 Java 数据类型,包括 long 和 double。32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2。在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者再调用其他方法的时候是通过操作数栈来进行参数传递的。
另外,在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递,重叠的过程如图 8-2 所示。
Java 虚拟机的解释执行引擎称为 “基于栈的执行引擎”,其中所指的 “栈” 就是操作数栈。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。通过前面的讲解,我们知道 Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化成为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分成为动态连接。
方法返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocatino Completion)。
另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是 Java 虚拟机内部产生的异常,还是代码中使用 athrow 字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的 PC 计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作又:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整 PC 计数器的值以指向方法调用指令后面的一条指令等。
附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
4.方法调用(解析,分派,动态语言支持)
请见我的另一篇文章