基础储备----JVM类文件结构

本文详细介绍了Java后台开发中重要的JVM类文件结构,包括魔数、版本号、常量池、访问标志、类索引、父类索引、接口索引集合、字段表集合、方法表集合和属性表集合等关键组成部分,并通过实例代码、二进制分析和javap工具进行深入解析。
摘要由CSDN通过智能技术生成


1. 前言

作为一名Java后台开发的程序员, 深入理解JVM, 重要性不言而喻, 这篇文章主要是记录JVM类文件结构相关知识.

2. 实例

这部分比较抽象, 所以以实例的形式来学习. 这部分作为资料, 以便后面的章节用来翻阅.

2.1 实例代码

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

2.2 实例二进制

cafe babe 0000 0034 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 064c 4d61 696e 3b01
0004 6d61 696e 0100 1628 5b4c 6a61 7661
2f6c 616e 672f 5374 7269 6e67 3b29 5601
0004 6172 6773 0100 135b 4c6a 6176 612f
6c61 6e67 2f53 7472 696e 673b 0100 0a53
6f75 7263 6546 696c 6501 0009 4d61 696e
2e6a 6176 610c 0007 0008 0700 1c0c 001d
001e 0100 0c48 656c 6c6f 2057 6f72 6c64
2107 001f 0c00 2000 2101 0004 4d61 696e
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7401 0010 6a61 7661 2f6c 616e 672f
5379 7374 656d 0100 036f 7574 0100 154c
6a61 7661 2f69 6f2f 5072 696e 7453 7472
6561 6d3b 0100 136a 6176 612f 696f 2f50
7269 6e74 5374 7265 616d 0100 0770 7269
6e74 6c6e 0100 1528 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 2956 0021 0005
0006 0000 0000 0002 0001 0007 0008 0001
0009 0000 002f 0001 0001 0000 0005 2ab7
0001 b100 0000 0200 0a00 0000 0600 0100
0000 0100 0b00 0000 0c00 0100 0000 0500
0c00 0d00 0000 0900 0e00 0f00 0100 0900
0000 3700 0200 0100 0000 09b2 0002 1203
b600 04b1 0000 0002 000a 0000 000a 0002
0000 0003 0008 0004 000b 0000 000c 0001
0000 0009 0010 0011 0000 0001 0012 0000
0002 0013 

2.3 javap分析

javap -verbose Main.class
javap分析
javap分析

2.4 手工分析

cafe babe // 魔数
0000 0034 // Class文件版本 
0022 // 常量池-容量计数器(34-1=33)
0a 0006 0014 // 第1个常量
09 0015 0016 // 第2个常量
08 0017 // 第3个常量
0a 0018 0019 // 第4个常量
07 001a // 第5个常量
07 001b // 第6个常量
01 0006 3c 69 6e 69 74 3e // 第7个常量 <init>
01 0003 28 29 56 // 第8个常量 ()V
01 0004 43 6f 64 65 // 第9个常量 Code
01 000f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 // 第10个常量 LineNumberTable
01 0012 4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 // 第11个常量 LocalVariableTable
01 0004 74 68 69 73 // 第12个常量 this
01 0006 4c 4d 61 69 6e 3b // 第13个常量 LMain;
01 0004 6d 61 69 6e // 第14个常量 main
01 0016 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 // 第15个常量 ([Ljava/lang/String;)V
01 0004 61 72 67 73 // 第16个常量 args
01 0013 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b // 第17个常量 [Ljava/lang/String;
01 000a 53 6f 75 72 63 65 46 69 6c 65 // 第18个常量 SourceFile
01 0009 4d 61 69 6e 2e 6a 61 76 61 // 第19个常量 Main.java
0c 0007 0008 // 第20个常量
07 001c // 第21个常量
0c 001d 001e // 第22个常量
01 000c 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 // 第23个常量 Hello World!
07 001f // 第24个常量
0c 0020 0021 // 第25个常量
01 0004 4d 61 69 6e // 第26个常量 Main
01 0010 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 // 第27个常量 java/lang/Object
01 0010 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d // 第28个常量 java/lang/System
01 0003 6f 75 74 // 第29个常量 out
01 0015 4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b // 第30个常量 Ljava/io/PrintStream;
01 0013 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d // 第31个常量 java/io/PrintStream
01 0007 70 72 69 6e 74 6c 6e // 第32个常量 println
01 0015 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 // 第33个常量 (Ljava/lang/String;)V
0021 // 访问标志 
0005 // 类索引
0006 // 父类索引
0000 // 接口索引集合
0000 // 字段表集合
0002 // 方法表集合.计数器(2)
0001 // 第1个方法:方法访问标识符
0007 // 第1个方法:方法名称索引项
0008 // 第1个方法:方法描述符索引项
0001 // 第1个方法:属性表计数器(1)
0009 // 第1个方法:属性名字
0000 002f // 第1个方法:属性长度(47)
00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 01 00 0b 00 00 00 0c 00 01 00 00 00 05 00 0c 00 0d 00 00 // 第1个方法:属性info
0009 // 第2个方法:方法访问标识符
000e // 第2个方法:方法名称索引项
000f // 第2个方法:方法描述符索引项
0001 // 第2个方法:属性表计数器(1)
0009 // 第2个方法:属性名字
0000 0037 // 第2个方法:属性长度(55)
00 02 00 01 00 00 00 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a 00 00 00 0a 00 02 00 00 00 03 00 08 00 04 00 0b 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00 // 第2个方法:属性info
0001 // Class文件属性表计数器(1)
0012 // 属性名字
0000 0002 // 属性长度(2)
00 13 // 属性info

