[Java] .class文件格式与其内部结构

前言


JDK里自带的Javap小工具能够帮我们把.class文件的内容打印到标准输出。那么这个工具是如何做到的呢?
笔者将在本文介绍.class文件的内容。


前提


在开始阅读本文之前,笔者将在本节申明几个基础知识,以便你更好的理解本文。

  1. 所有文件都存在磁盘上,其内容都是01序列。.exe、.txt、.class文件等都不例外。
  2. 文件之所以有特殊的含义,是因为有程序会对文件的01序列做解释,人为赋予其含义。
    当你把exe文件里的01序列用”文本编辑器“这个程序对其用”UTF-8“的方式做解释(解码)的时候,就会出现乱码,因为这个商量好的(File Format规定的)不一样。

看到这里你应该明白了,文件格式其实就是对文件不同位置01序列做定义的约定,约定使得文件使用者能根据约定解释其内容、当然也能使生产者生成其内容。

  • jvm会根据约定加载.class文件。
  • javap小工具也会根据约定去解析.class文件,
  • javac编译器根据约定去生成.class文件。

当然如果你想要自己写一个类似javap的小工具去偷窥class文件的内容的时候,你也可以参照约定去了解文件01序列的具体含义。


Class文件格式官方定义

.class文件格式的开发和管理现是由Oracle公司负责,其官方定义在JVM式样的「Chapter4. The class File Format」可以查到。

这里笔者贴出其结构的部分,接下来笔者将按照这个官方定义去解释.class文件的内容。
在这里插入图片描述

1. magic


文件最开始4bytes的数据,是一个固定的魔法数字 0xCAFEBABE,标识着文件格式是.class。

题外话:魔法数字在文件里的应用。

这里的magic number并不是指代的代码里未命名的数据。而是跨系统数据交换时候常用的手法。通常在
文件格式、数据交换协议会用到,用于标识后续数据的格式。
有趣的是.class文件和MAC可执行文件格式(Mach-O)的magic number都是0xCAFEBABE。

有兴趣可以翻阅: Magic number (programming) - Wikipedia

2. minor_version & major_version


接下来的2+2bytes分别是minor_version和major_version。用于标明class文件的版本信息。
其中在后的major_version是主版本。每一次Java SE的发布都会伴随着major_version + 1,目前Java SE 13的主版本号为57,可以在下表看有趣的一点是低版本Java是不支持高版本的class文件的,而高版本的Java如Java13甚至可以支持到Java1.02的上古class文件。

在这里插入图片描述
在Java SE 12之后,也就是56版与之后的版本中,Minor_version必须是0或65535,小版本已经被废弃,也是因为Java版本迭代的速度由原来的几年变成了现在的半年一release的原因吧。

3. constant_pool_count


2bytes的数据,其值为常量池的数据数量 + 1。也是因为如此,在.class文件里对常量池访问时下标是从1开始。如下图#1,并没有#0。
在这里插入图片描述

4. consant_pool


常量池数据部分,其长度不固定,其长度由常量池里的数据量(constant_pool_count - 1)和数据类型决定。目前class文件的常量池部分,目前有17种数据。在读到第一个byte的Tag时候,通过下表能知道接下来的数据究竟是何种类型。

常量池的数据类型
由于篇幅原因,笔者在这里就简单介绍其中比较典型的两种。CONSTANT_String和CONSTANT_Utf8。
一眼看过去,比较容易迷糊。

4.1 CONSTANT_String

结构体的定义如下,

CONSTANT_String_info {
	u1 tag; // 固定为8
	u2 string_index;
}

用于指代String类型的常量,不过其内部并没有数据。真正的数据都是由接下来的CONSTANT_Utf8保存的。
CONSTANT_String仅在string_index字段保有对CONSTANT_Utf8的引用。

4.2 CONSTANT_Utf8

结构体的定义如下,

CONSTANT_Utf8_info {
	u1 tag; // 固定为1
	u2 length;
	u1 bytes[length];
}

正真字符串的数据由CONSTANT_Utf8保存,bytes字段里保存的是字符串进行utf-8编码之后的字节。

