《深入理解java虚拟机》读书笔记1-类文件结构

  1. 1. 《深入理解java虚拟机》读书笔记1-类文件结构
    1. 1.1. 无关性基石
    2. 1.2. class类文件结构
      1. 1.2.1. 魔数
      2. 1.2.2. 版本号
      3. 1.2.3. 常量池
      4. 1.2.4. 访问标志
      5. 1.2.5. 类索引、父类索引和接口索引集合
      6. 1.2.6. 字段表集合
      7. 1.2.7. 方法表集合
      8. 1.2.8. 属性表(attribute_info)集合
        1. 1.2.8.1. code属性
        2. 1.2.8.2. Exceptions属性
        3. 1.2.8.3. LineNumberTable属性
        4. 1.2.8.4. LocalVariableTable及LocalVariableTypeTable属性
        5. 1.2.8.5. SourceFile及 SourceDebugExtension属性
        6. 1.2.8.6. ConstantValue属性
        7. 1.2.8.7. InnerClasses属性
        8. 1.2.8.8. Deprecated及Synthetic属性
        9. 1.2.8.9. StackMapTable属性
        10. 1.2.8.10. Signature属性
        11. 1.2.8.11. BootstrapMethods属性
        12. 1.2.8.12. MethodParameters属性
        13. 1.2.8.13. 模块化相关属性

《深入理解java虚拟机》读书笔记1-类文件结构

最近在看周志明老师写的《深入理解java虚拟机》,学到了很多,也有很多没理解的地方,觉得需要做读书笔记。

无关性基石

java虚拟机不与包括java语言在内的任何程序语言绑定,直至今日,商业企业和开源机构已经在java语言之外发展出一大批运行在java虚拟机之上的语言,比如Kotlin、Clojure、Groovy、JRuby、JPython、Scala等。java虚拟机它只与”calss文件“这种特定的二进制文件格式所关联。class文件中包含了java虚拟机指令集、符号表以及若干其他辅助信息。

class类文件结构

java技术能够一直保持着非常良好的向后兼容性,class文件结构的稳定性功不可没。

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文 件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前[2]的方式分割 成若干个8个字节进行存储。

类文件结构表格:

1646283335355.png

魔数

1
2
3
4
5
public class Hello {
public Hello() {
System.out.println("hellow fmyyy");
}
}

1646285992199.png

每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的class文件。

其值为 0xCAFEBABY(咖啡宝贝?)

版本号

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第七第八字节是主版本号(Major Version)

1646286227955.png

比如我这里此版本号为0x0000,而主版本号为0x0034,转换为10进制则为52,对照表格说明我这是可以被jdk8以上或以上版本的虚拟机执行的class文件

常量池

紧接着主、次版本号之后的是常量池入口,常量池(constant_pool)可以比喻为Class文件里的资源仓库。

在常量池之前还有一项u2类型的数据,代表着常量池容量计数值(constant_pool_count)

书中描述:

与Java中语言习惯不同,这个容量计数是从1而不是0开始 的,如图6-3所示,常量池容量(偏移地址:0x00000008)为十六进制数0x0016,即十进制的22,这就 代表常量池中有21项常量,索引值范围为1~21。在Class文件格式规范制定之时,设计者将第0项常量 空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下 需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。Class文件结构中只有 常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的 容量计数都与一般习惯相同,是从0开始。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比 较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译 原理方面的概念。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一项常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为了更好地支持动态语言调用,额外增加了4种动态语言相关的常量[1],为了支持Java模块化系统 (Jigsaw),又加入了CONSTANT_M odule_info和CONSTANT_Package_info两个常量,所以截至JDK 13,常量表中分别有17种不同类型的常量。

1646288258321.png

