对于如下的Java代码(Demo.java):
package cn.bjut;
public class Demo {
private int x = 10;
public void testMethod() {
}
}
它对应10个部分:
Magic Number: 0xCAFEBABE
Version of Class File Format: the minor and major versions of the class file
Constant Pool: Pool of constants for the class
Access Flags: for example whether the class is abstract, static, etc.
This Class: The name of the current class
Super Class: The name of the super class
Interfaces: Any interfaces in the class
Fields: Any fields in the class
Methods: Any methods in the class
Attributes: Any attributes of the class (for example the name of the sourcefile, etc.)
这张图是一张java字节码的总览图,我们也就是按照上面的顺序来对字节码进行解读的。
*.class字节码文件最开始的4个字节表示的是魔数,对应我们Demo.class
的是 0xCAFEBABE
。
什么是魔数?
魔数是用来区分文件类型的一种标志,一般都是用文件的前几个字节来表示。比如0xCAFEBABE
表示的是class文件。
版本号包含主版本号和次版本号,都是各占2个字节。
在此Demo.class
中为0x00000034
。其中前面的两个字节0000是次版本号,后面的两个字节0034是主版本号。通过进制转换得到的是次版本号为0,主版本号为52(16进制(0x34)转10进制(52))。
从oracle官方网站我们能够知道,52对应的是jdk1.8,而其次版本为0,所以该文件的版本为1.8.0。如果需要验证,可以在用javac --version
命令输出版本号,或者修改编译目标版本-target
重新编译,查看编译后的字节码文件版本号是否做了相应的修改。
Java SE 9 = 53 (0x35 hex)
Java SE 8 = 52 (0x34 hex)
Java SE 7 = 51 (0x33 hex)
Java SE 6.0 = 50 (0x32 hex)
Java SE 5.0 = 49 (0x31 hex)
JDK 1.4 = 48 (0x30 hex)
JDK 1.3 = 47 (0x2F hex)
JDK 1.2 = 46 (0x2E hex)
JDK 1.1 = 45 (0x2D hex)
紧接着主版本号之后的就是常量池入口。常量池是Class文件中的资源仓库,在接下来的内容中我们会发现很多地方会涉及到常量池,如Class Name,Interfaces等。常量池中主要存储2大类常量:字面常量和符号引用。字面常量如文本字符串,java中声明为final的常量值等等;而符号引用如类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
为什么需要类和接口的全限定名呢?系统引用类或者接口的时候不是通过内存地址进行操作吗?这里大家仔细想想,java虚拟机在没有将类加载到内存的时候根本都没有分配内存地址,也就不存在对内存的操作,所以java虚拟机首先需要将类加载到虚拟机中,那么这个过程设计对类的定位(需要加载A包下的B类,不能加载到别的包下面的别的类中),所以需要通过全局限定名来判别唯一性。这就是为什么叫做全局限定的意思,也就是唯一性。
在进行具体常量池分析之前,我们先来了解一下常量池的项目类型表:
上面的表中描述了11中数据类型,其实在jdk1.7之后又增加了3种(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info以及CONSTANT_InvokeDynamic_info)。这样算起来一共是14种。接下来我们按照Demo的字节码进行逐一翻译。
0x0012:由于常量池的数量不固定(n+2),所以需要在常量池的入口处放置一项u2类型的数据代表常量池数量。因此该16进制是12,表示有18项常量,索引范围为1~17。明明是有18项,为何索引范围是1~17呢?因为Class文件格式规定,设计者就讲第0项保留出来了,以备后患。从这里我们知道接下来我们需要翻译出17项常量。
0x0A:从常量类型表中我们发现,第一个数据均是u1类型的tag,16进制的0A是十进制的10,对应表中的CONSTANT_Methodref_info。
0x0004:CONSTANT_Class_info索引项 #4
0x000E:CONSTANT_NameAndType索引项 #14
0x09:CONSTANT_Fieldref_info
0x0003:CONSTANT_Class_info索引项 #3
0x000F:CONSTANT_NameAndType索引项 #15
0x07:CONSTANT_Class_info
0x0010:全局限定名常量索引为 #16
0x07:CONSTANT_Class_info
0x0011:全局限定名常量索引为 #17
0x01:CONSTANT_Utf-8_info
0x0001:UTF-8编码的字符串长度为1
0x78:”x”(十六进制转ASCII字符)
0x01:CONSTANT_Utf-8_info
0x0001:UTF-8编码的字符串长度为1
0x49:”I”
0x01:CONSTANT_Utf-8_info
0x0006:UTF-8编码的字符串长度为6
0x3C-69-6E-69-74-3E:”
”
0x01:CONSTANT_Utf-8_info
0x0003:UTF-8编码的字符串长度为3
0x28-29-56:”()V”
0x01:CONSTANT_Utf-8_info
0x0004:UTF-8编码的字符串长度为4
0x43-6F-64-65:”Code”
0x01:CONSTANT_Utf-8_info
0x000F:UTF-8编码的字符串长度为15
0x4C-69-6E-65-4E-75-6D-62-65-72-54-61-62-6C-65:”LineNumberTable”
0x01:CONSTANT_Utf-8_info
0x000A:UTF-8编码的字符串长度为10
0x74-65-73-74-4d-65-74-68-6f-64:”testMethod”
0x01:CONSTANT_Utf-8_info
0x000A:UTF-8编码的字符串长度为10
0x53-6F-75-72-63-65-46-69-6C-65:”SourceFile”
0x01:CONSTANT_Utf-8_info
0x0009:UTF-8编码的字符串长度为9
0x44-65-6D-6F-2E-6A-61-76-61:”Demo.java”
0x0C:CONSTANT_NameAndType_info
0x0007:字段或者方法名称常量项索引 #7
0x0008:字段或者方法描述符名称常量项索引 #8
0x0C:CONSTANT_NameAndType_info
0x0005:字段或者方法名称常量项索引 #5
0x0006:字段或者方法描述符名称常量项索引 #6
0x01:CONSTANT_Utf-8_info
0x000C:UTF-8编码的字符串长度为12
0x63-6E-2F-62-6A-75-74-2F-44-65-6D-6F:”cn/bjut/Demo”
0x01:CONSTANT_Utf-8_info
0x0010:UTF-8编码的字符串长度为16
0x6A-61-76-61-2F-6C-61-6E-67-2F-4F-62-6A-65-63-74:”java/lang/Object”
访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final。通过上面的源代码,我们知道该文件是类并且是public。
0x0021:是0x0020和0x0001的并集。
类索引用于确定类的全限定名
0x0003 表示引用第3个常量,同时第3个常量引用第16个常量,查找得”cn/bjut/Demo”。#3 -> #16
0x0004 同理:#4 -> #17(java/lang/Object)
通过 java字节码总览图 我们知道,这个接口有2+n个字节,前两个字节表示的是接口数量,后面跟着就是接口的列表。我们这个类没有任何接口,所以应该是0000。果不其然,查找字节码文件得到的就是0000。
字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
同样,接下来就是2+n个字段属性。我们只有一个属性x,按道理应该是0001。查找文件果不其然是0001。
那么接下来我们要针对这样的字段进行解析。附上字段表结构图
0x0002: 访问标志为private(ACC_PRIVATE)
0x0005: 字段名称索引为#5,对应的是”x”
0x0006: 描述符索引为#6,对应的是”I”
0x0000: 属性表数量为0,因此没有属性表
我们只有一个方法testMethod,按照道理应该前2个字节是0001。通过查找发现是0x0002。这是什么原因,这代表着有2个方法呢?且继续看……
上图是一张方法表结构图,按照这个图我们分析下面的字节码:
0x0001:访问标志 ACC_PUBLIC,表明该方法是public。(可自行搜索方法访问标志表)
0x0007:方法名索引为#7,对应的是”
”
0x0008:方法描述符索引为#8,对应的是”()V”
0x0001:属性表数量为1(一个属性表)
那么这里涉及到了属性表。什么是属性表呢?可以这么理解,它是为了描述一些专有信息的,上面的方法带有一张属性表。所有属性表的结构如下图:
一个u2的属性名称索引,一个u2的属性长度加上属性长度的info。
虚拟机规范预定义的属性有很多,比如Code,LineNumberTable,LocalVariableTable,SourceFile等等,这个网上可以搜索到。
按照上面的表结构解析得到下面信息:
0x0009:名称索引为#9(“Code”)。
0x00-00-00-27:属性长度为39字节。
那么接下来解析一个Code属性表,按照下图解析
0x0001:”public”
0x000B:#11(”testMethod”)
0x0008:”()V”
0x0001:属性数量=1
0x0009:”Code”
0x00-00-00-19:属性长度为25
解析一个Code表
0x0000:max_stack =0
0x0001:max_local =1
0x00-00-00-01:code_length =1
0xB1:return(该方法返回void)
0x0000:异常表长度=0
0x0001:属性表长度为1
//第一个属性表
0x000A:#10,LineNumberTable
0x00-00-00-06:属性长度为6
0x0001:line_number_length = 1
0x0000:start_pc =0
0x0006:line_number =6
0x0001: 同样的,表示有1个Attributes了。
0x000C: #12(“SourceFile”)
0x00000002: attribute_length=2
0x0010: sourcefile_index = #13(“Demo.java”)
SourceFile属性用来记录生成该Class文件的源码文件名称。
我们也可以使用java自带的反编译器来解析字节码文件:
javac cn/bjut/Demo.java
javap -verbose cn.bjut.Demo > info.txt
Classfile /D:/N3verL4nd/Desktop/cn/bjut/Demo.class
Last modified 2017-8-10; size 278 bytes
MD5 checksum cdb9f028281cce76494a801949018db5
Compiled from "Demo.java"
public class cn.bjut.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#14 // java/lang/Object."":()V
#2 = Fieldref #3.#15 // cn/bjut/Demo.x:I
#3 = Class #16 // cn/bjut/Demo
#4 = Class #17 // java/lang/Object
#5 = Utf8 x
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 testMethod
#12 = Utf8 SourceFile
#13 = Utf8 Demo.java
#14 = NameAndType #7:#8 // "":()V
#15 = NameAndType #5:#6 // x:I
#16 = Utf8 cn/bjut/Demo
#17 = Utf8 java/lang/Object
{
public cn.bjut.Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field x:I
10: return
LineNumberTable:
line 2: 0
line 3: 4
public void testMethod();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 6: 0
}
SourceFile: "Demo.java"
一文让你明白Java字节码
可视化字节码分析工具classpy:https://github.com/zxh0/classpy
Java class file
The class File Format