转载

深入理解类文件结构及类加载机制

写在前面

我们都知道 JVM 并不能直接运行 Java 源文件,而是开发者通过 JDK 自带的工具命令 javac 将 Java 源文件编译成 class 字节码文件,也就是二进制文件,然后供JVM加载并使用。

为了深入学习这一块的内容,先创建两个类 UserMath

  • User.java
package com.openmind;

/**
 * jishuzhan
 *
 * @author zhoujunwen
 * @date 2019-11-17
 * @time 20:28
 * @desc
 */
public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
  • Math.java
package com.openmind;

/**
 * ${name}
 *
 * @author zhoujunwen
 * @date 2019-11-17
 * @time 20:26
 * @desc
 */
public class Math {
    public static int initData = 666;
    public static User user = new User();

    public int compute() {
        int a =1;
        int b = 2;
        int c = (a+b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        int age = Math.user.getAge();
        System.out.println(age);

        int res = math.compute();
        System.out.println(res);
    }
}

类文件结构

Class类文件结构

编译 User.javaMath.java 两个类,我们用 Sublime Text 打开 User.class 字节码文件,可以看到如下十六进制代码如下:

cafe babe 0000 0034 0021 0a00 0500 1c09
0004 001d 0900 0400 1e07 001f 0700 2001
0004 6e61 6d65 0100 124c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b01 0003 6167
6501 0001 4901 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0012
4c6f 6361 6c56 6172 6961 626c 6554 6162
6c65 0100 0474 6869 7301 0013 4c63 6f6d
2f6f 7065 6e6d 696e 642f 5573 6572 3b01
0007 6765 744e 616d 6501 0014 2829 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
0100 0773 6574 4e61 6d65 0100 1528 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
2956 0100 104d 6574 686f 6450 6172 616d
6574 6572 7301 0006 6765 7441 6765 0100
0328 2949 0100 0673 6574 4167 6501 0004
2849 2956 0100 0a53 6f75 7263 6546 696c
6501 0009 5573 6572 2e6a 6176 610c 000a
000b 0c00 0600 070c 0008 0009 0100 1163
6f6d 2f6f 7065 6e6d 696e 642f 5573 6572
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7400 2100 0400 0500 0000 0200 0200
0600 0700 0000 0200 0800 0900 0000 0500
0100 0a00 0b00 0100 0c00 0000 2f00 0100
0100 0000 052a b700 01b1 0000 0002 000d
0000 0006 0001 0000 000b 000e 0000 000c
0001 0000 0005 000f 0010 0000 0001 0011
0012 0001 000c 0000 002f 0001 0001 0000
0005 2ab4 0002 b000 0000 0200 0d00 0000
0600 0100 0000 1000 0e00 0000 0c00 0100
0000 0500 0f00 1000 0000 0100 1300 1400
0200 0c00 0000 3e00 0200 0200 0000 062a
2bb5 0002 b100 0000 0200 0d00 0000 0a00
0200 0000 1400 0500 1500 0e00 0000 1600
0200 0000 0600 0f00 1000 0000 0000 0600
0600 0700 0100 1500 0000 0501 0006 0000
0001 0016 0017 0001 000c 0000 002f 0001
0001 0000 0005 2ab4 0003 ac00 0000 0200
0d00 0000 0600 0100 0000 1800 0e00 0000
0c00 0100 0000 0500 0f00 1000 0000 0100
1800 1900 0200 0c00 0000 3e00 0200 0200
0000 062a 1bb5 0003 b100 0000 0200 0d00
0000 0a00 0200 0000 1c00 0500 1d00 0e00
0000 1600 0200 0000 0600 0f00 1000 0000
0000 0600 0800 0900 0100 1500 0000 0501
0008 0000 0001 001a 0000 0002 001b
  • class文件是一组以 8 字节为基础的二进制流,用 u1,u2,u4,u8分别表示 1 个字节,2 个字节,4 个字节,8 个字节的无符号数,采用 Big-edian 形式,即高位字节在前
  • 各个数据项严格按照顺序紧凑排列在class文件中
  • class文件中没有任何分隔符,这使得class文件存储的几乎都是可执行代码(在class文件中注释信息已经不复存在)

一个class文件完整地描述了Java源文件的各种信息,Oracle JVM规范中的 4.1 The ClassFile Structure 详细定义了一个标准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];
}

我们需要特别注意,1 个字节是2个16进制位,也就是说上面 cafe babe 是 4 个字节,这 4 个字节称之为“魔数”。

1 个字节是 8 位二进制位,表示的范围是 xxxxxxxx,也就是从 00000000-11111111,表示 0 到 255。1 位 16 进制数(用二进制表示为 xxxx)最多只能表示到 15 (即对应的十六进制 F),要表示到 255 就需要两个十六进制位。所以, 1个字节=2个16进制字符,一个16进制位=0.5个字节

用图表表示 ClassFile 的数据结构:

深入理解类文件结构及类加载机制

魔数

