概述
在 Java 语言中,Java 虚拟机只能理解 字节码
(class文件
),它不面向任何处理器,不与任何语言绑定,只与 Class文件
这种特定的二进制文件格式所关联。
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
另一方面由于 JVM虚拟机
不与任何语言、机器绑定,因而任何语言的实现着都可以将 Java 虚拟机作为语言的运行基础,以 Class 文件作为他们的交付媒介。例如 Clojure
(Lisp 语言的一种方言)、Groovy
、JRuby
等语言都是运行在 Java 虚拟机之上。
下图展示了不同的语言被不同的编译器编译成 .class
文件最终运行在 Java 虚拟机之上的过程:
Class 文件的结构
根据《Java 虚拟机规范》,Class 文件通过 ClassFile
定义,而且文件结构采用一种类似 c 语言结构体的伪结构体。这种伪结构体只有两种两种数据类型:“无符号数”和“表”。
- 无符号数:属于基本的数据结构,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数。无符号数可以用来描述数字、索引引用、数字量值或者按照 UTF-8 编码构成字符串值
- 表:由多个无符号数或者其他表作为数据项构成的复合数据结构,为了便于区分,所有表的命名都习惯以 "_info" 结尾。
在正式开始讲,我们需要说明一点,Class 文件的结构不像 XML 那样的结构化描述语言,它以 8 个字节为基础单位,各个数据项目严格按照顺序紧凑地排列在文件中,中间没有添加任何分隔符号,因而在 Class 的数据项无论是顺序还是数量,甚至数据存储的字节序(大端存储,Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,全部都不允许改变。
ClassFile
的结构如下:
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类会可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
通过对 ClassFile
的分析,我们便可以知道 class 文件的组成。
上边的一些属性什么的,描述都很抽象,因而这里以一段典型的 java 代码产生的 class 文件为基础结合进行讲解。
一段典型的 Java 程序代码如下:
package com.test;
//接口类
interface Car {
void drive();
}
//实现类
public class BMWCar implements Car{
private String name;
public BMWCar() {
name = "宝马";
}
@Override
public void drive() {
System.out.println("BMW car drive." + name);
}
}
通过 javac
命令对代码进行编译,生成的 class 文件内容如下:
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: CA FE BA BE 00 00 00 3B 00 33 07 00 02 01 00 0F J~:>...;.3......
00000010: 63 6F 6D 2F 74 65 73 74 2F 42 4D 57 43 61 72 07 com/test/BMWCar.
00000020: 00 04 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F .....java/lang/O
00000030: 62 6A 65 63 74 07 00 06 01 00 0C 63 6F 6D 2F 74 bject......com/t
00000040: 65 73 74 2F 43 61 72 01 00 04 6E 61 6D 65 01 00 est/Car...name..
00000050: 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 .Ljava/lang/Stri
00000060: 6E 67 3B 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 ng;...<init>...(
00000070: 29 56 01 00 04 43 6F 64 65 0A 00 03 00 0D 0C 00 )V...Code.......
00000080: 09 00 0A 08 00 0F 01 00 06 E5 AE 9D E9 A9 AC 09 .........e..i),.
00000090: 00 01 00 11 0C 00 07 00 08 01 00 0F 4C 69 6E 65 ............Line
000000a0: 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F NumberTable...Lo
000000b0: 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 calVariableTable
000000c0: 01 00 04 74 68 69 73 01 00 11 4C 63 6F 6D 2F 74 ...this...Lcom/t
000000d0: 65 73 74 2F 42 4D 57 43 61 72 3B 01 00 05 64 72 est/BMWCar;...dr
000000e0: 69 76 65 09 00 18 00 1A 07 00 19 01 00 10 6A 61 ive...........ja
000000f0: 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 0C 00 va/lang/System..
00000100: 1B 00 1C 01 00 03 6F 75 74 01 00 15 4C 6A 61 76 ......out...Ljav
00000110: 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D a/io/PrintStream
00000120: 3B 07 00 1E 01 00 17 6A 61 76 61 2F 6C 61 6E 67 ;......java/lang
00000130: 2F 53 74 72 69 6E 67 42 75 69 6C 64 65 72 08 00 /StringBuilder..
00000140: 20 01 00 0E 42 4D 57 20 63 61 72 20 64 72 69 76 ....BMW.car.driv
00000150: 65 2E 0A 00 1D 00 22 0C 00 09 00 23 01 00 15 28 e....."....#...(
00000160: 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E Ljava/lang/Strin
00000170: 67 3B 29 56 0A 00 1D 00 25 0C 00 26 00 27 01 00 g;)V....%..&.'..
00000180: 06 61 70 70 65 6E 64 01 00 2D 28 4C 6A 61 76 61 .append..-(Ljava
00000190: 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 4C 6A /lang/String;)Lj
000001a0: 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 42 ava/lang/StringB
000001b0: 75 69 6C 64 65 72 3B 0A 00 1D 00 29 0C 00 2A 00 uilder;....)..*.
000001c0: 2B 01 00 08 74 6F 53 74 72 69 6E 67 01 00 14 28 +...toString...(
000001d0: 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 )Ljava/lang/Stri
000001e0: 6E 67 3B 0A 00 2D 00 2F 07 00 2E 01 00 13 6A 61 ng;..-./......ja
000001f0: 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 va/io/PrintStrea
00000200: 6D 0C 00 30 00 23 01 00 07 70 72 69 6E 74 6C 6E m..0.#...println
00000210: 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0B ...SourceFile...
00000220: 42 4D 57 43 61 72 2E 6A 61 76 61 00 21 00 01 00 BMWCar.java.!...
00000230: 03 00 01 00 05 00 01 00 02 00 07 00 08 00 00 00 ................
00000240: 02 00 01 00 09 00 0A 00 01 00 0B 00 00 00 3D 00 ..............=.
00000250: 02 00 01 00 00 00 0B 2A B7 00 0C 2A 12 0E B5 00 .......*7..*..5.
00000260: 10 B1 00 00 00 02 00 12 00 00 00 0E 00 03 00 00 .1..............
00000270: 00 0D 00 04 00 0E 00 0A 00 0F 00 13 00 00 00 0C ................
00000280: 00 01 00 00 00 0B 00 14 00 15 00 00 00 01 00 16 ................
00000290: 00 0A 00 01 00 0B 00 00 00 48 00 04 00 01 00 00 .........H......
000002a0: 00 1A B2 00 17 BB 00 1D 59 12 1F B7 00 21 2A B4 ..2..;..Y..7.!*4
000002b0: 00 10 B6 00 24 B6 00 28 B6 00 2C B1 00 00 00 02 ..6.$6.(6.,1....
000002c0: 00 12 00 00 00 0A 00 02 00 00 00 13 00 19 00 14 ................
000002d0: 00 13 00 00 00 0C 00 01 00 00 00 1A 00 14 00 15 ................
000002e0: 00 00 00 01 00 31 00 00 00 02 00 32 .....1.....2
魔数
u4 magic; //Class 文件的标志
每一个 Class 文件的头 4 个字节被称为 魔数
,它唯一的作用就是确定这个文件是否是一个能够被虚拟机接受的 Class
文件。
其在 class 文件中的具体位置如下图所示:
Clsss 文件的魔数选的很有浪漫气息,值为 0xCAFFEBABY
(咖啡宝贝?)
Class 文件版本号(Minor&Major Version)
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
紧跟着魔数的 4 个字节存储的是 CLass 文件的版本号:第 5 和第 6 个字节是次版本号(Minor Version),第 7 和 8 个字节存储的是主版本号(Manjor Version)。
每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 javap -v
命令来快速查看 Class 文件的版本号信息。
高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。 所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。
class 版本号具体位置如下图所示:
从图中可以看到,我们 class 文件的主版本号是 0x003B
,也就是十进制的 59,这个版本说明是可以被 JDK15 及 其以上版本的虚拟机运行。
常量池(Constant Pool)
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
紧接着主、次版本号之后的是常量池的入口,常量池的数量为 constant_pool_count - 1
(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”)。
常量池中主要存放两大类常量:
- 字面量:比较接近于 Java 语言层面的常量概念,如文本字符串、被声明的 final
"符号引用" :属于编译原理方面的概念,主要包括三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
常量池中,每一项常量都是一个表,截止到 JDK13,常量表中共有 17 种不同类型的常量,它们有一个共同的特点即表结构起始的第一位是一个 u1 类型的标志位(tag
),代表当前常量属于哪种常量。
17 中常量及其所对应的标志位如下表所示:
类型 | 标志(tag) | 描述 |
---|---|---|
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_MothodType_info | 16 | 标志方法类型 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
结合前边的 Class 文件:
可以看到常量池中常量的数量为 0x33
即有 50 个常量(因为从 1 开始计数),通过 javap -v BMWCar
命令可以查看 Class 文件的信息如下:
Classfile /C:/Users/vcjmhg/Desktop/test/com/test/BMWCar.class
Last modified 2021-4-17; size 748 bytes
MD5 checksum e3bb3d3eaf56cc12d92423d7b99781d2
Compiled from "BMWCar.java"
public class com.test.BMWCar implements com.test.Car
minor version: 0
major version: 59
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // com/test/BMWCar
#2 = Utf8 com/test/BMWCar
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Class #6 // com/test/Car
#6 = Utf8 com/test/Car
#7 = Utf8 name
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Methodref #3.#13 // java/lang/Object."<init>":()V
#13 = NameAndType #9:#10 // "<init>":()V
#14 = String #15
#15 = Utf8
#16 = Fieldref #1.#17 // com/test/BMWCar.name:Ljava/lang/String;
#17 = NameAndType #7:#8 // name:Ljava/lang/String;
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lcom/test/BMWCar;
#22 = Utf8 drive
#23 = Fieldref #24.#26 // java/lang/System.out:Ljava/io/PrintStream;
#24 = Class #25 // java/lang/System
#25 = Utf8 java/lang/System
#26 = NameAndType #27:#28 // out:Ljava/io/PrintStream;
#27 = Utf8 out
#28 = Utf8 Ljava/io/PrintStream;
#29 = Class #30 // java/lang/StringBuilder
#30 = Utf8 java/lang/StringBuilder
#31 = String #32 // BMW car drive.
#32 = Utf8 BMW car drive.
#33 = Methodref #29.#34 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
#34 = NameAndType #9:#35 // "<init>":(Ljava/lang/String;)V
#35 = Utf8 (Ljava/lang/String;)V
#36 = Methodref #29.#37 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#37 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#38 = Utf8 append
#39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = Methodref #29.#41 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#41 = NameAndType #42:#43 // toString:()Ljava/lang/String;
#42 = Utf8 toString
#43 = Utf8 ()Ljava/lang/String;
#44 = Methodref #45.#47 // java/io/PrintStream.println:(Ljava/lang/String;)V
#45 = Class #46 // java/io/PrintStream
#46 = Utf8 java/io/PrintStream
#47 = NameAndType #48:#35 // println:(Ljava/lang/String;)V
#48 = Utf8 println
#49 = Utf8 SourceFile
#50 = Utf8 BMWCar.java
{
public com.test.BMWCar();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #14 // String
7: putfield #16 // Field name:Ljava/lang/String;
10: return
LineNumberTable:
line 13: 0
line 14: 4
line 15: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/test/BMWCar;
public void drive();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=4, locals=1, args_size=1
0: getstatic #23 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #29 // class java/lang/StringBuilder
6: dup
7: ldc #31 // String BMW car drive.
9: invokespecial #33 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
12: aload_0
13: getfield #16 // Field name:Ljava/lang/String;
16: invokevirtual #36 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #40 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: invokevirtual #44 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 19: 0
line 20: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 this Lcom/test/BMWCar;
}
SourceFile: "BMWCar.java"
首先我们尝试对第一个常量进行解析,首先找到它对应的标志位(表的第一个字节)为 7,查询上边的常量表可知,该常量为一个 CONSTANT_CLASSS_info
(类或者接口的符号引用)
查询 CONSTANT_class_info
的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 标志位 |
u2 | name_index | 1 |
tag
位前边我们说了,它是所有表的一个共同特征,用来指明表的类型;
name_index
是常量池的索引值,指向常量池中一个 CONSTANT_Utf8_info
类型常量,代表这个类的全限定名。
由于第一个常量的 name_index = 2
,也就是指向了常量池中的第二个常量。
首先可以看到它的 tag=1
,是一个 CONSTANT_UTF8_info
类型的常量,该类型的结构表如下图所示:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
length
属性说明这个 UTF-8 编码的字符串的长度是多少个字节,后边紧跟着 length
字节的连续数据表示一个使用 UTF-8 缩略编码表示的字符串。
说明:此处缩略编码与普通 UTF 编码的区别在于:从'\u0001'到'\u07ff'(相当于 Ascii 编码 1 到 217)使用一个字节编码,从'\u0080'到 '\u007f'之间的字符使用两个字节编码,剩余部分按照普通 UTF-8 编码规则使用三个字节进行编码。
我们可以看到该字符串长度为 0x000f
即有 15 个字节,然后紧接着 15 个字节构成了该字符串的值:com/test/BMWCar
。
将前边两个常量结合在一起我们就了解到该类的全限定名为:com/test/BMWCar
。
其他常量分析与之类似,我们计算出常量池在 class 中所占用的空间位置如下图所示:
访问标志(Access Flags)
常量池结束之后,紧接着的 2 个字节表示 Class 的 访问标志
(access_flags
),这个标志用来识别类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public
类型,是否定义为 abstract
类型;如果是类的话,是否定义为 final
等等。
具体的标志位及其含义如下表所示:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令新语义,JDK1.0.2 之后都为 true |
ACC_INTERFACE | 0x0200 | 标志这是个接口 |
ACC_ABSTRACT | 0x0400 | 是否是 Abstract 类型,对于抽象类或者接口来说为 true,其他情况为 false |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
access_flags 中一共有 16 个标志位可以使用,当前只定义了 9 个,没有使用到的标志位一律为零(工程上的一种冗余设计思想,值得学习 😁😊)。
结合我们的 Class 文件,可以看到该文件的 访问标志
为 0x0021
相当于 0x0020 | 0x0001
查询访问标志表可知,该类是一个 public 类型且可以使用 invokespecial
指令新语义的普通类。
类索引(This Class)、父类索引(Super Class)、接口(Interfaces)索引集合
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
访问标识之后,紧接着的便是类索引、父类索引与接口索引集合。其中类索引、父类索引都是一个 u2 类型数据,而接口索引集合是一个 u2 类型的数据集合,这三项数据构成了 Class 文件的继承关系。
类索引用来确定这个类的全限定名,父类索引用来用于确定这个类的父类的全限定名。由于 Java 不允许多重继承,所以父类索引有只有一个(java.lang.Object
类除外)。
接口索引集合就是用来描述这个类实现了哪些接口,这些接口将按照 implements
关键字(如果这个 Class 文件表示的是一个接口,则应当使用 extends 关键字)后的接口顺序从左到右排列在接口索引集合中。
类索引和父类索引使用两个 u2 类型的索引值表示,它们各自指向一个类型为 CONSTANT_Class_info
的类描述常量,进而找到一个定义在 CONSTANT_Utf8_info
类型中的索引全限定名字符串。
结合前边的 Class 文件,类索引查找全限定名的过程如下图所示:
首先根据从 Class 索引值为 0x0001
也即指向常量池中的第一个常量,该常量是一个 CONSTANT_Class_info
类型的数据,该数据的权限定名称指向了常量池中的第三个常量,第三个常量的常量值是 com/test/BMWCar
,将整个过程结合在一起我们就知道该类文件的全限定名为 com/test/BMWCar
。
父类索引的查找过程与之类似,此处不再详述,最终可以定位到该类文件的父类为 java/lang/Object
。
接口索引由于是集合类型,查找过程与类查找过程可能有些许不同:
首先找到第一个 u2
类型接口计数器,其值为 0x0001
也就是说该类文件实现了一个接口,其接口索引为 0x0005
即接口索引指向常量池中第五个常量。常量 #5
为一个 CONSTAN_Class_info
类型的常量,指向第六个 CONSTANT_Utf8_info
类型的常量 #6
,该常量的值为 com/test/Car
。
整个分析下来,我们可以得到该 Class 文件是一个实现了一个全限定名为 com/test/Car
接口的类。
字段表集合(Fields)
u2 fields_count;//Class 文件的字段的个数
field_info fields[fields_count];//一个类会可以有个字段
接口索引后边紧跟着的就是字段表信息,字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
字段表的结构如下表所示:
类型 | 名称 | 数量 | 备注 |
---|---|---|---|
u2 | access_flags | 1 | 字段的作用域(public、private、protected 修饰符),是实例变量还是类变量,可否被序列化(transient 修饰符),可变性(final),可见性(volatitle 修饰符,是否强制从主内存读写) |
u2 | name_index | 1 | 对常量池的引用,表示字段的简单名称 |
u2 | descriptor_index | 1 | 对常量池的引用,表示字段和方法的描述符 |
u2 | attributes_count | 1 | 一个字段可能会额外拥有一些属性,attributes_count 用来存放属性的数量 |
attribute_info | attributes | attributes_count | 存放属性的具体内容 |
字段访问 access_flags
的标志及其含义如下表所示:
权限名称 | 值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static,静态 |
ACC_FINAL | 0x0010 | final |
ACC_VOLATILE | 0x0040 | volatile,不可和 ACC_FIANL 一起使用 |
ACC_TRANSIENT | 0x0080 | 在序列化中被忽略的字段 |
ACC_SYNTHETIC | 0x1000 | 由编译器产生,不存在于源代码中 |
ACC_ENUM | 0x4000 | enum |
紧随 access_flags
标志的是 name_index
和 descriptor_index
,他们都是对常量池的引用。name_index
代表着字段的简单名称,descriptor_index
代表着字段的描述符。相比于全限定名和简单名称,方法和字段的描述符要复杂一些。
描述符的主要作用是用来描述字段的数据类型、方法和参数列表(包括数量类型以及顺序)和返回值。因而描述符在设计时,设计了一系列描述规则:
- 基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的 void 类型都用一个大写字符来表示。
- 对象类型则用字符 L 加上对象的全限定名来表示
描述符标识字含义如下表所示:
标识字符 | 含义 |
---|---|
B | 基本类型 byte |
C | 基本类型 char |
D | 基本类型 double |
F | 基本类型 float |
I | 基本类型 int |
J | 基本类型 long |
S | 基本类型 short |
Z | 基本类型 boolean |
V | 特殊类型 void |
L | 对象类型,如 Ljava/lang/Object; |
[ | 数组类型,多个维度则有多个[ |
用描述符来描述方法时,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组“()”之内。
例如方法 int getAge()
的描述符为“()I”,方法 void print(String msg)
的描述符为“(Ljava/lang/String;)V”,方法 int indexOf(int index, char[] arr)
的描述符为“(I[C)I”
结合我们的 Class 文件,可以看到该类的第一个方法是构造方法,方法名称为 com.test.BMWCar,描述符为 ()V
,也即是一个入参为空且返回值为空的函数。
方法表集合(Methods)
u2 methods_count;//Class 文件的方法的数量
method_info methods[methods_count];//一个类可以有个多个方法
字段表集合结束之后,紧接着就是方法表集合,与字段表的结构一样,一次包括访问标志(access_flags)、名称索引(name_index)、描述符(descriptor_index)、属性表集合(attributes)几项,具体结构如下表所示:
类型 | 描述 | 备注 |
---|---|---|
u2 | access_flags | 记录方法的访问标志 |
u2 | name_index | 常量池中的索引项,指定方法的名称 |
u2 | descriptor_index | 常量池中的索引项,指定方法的描述符 |
u2 | attributes_count | attributes 包含的项目数 |
attribute_info | attributes[attributes_count] | 存放属性的具体内容 |
具体方法标志及其含义如下表所示:
权限名称 | 值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static,静态 |
ACC_FINAL | 0x0010 | final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否为 synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是由编译器产生的桥接方法 |
ACC_VARARGE | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为 native |
ACC_ABSTRACT | 0x0400 | 方法是否为 abstract |
ACC_SYNTHETIC | 0x0800 | 方法是否为 strictfp |
ACC_SYNTHETIC | 0x1000 | 由编译器产生,不存在于源代码中 |
与属性表的方法标志进行比较,我们不难发现,两者大体上是类似的,但有诸多不同之处:
因为 volatile 关键字和 transient 关键字不能够修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE
标志和 ACC_TRANSIENT
标志。与之相对应的,synchronized、native、strictfp 和 abstract 等关键字可以修改方法但不能修饰属性,因此增加了 ACC_SYNCHRONIZED
、ACC_NATIVE
、ACC_SYNTHETIC
、ACC_ABSTRACT
。
分析到这里,可能会有小伙伴有疑问了:前面说的好像都只是方法的定义,那方法的主体逻辑代码怎么描述呢?
简单来说,方法体中的 Java 代码,经过 Javac 编译成字节码指令之后,存放在方法属性中的一个名为 Code
的属性里面了。
结合我们的 Class 文件可以看到,该文件中有一个 drive()
方法,该方法的入参和返回值都为空,访问限定符为 public
。
当然与字段表集合相应的,如果父类方法在子类中被重写(Override),方法表集合中就不会出现来自父类的方法信息。如果未被覆盖就有可能出现由编译器自动添加的方法,最常见的便是类构造器(<clinit>()
)以及实例构造器 <init>()
方法。
结合我们的 Class 文件可以看到,该 Class 文件也是具有 <init>()
方法的。
属性表集合(Attributes)
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
属性表(attribute_info
)前边实际上已经提到了数次,Class 文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有信息。
与其他数据项目的要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一点,不要求各个属性的严格顺序,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机在运行时会忽略掉它所不熟悉的信息。
由于 Jave 当前支持的属性种类很多已经达到了 29 项,因而此处只列出常见的几种重要属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java 代码编译成的字节码指令 |
ConstantValue | 字段表 | final 关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为 deprecated 的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code 属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
Code 属性
Java 方法里的代码被编译处理后,变为字节码指令存储在方法表的 Code 属性里,但并不是所有的方法表里都有 Code 属性,例如接口或抽象类中的方法就可能没有该属性。
Code属性
如下表所示:
类型 | 名称 | 含义 |
---|---|---|
u2 | attribute_name_index | 属性名称索引 |
u4 | attribute_length | 属性长度 |
u2 | max_stack | 操作数栈深度的最大值 |
u2 | max_locals | 局部变量表所需的存储空间 |
u4 | code_length | 字节码长度 |
u1 | code[code_length] | 存储字节码指令的一系列字节流 |
u2 | exception_table_length | 异常表长度 |
exception_info | exception_table | 异常表的值 |
u2 | attributes_count | 属性数量 |
attribute_info | attributes[attributes_count] | 属性的值 |
结合我们的 Class 文件可以看到其 Code 属性如下:
从图中可以看出,Code属性
本身也是一个复合属性,其中包含了其他属性,比如包含了 LineNumberTable
和 LocalVariableTable
。
ConstantValue 属性
只有当一个字段被声明为 static final 时,并且该字段是基本数据类型或 String 类型时,编译器才会在字段的属性表集合中增加一个名为 ConstantValue 的属性,所以 ConstantValue 属性只会出现在字段表中,其数据结构为:
类型 | 名称 | 含义 |
---|---|---|
u2 | attribute_name_index | 属性名称索引 |
u2 | attribute_length | 属性长度 |
u2 | constantvalue_index | 常量池常量的索引 |
总结
Class 文件是 Java 虚拟机执行引擎的数据入口,也是 Java 技术体系的基础支柱之一,因而学习 Class 文件的结构很有意义。本文主要讲解了 Class 文件结构中的各个组成部分,以及每个部分的定义、数据结构和使用方法。并结合一个例子(文中有代码,引用处附带有链接),讲解了 Class 文件是如何被存储和访问的。
参考
- Java Class 文件结构解析
- 类文件结构
- 《深入理解 JVM 虚拟机》
- 文中使用的 class 文件 BMWCar.class
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于