JVM-Learning-2

JVM学习笔记(二):虚拟机执行子系统

《深入理解Java虚拟机:JVM高级特性与最佳实践》摘录

引用

下面的内容是我在阅读《深入理解Java虚拟机:JVM高级特性与最佳实践》后摘录下来的要点(我能看懂的),想了解更多细节请在下面的链接中观看。

摘自:《深入理解Java虚拟机:JVM高级特性与最佳实践》 — 周志明
在豆瓣阅读书店查看:https://read.douban.com/ebook/15233695/
本作品由华章数媒授权豆瓣阅读全球范围内电子版制作与发行。
© 版权所有,侵权必究。

类文件结构

Class类文件的结构

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

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以”_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。如下所示
类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count-1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。

如果想要分析Class文件字节码,可以使用javap工具(如果你已经配置好java环境,那么应该已经有这个工具了,可以在终端试一下)

Magic Number与Class文件的版本

每个Class文件的头4个字节称为Magic Number,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用Magic Number来进行身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有Magic Number。使用Magic Number而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择Magic Number值,只要这个Magic Number值还没有被广泛采用过同时又不会引起混淆即可。Class文件的Magic Number的获得很有“浪漫气息”,值为:0xCAFEBABE(咖啡宝贝?)

据Java开发小组最初的关键成员Patrick Naughton所说:“我们一直在寻找一些好玩的、容易记忆的东西,选择0xCAFEBABE是因为它象征着著名咖啡品牌Peet’s Coffee中深受欢迎的Baristas咖啡”,这个Magic Number似乎也预示着日后”Java”这个商标名称的出现。

紧接着Magic Number的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一项常量都是一个表,有多种结构各不相同的表结构数据。各种常量类型各自均有自己的结构。共同的特点是表开始的第一位是一个u1类型的标志位(tag)例如:

CONSTANT_Class_info类型有一个标志位和索引值(tag+name_index),索引值指向一个CONSTANT_Utf8_info类型常量

CONSTANT_Utf8_info类型则是由标志位、长度和多个字节组成(tag+length+bytes),字节数等于长度,而长度的最大值是64KB

访问标志

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

例如:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是public类型
ACC_FINAL 0x0010 是final
ACC_SUPER 0x0020 允许使用invokespecial字节码指令新语意
ACC_INTERFACE 0x0200 是接口
ACC_ABSTRACT 0x0400 是acstract类型(抽象类或接口)
ACC_SYNTHETIC 0x1000 并非由用户代码产生
ACC_ANNOTATION 0x2000 是注解
ACC_ENUM 0x4000 是枚举

标志值之间做或操作获得最终的标志值,例如一个公共接口的ACC_PUBLIC、ACC_SUPER、ACC_INTERFACE、ACC_ABSTRACT都为真,其他为假,则最终标志值为:0x0001|0x0020|0x0200|0x0400=0x0621

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

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量。而对于接口索引集合,入口的第一项——u2类型的数据为接口计数器interfaces_count,表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。

字段表集合

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

字段=字段修饰符+简单名称索引+字段描述符索引+属性数量+属性表(access_flags+name_index+descriptor_index+attributes_count+attributes)

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
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_VOLAYILE 0X0040 是volatile
ACC_TRANSIENT 0x0080 是transient
ACC_SYNTHETIC 0X1000 由编译器自动产生
ACC_ENUM 0X4000 是enum

跟在后面的name_index和descriptor_index是索引值,都是对常量池的都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。

全限定名:例,”org/fenixsoft/clazz/TestClass”是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。

简单名称:没有类型和参数修饰的方法或者字段名称。例,这个类中的inc()方法和m字段的简单名称分别是”inc”和”m”。

