Javaclass文件结构说明
Table of Contents
1 前言
这篇文章是基于读者对Java,编译原理,jvm规范有一定了解后书写的。对上述知识缺乏了解的可参考jvm规范,第四章 改章节对class文件结构,jvm字节码质量有详细描述。
+本文中主要使用到的字节码指令有 -dup:复制栈顶数据并重新压入栈 -iload:将int型数值压入栈 -iadd:加法运算,需要两个参数 -iconst_1:将int类型1压入栈 -istore:将栈顶int值存到本地变量中 -new:创建一个对象,并将引用值压入栈 -invokervirtual:调用实例方法 +本文中使用的Java命令有 -javac:Java语言编译命令,将java源文件编译成虚拟机可执行的class文件 -javap:Java反编译命令,讲class文件反编译成Java文件,或通过-verbose参数查看class文件结构
2 JVM解释运行过程
Java语言的运行,是将Java文件编译成class文件,然后加载到虚拟机上运行的。在不考虑JIT(Java及时编译) 的情况下,jvm是解释执行的,而且是基于栈实现的运算。 这句话是什么意思呢?比如我们常见的一行代码:
int a = 2 + 3;
这是一个常见的赋值表达式,含义是声明一个变量a,将2+2的值赋值给变量a。其中2+3是生活中常见的数学表达式,我们称这种形式的表达式为中缀表达式。而经过javac编译后 将形成后缀表达式的形式:2 3 + 的形式。Java的解释执行器在读入该行代码的时候,会将2从内存加载到栈里面;然后读取3加载到栈里面,然后读取到+运算符,就会将+运算符需要 的两个参数,也就是栈里面的2和3弹出来,然后送给CPU做加法运算。CPU运算完成后,将运算结果4压入栈中。然后通过istore命令将4的值存到变量a指向的内存地址中。 关于前缀表达式,中缀表达和后缀表达式三种表达式求值的运算逻辑可参考csdn博客 。
3 class文件结构说明
源文件T.java
public class T {
public void test() {
int a=1,b=1,c=1,d=2;
System.out.println(a + "s" + b + c + d);
}
}
我们通过javac T.java 命令编译,然后再使用javap -verbose T命令查看编译后的class文件结构
编译生成的T.class文件结构:
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iconst_1
3: istore_2
4: iconst_1
5: istore_3
6: iconst_2
7: istore 4
9: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
12: new #3 // class java/lang/StringBuilder
15: dup
16: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
19: iload_1
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
23: ldc #6 // String s
25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: iload_2
29: invokevirtual #5 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
32: iload_3
33: invokevirtual #5 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
36: iload 4
38: invokevirtual #5 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
41: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
44: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
47: return
LineNumberTable:
line 3: 0
line 4: 9
line 5: 47
上面的源码只截取了test方法的编译结果。我们重点关注Code部分。
3.1 stack
我们首先关注的是stack=3,这句话的含义是test方法在执行的过程中需要的最大操作数栈深度为3。根据我们上一节解释的,jvm是基于栈进行解释执行的。 我们按照编译出来的jvm字节码一行一行的模拟jvm的运行过程
0: iconst_1 :将int1压入栈
1: istore_1 :将栈顶的1弹出赋值给变量a
2: iconst_1 :将int1压入栈
3: istore_2 :将栈顶的1弹出赋值给变量b
4: iconst_1 :
5: istore_3 :
6: iconst_2 :将int2压入栈
7: istore 4 :将栈顶的2弹出赋值给变量d
9: getstatic #2 :获取PrintStream对象
12: new #3 :新建StringBuilder对象,并将对象地址值压入栈,stack=1
15: dup :复制栈顶值,并压入栈,stack=2
16: invokespecial #4 :执行SB对象的初始化方法
19: iload_1 :将变量a的值压入栈,stack=3
20: invokevirtual #5 :执行append方法,依次弹出栈顶的a和sb,stack=1,执行完成append方法后,将sb对象压入栈,stack=2。
23: ldc #6 :将字符s压入栈,stack=3
25: invokevirtual #7 :执行append方法,将s和sb弹出栈,stack=1,执行完成append方法,将sb压入栈,stack=2。
28: iload_2 :同19
29: invokevirtual #5 :同20
32: iload_3 :同19
33: invokevirtual #5 :同20
36: iload 4 :同19
38: invokevirtual #5 :同20
41: invokevirtual #8 :执行sb的toString方法,并将返回的字符串压入栈,stack=2
44: invokevirtual #9 :将栈顶的字符串弹出,执行println方法,stack=1
47: return :方法返回,无须返回值
观察整个执行过程后,可以得出,test在执行的过程中,使用到的最大操作数栈的深度为3。
3.2 locals
locals是本地变量的数量。本例中,共需要储存三个1,一个2和一个this指针,所以本地变量表中需要5个slot储存变量。主意long和double是64位的,所以需要两个slot存储。但本例中, 5个变量都是32位的,不需要扩展slot。
3.3 arg_size
arg_size是方法参数的个数,因为该方法是实例方法,所以会默认传入this指针作为参数,所以需要占用一个本地变量表的位置和一个参数位。如果将test方法改为static的,则不需要传入 this指针,就不会占用了,locals将变成4,arg_size变成0。读者可自行实验验证。
4 结论
-javac编译的时候,会把我们常见的中缀表达式翻译成jvm使用的后缀表达式 -jvm是基于栈进行解释执行的 -class文件中stack是方法执行过程中用到的最大操作数栈深度 -class文件中locals是本地变量表中变量需要的slot的个数 -class文件中arg\_size是方法的参数个数 -static方法不需要传入this指针,但非static方法默认会传入this指针。this要占用本地变量表中slot的个数和方法参数的个数