写在前面
读懂 Class 文件是了解虚拟机运行原理的重要步骤,本文将结合 《深入理解Java虚拟机》中的内容,和大家分享解读 Class 文件的过程。
一、什么是 Class 文件
定义
Class 文件是一组以 8 位字节为基础单位的二进制流
简单地说,Class 文件是由一堆二进制组成的。这些二进制的排列是有一定规则的,虚拟机就是根据这些规则把数据加载到内存中使用的。
实例
我们知道 Class 文件的生成可以由 Java 编译而来。接下来是一个简单的例子:
- 首先准备一个 java 文件,里面包含一个属性和方法:
package com.sky.test;
public class Test {
private int i = 10;
private void test(){
}
}
- ide 编译一下生成 Class 文件,这里用的是 AndroidStudio。早就知道编译后会生成 Class 文件,最后果然在 build 文件夹下找到了 Test.class:
CommonBaseApp\test\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\com\sky\test
找到文件后使用 Sublime Text(一个文字编辑器)打开,需要注意的是这里该 Class 文件是用十六进制的格式打开的,里面代表的是十六进制的值:
好家伙,这一堆字符。但是不要慌,前面提到过 Class 文件是遵循一定规则的,熟悉了这个规则很容易就看懂了。
二、Class 文件的结构
一张比较经典的图描述了 Class 文件的结构:
这里的字节实际上是 Java 虚拟机规定的数据类型,1 个字节即 U1、两个字节即 U2 类型。 下文的字节描述对应 U* 类型。
三、Class 文件解析
接下来我们试着用刚才的文件,试着匹配下这个结构图。
Magic Number
魔数:Test.class 开头四个字节 cafe babe
叫做 Magic Number。加上这么一个开头是出于安全性考虑,也是虚拟机识别 Class 文件的重要标识。
Version 版本
后面的四个字节为 0000 0033
。这部分前两个字节 0x0000
表示次版本号为 0,主版本号 0x0033
转换为十进制是 51,说明当前文件版本为 51。
这个版本的 Class 文件可以被 JDK 1.7.0 版本以上虚拟机执行,如果用版本不兼容的虚拟机加载 Class 文件会报错。
Constant Pool 常量池
再往后面的 0x0015
表示常量池元素的数量,0x0015
转换为十进制为 21,说明该常量池有 20 个元素。
这里的下标是从 1 开始的,所以是 20 个元素而不是 21 个。把开始下标 0 置空是为了表达某些情况下 “不引用任何一个常量池项目” 的含义。
#1 常量池第一个元素
接下来就是常量池里面的元素了,紧接着的值是 0x0a=10
。
我们可以把常量池中的元素看作是一个整体串,其中有 tag 表示其类型、index 表示其指向的常量池下标、length 表示其长度等等。这么一个个整体组成了常量池表。
那么这个 0x0a=10
代表的什么意思呢,看下面的表:
去找 tag 为 10 的常量,是 CONSTANT_Methodref_info,说明这个元素记录的是一个方法的信息。
[图片上传失败...(image-f0f77-1600151856238)]
说明接下来的数据是一个方法引用的信息,把这个 CONSTANT_Methodref_info 看作是一个类,看这个类的构成:
- U1(一个字节)的 tag 标记,也就是
0a=10
。 - U2(两个字节)的指向该方法的类在常量池中的索引值,也就是这个方法属于哪个类,并指出这个类在常量池中的位置。
在字节码中是0x0004=4
说明属于常量池 #4 的元素所表示的类。 - U2(两个字节)的指向其名称及类描述符的索引值,也就是这个方法的名字以及返回值所在的位置。
在字节码中是0x0011=17
说明属于常量池 #17 的元素所表示的信息。
#2 常量池第二个元素
下一个元素的 tag 为 0x09=9
。
从表中可知为 CONSTANT_Fieldref_info,即字段引用信息。
它也包含三种信息;
- U1(一个字节) 的 tag 标记,也就是
0x09=9
; - U2(两个字节)的指向该字段所属类或接口的常量池索引,也就是这个字段所属类或接口在常量池中的位置。
在字节码中为0x0003=3
说明属于常量池下标为 3 的元素所表示的类。 - U2(两个字节)的指向该字段描述符的常量池索引,也就是这个字段的名称和描述符在常量池中的位置。
在字节码中为0x0012=18
说明指向常量池下标为 18 的元素所表示的信息。
#F 利用工具查看字节码
其实整个 Class 字节码可以通过工具查看,我们直接借用工具来看。IDE 中安装 jclasslib Bytecode viewer,安装重启。选中要查看字节码的类,点这个:
如果点击提示未找到 class 文件,说明需要编译一下。编译之后再次点击出现如下信息:
这样这个 Class 文件的所有信息就展示出来了,开始是总览信息,包含 Java 版本、类名、常量池数量等信息。前面我们已经分析过了,这个工具也是根据规则去读取 Class 文件的。
第二个元素的详细信息
前面我们已经知道第二个元素所属类的索引是 3,点到常量池第三个选项可以看到如下信息:
可以看到工具告诉我们第三个元素是 CONSTANT_Class_info 也就是类信息,并且右边的 Class name 指向了 cp_info #19,也就是常量池的 #19 表示了第二个元素所属的类。可以从工具看到 #19 表示的字符串是 com/sky/test/Test,说明第二个元素属于该类。
回头看第二个元素的字段描述符所在的索引,18。我们就不再去字节码挨个找第 18 个元素了,直接借助工具看:
第 18 个元素是 CONSTANT_NameAndType_info 类型的,说明是名称和类型的描述:
- tag:一个字节的 tag,倒推一下是
0x0c=12
; - 两个字节的方法名称索引:
0x0005=5
。 - 两个字节的方法描述符索引:
0x0006=6
。
如果去字节码去找的话是能找到一串连续的 0c 0005 0006
的,说明倒推的数据也是正确的。
到这里简单捋一捋,第二个元素的方法名和类型指向了 #18,而 #18 的信息又指向了 #5 和 #6。就是说搞清楚 #5 和 #6 就搞清楚第二个元素到底是什么了。
#5 #6
借助工具可知 #5 #6 的类型为 CONSTANT_Utf8_info 也就是一个字符串,字符串是怎么描述的呢,需要再看之前的表:
- tag:一个字节的 tag
0x01=1
表示当前信息是字符串; - length:两个字节的信息表示当前字符串的长度;
- bytes:一个字节的信息表示当前字符串的 ASCII 码。
字节码中找到第 5 个元素如下图:
0x01
表示字符串,0x0001
表示长度为 1。0x69=69
表示字符串的值,而 69 在 ASCII 码中表示字母 i。
千辛万苦终于找到了个字符串 i,可以猜测是我们之前 java 代码里定义的 int 变量 i。
接着再找字节码的第 6 个元素 01000149
,0x01
代表字符串、0x0001
表示字符串长度为 1、0x49=73
在 ASCII 码中表示大写字母 I。
所以 #5 和 #6 表示的属性类型为 I 说明这是一个 int 类型的值,并且属性名为 i。
#19 的名字
看懂了字符串怎么表示,#19 这个表示类名的字符串也可以看懂了。无非就是 0x01 开头的,后面跟了长度以及一堆跟 ASCII 码对应的数值。这里就不再逐一分析了,直接看答案就好:
描述了 Test 类的全限定名,也就是能表示 Test 类唯一性的路径以及类名。
访问标志
常量池结束后,紧接着两个字节代表访问标志,这个标志用于识别这个 Class 是类还是接口、是 public 还是 abstract。如果是类,是否为 final 等。
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为Public |
ACC_PRIVATE | 0x0002 | 是否为Private |
ACC_PROTECTED | 0x0004 | 是否为Protected |
ACC_STATIC | 0x0008 | 是否为static |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义.jdk 1.0.2 以后编译出来的都必须为真. |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
ACC_ENUM | 0x4000 | 标志这是一个枚举 |
我们知道 Test 是一个类,并且是高版本 jdk 编译出来的,所以它的 flag 应该是 0x0001
和 0x0020
。把他们进行或运算,结果为 0x0021
。无论是从字节码还是用工具看,都能印证这个结果。
类索引
类索引最终表示的是该类的类名。它指向的是常量池中一个 CONSTANT_Class_info 类型的常量,而该常量又指向表示其名称的字符串常量。
图中标红的位置就是类索引在常量池中的下标,前文分析字段的时候已经知道 #3 为一个 CONSTANT_Class_info 类型,指向表名其名称的字符串为
com/sky/test/Test
正是该类的全限定名。
父类索引
父类索引紧随着类索引,字节码中是 0x0004 指向的是 #4。与类索引相同,最终表示为 java/lang/Object
。说明 Test 类的父类为 Object。
接口索引
接口索引在父类索引之后,它的表示为一组 u2 类型的数据的集合。字节码中为 0x0000
说明没有实现接口。如果有接口实现,则先表示数量,然后再指向接口全限定名在常量池中的下标。
假如 Test 实现了两个接口,并且两个接口在常量池中的表示位置为 3 和 4,那么对应的字节码应该是 0002 0003 0004
。
字段表集合
字段表用于描述接口或者类中声明的变量。
紧接着的字节码表示字段表,字段表的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attributes_info | attributes | attributes_count |
[图片上传失败...(image-688c7b-1600151856238)]
- 0x0001:该 Class 只有一个字段;
- 0x0002:该字段的访问标志(access_flags),也就是 private,可以参照上文的访问标志表;
- 0x0005:字段名(name_index),常量池索引 #5,也就是 i;
- 0x0006:字段描述符(descriptor_index),常量池索引 #6,也就是 I。说明是 int 类型的数据。
- 0x0000:属性表(attributes_info)为 0,说明不需要额外的信息描述该变量。如果该变量为
final sttic int i = 12
,则可能会存在一个名称为 ConstantVaule 的属性,并指向常量 12。
方法表集合
方法表的表现形式与字段表的描述几乎完全一致,方法表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attributes_info | attributes | attributes_count |
接下来就是方法表的内容了,看字节码:
开头的 0x0002
:表示有两个方法,一个是由编译器生成的方法,一个是我们写的 test 方法。
第一个方法
- 0x0001:access_flag,表示该方法为 public;
- 0x0007:方法名常量池下标,执行 #7,方法名为 "< init >";
- 0x0008:方法描述符的索引,对应常量为 "()V" 表示返回 void;
- 0x0001:该方法有一个属性表;
这里涉及到属性表,上文不止一次提到过属性表。在 Class 文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景的专有信息。
我们接着看后面的字节码:
- 0x0009:该方法属性表的属性索引,对应常量为 "Code";
- 0x0000 0039:该方法属性表的长度;
- 再往后就是 Code 属性的详细描述。
Code 是虚拟机预定义的属性之一,表示接下来是一段由编译器生成的字节码,也就是虚拟机可执行的代码。
Code 也有自己的结构,接下来的数据组成为一个完整的 Code,Code 属性表的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute-info | attributes | attributes_count |
attribute_name_index("Code")和 attribute_length("0x0000 0039"
)已经分析过,接下来根据这个表继续对字节码进行分析。
- 0x0002:max_stack = 2,操作数栈最大深度,这里是 2。在方法执行的任何时刻,操作数栈都不会超过这个深度。
- 0x0001:max_locals = 1,局部变量表所需的存储空间。
局部变量表的单位是 Slot,对于 byte、char、float、int、short、boolean 和 returnAddress 等长度不超过 32 位的数据类型,每个局部变量占用 1 个 Slot。double 和 long 这两种 64 位的数据类型需要两个 Slot 来存放。 - 0x0000 000b:code_length = 11,说明接下来 11 个字节都存放的是字节码操作指令。
接下来的字节码指令串为 2a b7 00 01 2a 10 0a b5 00 02b1
,每个字节都代表了一种字节码指令。
- 2a:aload_0 从局部变量0中装载引用类型值入栈,这里是装入 this;
- b7:invokespecial 调用操作数栈顶对象的实例构造器方法、private 方法或者它父类的方法,这里调用的是父类 Object 的方法。
这个方法有两个字节说明具体调用的哪一个方法,紧随其后的是 0x00 01 是常量池索引 #1 的 <init> 方法。 - 2a:aload_0 从局部变量0中装载引用类型值入栈;
- 10:valuebyte值带符号扩展成int值入栈。
- 0a:1(long)值入栈。
- b5:给对象字段赋值。两个字节的参数表示给哪个对象赋值,也就是后面的 0x0002,查常量池是为参数 i 赋值。
- b1:返回 void。
字节码指令本文不再深入,继续看后面的字节码:
- 0x0000 :exception_table_length = 0,说明该方法不存在异常信息表;
- 0x0002 :attributes_count=2,说明 Code 属性表内部还有两个表。
接下来的内容是第一个表 LineNumberTable,它的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
- 0x000a:第一个表名,常量池 #10 为 LineNumberTable,这个表用于描述 Java 源码行号与字节码(偏移量)之间的对应关系。
- 0x0000000a:属性长度为 10;
- 0x0002:LineNumberTable 表长度为 2;
再往后面记录的是 LineNumberTable 中 line_number_info 的信息。line_number_info 是由两个 u2 类型数据组成的,前面 u1 表示字节码行号、后面 u1 表示代码行号。
我们知道 LineNumberTable 的长度为 2,line_number_info 应该有两对 u2 数据:
- 0x0000:start_pc 字节码行号 0;
- 0x0009:line_number 行号 9;
- 0x0004:start_pc 字节码行号 4;
- 0x000b:line_number 行号 11;
再往后是第二个表 "LocalVariableTable",这个表用于描述栈帧中局部变量表中的变量与 Java 源码中定义的变量直接的关系:
- 0x000b:常量池 #11,指向 “LocalVariableTable” 字符串;
- 0x0000000c:属性长度为 12;
- 0x0001:局部变量信息表长度为 1;
- 0x0000 : start_pc = 0 该局部变量字节码偏移量;
- 0x000b:length = 11,该变量作用的长度 ;
- 0x000c : name_index= "this",局部变量名称;
- 0x000d : descriptor_index #13 ("Lcom/sky/test/Test"),局部变量描述符;
- 0000 index=0,局部变量在栈帧局部变量表 Slot 的位置。
到此第一个方法以及描述完毕了,这个方法大概就是调用了父类 Object 的 init 方法,用来初始化变量等一系列操作。
鉴于篇幅第二个方法本文就不再分析,猜想也知是 Test.class 中的 test 方法,这块内容有一个了解即可。
Attribute
再往后就是 Attribute 了,Attribute 包含 "SourceFile" 表示 Class 文件的名称。通过工具查询是 #16 变量,名称为 Test.java。
总结
以上就是本文的全部内容了,感谢阅读。