深入理解JVM类文件格式(一)

简介: 深入理解JVM类文件格式

正文


我们知道Java最有名的宣传口号就是:“一次编写,到处运行(Write Once,Run Anywhere)”,而其平台无关性则是依赖于JVM, 所有的java文件都被编译成字节码(class)文件,而虚拟机只需要认识字节码文件就可以了。想要弄懂虚拟机以及类加载机制,这部分内容是不可不知的。


Class文件是一组以8字节为基础单位的二进制流,所有数据无间隔的排列在Class文件之中,多字节数据以大端(big-endian order)的方式存储。Class文件以一种接近于C中结构体的伪代码形式存储数据结构,并且只包含无符号数和表两种数据结构:

  • 无符号数:u1、u2、u4、u8分别表1、2、4、8字节的无符号数
  • :由多个无符号数或者其他表组成的复合数据类型, Class文件本身也是一张表。

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];
}


参照上面的数据结构,Class文件由10个部分组成:
1 . 魔数
2 . Class文件主次版本号
3 . 常量池
4 . 访问标记
5 . 当前类名
6 . 父类名
7 . 继承的接口名
8 . 包含的所有字段的数量+字段
9 . 包含的所有方法的数量+方法
10 . 包含的所有属性的数量+属性
下面我们依次对每个部分进行分析:


1. 魔数


魔数(Magic number)用来确定文件类型,这里就是检测文件是否是能够被虚拟机接受的Class文件。很多文件都使用魔数来确定文件类型,而不是扩展名(因为扩展名可以任意修改)。可以参看我的深入理解程序构造(一)。Class文件的魔数是“0xcafebabe”,咖啡宝贝?Java本身也是一种爪哇咖啡,真是挺有缘的。
这里我也写个小的测试程序,来看看它的二进制码流:

package com.shuqing28;
public class TestClass {
    private int m;
    public int inc() {
        return m+1;
    }
}


我们使用javac编译成.class文件,Windows下可以使用WinHex打开,Linux下则可以使用hexdump打开二进制,命令如下:

1$ hexdump -C TestClass.class
 200000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........|
 300000010  00 03 00 13 07 00 14 07  00 15 01 00 01 6d 01 00  |.............m..|
 400000020  01 49 01 00 06 3c 69 6e  69 74 3e 01 00 03 28 29  |.I...<init>...()|
 500000030  56 01 00 04 43 6f 64 65  01 00 0f 4c 69 6e 65 4e  |V...Code...LineN|
 600000040  75 6d 62 65 72 54 61 62  6c 65 01 00 12 4c 6f 63  |umberTable...Loc|
 700000050  61 6c 56 61 72 69 61 62  6c 65 54 61 62 6c 65 01  |alVariableTable.|
 800000060  00 04 74 68 69 73 01 00  19 4c 63 6f 6d 2f 73 68  |..this...Lcom/sh|
 900000070  75 71 69 6e 67 32 38 2f  54 65 73 74 43 6c 61 73  |uqing28/TestClas|