3. Class类文件结构

Java 虚拟机规范中定义了许多规范, 其中有一部分定义了字节码的结构和规范. Java 虚拟机规范定义了两种数据类型来表示 Class 文件格式, 分别是: 无符号数和表.
无符号数属于最基本的数据类型, 以 u1, u2, u4, u8分别代表 1 个字节, 2 个字节, 4 个字节, 8 个字节的无符号数, 无符号数可以用来描述数字, 索引引用, 数量值或者按照 UTF-8 编码构成的字符串值.
表是由多个无符号数或者其他表作为数据项构成的复合数据类型, 所有表都习惯性地以”_info”结尾. 表用于描述由层次关系的复合结构的数据, 整个Class文件本质上就是一张表.
整个 Class 文件本质上就是一张表, 它由表下表所示的数据项构成.
Class类文件结构

3.1 魔数

Class 文件的第 1 - 4 个字节代表了该文件的魔数()Magic Number). 它唯一的作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件, 其值固定是: 0xCAFEBABE(咖啡宝贝). 如果一个 Class 文件的魔数不是 0xCAFEBABE, 那么虚拟机将拒绝运行这个文件.

3.1.1 实例对照

我们看看实例部分, 其前 4 个字节分别是:cafe babe

3.2 Class文件版本

Class 文件的第 5 - 6 个字节代表了 Class 文件的次版本号(Minor Version), 即编译该 Class 文件的 JDK 次版本号。
Class 文件的第 7 - 8 个字节代表了 Class 文件的主版本号(Major Version), 即编译该 Class 文件的 JDK 主版本号。
高版本的 JDK 能向下兼容以前的 Class 文件, 但不能运行新版本的 Class 文件.
例如一个 Class 文件是使用 JDK 1.5 编译的, 那么我们可以用 JDK 1.7 虚拟机运行它, 但不能用 JDK 1.4 虚拟机运行它.
下表列出了各个版本 JDK 的十六进制版本号信息:
Class文件版本

3.2.1 实例对照

我们看看实例部分, 其 5 - 8 个字节分别是:0000 0034, 那么我们可以知道, 这个 Class 文件是由 JDK1.8 编译的.

3.3 常量池

