知识背景:JVM(Java Virtual Machine)是不能直接运行java的源码文件的,必须要经过编译变成字节码文件才能运行。
计算机是不能直接运行java代码的,必须要先运行java虚拟机,再由java虚拟机运行编译后的java代码。这个编译后的java代码,就是本文要介绍的java字节码。
为什么jvm不能直接运行java代码呢,这是因为在cpu层面看来计算机中所有的操作都是一个个指令的运行汇集而成的,java是高级语言,只有人类才能理解其逻辑,计算机是无法识别的,所以java代码必须要先编译成字节码文件,jvm才能正确识别代码转换后的指令并将其运行。
class文件本质上是一个以8位字节为基础单位的二进制流
,各个数据项目严格按照顺序紧凑的排列在class文件中。jvm根据其特定的规则解析该二进制数据,从而得到相关信息。
Class文件采用一种伪结构来存储数据,它有两种类型:无符号数
和表
。这里暂不详细的讲。
下图是接下来本文的简单的java例子编译后的文件,已经将class文件转为16进制。可以看到,我们熟悉的java代码经过编译转换为只有机器能识别的数据。
新建一个Test.java文件,然后编辑
public class Test{
public static void main(String[] args){
Integer a=1;
Integer b=2;
Integer c=a+b;
System.out.println(c+"");
}
}
将该代码进行编译并保存在当前目录下:
编译完成目录下会多一个Test.class编译文件。使用16进制打开,内容如下:
在图中,前4个字节cafe babe
就是魔数,紧接着魔数的4个字节代表的是Class文件的版本号。
第5,6个字节表示的是次版本号(minor version),在上图中为0000
,说明class文件的次版本号为 0 。
第7,8个字节代表主版本号(major version),在上图中为0034
,因为是16进制,计算可以得到该class文件的主版本号为52.
以此类推,根据java字节码的规则,可以依次解析成该字节码文件的所有内容。
当然,jdk中包含了一个可以将字节码文件“可视化”操作的命令javap
,运行该命令,可以将java字节码文件解析为符合人类逻辑的文件。
查看java字节码可以使用 javap
命令,当然也可以使用IDE的插件进行查看(比如在Idea中使用jclasslib插件进行查看)
在当前目录下运行
javap -v Test.class
会出现我们可以正常阅读的字节码
Classfile /F:/Test.class
Last modified 2018-10-21; size 752 bytes
MD5 checksum 4848a65fcbc8b0bc8ce60eb32172471b
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #13.#22 // java/lang/Object."":()V
*** 省略部分代码
#13 = Class #36 // java/lang/Object
#14 = Utf8 <init>
#15 = Utf8 ()V
*** 省略部分代码
#22 = NameAndType #14:#15 // "":()V
*** 省略部分代码
#36 = Utf8 java/lang/Object
*** 省略部分代码
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: iconst_2
6: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: astore_2
10: aload_1
11: invokevirtual #3 // Method java/lang/Integer.intValue:()I
14: aload_2
15: invokevirtual #3 // Method java/lang/Integer.intValue:()I
18: iadd
19: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: astore_3
23: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
26: new #5 // class java/lang/StringBuilder
29: dup
30: invokespecial #6 // Method java/lang/StringBuilder."":()V
33: aload_3
34: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lan
/StringBuilder;
37: ldc #8 // String
39: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lan
/StringBuilder;
42: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
45: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
48: return
LineNumberTable:
line 3: 0
line 4: 5
line 5: 10
line 6: 23
line 7: 48
}
SourceFile: "Test.java"
javap -v
命令显示的字节码文件我们从上到下看看例子的字节码文件各个字段代表的意思
Classfile /F:/Test.class
Last modified 2018-10-21; size 752 bytes
MD5 checksum 4848a65fcbc8b0bc8ce60eb32172471b
Compiled from "Test.java"
这一部分一看就很明白
Last modified
最后修改时间size
: 该字节码文件大小MD5 checksum
该文件的md5Compiled from "Test.java"
表示该字节码文件是由“Test.java”编译而来public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
这一部分表示Test类的各个属性
minor version: 0
表示可以支持最小的jdk版本,java的jdk都是向下兼容的,所以该值一般都为0major version: 52
编译Test.java的jdk版本,我使用的是jdk1.8,所以52代表的是jdk8flags: ACC_PUBLIC, ACC_SUPER
表示该类的访问属性flags属性类型及含义如下
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为Public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义. |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
ACC_ENUM | 0x4000 | 标志这是一个枚举 |
Constant pool:
#1 = Methodref #13.#22 // java/lang/Object."":()V
*** 省略部分代码
#13 = Class #36 // java/lang/Object
#14 = Utf8 <init>
#15 = Utf8 ()V
*** 省略部分代码
#22 = NameAndType #14:#15 // "":()V
*** 省略部分代码
#36 = Utf8 java/lang/Object
*** 省略部分代码
常量池,可以理解为java资源的仓库,JVM运行方法时需要用到的数据都会在这里拿。下面从第一个常量开始分析各个字符的意义:
#1 = Methodref #13.#22 // java/lang/Object."
#1
表示常量池值的序号,该数字表示为第一个#1 = Methodref
表示第一个值是一个方法的引用#13.#22
表示该方法的完整属性还需要#13
和#22
的来复合表示
#13 = Class #36 // java/lang/Object
#36 = Utf8 java/lang/Object
#13
说明属性为class,而#36
指明了该class为object类- 将
#13
和36
结合一起,#13
就得属性就是一个class类,且其具体的类为java.lang.Object- 仔细观察
#13
后的注释,你发现了什么?其实在将java字节码可视化的时候,javap就已经将各个常量的属性都给关联好了
#22 = NameAndType #14:#15 // "
NameAndType
这一看知道是代表名字和类型- 按照上述的方式解析,会发现
#22
的名字为,类型为
()V
表示无参数并且返回值为void
根据上面的步骤分析可以得出:#1
引用的方法就是Test类的初始构造方法
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
这个Test()方法就是我们Test类中默认的构造方法,在我们没有重写或者没有指定对应的构造方法时,java编译的时候会默认生成一个空的构造方法。
下面我们来看看这个方法里面的各个字段代表的意思:
descriptor: ()V
·descriptor· 表示该方法的描述,这里表示的是该方法的参数为空,且方法值为void
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
stack
最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1locals
局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的。args_size
方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数this0: aload_0
1: invokespecial #1 // Method java/lang/Object."
":()V
4: return
表示运行方法时,各个指令的顺序,该顺序在下一章会进行详细的介绍。LineNumberTable
该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。可以使用 -g:none 或 -g:vars来取消或生成这项信息,如果没有生成这项信息,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0, arg1这样的占位符。
start 表示该局部变量在哪一行开始可见,length表示可见行数,Slot代表所在帧栈位置,Name是变量名称,然后是类型签名。
至此,把java字节码文件的各个属性都简单的介绍了一次。当然,还有很多其他的属性没有在示例代码中体现出来,这里就不进一步的介绍。本篇文章目的让大家对java字节码有个清晰的认识,在大脑中有个基础的概念,以后如果想深入了解,就知道从哪一方面入手,进行快速的学习。
参考文章:
深入理解JVM-字节码详解
轻松看懂字节码文件
详解java字节码class文件
《深入理解java虚拟机》