1000000080  73 3b 01 00 03 69 6e 63  01 00 03 28 29 49 01 00  |s;...inc...()I..|
1100000090  0a 53 6f 75 72 63 65 46  69 6c 65 01 00 0e 54 65  |.SourceFile...Te|
12000000a0  73 74 43 6c 61 73 73 2e  6a 61 76 61 0c 00 07 00  |stClass.java....|
13000000b0  08 0c 00 05 00 06 01 00  17 63 6f 6d 2f 73 68 75  |.........com/shu|
14000000c0  71 69 6e 67 32 38 2f 54  65 73 74 43 6c 61 73 73  |qing28/TestClass|
15000000d0  01 00 10 6a 61 76 61 2f  6c 61 6e 67 2f 4f 62 6a  |...java/lang/Obj|
16000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|
17000000f0  05 00 06 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|
1800000100  09 00 00 00 2f 00 01 00  01 00 00 00 05 2a b7 00  |..../........*..|
1900000110  01 b1 00 00 00 02 00 0a  00 00 00 06 00 01 00 00  |................|
2000000120  00 03 00 0b 00 00 00 0c  00 01 00 00 00 05 00 0c  |................|
2100000130  00 0d 00 00 00 01 00 0e  00 0f 00 01 00 09 00 00  |................|
2200000140  00 31 00 02 00 01 00 00  00 07 2a b4 00 02 04 60  |.1........*....`|
2300000150  ac 00 00 00 02 00 0a 00  00 00 06 00 01 00 00 00  |................|
2400000160  06 00 0b 00 00 00 0c 00  01 00 00 00 07 00 0c 00  |................|
2500000170  0d 00 00 00 01 00 10 00  00 00 02 00 11           |.............|
260000017d

看第一行的前4个字节的十六进制就是0xcafebabe,所以文件类型确实为.class文件。


2. 版本号


第5和第6字节是次版本号(Minor Version),第7和第8字节是主版本号(Major Version)。这里看出我们的主版本号是0x0034,也就是52,下面是JDK与其对应的版本号关系:

JDK 1.8 = 52JDK 1.7 = 51JDK 1.6 =50JDK 1.5 = 49JDK 1.4 = 48JDK 1.3 = 47JDK 1.2 = 46JDK 1.1 = 45

可以看出我使用的是Java8编译的代码。


3. 常量池


我们继续看二进制文件的第一行:

00000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........|



在主版本号0x0034后的是0x0016,这个值表示常量池的容量。常量池可以理解为Class文件的资源仓库,常量池中包含的数据结构是这样的:

cp_info {
    u1 tag;
    u1 info[];
}



常量池中的每个项目都包含一个tag开头的cp_info对象,代表着常量类型,info则根据不同的类型各有各的结构。目前一共有14种常量类型:Constant TypeValue



CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

上面的0x0016翻译成十进制是22,那么常量池中有21个常量,因为常量池中索引是从1开始计数的,所以常量索引范围是1~21。

00000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........|

接下看常量池的第一个常量, tag是0x0a, 查上面的常量表就是CONSTANT_Methodref,表示接下来定义的是一个方法,知道类型后,我们可以查一下CONSTANT_Methodref的结构,这里可以参考Oracle的官方文档The class File Format,

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}



由于.class文件是无间隔的二进制文件,所以接着读:

  • tag: 0x0a,上面已经说了指代CONSTANT_Methodref常量
  • class_index:指向常量池中CONSTANT_Class_info类型的常量,代表上面方法的名称
  • name_and_type_index : 指向常量池中CONSTANT_NameAndType_info常量,是对方法的描述

因为class_index占两个字节,所以紧接着读到了0x0004,也就是4,指向常量池中的第4个常量,name_and_type_index是0x0012,指向第18个常量。后面会分析到第4和第18个常量。继续往下读,到第一行的最末了,是个0x09,指示的是CONSTANT_Fieldref,表示接下来是对一个域的定义, 查官方文档,格式为:


CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}


结构和CONSTANT_Methodref_info一样,这时候读到了第二行:

00000010 00 03 00 13 07 00 14 07 00 15 01 00 01 6d 01 00 |.............m..|


class_index为0x0003,指向第3个常量,name_and_type_index为0x0013指向第13个常量。这时候继续往后读,终于读到第3个常量了。此时tag是0x07,查表可得为CONSTANT_Class类型,此类型的常量代表一个类或者接口的符号引用,CONSTANT_Class的结构:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

tag是7, name_index是0x0014,十进制就是20,指向第20个常量,这样我们已经读了很多个字节了。但是这样解析下去很累,还好java自带的javap工具可以帮我们分析出字节码的内容。
执行下面语句:

javap -verbose TestClass.class


我们可以得到:

Last modified Nov 14, 2017; size 381 bytes
  MD5 checksum 102d643185c4823ef103931ff3e34462
  Compiled from "TestClass.java"
public class com.shuqing28.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref #4.#18 // java/lang/Object."<init>":()V
   #2 = Fieldref #3.#19 // com/shuqing28/TestClass.m:I
   #3 = Class #20 // com/shuqing28/TestClass
   #4 = Class #21 // java/lang/Object
   #5 = Utf8 m
   #6 = Utf8 I
   #7 = Utf8 <init>
   #8 = Utf8 ()V
   #9 = Utf8 Code
  #10 = Utf8 LineNumberTable
  #11 = Utf8 LocalVariableTable
  #12 = Utf8 this
  #13 = Utf8 Lcom/shuqing28/TestClass;
  #14 = Utf8 inc
  #15 = Utf8 ()I
  #16 = Utf8 SourceFile
  #17 = Utf8 TestClass.java
  #18 = NameAndType #7:#8 // "<init>":()V
  #19 = NameAndType #5:#6 // m:I
  #20 = Utf8 com/shuqing28/TestClass
  #21 = Utf8 java/lang/Object
{
  public com.shuqing28.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
...//省略


这里我们可以看到Constant pool字段,后面依次列出了21个常量,可以看出第一个是Methodref型的常量,class_index指向第4个常量,第4个常量呢是CONSTANT_Class类型,name_index又指向第20个常量,可知是一个CONSTANT_Utf8类型的常量,前面没说到CONSTANT_Utf8,下面是它的结构:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

第一位tag为1,length指示字符数组的长度,bytes[length]是使用UTF-8缩略编码表示的字符串,这里解析出来是com/shuqing28/TestClass,即类的全限定名。继续回到第一个Methodref常量,它的name_and_type_index值是18, 继续找到第18个常量,是CONSTANT_NameAndType_info类型,代表的是一个方法的信息:

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}


name_index指向了常量7, 即#7 = Utf8 <init>, 是一个CONSTANT_Utf8_info类型,值为<init>,这个是方法的名称,descriptor_index指向了常量8,即#8 = Utf8 ()V,是方法的描述,下文会说这个表达式是什么意思。
这样我们就可以一一把这21个常量分析清楚了。
其实Class文件就是在一开始列出了一堆常量,后面的各种描述都是各种index,指向前面常量池中的各种常量,来描述整个类的定义。就像有一本字典,我们使用字典中的字来造我们的句子,只不过Class文件中造句是有严格格式规定的,下面的内容基本都按照固定格式,无间隔的描述一个类的内容。


4. 访问标志


常量池结束后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口的访问信息,包括这个Class是类还是接口,是否是public的,是否是abstract,是否是final的。
访问标记含义如下表:
标志名称标志值含义




ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

access_flags中一共有16个标志位可用,当前只定义了8个,别的都为0,TestClass是public类型的,且使用JDK1.2以后的编译器进行编译的(使用JDK1.2以后的编译器编译,这个值都为真),别的标志都为假。所以access_flags的值应为:0x0001|0x0020 = 0x0021。我们找到刚才常量池最后一行的地方:

000000e0 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 |ect.!...........|


65 63 74分别对应ect,紧接着是0x0021,与我们的分析结果一致。


相关文章
|
3天前
|
安全 Java
对 JVM 的类加载机制以及寻找字节码文件的“双亲委派模型”的理解
对 JVM 的类加载机制以及寻找字节码文件的“双亲委派模型”的理解
6 0
|
16天前
|
安全 Java
JVM的类的生命周期
JVM的类的生命周期
|
3天前
|
存储 前端开发 Java
深入浅出JVM(四)之类文件结构
深入浅出JVM(四)之类文件结构
深入浅出JVM(四)之类文件结构
|
4天前
|
存储 缓存 安全
JVM 类的加载篇
JVM 类的加载篇
9 0
|
18天前
|
Java
[JVM] Java类的加载过程
[JVM] Java类的加载过程
[JVM] Java类的加载过程
|
26天前
|
监控 安全 Java
JVM工作原理与实战(七):类的生命周期-初始化阶段
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了类的生命周期、类的初始化阶段等内容。
25 5
|
26天前
|
存储 安全 Java
JVM工作原理与实战(六):类的生命周期-连接阶段
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了类的生命周期、类的连接阶段等内容。
30 4
|
3天前
|
存储 缓存 算法
深入浅出JVM(二)之运行时数据区和内存溢出异常
深入浅出JVM(二)之运行时数据区和内存溢出异常
|
12天前
|
存储 Java
深入理解Java虚拟机:JVM内存模型
【4月更文挑战第30天】本文将详细解析Java虚拟机(JVM)的内存模型,包括堆、栈、方法区等部分,并探讨它们在Java程序运行过程中的作用。通过对JVM内存模型的深入理解,可以帮助我们更好地编写高效的Java代码,避免内存溢出等问题。
|
1天前
|
Java Linux Arthas
linux上如何排查JVM内存过高?
linux上如何排查JVM内存过高?
13 0