紧接着主次版本号之后是常量池入口, 由于常量池中常量的数量是不固定的, 所以在常量池的入口需要放置一个常量池容量计数值(constant_pool_count), 这个容量计数是从1而不是0开始的, 设计者这样设计的目的是为了满足后面某些指向常量池的索引值的数据在特殊情况下需要表达”不引用任何一个常量池项目”的含义.
Class文件结构中只有常量池的容量计数是从1开始的, 索引集合. 字段集合. 方法集合. 属性集合的容量计数都是从0开始的.
注意, Long和Double型占用两个计数.
常量池中主要存放两大类常量: 字面量(Literal)和符号引用.
字面量接近Java语言层面的常量概念, 如文本字符串. 声明为final的常量值等.
符号引用属于编译原理的概念, 包括三类常量:

  1. 类和接口的全限定名;
  2. 字段的名称和描述符;
  3. 方法的名称和描述符.

常量池中每一项常量都是一个表, 在JDK1.7之后共有14种表结构, 它们有一个共同的特点, 就是表开始的第一位是一个u1类型的标志位(tag, 取值见下表), 代表当前这个常量属于哪种常量类型.
每个常量池的常量都用一个类型为 cp_info 的表表示, 该表有 14 个值, 分别是:
常量池
常量池中的14种常量项的结构总表:
结构总表
结构总表

3.3.1 实例对照

我们Class 文件第 9 - 10 个字节为 0022, 表示有 33 个常量.

第 1 个常量. 紧接着 0022 的后一个字节为 0a, 表示该常量为CONSTANT_MethodHandle_info. 从上面的总表查阅知道, 该常量项第 2 - 3 个字节表示方法的类描述符, 这里是 0006 表示指向常量池第 6 个常量所表示的信息. 该常量项的第 4 - 5 个字节表示名称及类描述符, 这里值为 0014 表示指向常量池第 20 个常量所表示的信息.

第 2 个常量. 紧接着 0014 的后一个字节为 09, 表示该常量为CONSTANT_Fieldref_info. 从上面的总表查阅知道, 该常量项第 2 - 3 个字节表示字段的类或者接口描述符, 这里是 0015 表示指向常量池第 21 个常量所表示的信息. 该常量项的第 4 - 5 个字节表示字段描述符, 这里值为 0016 表示指向常量池第 22 个常量所表示的信息.

第 3 个常量. 紧接着 0016 的后一个字节为 08, 表示该常量为CONSTANT_String_info. 从上面的总表查阅知道, 该常量项第 2 - 3 个字节表示指向字符串字面量的索引, 这里是 0017 表示指向常量池的第 23 个常量.

第 4 个常量. 紧接着 0017 的后一个字节为 0a, 表示该常量为CONSTANT_MethodHandle_info的常量. 从上面的总表查阅知道, 该常量项第 2 - 3 个字节表示方法的类描述符, 这里是 0018 表示指向常量池第 24 个常量所表示的信息. 该常量项的第 4 - 5 个字节表示名称及类描述符, 这里值为 0019 表示指向常量池第 25 个常量所表示的信息.

第 5 个常量. 紧接着 0019 的后一个字节为 07, 表示该常量为CONSTANT_Class_info的常量. 从上面的总表查阅知道, 该常量项第 2 - 3 个字节表示全限定名常量项, 这里是 001a 表示指向常量池第 26 个常量所表示的信息.
……
更多可以参考 2.实例 部分中的分析.

3.4 访问标志

在常量池结束之后, 紧接着的两个字节代表访问标记(access_flags), 这个标志用于识别一些类或者接口层次的访问信息, 包括: 这个Class是类还是接口, 是否定义为public类型, 是否定义为abstract类型等. 具体的标志位以及标志的含义见下表.
访问标志

3.4.1 实例对照

在实例里面, 这两个字节是 00 21, 通过查看我们并没有发现有标志值是 00 21 的标志名称. 这是因为这里的访问标志可能是由多个标志名称组成的, 所以字节码文件中的标志值其实是多个值进行或运算的结果.
通过查阅上述表格, 我们可以知道, 00 21 由 00 01 和 00 20 进行或运算得来, 也就是说该类的访问标志是 public 并且允许使用 invokespecial 字节码指令的新语义.

3.5 类索引, 父类索引和接口索引集合

类索引和父类索引都是一个u2类型的数据, 而接口索引集合是一组u2类型的数据的集合, Class文件中由这三项数据来确定这个类的继承关系.

