嚼一嚼 class 文件结构
0、准备
- win10操作系统
- 安装 jdk1.8.0_161 并配置好环境变量
- 安装 Notepad++ 和里面的 HEX-Editor 插件
1、生成一个 class 文件
我们平时写的java代码存储在 .java 文件中,称之为 java 源文件。下面是 Test.java 文件的内容:
我们在 Test.java 文件的同目录下,按住 Shift+鼠标右键,选择”在此处打开**窗口“,然后输入。这个时候,会发现当前目录中生成了一个 Test.class 文件。
这个 class 文件就是我们今天的主角,它是由 java 编译器(javac)翻译 java 源文件(Test.java)得到的字节码文件(Test.class),这种字节码就是 JVM 的“机器语言”。
2、查看 class 文件内容
使用 Notepad++ 打开 Test.class 文件,然后依次选择 插件–>HEX-Editor–>View in HEX。
所以此时看到的内容,是class 文件的十六进制形式。十六进制数字两两相连,这是为什么呢都知道,1byte = 8bit,而 4bit 刚好可以用一个十六进制数字表示,如 1111 = 0xf。所以一个字节刚好可以用两个十六进制数字表示。
下面贴上 class 文件内容:
表的类型有很多,在这里以 java 类形式表示一下第一个表类型:
现在,已经基本了解了常量池中常量项的结构,接下来我们结合上面的表格来翻译常量。
5.3、常量集合
常量池入口指定了常量池的大小为 18,接下来我们一个个读取这些常量。
- [0a,00,04,00,0f]:0x0a = 10 表示类型是 Methodref,接下来 0x0004 = 4、0x000f=15,表示指向的是第 4 个和第 15 个常量。我猜这是为了缩减 class 文件的大小,故将相同的常量以地址指向的形式来引用。
- [09,00,03,00,10]:0x09 = 0 表示类型是 Fieldref,后面 0x0003 和 0x0010 表示分别指向第 3 个和第 16 个常量。
- []07,00,11]:0x07 = 7 表示类型是 Class,引用指向第 17 个常量。
- [07,00,12]:与上面相同,指向第 18 个常量。
- [01,00,01,69]:0x01 = 1 表示类型是 Utf8,字符串的长度为 0x0001 = 1,故只需要读取后面一个字节 69。十六进制数字 0x69 对应的字符为小写字母 i,这就是我们前面定义的变量名。
- …
翻译就到这里了,跟着前面的结构对照表,相信你已经可以轻松翻译剩下的常量。
5.4、验证翻译结果
只写代码不做测试是耍流氓,同样的,如果我只告诉各位怎么翻译而不告诉你们怎么验证,那也是在耍流氓。现在我们就通过 JDK 提供的工具,来验证一下,常量到底是不是我前面所说的那样。
在 Test.class 文件的目录中打开命令行,运行 命令,就可以看到常量池的内容。这里贴一下运行结果:
0x21 的二进制为 100001 ,表示 ACC_SUPER 和 ACC_PUBLIC 为 1。这与代码情况也是一致的,我们是使用 JDK1.8 编译的,并且 Test 类确为 public 类型。
7、类索引、父类索引和接口索引集合
7.1、类索引[00,03]
指向第 3 个常量,而第 3 个常量引用第 17 个,值为 Test。表示当前类为 Test。
7.2、父类索引[00,04]
与上面类似,值为 java/lang/Object,表示 Test 父类为 Object。
7.3、接口索引集合
类索引和父类索引之后,紧接着的两个字节存储的是一个 u2 类型的数据,表示接口计数器。这里是[00,00],长度为 0 表示没有实现接口。
8、字段表集合
8.1、字段数量[00,01]
接口索引集合之后,紧接着就是字段表集合,头两个字节存储的便是这个集合的大小,这里可以看到集合大小为 1,表示只有一个字段。
相信你也发现了一些规律,比如集合类型的表,前面就会用一个 u2 类型的数据存储大小。
8.2、字段表[00,02,00,05,00,06]
字段表结构可以分为三部分,分别是 access_flags(u2,修饰符)、name_index(u2,名称)、descriptor_index(u2,类型)。
access_flags 各个标志位含义如下:
方法1:access_flags(u2,访问标志)存储值为 [00,01],name_index(u2,名称)存储值为 [00,07],descriptor_index(u2,类型)存储值为 [00,08],最终得到的结果是 。
表示这是一个构造方法,()V 表示返回值类型是 void。这与我们平常写 java 代码时的语法有点区别,java 代码中构造方法是没有返回值的,想来是因为构造方法都没有返回值,所以让编译器在编译时处理进而方便开发人员,也略微减小了 java 源文件的大小。
9.3、属性表集合
属性表计数器 [00,01] 表示这里有 1 个属性,属性名称 [00,09] 指向第 9 个常量 Code,说明此属性是方法的字节码描述。
对于每个属性,它的名称需要从常量池中引用一个 Utf8 类型,如这里的 [00,09] 引用的 Code。而属性值的结构是完全自定义的,只需要通过一个 u4 的长度属性来说明属性值所占用的位数即可。
这里 [00,00,00,1d] 表示,后面 29 个字节都是属性值。
9.4、另一个方法
前面 9.2 和 9.3 加在一起,翻译完了默认的构造方法。下面快速翻译一下另一个方法。
方法2:access_flags[00,01] –> public,name_index[00,0b] –> 常量11 –> add,descriptor[00,0c] –> 常量12 –> ()I。最终得到的结果是。
属性表计数器 [00,01],属性名称 [00,09] –> Code,属性值长度 [00,00,00,1f] –> 31。
10、附加属性集合
方发表结合之后,就剩最后一部分附加属性集合了。
附加属性计数器 [00,01] 表示这里只有一个附加属性。接下来是一个 u2 类型的数据 [00,0d] –> 常量13 –> SourceFile,SourceFile 属性的结构为 u2(attribute_name_index) + u4(attribute_length) + u2(sourcefile_index),第一个 u2 已经翻译了是 SourceFile,第二个 u4[00,00,00,02] 表示长度是 2,第三个 u2[00,0e] –> 常量14 –> Test.java。
11、总结
最后贴一张图,总结以上的翻译过程:

文章知识点与官方知识档案匹配,可进一步学习相关知识Java技能树首页概览93596 人正在系统学习中
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!