[TOC]
1. 前言
这边文章是基于读者对Java,编译原理,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: ()Vflags: ACC_PUBLICCode: 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."":()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弹出并赋值给变量a2: iconst_1 :将int1压入栈顶3: istore_2 :将栈顶的1弹出并赋值给变量b4: iconst_1 :将int1压入栈顶5: istore_3 :将栈顶的1弹出并赋值给变量c6: iconst_2 :将int2压入栈顶7: istore 4 :将栈顶的2弹出并赋值给本地变量d9: getstatic #2 :获取PrintStream对象12: new #3 :创建StringBuilder对象,并将对象的引用值压入栈顶,stack=115: dup :复制栈顶的值,并且压入栈顶,stack=216: invokespecial #4 :执行StringBuilder的初始化方法 19: iload_1 :将变量a的值压入栈顶,stack=320: invokevirtual #5 :执行append方法,依次弹出栈顶的a和StringBuilder,stack=1,执行完成后将StringBuilder对象的引用值压入栈顶,stack=2 23: ldc #6 :将字符s压入栈顶,stack=325: invokevirtual #7 :执行append方法,依次弹出栈顶的s和StringBuilder,stack=1,执行完成后将StringBuilder对象的引用值压入栈顶,stack=2。28: iload_2 :将变量b的值压入栈顶,stack=329: invokevirtual #5 :执行append方法,依次弹出栈顶的b和StringBuilder,stack=1,执行完成后将StringBuilder对象的引用值压入栈顶,stack=2 32: iload_3 :同2833: invokevirtual #5 :同29 36: iload 4 :同2838: invokevirtual #5 :同29 41: invokevirtual #8 :执行StringBuilder的toString方法,并将返回的字符串压入栈顶,stack=2 44: invokevirtual #9 :将栈顶的字符串弹出,执行println方法,stack=147: 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个数和方法参数的个数。