3.5.1 实例对照

类索引. 类索引用于确定这个类的全限定名, 它用一个 u2 类型的数据表示. 这里的类索引是 00 05 表示其指向了常量池中第 5 个常量, 通过我们之前的分析, 我们知道第 5 个常量其最终的信息是 Main 类.
父类索引. 父类索引用于确定这个类的父类的全限定名, 父类索引用一个u2类型的数据表示. 这里的父类索引是 00 06 表示其指向了常量池中第 6 个常量, 通过我们之前的分析, 我们知道第 6 个常量其最终的信息是 Object 类. 因为其并没有继承任何类, 所以 Demo 类的父类就是默认的 Object 类.
接口索引. 接口索引集合就用来描述哪个类实现了哪些接口, 这些被实现的接口将按 implements 语句(如果这个类本身就是一个接口, 则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中. 对于接口索引集合, 入口第一项是 u2 类型的数据为接口计数器(interfaces_count), 表示索引表的容量, 而在接口计数器后则紧跟着所有的接口信息. 如果该类没有实现任何接口, 则该计数器值为0, 后面接口的索引表不再占用任何字节.
这里 Main 类的字节码文件中, 因为并没有实现任何接口, 所以紧跟着父类索引后的两个字节是0x0000, 这表示该类没有实现任何接口. 因此后面的接口索引表为空.

3.6 字段表集合

字段表集合用于描述接口或者类中声明的变量. 这里说的字段包括类级变量和实例级变量, 但不包括在方法内部声明的局部变量.
在类接口集合后的2个字节是一个字段计数器, 表示总有有几个属性字段. 在字段计数器后, 才是具体的属性数据. 字段表的每个字段用一个名为 field_info 的表来表示, field_info 表的数据结构如下所示:
字段表集合
字段访问标志:
字段访问标志
跟随 字段访问标志 的是两项索引值: name_index和 descriptor_index. 它们都是对常量池的引用, 分别代表字段的简单名称 以及 字段和方法的描述符.
描述符的作用是描述字段的数据类型, 方法的参数列表(包括数量, 类型及顺序)和返回值.
根据描述符的规则, 基本数据类型以及代表无返回值的void类型都用一个大写字符来表示, 而对象类型则用字符L加对象的全限定名表示, 见下表
描述符标识字符含义
对于数组类型, 每一维度将使用一个前置的”[“字符来描述. 如”String[][]”, 会被记录为”[[Ljava/lang/String”,”int[]”被记录为”[I”.
描述符描述方法时, 按照先参数列表, 后返回值的顺序描述. 参数列表按照参数的严格顺序放置一组小括号“()”内, 如void inc()的描述符为“()V”,“viod main(String[] args)”的描述符为“([Ljava/lang/String;)V”,“int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)”的描述符为“([CII[CIII)I”.
字段表都包含的固定数据项到descriptor_index为止就结束了, 不过在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息, 字段都可以在属性表中描述零至多项的额外信息.
字段表集合中不会列出从超类或者父类接口中继承而来的字段, 但有可能列出原本Java代码之中不存在的字段.

3.6.1 实例对照

因为我们并没有声明任何的类成员变量或类变量, 所以在 Main 的字节码文件中, 字段计数器为 00 00, 表示没有属性字段.

3.7 方法表集合

在字段表后的 2 个字节是一个方法计数器, 表示类中总有有几个方法. 在字段计数器后, 才是具体的方法数据, 方法表中的每个方法都用一个 method_info 表示, 其数据结构如下:
方法表集合
方法表所包含的数据项目的含义也和字段表集合的非常的类似, 仅在访问标志和属性表集合的可选项中有所区别. 由于volatile, transient关键字不能修饰方法, 同时synchronized, native, strictfp和abstract关键字可以修饰方法. 对于方法表, 所有标志位及其取值如下
方法访问标志
通过访问标志, 名称索引, 描述符索引可清楚的表达方法的定义. 那方法里面的代码去哪里了呢? 方法里的Java代码经过编译器编译成字节码指令后, 存放在方法属性表集合中属性表中; 这个属性表的名称为”Code”. 属性表是Class文件格式中最具扩展性的一种数据项目.
与字段表集合相对应的, 如果父类方法在子类中没有被重写(Override), 方法表集合中就不会出现来自父类的方法信息, 但可能出现编译器自动添加的方法, 最典型的便是类构造器””方法和实例构造器””方法.
在Java语言中, 重载(Overload)一个方法, 1.要与原方法具有相同的简单名称. 2.要与原方法有不同的特征签名.
Java代码的方法特征签名只包括方法名称, 参数顺序及参数类型; 而字节码的特征签名还包括方法返回值以及受查异常表.

3.7.1 实例对照

Main 类的字节码文件中, 方法计数器的值为 00 02, 表示一共有 2 个方法.
第 1 个方法. 方法计数器后 2 个字节表示方法访问标识, 这里是 00 01, 表示其实 ACC_PUBLIC 标识, 即该方法访问表示为 public.紧 接着 2 个字节表示方法名称的索引, 这里是 00 07 表示指向了常量池第 7 个常量, 查阅可知其指向了. 紧接着的 2 个字节表示方法描述符索引项, 这里是 00 08 表示指向了常量池第 8 个常量, 查阅可知其指向了()V. 紧接着 2 个字节表示属性表计数器, 这里是 00 01 表示该方法一共有 1 个属性. 紧接着的一连串就是属性表的内容.

3.8 属性表集合

在Class文件, 字段表, 方法表, 属性表都可以携带自己的属性表集合, 用于描述某些场景专有的信息.
与Class文件中其他的数据项目要求严格的顺序, 长度和内容不同, 属性表集合的限制稍微宽松了一些, 不再要求各个属性表具有严格顺序, 并且只要不与已有属性名重复, 任何人实现的编译器都可以想属性表中写入自己定义的属性信息, Java虚拟机运行时会忽略掉它不认识的属性. 下边将介绍一些关键常用的属性.
虚拟机规范预定义的属性:

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final关键字定义的常量值
Deprecated类、方法表、字段表被声明为deprecated的方法和字段
Exceptions方法表方法抛出的异常
EnclosingMethod类文件仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClasses类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
StackMapTableCode属性JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature类、方法表、字段表JDK1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
SourceFile类文件记录源文件名称
SourceDebugExtension类文件JDK1.6中新增的属性,SourceDebugExtension属性用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension属性就可以用于存储这个标准所新加入的调试信息
Synthetic类、方法表、字段表标识方法或字段为编译器自动生成的
LocalVariableTypeTableJDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类、方法表、字段表JDK1.5新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性用于注明哪些注解是运行时(实际上运行时就是进行反射调用)可见的
RuntimeInvisibleAnnotations类、方法表、字段表JDK1.5新增的属性,与RuntimeVisibleAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotations方法表JDK1.5新增的属性,作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法参数
RuntimeInvisibleParameterAnnotations方法表JDK1.5新增的属性,作用与RuntimeInvisibleAnnotations属性类似,只不过作用对象为方法参数
AnnotationDefault方法表JDK1.5新增的属性,用于记录注解类元素的默认值
BootstrapMethods类文件JDK1.7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符

对于每个属性, 它的名称需要从常量池引用一个CONSTANT_Utf8_info类型的常量来表示, 而属性值的结构则完全自定义的, 只需要通过一个u4的长度属性去说明属性值所占用的位数即可. 一个符合规则的属性表应该满足以下定义结构.
属性表结构:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1infoattribute_length

attribute_name_index是指向CONSTANT_Utf8_info类型常量的索引, CONSTANT_Utf8_info类型常量记录着属性的名称; attribute_length标识属性值所占用的位数.

3.8.1 实例对照

这里不做过多扩展了, 每种属性具体的定义参考书就可以了.

4. 参考链接

<<深入理解Java虚拟机—-JVM高级特性与最佳实践>>(第二版, 周志明)
https://www.cnblogs.com/chanshuyi/p/head_first_of_java_byte_code.html
https://blog.csdn.net/A_zhenzhen/article/details/77977345

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值