JavaClass文件结构
[TOC]
1. 前言
这边文章是基于读者对Java,编译原理,jvm规范有一定了解后书写的。对上述知识缺乏了解的可以执行参考
jvm规范第四章,该章节对class文件结构,jvm字节码质量有详细描述。
本文中主要使用到的字节码指令有 :
-
-dup -
iadd -
iconst_1,iconst_2,iconst_3 -
istore,istore_1,istore_2,istore_3 -
iload,iload_1,iload_2,iload_3 -
new -
getstatic -
dup -
invokespecial -
invokevirtual -
retun本文使用的java命令有:
-
javac -
javap
2. JVM解释运行过程
java语言的运行,是将java文件编译成class文件,然后加载到虚拟机上运行的。在不考虑JIT的情况下,jvm是解释执行的。而且是基于栈实现的运算。这句话是什么意思呢?比如我们常见的一行代码运算
int a = 2+ 3;
这是一个常规的赋值表达式,含义是声明一个变量a,将2+3发的值赋值给变量a。其中2+3是生活中常见的数学表达式。我们称这种表达式为中缀表达式。而经过javac编译之后将形成后缀表达式的形式:2 3 + 的形式。java的解释执行器在读入该行代码的时候,会将2从内存加载到栈里面;然后读取3加载到栈顶,然后读取到+运算符,就会将+运算符需要的两个参数,就是存放在栈里面的2和3弹出来,然后送给CPU做加法运算。CPU完成运算之后,将运算结果5压入栈中。然后通过istore命令将5的值存到变量a指向的内存地址中。
3. class文件结构说明
我们来看一下源文件Test.java
public class Test{
public void concat(){
int a = 1;
int b = 1;
int c = 1;
int d = 2;
System.out.println(a + "s" + b + c + d);
}
}
我们通过javac Test.java 命令编译,然后再使用javap -verbose Test命令查看变异后的class文件结构,我们截取一部分主要内容如下:
public void concat();
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 12: 0
line 13: 9
line 14: 47
上面的源码只截取的concat方法的的编译结果,我们重点关注下Code部分。
3.1 stack
我们首先关注的是stack=3,这句话的含义是concat方法在执行的过程中需要最大的操作栈深度为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 :将int1压入栈顶
5: istore_3 :将栈顶的1弹出并赋值给变量c
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 :执行StringBuilder的初始化方法
19: iload_1 :将变量a的值压入栈顶,stack=3
20: invokevirtual #5 :执行append方法,依次弹出栈顶的a和StringBuilder,stack=1,执行完成后将StringBuilder对象的引用值压入栈顶,stack=2
23: ldc #6 :将字符s压入栈顶,stack=3
25: invokevirtual #7 :执行append方法,依次弹出栈顶的s和StringBuilder,stack=1,执行完成后将StringBuilder对象的引用值压入栈顶,stack=2。
28: iload_2 :将变量b的值压入栈顶,stack=3
29: invokevirtual #5 :执行append方法,依次弹出栈顶的b和StringBuilder,stack=1,执行完成后将StringBuilder对象的引用值压入栈顶,stack=2
32: iload_3 :同28
33: invokevirtual #5 :同29
36: iload 4 :同28
38: invokevirtual #5 :同29
41: invokevirtual #8 :执行StringBuilder的toString方法,并将返回的字符串压入栈顶,stack=2
44: invokevirtual #9 :将栈顶的字符串弹出,执行println方法,stack=1
47: return :当前方法返回,void无返回值。
观察整个执行过程后,可以得出,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指针作为参数,所以就需要占用一个本地变量表的位置和一个参数位。如果将concat方法改为static的,则不需要传入this指针,就不会去占用了。此时locals将变成4,arg_size则变为0。读者可自行验证。
4. 结论
-javac编译的时候,会把我们常见的中缀表达式翻译成jvm使用的后缀表达式。- jvm是基于栈进行解释执行的。
- class文件中的statck是方法执行过程中调用的最大操作数栈深度。
- class文件中的locals是本地变量表中的变量需要的slot的个数。
- clsss文件中的arg_size是方法的参数个数,static方法不需要传入this指针,单非static防范默认会传入this指针。
- this指针要占用本地变量表中的slot个数和方法参数的个数。