Class文件来龙去脉
java之所以能够跨平台,是因为 jdk可以生成可以在各个平台的JVM上运行的中间代码产物 ---.class文件。class文件解除了JVM和java语言之间的耦合。
JVM被设计出来,不仅仅是为了运行java程序,还有 Ruby,Groovy(Gradle中常用),scala(大数据常用)
Ruby,groovy,scala经过各自编译过程之后,都会生成class文件,然后统一让JVM运行。
class文件结构
基本构成
从整体来看class文件,它内部只有两种东西:无符号数 和 表.
-
无符号数
属于基本数据类型,以u1,u2,u4,u8分别代表 1,2,4,8个字节的无符号数,无符号数可以用来表示 数字,索引引用,数量值 或者 UTF编码的字符串.
-
表
表示 由多个无符号数 或者 表 构成的复合数据结构
class文件中所有的表都以
_info作为后缀整个class文件本质上就是一张大表
形象理解
以生物化学来类比,一个class文件作为一个完整生命体,它其中由 C,H,OMN等基本的化学元素构成,而 C,H,O这些基本化学元素又可以先构成 生命体的各个组织,器官, 组织和奇怪再构成完整生命体的各个部分。
基本的化学元素其实就是 无符号数,
完整构成
无符号数和表,按照预先设定好的顺序 紧密排序,相邻结构之间没有间隙。
JVM则是按照这个顺序来解析class文件。
每个部分的含义如下:
案例
我们编辑一个 Main.java文件:
import java.io.Serializable;
public class Main implements Serializable, Cloneable {
private int num = 1;
public int add(int i) {
int j = 10;
num += i;
return num;
}
}
当它编译成class之后,使用十六进制编辑器查看class的内容(先下载winhex):
这些数字看上去毫无规律,但是在JVM眼中,他们是经过了严格排序的。
魔数
占用4个字节,也就是上图中的CA FE BA BE ,这是一组固定的数字,它是判断当前文件是否是 class文件的身份标志,如果开头判断就不是class,那么JVM就不会将它视作class文件去执行。
版本号
分为副、主版本号,分别占2个字节,也就是上图中的。
- 副版本号:
00 00,对应十进制的0 ,也就是minor_version 是0 - 主版本号:
00 3E,对应十进制的 62,major_version是 62,
也就是说,主版本号62,副版本号0,所以完整版本号是 62.0, 它代表了 Java SE 8(JDK 1.8)所对应的class文件格式。
常量池 (重点)
魔数和版本号中两个部分都是无符号数组成的,而 紧跟在版本号之后的是 常量池表,它是一个表(无符号数组成的复杂结构)。
它的作用是,保存类的各种信息。
类的名称,父类的名称,类中的方法名,参数名称,参数类型等。
常量池中又包含很多的表,如下:
举例 CONSTANT_class_info 类信息表
它的表结构为:
tag是区分于其他表的标志位,7 就表示 是classInfo表,u1表示占用一个字节。
name_index是一个指针,如果 值为2,那么就是指向常量池中第2个常量。
举例 CONSTANT_utf8_info
tag值为1,表示 它是utf8类型的表
length 数组长度,如果值为5,那么 接下来就是 5个连续的u1(单字节)类型的数据。
bytes, 数组内容,因为上面length是5,那么这个数组的长度就是5。
面试题 Java字符串的最大长度是多少
其实这个问题存在歧义。需要分解问题来回答:
如果是 在java代码中声明一个字符串常量
如果提问的重点是 在java代码中声明一个字符串常量,我最大可以用多大长度,那么答案如下:
我们在 java代码中声明的字符串,最终存储在class文件的CONSTANT_utf8_info表中,
因此一个字符串所能使用的最大字节数是也就是
u2所能代表的最大值 16进制为FF FF(对应十进制的65536),但是由于需要两个字节来保存null值,所以 class文件中定义字符串常量的最大长度是 2^16-2 = 65534
如果是 java代码在运行时,允许传递的字符串最大长度
如果提问的重点是 java代码在运行时,允许传递的字符串最大长度是多少
程序运行时, 如果是方法调用之间传字符串类型的实参,那么这个长度就收到JVM和当前可用内存的限制,理论上可用的最大长度是
2 ^ 31. 之所以是2^31,是因为字符串String实际上是存储在Char[]数据结构中的,这是一个数组,字符数组的最大长度受到 整形数组的最大长度限制,而这个长度限制就是Integer.MAX_VALUE,也就是 (2^31).上面说的是理论值,但是如果当前内存已经不足以分配
2^31个字节数的空间, 那么你如果仍然申请这么大的空间,就会报出 OutOfMemoryError 内存溢出的错误。
表之间相互引用
如果类信息表 class_info 中,有一个字符串类型的常量,那么就有可能 name_index指向 某个utf8_info表。他们之间存在引用关系。
常量计数器
开发者在定义一个class的时候,所定义的常量数不尽相同,但是在一个类写完了之后,常量的数目是确定的。所有,在 字节码中,常量池部分的开头,就是 容量计数器,用u2长度(2位的16进制数)表示 常量池中一共有多少个 常量。
这里的00 17 是16进制数,换算成10进制是,23.
但是由于下标为0的常量被JVM预留为其他用途,所以实际的常量数目是22.
常量解析过程
常量计数器之后,紧跟的就是 一个个具体的常量了,
16进制的 0A转化为10进制为10,那么这个表就是 methodref_info 方法引用表,它的结构为:
CONSTANT_Mehtodref_info{
u1 tag = 10;
u2 class_index; // 指向此方法所属类
u2 name_type_index; // 指向此方法的名称和类型
}
这里表示,用u1表示 表类型,u2 的长度的无符号数 指向 次方法所属类的index,u1长度的无符号数 指向方法的名称和类型。
00 02 -> class_index 表示指向常量池中第2个常量
00 03 -> name_type_index 表示指向此方法的名称和类型,指向常量池中第3个常量
继续往后解析过程也是类似的。但是我们手工去解析是比较费劲的,实际上,JDK提供了javap命令来帮我们解析class文件结构。
javap
将这个class文件用 命令 javap -v Main.class 解析的结果如下:
可以看出:
-
manor version 和 major version 分别为 00 和 62
-
常量池 (Constant poll)中,显示了常量的顺序以及相互的引用关系
- 排在第一的是 Methodref, 它引用了#2和#3
- 第二和第三的是 Class和NameAndType ,他们又分别引用了 #4 和 (#5#6)
Classfile /C:/Users/zwx1245985/Desktop/JAVA/Main.class
Last modified 2023年9月5日; size 351 bytes
SHA-256 checksum 042459bcab65b80c6bfcb9506a08a242f61034fafe932b8a406e02d13a515f47
Compiled from "Main.java"
public class Main implements java.io.Serializable,java.lang.Cloneable
minor version: 0
major version: 62
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // Main
super_class: #2 // java/lang/Object
interfaces: 2, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // Main.num:I
#8 = Class #10 // Main
#9 = NameAndType #11:#12 // num:I
#10 = Utf8 Main
#11 = Utf8 num
#12 = Utf8 I
#13 = Class #14 // java/io/Serializable
#14 = Utf8 java/io/Serializable
#15 = Class #16 // java/lang/Cloneable
#16 = Utf8 java/lang/Cloneable
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 add
#20 = Utf8 (I)I
#21 = Utf8 SourceFile
#22 = Utf8 Main.java
{
public Main();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #7 // Field num:I
9: return
LineNumberTable:
line 3: 0
line 5: 4
public int add(int);
descriptor: (I)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=3, args_size=2
0: bipush 10
2: istore_2
3: aload_0
4: dup
5: getfield #7 // Field num:I
8: iload_1
9: iadd
10: putfield #7 // Field num:I
13: aload_0
14: getfield #7 // Field num:I
17: ireturn
LineNumberTable:
line 8: 0
line 9: 3
line 10: 13
}
SourceFile: "Main.java"
访问标志 access_flags
紧接在常量池之后的是 访问标志区, 它占用两个字节, 它表示的是,该class文件是类还是接口,是不是public,是不是abstract,是不是final等。
完整的访问标志如下图所示:
上面 javap解析的结果中,存在 ACC_PUBLIC ,说明这个类是一个public的。
类索引,父类索引 接口索引计数器
访问标志之后,2个字节为类索引,然后是 2个字节的父类索引,再往后是接口索引计数器
对照上文javap的结果日志来看:
类索引为08,父类索引为02,
说明 当前类 为 Main,它的父类是 Object,
接口索引计数器为02 ,则说明这个Main类实现了2个接口。
从这3个部分可以得出,当前类,当前类的父类,当前类实现的接口数量信息。
字段表
紧跟在 接口索引计数器之后的是字段表,它的作用是,描述类和接口中所声明的变量。不包含方法内部的局部变量。
由于字段数量不可确定,所以字段表的开头仍然是一个 2个字节的 字段计数器部分。
00 0D 为字段计数器,后面跟的就是 00 0F 字段计数器,00 01 变量索引名,和 00 02 变量类型索引。
每一个字段表结构如下:
CONSTANT_Fieldref_info{
u2 access_flag 字段访问标志
u2 name_index 字段名称索引
u2 descriptor_index 字段描述索引
u2 attributes_count 属性计数器
attribute_info
}
字段表的开头的2个字节为 字段访问标志
字段访问标志
- 字段表集合中不会出现来自父类或者接口中的字段
- 内部类为了保持对外部类的访问,会自动添加外部类实例字段
方法表
方法表的开头也是一个 计数器,因为一个类中的方法数量也是不固定的,数量之后就是每一个具体的方法表。此案例中,仅存在一个add方法,但是 实际分析 class文件时,发现了2个方法,因为默认的构造方法也被算进去了。
方法表访问标志
Code属性表
方法表之后就是属性表,属性表的开头是 也是属性计数器,接着是 属性表类型索引,这个索引指向的是常量池中名为 Code的属性,这其中主要是保存了一系列字节码。
通过javap命令得出的产物中最后一部分,也就是Code属性表中的内容。比如如下add方法:
public int add(int);
descriptor: (I)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=3, args_size=2
0: bipush 10
2: istore_2
3: aload_0
4: dup
5: getfield #7 // Field num:I
8: iload_1
9: iadd
10: putfield #7 // Field num:I
13: aload_0
14: getfield #7 // Field num:I
17: ireturn
LineNumberTable:
line 8: 0
line 9: 3
line 10: 13
这里包含了很多字节码指令,比如 bipush,istore,ireturn等,这些都是JVM直接执行的字节码指令动作。
总结
本文描述了Class文件结构,并通过简单案例模拟了JVM解析class文件的过程。
Class文件结构中 最重要的是 常量池。它相当于 class文件的资源仓库,其他的结构,多多少少都会引用到这个资源仓库。
平时我们很少会用16进制编辑器(比如windows下的winhex)来打开一个class文件去读那些数字。通常更好的方法是通过javap命令来看class文件结构。
class文件结构,主要包括
-
标记此文件为class文件格式的 魔数
-
标记 字节码版本号的 minor version和major version
-
保存其他数据结构所需的资源的 常量池
- 常量池以 常量计数器开头,这个数字表示 常量池中 表 的数量,由于是由 2个字节的16进制数表示,所以常量的最大数量为 FFFF, 也就是 2^16。
- 同样如果在java类中定义了字符串常量,指定了字符串的内容,那么这个字符串被存放在
utf8_info表中,这个表中有一个长度字段,也是2个字节的16进制数(u2),所以在这种情况下,直接指定字符串常量时 字符串的最大长度是2^16这个数量级。
-
描述类和接口中的声明的变量 的
- 也就是 类的成员变量以及 接口中声明的形参
-
描述 这个类中所有方法的 方法表
- 注意:仅仅会包含本类中的方法,继承自父类或者接口的方法不会包含进去
-
保存字节码指令的 Code
- 这部分包含了本类中所有的方法转化成的字节码指令,JVM将会严格按照指令顺序去执行每一个方法
-
字节码中有一种通用设计,那就是 计数器+对象集合,先用计数器字段描述有多少个,然后后面再按照 固定的结构来组装对象。