字段的描述符:非数组的话是一个标志字符,数组的话是[接标志字符。例,一个整型数组int[]将被记录为[I。数组的标志字符是L加对象的全限定名。

方法的描述符:参数放在一()里面,返回类型放在()后面,方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为([CII[CIII)I

标志字符 含义
B 基本类型 byte
C 基本类型 char
D 基本类型 double
F 基本类型 float
I 基本类型 int
J 基本类型 long
S 基本类型 short
Z 基本类型 boolean
V 特殊类型 void
L 对象类型,如 Ljava/lang/Object

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
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_VARARGS 0X0080 接受不定参数
ACC_NATIVE 0x0100 是native
ACC_ABSTRACT 0X0400 是abstract
ACC_STRICTFP 0x0800 是strictfp
ACC_SYNTHETIC 0X1000 由编译器自动产生

而方法里的代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里

与字段表集合相对应的,如果父类方法在子类中没有被重写Override,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器<clinit>方法和实例构造器<init>方法。

属性表集合

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

例如下面这些关键属性

属性名称 使用位置 含义
Code 方法表 Java代码编译的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为 deprecated 的方法和字段
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为局部类或匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 供类型检查验证器Type Checker检查和处理目标方法的局部变量和操作数栈所需类型是否匹配
Signature 类、方法表、字段表 用于支持泛型情况下的方法签名。在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含来类型变量(Type Variables)或参化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息。
SourceFile 类文件 记录源文件名称
BootstrapMethods 类文件 保存 invokedynamic 指令引用的引导方法限定符

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

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

Code属性

Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。Code属性如下:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attribute_count 1
attribute_info attributes attributes_count
  • attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为Code,它代表了该属性的属性名称。

  • attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节。

  • max_stack代表了操作数栈Operand Stacks深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧Stack Frame中的操作栈深度。

  • max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。

  • code_length代表字节码长度。

  • code是用于存储字节码指令的一系列字节流。每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。

  • exception_table_length异常表的长度

  • exception_table显示异常处理表集合(异常表),异常表对于Code属性来说并不是必须存在的。它的属性结构如下

    类型 名称 数量
    u2 start_pc 1
    u2 end_pc 1
    u2 handler_pc 1
    u2 catch_type 1

Exceptions属性

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

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exceptions

Exceptions属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。

LineNumberTable属性

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。LineNumberTable属性的结构如下:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

LocalVariableTable属性

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table 1
local_variable_table_info local_variable_table local_variable_table_length

local_variable_info项目结构如下:

类型 名称 数量
u2 start_pc 1
u2 length 1
u2 name_index 1
u2 descriptor_index 1
u2 index 1

start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。

name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。

index是这个局部变量在栈帧局部变量表中Slot的位置。

SourceFile属性

SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。类似”int x=123”和”static int x=123”这样的变量定义在Java程序中是非常常见的事情,但虚拟机对这两种变量赋值的方式和时刻都有所不同。对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。

目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 constantvalue_index 1

InnerClasses属性

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

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_classes 1
inner_classes_info inner_classes number_of_classes

数据项number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息都由一个inner_classes_info表进行描述。inner_classes_info表的结构见

类型 名称 数量
u2 inner_class_info_index 1
u2 outer_class_info_index 1
u2 inner_name_index 1
u2 inner_class_access_flags 1
  • inner_class_info_indexouter_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类和宿主类的符号引用。

  • inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为0。

  • inner_class_access_flags是内部类的访问标志,类似于类的access_flags

  • 标志名称 标志值 含义
    ACC_PUBLIC 0x0001 是public类型
    ACC_PRIVATE 0X0002 是private类型
    ACC_PROTECTED 0X0004 是protected类型
    ACC_STATIC 0X0008 是static类型
    ACC_FINAL 0x0010 是final
    ACC_INTERFACE 0x0200 是接口
    ACC_ABSTRACT 0x0400 是acstract类型(抽象类或接口)
    ACC_SYNTHETIC 0x1000 并非由用户代码产生
    ACC_ANNOTATION 0x2000 是注解
    ACC_ENUM 0x4000 是枚举

Deprecated和Synthetic属性

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

  • Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated注释进行设置。
  • Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1

StackMapTable属性

StackMapTable属性在JDK 1.6发布后增加到了Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。StackMapTable属性的结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_entries 1
stack_map_frame stack_map_frame_entries number_of_entries

Signature属性

任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量Type Variables或参数化类型Parameterized Types,则Signature属性会为它记录泛型签名信息。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 signature_index 1

BootstrapMethods属性

这个属性用于保存invokedynamic指令引用的引导方法限定符。

如果某个类文件结构的常量池中曾经出现过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确的BootstrapMethods属性,另外,即使CONSTANT_InvokeDynamic_info类型的常量在常量池中出现过多次,类文件的属性表中最多也只能有一个BootstrapMethods属性。BootstrapMethods属性与JSR-292中的InvokeDynamic指令和java.lang.Invoke包关系非常密切。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 num_bootstrap_methods 1
bootstrap_method bootstrap_methods num_bootstrap_methods

bootstrap_method结构:

类型 名称 数量
u2 bootstrap_method_ref 1
u2 num_bootstrap_arguments 1
u2 bootstrap_arguments num_bootstrap_arguments

虚拟机类加载机制

与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

类加载的时机

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。

被动引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.fenixsoft.classloading;
/**
*被动使用类字段演示一:
*通过子类引用父类的静态字段, 不会导致子类初始化
**/
public class SuperClass{
static{
System.out.println("SuperClass init!");
}
public static int value=123
}
public class SubClass extends SuperClass{
static{
System.out.println("SubClass init!");
}
}
/**
*非主动使用类字段演示
**/
public class NotInitialization{
public static void main(String[]args){
System.out.println(SubClass.value);
}
}

这里只输出SuperClass init!,因为只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

1
2
3
4
5
6
7
8
9
10
package org.fenixsoft.classloading;
/**
*被动使用类字段演示二:
*通过数组定义来引用类, 不会触发此类的初始化
**/
public class NotInitialization{
public static void main(String[]args){
SuperClass[]sca=new SuperClass[10];
}
}

这里并不会输出SuperClass init!,因为在对数组的实现中,需要注意的一点是,在虚拟机内部,自动生成了一个类来封装数组数据,该类只暴露了公有的length属性和clone()方法。因而在new一个数组的时候,其并不会初始化数组元素对象,而是初始化这个自动生成的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.fenixsoft.classloading;
/**
*被动使用类字段演示三:
*常量在编译阶段会存入调用类的常量池中, 本质上并没有直接引用到定义常量的类, 因此不会触发定义常量的类的初始化。
**/
public class ConstClass{
static{
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD="hello world"
}
/**
*非主动使用类字段演示
**/
public class NotInitialization{
public static void main(String[]args){
System.out.println(ConstClass.HELLOWORLD);
}
}

这里并不会输出ConstClass init!,因为常量在编译阶段通过常量传播优化存储到了NotInitialization类的常量池中,以后引用该常量都在自身的常量池中取,编译成Class后两个类再无联系。

接口的加载过程与类加载过程稍有一些不同,编译器仍然会为接口生成”<clinit>()”类构造器,用于初始化接口中所定义的成员变量。但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

类加载的过程

加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

  1. 文件格式验证
    • 第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
    • 该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。
  2. 元数据验证
    • 第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
    • 第二阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
  3. 字节码验证
    • 第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
    • 这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
  4. 符号引用验证
    • 符号引用验证对常量池中的各种符号引用以及符号引用中的类、字段、方法的访问性等等内容进行验证
    • 符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类

对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要(因为对程序运行期没有影响)的阶段。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用Symbolic References:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用Direct References:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

初始化

初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

1
2
3
4
5
6
7
public class Test{
static{
i=0//给变量赋值可以正常编译通过
System.out.print(i);//这句编译器会提示"非法向前引用"
}
static int i=1
}

虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object

<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。

如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

类加载器

把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

类与类加载器

每个类加载器都有其类名称空间,如果一个类由两个不同的类加载器中都进行了加载,那么就会认为这是两个不同的类,其创建的实例对象也是属于不同类的。

虚拟机字节码执行引擎

执行引擎是Java虚拟机最核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。

运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

局部变量表

局部变量在变量表中占用一个Slot的空间,当离开这个变量的作用域时,这个变量的Slot便可被其他变量复用。但这也意味着,如果没有被复用,这个变量可能还存在这个Slot中,可能会影响GC的回收。

例如:

1
2
3
4
5
6
public static void main(String[]args)(){
{
byte[]placeholder=new byte[64*1024*1024];
}
System.gc();
}

这里placeholder并不会被GC回收,因为它还在局部变量表的Slot还没被复用,作为GC Roots一部分的局部变量表仍然保持着对它的关联。所以当这个对象不再使用时,应该手动将其设为null

1
2
3
4
5
6
7
public static void main(String[]args)(){
{
byte[] placeholder = new byte[64*1024*1024];
placeholder = null;
}
System.gc();
}

另外还有一定要注意的是:Java并不会给局部变量自动设置初始值(类变量会自动初始化),当一个局部变量没有设置初始值时代码时不能执行的,不过这些在编译过程就会被检查出来。

操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。

在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递。

动态连接

对于方法调用的字节码指令,该指令后面接的是一个常量池中的符号引用,在执行该指令的时候,其会分两种情况对其后的符号引用进行处理:

  1. 将符号引用转化为直接引用,这种转化方式成为静态分析;
  2. 在运行期间将符号引用转化为直接引用,这部分称为动态连接。

方法返回地址

方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

方法调用

解析

解析的过程指的是在程序运行,并且调用某个方法时,其需要解析当前调用的方法的版本。因为对于部分方法,由于子类继承的存在,其在编译阶段是无法确认真正需要调用的版本的。这里能够再编译阶段就确认其版本的方法有四种:①构造方法;②静态方法;③私有方法;④父类方法。

分派

Java语言是一门静态多分派、动态单分派的语言。

静态分派

1
Parent son = new Son();

在上面的例子中Parent时静态类型,而Son是实际类型。

静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。如下,编译器只知道这个son对象的静态类型是Parent,所以在实际调用中会使用第一个demo。

1
2
3
4
5
public void demo(Parent p){}
public void demo(Son s){}

Parent son = new Son();
demo(son)

因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。

动态分派

动态分派的过程和多态性的另外一个重要体现——重写(Override)有着很密切的关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
public Parent{
public void demo(){
System.out.println("parent");
}
}
public Son extends Parent{
public void demo(){
System.out.println("son");
}
}

Parent son = new Son();
son.demo();

在上面的例子中,显然会输出son,因为这里不再根据静态类型来决定,而是通过实际类型来决定。