每一项常量都是一个表。java自带的javap命令可以输出常量表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Classfile /Users/fmyyy/Myself/yso/untitled/target/classes/Hello.class
Last modified 202232日; size 405 bytes
MD5 checksum 1dfd29b6948f4ccee8aad381b980b859
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // Hello
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 1, attributes: 1
Constant pool:
#1 = Methodref #6.#16 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #19 // hellow fmyyy
#4 = Methodref #20.#21 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #22 // Hello
#6 = Class #23 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LHello;
#14 = Utf8 SourceFile
#15 = Utf8 Hello.java
#16 = NameAndType #7:#8 // "<init>":()V
#17 = Class #24 // java/lang/System
#18 = NameAndType #25:#26 // out:Ljava/io/PrintStream;
#19 = Utf8 hellow fmyyy
#20 = Class #27 // java/io/PrintStream
#21 = NameAndType #28:#29 // println:(Ljava/lang/String;)V
#22 = Utf8 Hello
#23 = Utf8 java/lang/Object
#24 = Utf8 java/lang/System
#25 = Utf8 out
#26 = Utf8 Ljava/io/PrintStream;
#27 = Utf8 java/io/PrintStream
#28 = Utf8 println
#29 = Utf8 (Ljava/lang/String;)V
{
public Hello();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String hellow fmyyy
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
LineNumberTable:
line 2: 0
line 3: 4
line 4: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this LHello;
}
SourceFile: "Hello.java"

书中还有更具体的分析,但篇幅太长。感兴趣可以去看看书。

访问标志

常量池之后的两个字节代表访问标志(access_flags),用于识别类或者接口层次的访问信息。

1646289628809.png

我这里的hello.class只是一个普通的class,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、 ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_MODULE这七 个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021

1646289545427.png

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

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

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变 量以及实例级变量,但不包括在方法内部声明的局部变量。

字段可以包括的修饰符有字段的作用域(public、privat e、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、 字段名称。

1646290076732.png

字段修饰符放在access_flags中,他跟前面的访问标志挺像的。

1646290208503.png

由于语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所导致的。

在access_flags之后的是两项索引值:name_index和descriptor_index。它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。具体可以看书,放一张书中的表格。

1646290529039.png

之后跟随着一个属性表集合,用于存储一些额外的信息,字段表可以在属性表中附加描述零至多项的额外信息。

方法表集合

Class文件存储 格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,仅在访问标志和属性表集合的可选项中有所区别。依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descrip tor_index)、属性表集合(attributes)几项

1646290590692.png

1646290702550.png

因为volatile关键字和transient 关键字不能修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE标志和ACC_TRANSIENT标志。

1646290930809.png

如图,

第一个0x0001表示methods_count

第二个0x0001表示access_flags

0x0007表示name_index

0x0008表示descriptor_index

最后的0x0001和0x0009分别表示attributes_count和attribute_name_index

属性表(attribute_info)集合

这一节是书中表示类文件结构篇幅最长的,这里简单记一下。

与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一 些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任 何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

1646291369295.png

1646291385159.png

code属性

Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内

Exceptions属性

Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也 就是方法描述时在throws关键字后面列举的异常。

LineNumberTable属性

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。

LocalVariableTable及LocalVariableTypeTable属性

LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系。

SourceFile及 SourceDebugExtension属性

SourceFile属性用于记录生成这个Class文件的源码文件名称。

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。

InnerClasses属性

InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。

Deprecated及Synthetic属性

Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

StackMapTable属性

StackMapTable属性在JDK6增加到Class文件规范之中,它是一个相当复杂的变长属性,位于Code 属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(TypeChecker)使用(详见第7章字节码验证部分),目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

Signature属性

Signature属性在JDK 5增加到Class文件规范之中,它是一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中。在JDK5里面大幅增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(Parameterized Type),则Signature属性会为它记录泛型签名信息。

BootstrapMethods属性

BootstrapMethods属性在JDK 7时增加到Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedy namic指令引用的引导方法限定符。

MethodParameters属性

MethodParameters是在JDK 8时新加入到Class文件格式中的,它是一个用在方法表中的变长属性。

MethodParameters的作用是记录方法的各个形参名称和信息。

模块化相关属性

JDK 9的一个重量级功能是Java的模块化功能,因为模块描述文件(module-info.java)最终是要编译成一个独立的Class文件来存储的,所以,Class文件格式也扩展了Module、ModulePackages和
ModuleMainClass三个属性用于支持Java模块化相关功能。

书中有对每一个属性的具体结构和作用有详细的分析,但篇幅太长就不记在笔记里了。