想知道utf-8编码相关的可以参考笔者以前写的 [c#]如何验证byte[]是否是UTF-8编码 一文。

例子

#5常量是String类型,其引用了#24常量,#24 utf8常量的值为“myfirstjnilibrary”
在这里插入图片描述

5. access_flags


2bytes的数据,存储该类的访问控制信息。如Public Class HelloClass的access_flags就是

flags: (0x0021) ACC_PUBLIC, ACC_SUPER

在这里插入图片描述


6. this_class & super_class


分别占2bytes长度,其内容是数字,表示类名在常量池里的位置。如下,

this_class => #2 => #22 => info/systemengineer/examples/JNIHelloWorld
super_calss => #7 => #27 => java/lang/Object

在这里插入图片描述

7. interfaces_count


2bytes的数据,表明当前类/接口的接口数量。爷接口不计算,即只计算直接接口的数量。
2bytes的字段大小也是为什么java类的接口数量不能超过65535的原因。

8. interfaces


一个数组,长度为2bytes * interfaces_count,保有当前(类/接口)所有的接口信息。
其内部值是常量池的下标(1开始),下标指向的常量的数据类型必须是Consant_Class。

9. fields_count


一个数字,长度为2bytes,表明接下来的field_info表的数据数量。

10. field_info


一个数组,记录了所有的class field信息。其结构体的定义如下,

field_info {
	u2 access_flags;
	u2 name_index;
	u2 descriptor_index;
	u2 attributes_count;
	attribute_info attributes[attributes_count];
}

其每一部分含义如下:

  1. access_flags:访问控制的信息,如public、private、protected、static、final,以及面试常见的volatile等。
  2. name_index:常量池下标,对应位置保存着field名,其类型必须是CONSTANT_Utf8。
  3. descriptor_index:常量池下标,对应位置保存着field类型信息,其类型必须是CONSTANT_Utf8。
  4. attributes_count:属性的数量。
  5. attributes表:该field的具体属性信息。如被@Deprecated会被记录到此。

11. methods_count


一个数字,长度为2bytes,表明接下来的method_info表的数据数量。

12. method_info


一个数组,记录了所有的method的信息。其结构体的定义如下,每一部分的含义与field_info类似。不过每一部分合法的值的要求不一样。如volatile关键词不能修饰method但是可以出现在field上,类似于这种区别。

method_info {
	u2 access_flags;
	u2 name_index;
	u2 descriptor_index;
	u2 attributes_count;
	attribute_info attributes[attributes_count];
}

对于method_info的属性部分,有非常非常重要的Code属性。

Code属性


Code属性仅被Method使用,其记录了一个java方法的大部分要素,包括了参数、异常、返回类型、
其伪代码结构体定义如下:

Code_attribute {
	u2 attribute_name_index;
	u2 atttribute_length;
	u2 max_stack;
	u2 max_locals;
	u4 code_length;
	u1 code[code_length];
	u2 exception_table_length;
	{	u2 start_pc;
		u2 end_pc;
		u2 handler_pc;
		u2 catch_type;
	} exception_table[exception_table_length];
	u2 attributes_count;
	attribute_info attributes[attributes_count];
}

而我们所写的Java代码,被编译到JVM指令的时候,就是存放在Code属性的code[code_length]数组里。

13. attributes_count


一个数字,长度为2bytes,表明接下来的attribute_info表的数据数量。

14. attribute_info


一个数组,记录了当前Class的所有的attribute_info的信息,但不包括Method、Field的attribute_info,这些信息在它们各自的部分已经被记录了。这一部分记录对象仅为当前class。

总结


通过仔细查看class文件各部分的含义,你能知道

  • java的接口数量不能超过65535的原因。
  • java源文件里如有多个class会生成多个class文件的原因。
  • javap输出的各部分内容的含义。
  • 你写的Java代码究竟保存在class文件的哪里了。

你也会因此知道java源代码、class文件里的bytecode,其实没有那么神秘,你也能对其内容做解析。
在计算机领域,最关键就是信息,理解到我们所有的操作本质都是处理信息,你会获得从源头发现、分析并解决问题的能力。而加密,就是把本来商量好的公开的解析方法,给私有化了,导致第三者无法轻易解析信息内容。试想如果Oracle没有公开class文件的文件结构,我们就无法轻易解析其中内容。

发布了24 篇原创文章 · 获赞 24 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ToraNe/article/details/102993156
今日推荐