每个 class 文件的前四个字节称为魔数,它的唯一作用就是鉴定是否为一个合法的 class 文件。很多文件存储标准中都使用了魔数来进行身份识别的,譬如 WAV 语音文件的魔数也用 4 个字节表示为: 0x52494646,转为字符串为 RIFF

User.class 的 16 进制字节码可以看到,class 的魔数为:0xCAFEBABE,我们可以亲切的称呼为“咖啡宝贝”。

Class文件的版本

class 文件中第 5 到 第 8 个字节表示版本号,其中 5、6表示次版本,7、8表示主版本号。Java 版本号是从 45 开始的,JDK1.1 之后的每个JDK大版本发布时,主版本号都要加 1。我们看一看到主版本号为 0x0034 ,转为十进制表示为 52,由此可以计算出编译 class 文件的JDK版本为JDK8(52-45=7,7+1=8)。

JDK高本版能向下兼容以前的 class 版本,但不能运行以后的版本,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件,否则会抛出 java.lang.UnsupportedClassVersionError

主版本号和次版本号一起决定了类文件格式的版本。 如果一个类文件的主版本号为M,次版本号为m,那么我们将它的类文件格式的版本表示为M.m. 因此,类文件格式版本可以按照字典顺序排列,例如,1.5 <2.0 <2.1。 Java虚拟机实现可以支持版本v的类文件格式,当且仅当v处于某个连续范围Mi.0≤v≤Mj.m. 范围基于实现符合的Java SE平台的版本。 符合给定Java SE平台版本的实现必须支持表4.1-A中为该版本指定的范围,并且不支持其他范围。

Java SE版本 class文件格式版本号范围
1.0.2 45.0 ≤ v ≤ 45.3
1.1 45.0 ≤ v ≤ 45.65535
1.2 45.0 ≤ v ≤ 46.0
1.3 45.0 ≤ v ≤ 47.0
1.4 45.0 ≤ v ≤ 48.0
5.0 45.0 ≤ v ≤ 49.0
6 45.0 ≤ v ≤ 50.0
7 45.0 ≤ v ≤ 51.0
8 45.0 ≤ v ≤ 52.0
9 45.0 ≤ v ≤ 53.0
10 45.0 ≤ v ≤ 54.0
11 45 ≤ v ≤ 55
12 45 ≤ v ≤ 56
13 45 ≤ v ≤ 57

常量池

紧接着版本的则是常量池,常量池是Java内存中很重要的一部分。它是 class 文件结构与其他内存结构关联最多的数据类型,也是占用class文件空间最大的数据项之一。

常量池主要存放两类数据:字面量和符号引用。字面量比较接近Java层面的常量,如文本字符串,声明为final的常量值等。而符号引用则包括:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

符号引用属于编译原理中的知识,Java在用javac编译时,不需要在class文件中保存各个方法、属性、字段的最终内存布局信息,而是在jvm虚拟机加载class文件的时候动态链接的。因此,这些方法、字段、属性的符号不经过运行期转换的话是无法直接得到内存入口地址,也就无法直接被虚拟机直接使用。当运行虚拟机时,需要从常量池获取对应的符号引用,再在类创建时或运行时解析,翻译到具体的内存地址。

常量池首先是用 2 字节来表示常量的个数。根据 class 文件的格式,第9、第10表示常量池的个数,我们看看上面 User.class 的16进制,取第9位和第10位来计算:0x0021=33,这说明我们有 32 个常量,因为第0个常量表示不可用,其范围为[1,33]。

  • 常量池是一系列的数组,它的下标是从 1 开始的,即有效大小实际为 constant_pool_count - 1 ,第0项是无效的,因此有些结构可以用第0项表示对常量没有引用。
  • 常量池的设计有效减少class文件的大小,想想那些重复使用的类名,字符串表示现在只需要保留一份,并且引用的地方只需要用 u2 保存它在常量池的引用即可。

因为每个常量都有一种具体的类型来代表不同的含义,光知道常量的个数还没办法解析出具体的常量项来,所以定义每个常量的第一个字节u1表示该常量的类型tag,然后就可以根据该类型常量的存储结构来解析了。

​常量的 tag 有: CONSTANT_Utf8 , CONSTANT_Integer , CONSTANT_Float , CONSTANT_Long , CONSTANT_Double , CONSTANT_Class , CONSTANT_StringCONSTANT_FieldrefCONSTANT_MethodrefCONSTANT_InterfaceMethodref , CONSTANT_NameAndType , CONSTANT_MethodHandle , CONSTANT_MethodType , CONSTANT_InvokeDynamic 等14种。

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整形字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法符号的引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodHandle_info 16 表示方法类型
CONSTANT_MethodType_info 15 表示方法句柄
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点|​​

访问标志

类索引、父索引与接口索引集合

字段表集合

方法集合

属性表集

Code属性

Exception属性

LineNumberTable属性

LocalVariableTable属性

SourceFile属性

ConstantValue属性

InnerClass属性

Deprecated及Synthetic属性

StackMapTable属性

原文  https://www.zhoujunwen.com/2019/深入理解类文件结构及类加载机制
正文到此结束
Loading...