这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战
字节:1字节 = 8位bit。
类文件结构
大概的构造可见:未命名文件 - ProcessOn
魔数、主次版本号
魔数4字节:0xCAFEBABE。
主版本号2字节(假设为X):X-44 = JDK(Y)
副版本号2字节:标识特性(JDK12后启用)
常量池
常量池可以比喻为Class文件里的资源仓库,它是Class 文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它 还是在Class文件中第一个出现的表类型数据项目。
由于常量池包括的内容数量不固定,因此在常量池前,给了一个U2(2字节)来记录常量池容量计数值,从1开始计数,最多可以表示65534个。
在Class文件格式规范制定之时,设计者将第0项常量 空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下 需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译 原理方面的概念,主要包括下面几类常量:
-
被模块导出或者开放的包(Package)
-
类和接口的全限定名(Fully Qualified Name)
-
字段的名称和描述符(Descriptor)
-
方法的名称和描述符
-
方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
-
动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
访问标志
常量池之后,接着2个字节代表访问标志,用来记录一些类、接口层次的信息,包括:
- Class是类还是接口
- 是否是public
- 是否是abstract
- 是否是final
类名以及继承关系
这里包括:
- 类索引(确定类的全限定名 - 在常量池中)
- 父类索引(确定父类的全限定名 - 在常量池中)
- 接口索引集合
- 由于java支持多实现,因此这里需要2个字节记录实现接口数量
- 随后的2*n个字节,描述接口的全限定名 - 在常量池中
字段表集合
这里描述的范围是
包括类级变量、实例级变量,不包括方法内部的局部变量
字段表结构为:
- 字段数
- 字段表集合
- 访问标志
- 名称索引
- 描述符索引
- 变量个数
- 变量信息
字段访问标志
这里和类的访问标志类似,描述的是字段的属性,包括:
- 访问权限(public、private、protected)
- 可见域(static)
- 是否可变(final)
- 线程可见性(volatile)
- 可序列化(transient)
- 是否有编译器自动产生(synthetic)
- 是否为枚举(enum)
名称索引、描述符索引
这里和类继承关系中的类似,也是常量池中引用。
-
名称索引代表的是字段的简单名称。
-
描述符索引代表的是字段和方法的描述符。
简单名称、描述符、全限定名见附录。
变量信息
这部分内容放在属性表集合中。
方法表集合
方法表的结构和上面的字段表几乎是相同的,都是由一个描述个数的字段开始,后面带着若干的方法表。
-
需要注意的是,方法表不可能为空,因为即使没有方法,编译器都会提供一个类构造器和实例构造器。
-
方法表中方法里具体的代码,放到了方法属性表的名为Code的属性中。
属性表集合
属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以 携带自己的属性表集合,以描述某些场景专有的信息。
属性表中,不再严格要求顺序,只要不和已有的重名,任何编译器都可以往属性表中写入自定义信息。JVM运行时,只会解析它认识的信息,丢弃那些不认识的。
属性表中,包括了大量上述几个区域中长度无法预先定义的内容,其中就包括整个类文件最重要的code属性表。
Code属性表
Code属性存放的是经过Javac编译器处理之后变为字节码的Java程序方法体中的代码。
Code属性表出现在方法表的属性集合之中。
并非所有的方法表都必须存在Code属性表,例如接口或抽象类中的方法,就不存在Code属性。
为什么说Code属性表是Class文件中最重要的属性?
- 因为一个Java文件包括的信息,可以分为代码和元数据(描述类、字段、方法定义和一些具体的定义信息),代码属性有且仅由Code属性表描述,其他的是用来描述元数据的。
其中,每个字段都有些特定的规则:
maxLocals
- max_locals: U2,单位变量槽,对于byte、char、float、int、short、boolean和 returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放。
- 这个大小是根据同时生存的最大局部变量决定的,而不是所有的局部变量。
code_length和code
存储源程序编译后的字节码指令。
code只有1个字节,代表的是编译出的一条字节码指令。
code_length虽然有4字节,但java虚拟机规范只允许65535,超过了javac就拒绝编译了。
异常表
如果代码中没有显式处理异常,那么异常表中没有信息。
异常表描述的是:在代码的哪个位置,如果抛出了哪个异常,则跳转到哪个指令处理。
同样地,异常表中具体的信息,也是通过一个指明数量的数加上一堆指向常量池ClassInfo的指针确定。
常数值属性(ConstantValue)
ConstantValue属性在属性表中的使用范围是针对字段表的。
字段表中,如果为静态字面量值赋值了,那么字段表中就会有指向这个区域的对应信息。
这里的记录信息也较为简单,最终的指向依然是依靠对常量池的值的引用。
因此,这里的类型就只能局限于基本类型以及String,这是由常量池的特性决定的。
字段属性
这个属性的作用是通知虚拟机自动为静态变量赋值。
也就是说:只有被static修饰的基本变量或String才可以使用这个区域。
对于非静态变量(实例变量),赋值是在实例构造器方法中进行的。
对于类变量(也就是静态变量),这里的初始化有2种情况(这是Oracle的javaC编译器的实现,
java虚拟机规范并不强制要求是final):
- 如果同时使用final和static修饰,且是基本类型或String,那么就会生成ConstantValue属性进行初始化;
- 如果不满足上面的条件,那么就会在中初始化。
由于书中内容很多,这里只挑了一些日常中比较能够接触到的进行记录,如果后面需要,再进行补充。
附录
全限定名、描述符、简单名称
全限定名
以类org.fenixsoft.clazz.TestClass为例,全限定名为org/fenixsoft/clazz/TestClass。
为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”号表示全限定名结束。
简单名称
简单名称则就是指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别就是“inc”和“m”。
描述符
描述符用来描述字段的数据类型、方法的参数列表(数量、类型、书匈奴)、防范的返回值。
- 基本数据类型以及void都用大写字符表示,对象用L+全限定名表示。
| 字符 | 含义 | 字符 | 含义 |
|---|---|---|---|
| B | byte | J | long |
| C | char | S | short |
| D | double | Z | boolean |
| F | float | V | void |
| I | int | L | 对象,例如Ljava/lang/Object; |
- 数组:在数据类型前用**一个[**描述,例如:
- String[] [] - > [[Ljava/lang/String;
- int[] -> [I
而描述一个方法时,顺序是:
- 参数列表,用()包起来
- 返回值
例如:
- void ()方法的描述符:()V
- String toString():()Ljava/lang/String;