1.加载,将二进制字节流加载到方法区,然后在java堆中实例化一个java.lang.Class类的对象
2.验证:文件, 验证class文件格式规范-魔数、主次版本、文件编码;元数据,对字节码描述的信息进行语义分析:类父类、final、抽象实现,字节码,进行数据流和控制流分析:类的方法、类型转换,符号引用-是否存在指定类、权限
3.准备:类变量(static变量)赋0值(注:不是指定值),final变量直接赋指定值
4.解析:符号引用转为直接引用,主要针对类或接口、字段、类方法、接口方法四类符号引用进行
5.初始化:遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,反射时如果类没有进行过初始化,则需先触发其初始化,包括对静态变量赋指定值、静态代码块的初始化,1.按代码顺序初始化,2.父类先于子类初始化,3.clinit ()方法不是必须的4.接口也有clint,5.线程安全
验证-准备-解析称为连接阶段
init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。
对于这五种会触发类进行初始化的场景,这五种场景中的行为称为对一个类进行 主动引用。除此之外,所有引用类的方式,都不会触发初始化,称为 被动引用。
类的实例化是指创建一个类的实例(对象)的过程;
类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期中的一个阶段。
通过子类引用父类的静态字段,不会导致子类初始化:例子
实例初始化不一定要在类初始化结束之后才开始初始化。例子
创建一个对象常常需要经历如下几个过程:父类的类构造器
浅克隆:复制的对象时不同的对象,对象内部的基本数据克隆正常,但是引用类型不会克隆,只会指向同一个
引用静态常量时,常量在编译阶段会存入类的常量池中,本质上并没有直接引用到定义常量的类文章最后有说明
类加载的过程(加载、验证、准备、解析、初始化)
JVM(三):类加载机制(类加载过程和类加载器)
深入理解Java类加载器
1.加载
“加载”是”类加载”过程的一个阶段。由类加载器完成,在加载阶段,虚拟机需要完成以下3件事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流。(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
相对于类加载过程的其他阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。
加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,
但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
2.验证:
文件验证:魔数开头、主次版本、文件编码
元数据验证:类相关验证、父类、抽象方法、方法重载出错
字节码验证:对类的方法体验证、类型转换
符号引用验证:对类引用的常量池中的各种符号引用检查能否找到、权限、public、private等
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的自身安全。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响。
1.文件格式验证:
验证class文件格式规范,
(1)是否以魔数0xCAFEBABE开头。
(2)主、次版本号是否在当前虚拟机处理范围之内。
(3)常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
(4)指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
(5)CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
(6)Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
......
2.元数据验证:
这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求。
(1)这个类是否有父类(除了java.lang.Object之外,所有类都应当有父类)。
(2)这个类是否继承了不允许被继承的类(被final修饰的类)。
(3)如果这个类不是抽象类,是否实现了其父类或接口之中所要求实现的所有方法。
(4)类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。
......
3.字节码验证:
进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,确定程序语义是合法的、符合逻辑的。保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:
(1)保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
(2)保证跳转指令不会跳转到方法体以外的字节码指令上。
(3)保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。
......
(Halting Problem:通过程序去校验程序逻辑是无法做到绝对准确的——不能通过程序准确的检查出程序是否能在有限时间之内结束运行。)
4.符号引用验证:
符号引用中通过字符串描述的全限定名是否能(在常量池中)找到对应的类,符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。
符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:
(1)符号引用中通过字符串描述的全限定名是否能够找到对应的类。
(2)在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
(3)符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外的信息进行匹配性的校验,通常需要校验以下内容:
符号引用中通过字符串描述的全限定名是否能找到对应的类。
在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问。
。。。。。。
符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,将会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
验证阶段对于虚拟机的类加载机制来说,是一个非常重要、但不一定是必要的阶段。如果所运行的全部代码都已经被反复使用和验证过,在实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施
3. 准备
准备阶段是正式为类变量分配内存并设置类变量初始值(\零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。
如果类字段的字段属性表中存在ConstantValue(不变)属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,假设类变量value定义为:
public static final int value = 123;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123.
4.解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用(Symbolic References):
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
Java虚拟机 9 :Java 类加载机制
符号引用其实是属于编译原理方面的概念,符号引用包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
这么说可能不太好理解,结合实际看一下,写一段很简单的代码:
package com.xrq.test6;
public class TestMain
{
private static int i;
private double d;
public static void print()
{
}
private boolean trueOrFalse()
{
return false;
}
}
用javap把这段代码的.class反编译一下:
Constant pool:
#1 = Class #2 // com/xrq/test6/TestMain
#2 = Utf8 com/xrq/test6/TestMain
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8 d
#8 = Utf8 D
#9 = Utf8
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Methodref #3.#13 // java/lang/Object."":()V
#13 = NameAndType #9:#10 // "":()V
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/xrq/test6/TestMain;
#18 = Utf8 print
#19 = Utf8 trueOrFalse
#20 = Utf8 ()Z
#21 = Utf8 SourceFile
#22 = Utf8 TestMain.java
看到Constant Pool
也就是常量池中有22项内容,其中带Utf8
的就是符号引用。比如#2,它的值是com/xrq/test6/TestMain
,表示的是这个类的全限定名;又比如#5为i,#6为I,它们是一对的,表示变量时Integer(int)类型的,名字叫做i;#6为D、#7为d也是一样,表示一个Double(double)类型的变量,名字为d;#18、#19表示的都是方法的名字。
那其实总而言之,符号引用和我们上面讲的是一样的,是对于类、变量、方法的描述。符号引用和虚拟机的内存布局是没有关系的,引用的目标未必已经加载到内存中了。
- 直接引用(Direct References):
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。
虚拟机规范并没有规定解析阶段发生的具体时间,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。
1.类、接口的解析
2.字段解析
3.类方法解析
4.接口方法解析
1.类或接口的解析
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的引用,那虚拟机完成整个解析过程需要以下3个步骤:
(1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。
(2)如果C是一个数组类型,并且数组的元素类型为对象,那将会按照第1点的规则加载数组元素类型。
(3)如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为了一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具有对C的访问权限。如果发现不具备访问权限,则抛出java.lang.IllegalAccessError
异常。
2.字段解析
首先解析字段表内class_index项中索引的CONSTANT_Class_info符号引用,也就是字段所属的类或接口的符号引用,如果解析完成,将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。
(1)如果C 本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(2)否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(3)否则,如果C 不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(4)否则,查找失败,抛出java.lang.NoSuchFieldError
异常。
如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessErro
r异常。
如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己的父类或多个接口中出现,那编译器可能拒绝编译,并提示”The field xxx is ambiguous”。
3.类方法解析
首先解析类方法表内class_index项中索引的CONSTANT_Class_info符号引用,也就是方法所属的类或接口的符号引用,如果解析完成,将这个类方法所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续类方法的搜索。
(1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C 是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
(2)如果通过了第一步,在类C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(4)否则,在类C实现的接口列表以及他们的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在相匹配的方法,说明类C是一个抽象类这时查找结束,抛出java.lang.AbstractMethodError异常。
(5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
最后,如果查找成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备此方法的访问权限,则抛出java.lang.IllegalAccessError异常。
4.接口方法解析
首先解析接口方法表内class_index项中索引的CONSTANT_Class_info符号引用,也就是方法所属的类或接口的符号引用,如果解析完成,将这个接口方法所属的接口用C表示,虚拟机规范要求按照如下步骤对C进行后续接口方法的搜索。
(1)与类解析方法不同,如果在接口方法表中发现class_index中的索引C是个类而不是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
(2)否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(3)否则,在接口C的父接口中递归查找,直到java.lang.Object类(查找范围包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(4)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
由于接口中所有的方法默认都是public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccessError异常。
5.初始化
类的初始化阶段是类加载过程的最后一步,在准备
阶段,类变量已赋过一次系统要求的初始值0值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器
。
在以下四种情况下初始化过程会被触发执行:
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。生成这4条指令的最常见的java代码场景是:1.使用new关键字实例化对象;2-3.读取或设置一个类的静态变量的时候(final除外,被final修饰、已在编译器把结果放入常量池);4.调用类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先出发其父类的初始化
- jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类
类初始化阶段是类加载过程的最后一步,到了这个阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经赋过一次系统要求的初始值0值,而在初始化阶段,则对类变量赋指定值。
1.编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,而定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问,代码解释如下:
public class Test {
static {
i = 0; //给变量赋值可以正常编译通过
System.out.print(i); //编译器会提示“非法向前引用”
}
static int i = 1;
}
2.初始化方法执行的顺序,虚拟机会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕,也就是说子类初始化时父类的所有静态变量、静态代码块 都执行完了。
3.clinit ()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,或者没有对静态变量的赋值操作,那么编译器可以不为这个类生成clinit()方法。
4.接口中不能使用静态语句块,但仍然有变量初始化的操作,因此接口与类一样都会生成clinit()方法,但与类不同的是,执行接口的初始化方法之前,不需要先执行父接口的初始化方法。只有当父接口中定义的变量使用时,才会执行父接口的初始化方法。另外,接口的实现类在初始化时也一样不会执行接口的clinit()方法。
5.虚拟机会保证一个类的clinit()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行类初始化方法完毕。
. Java虚拟机加载.class过程
虚拟机把Class文件加载到内存,然后进行校验,解析和初始化,最终形成java类型,这就是虚拟机的类加载机制。加载,验证,准备,初始化这5个阶段的顺序是确定的,
类的加载过程,必须按照这种顺序开始。这些阶段通常是相互交叉和混合进行的。解析阶段在某些情况下,可以在初始化阶段之后再开始---为了支持java语言的运行时绑定。
Java虚拟机规范中,没有强制约束什么时候要开始加载,但是,却严格规定了几种情况必须进行初始化(加载,验证,准备则需要在初始化之前开始):
1)遇到 new、getstatic、putstatic、或者invokestatic 这4条字节码指令,如果没有类没有进行过初始化,则触发初始化
2)使用java.lang.reflect包的方法,对垒进行反射调用的时候,如果没有初始化,则先触发初始化
3)初始化一个类时候,如果发现父类没有初始化,则先触发父类的初始化
- 加载,验证,解析
加载就是通过指定的类全限定名,获取此类的二进制字节流,然后将此二进制字节流转化为方法区的数据结构,在内存中生成一个代表这个类的Class对象。验证是为了确
保Class文件中的字节流符合虚拟机的要求,并且不会危害虚拟机的安全。加载和验证阶段比较容易理解,这里就不再过多的解释。解析阶段比较特殊,解析阶段是虚拟机
将常量池中的符号引用转换为直接引用的过程。如果想明白解析的过程,得先了解一点class文件的一些信息。class文件采用一种类似C语言的结构体的伪结构来存储我们编
码的java类的各种信息。其中,class文件中常量池(constant_pool)是一个类似表格的仓库,里面存储了我们编写的java类的类和接口的全限定名,字段的名称和描述符,
方法的名称和描述符。在java虚拟机将class文件加载到虚拟机内存之后,class类文件中的常量池信息以及其他的数据会被保存到java虚拟机内存的方法区。我们知道class文件
的常量池存放的是java类的全名,接口的全名和字段名称描述符,方法的名称和描述符等信息,这些数据加载到jvm内存的方法区之后,被称做是符号引用。而把这些类的
全限定名,方法描述符等转化为jvm可以直接获取的jvm内存地址,指针等的过程,就是解析。虚拟机实现可以对第一次的解析结果进行缓存,避免解析动作的重复执行。
在解析类的全限定名的时候,假设当前所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或者接口C的直接引用,具体的执行办法就是虚拟机会把代表N的
全限定名传递给D的类加载器去加载这个类C。这块可能不太好理解,但是我们可以直接理解为调用D类的ClassLoader来加载N,然后就完成了N--->C的解析,就可以了。