今天我们来聊一聊Java虚拟机的类加载机制。首先要想了解Java虚拟机是如何加载一个类的,那么首先要对.class字节码文件的结构有一个相对深入的理解。
.class字节码文件的结构
.class文件是一组以8字节为一组(基础单位)的二进制字节流,中间没有任何分隔符。当遇到需要占用8位字节以上的数据项,会按照高位在前的方式分割成若干个8位字节进行存储,高位在前的数据存储顺序(Big-Endian)与x86处理器的(Little-Endian)对应。
.class文件采用类似C语言结构体的伪结构来存储数据,里面只有无符号数和表两种数据结构,解析都要以这两种类型为基础。无符号数属于基本的数据类型,以u1,u2, u4, u8来分别代表1, 2, 4, 8个字节的无符号数。无符号数可以用来表示数字, 索引引用,数量值或者按照UTF-8编码构成字符串值。
下面是一张class字节码文件的结构图:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个class文件本质就是一张表。
下面我们先来看一段代码:
public class MyTest2 {
String str = "Welcome";
private int x = 5;
public static Integer in = 10;
public static void main(String[] args) {
MyTest2 myTest2 = new MyTest2();
myTest2.setX(8);
in = 20;
}
public void setX(int x) {
this.x = x;
}
}
编译后对应生成的字节码文件格式如下:

.class文件的头4个字节称为魔数,它的作用是确定这个文件是否为一个能被虚拟机接受的.class文件,这4个字节的内容为0xCAFEBABE, 接下来紧接着的四个字节是版本号。其中,第5-6字节存储的是次版本号,第7-8存储的是主版本号。再下来是常量池入口,它可以理解为.class文件的资源仓库。
通过javap -v MyTest2.class可以生成字节码相关的信息,我们将常量池部分截取展示如下:
Constant pool:
#1 = Methodref #10.#34 // java/lang/Object."<init>":()V
#2 = String #35 // Welcome
#3 = Fieldref #5.#36 // com/shengsiyuan/jvm/bytecode/MyTest2.str:Ljava/lang/String;
#4 = Fieldref #5.#37 // com/shengsiyuan/jvm/bytecode/MyTest2.x:I
#5 = Class #38 // com/shengsiyuan/jvm/bytecode/MyTest2
#6 = Methodref #5.#34 // com/shengsiyuan/jvm/bytecode/MyTest2."<init>":()V
#7 = Methodref #5.#39 // com/shengsiyuan/jvm/bytecode/MyTest2.setX:(I)V
#8 = Methodref #40.#41 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#9 = Fieldref #5.#42 // com/shengsiyuan/jvm/bytecode/MyTest2.in:Ljava/lang/Integer;
#10 = Class #43 // java/lang/Object
#11 = Utf8 str
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 x
#14 = Utf8 I
#15 = Utf8 in
#16 = Utf8 Ljava/lang/Integer;
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 Lcom/shengsiyuan/jvm/bytecode/MyTest2;
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 args
#27 = Utf8 [Ljava/lang/String;
#28 = Utf8 myTest2
#29 = Utf8 setX
#30 = Utf8 (I)V
#31 = Utf8 <clinit>
#32 = Utf8 SourceFile
#33 = Utf8 MyTest2.java
#34 = NameAndType #17:#18 // "<init>":()V
#35 = Utf8 Welcome
#36 = NameAndType #11:#12 // str:Ljava/lang/String;
#37 = NameAndType #13:#14 // x:I
#38 = Utf8 com/shengsiyuan/jvm/bytecode/MyTest2
#39 = NameAndType #29:#30 // setX:(I)V
#40 = Class #44 // java/lang/Integer
#41 = NameAndType #45:#46 // valueOf:(I)Ljava/lang/Integer;
#42 = NameAndType #15:#16 // in:Ljava/lang/Integer;
#43 = Utf8 java/lang/Object
#44 = Utf8 java/lang/Integer
#45 = Utf8 valueOf
#46 = Utf8 (I)Ljava/lang/Integer;
常量池结构分析
既然常量池被称为是.class文件的资源仓库, 它自然是.class文件结构中与其他项目关联最多的数据类型,也是占用.class文件空间最大的数据项目之一。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置u2类型的constant_pool_count来计数常量池中的常量个数。但是仔细观察你会发现,在十六进制的表示中,第9-10个字节表示constant_pool_count, 即00 2F也就是2*16+15=47,但是在常量池中我们只看到了46个常量,这是为什么呢?其实,这是因为0这个位置用来表示常量池不引用任何常量的含义。
网友评论