Java class文件格式说明

Javaclass文件结构说明

1 前言

我先默默的宣传一下。吐舌头讨论Java技术请加群:323849607。本文由群内成员(战队,spring等人)讨论整理而成。

这篇文章是基于读者对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的个数和方法参数的个数

Author: 王月阳

Created: 2017-09-17 周日 11:05

Emacs 24.5.1 (Org mode 8.2.10)

Validate

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值