Java虚拟机 JVM类加载机制(类加载过程、时机、双亲委派模型)

什么是类加载

java平台实现了一次编译到处运行,离不开JVM的帮助,当我们的程序经过编译器编译,它会形成一个个.class后缀的文件,java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转码解析和初始化,最终形成可以被虚拟机直接使用的JAVA类型,这个过程被称为虚拟机的类加载机制。
一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构
JAVA程序的运行

类加载的过程(类的生命周期)

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示
类的生命周期
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。特别注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

什么时候进行类加载

关于什么情况下开始类加载的过程的第一个阶段加载,《java虚拟机规范》中并没有进行约束,但是对于初始化阶段,虚拟机规范严格规定了有且只有六种情况必须立即对类进行初始化(加载、验证、准备自然需要在此之前开始):
1、使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(被fanal修饰、已在编译期就放入常量池中的常量除外),或者invokestatic调用一个静态方法的时候,对应类必须进行过初始化。

2、通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。

3、当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。

4、当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。

5、使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

6、当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前进行初始化。

以上六种会触发类型进行初始化的场景,称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会出现初始化,称为被动引用。

被动引用的例子一:
通过子类引用父类的静态字段,对于父类属于“主动引用”的第一种情况,对于子类,没有符合“主动引用”的情况,故子类不会进行初始化。代码如下:


//父类
public class SuperClass {
	//静态变量value
	public static int value = 666;
	//静态块,父类初始化时会调用
	static{
		System.out.println("父类初始化!");
	}
}
 
//子类
public class SubClass extends SuperClass{
	//静态块,子类初始化时会调用
	static{
		System.out.println("子类初始化!");
	}
}
 
//主类、测试类
public class NotInit {
	public static void main(String[] args){
		System.out.println(SubClass.value);
	}

输出结果:

父类初始化!
666

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

被动引用例子二:
通过数组定义来引用类,不会触发此类的初始化,因为是数组new,而类没有被new,所以没有触发任何“主动引用”条款,属于“被动引用”。代码如下:

//父类
public class SuperClass {
	//静态变量value
	public static int value = 666;
	//静态块,父类初始化时会调用
	static{
		System.out.println("父类初始化!");
	}
}
 
//主类、测试类
public class NotInit {
	public static void main(String[] args){
		SuperClass[] test = new SuperClass[10];
	}

输出结果:


程序没有输出结果,说明并没有触发类的初始化。这段代码实际上触发了另一个名为“Lorg.fenixsoft.classloading.SuperClass”的类的初始化。它是由一个由虚拟机自动生成的、直接继承于java.lang.Object的子类。

被动引用例子三:
常量会在编译阶段直接存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

//常量类
public class ConstClass {
	static{
		System.out.println("常量类初始化!");
	}
	
	public static final String HELLOWORLD = "hello world!";
}
 
//主类、测试类
public class NotInit {
	public static void main(String[] args){
		System.out.println(ConstClass.HELLOWORLD);
	}
}

输出结果:

hello world!

程序没有输出常量初始化,是因为java源码在编译时,已经将ConstClass类中的常量HELLOWORLD直接存储到了NotInt类中的常量池中,实际上ConstClass.HELLOWORLD已经转化为对自身常量池的引用,所以不会调用到定义常量的类,因此,ConstClass类没有初始化。

加载

”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:

(1)通过一个类的全限定名来获取其定义的二进制字节流

(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构

(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。

相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。我们在最后一部分会详细介绍这个类加载器。在这里我们只需要知道类加载器的作用就是上面虚拟机需要完成的三件事,仅此而已就好了。

验证

验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。此阶段主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。

(1)文件格式的验证:基于字节流验证。验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。只有通过了文件格式验证,这段字节流才会被允许进入方法区进行存储,之后的三个验证只会基于方法区的存储结构验证,不会再直接读取、操作字节流。

(2)元数据验证:基于方法区的存储结构验证。主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。

(3)字节码验证:基于方法区的存储结构验证。这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出危害虚拟机安全的行为。

(4)符号引用验证:基于方法区的存储结构验证。它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。

对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。

准备

这个阶段是正式为类中定义的变量(被static修饰的静态变量)分配内存并设置类变量的初始值的阶段。这些变量内存在方法区中分配。这里要注意类变量和初始值的概念。
(1)类变量指的是被static关键字修饰的变量,而非实例变量,实例变量只在对象实例化的时候随对象一起存放在堆中,类变量会存放在方法区。
(2)初始值只是数据类型默认值,并非执行程序代码的赋值语句中指定的值。例如:

public static int value = 123 ;

变量value在准备阶段过后的初始值是0,因为java中int类型默认值为0。而将value赋值为123的这个指令,要在类的初始化过程中调用的类构造器 < clinit > ()中执行。

基本数据类型的零值:
数据类型准备阶段的默认值数据类型准备阶段的默认值
int0booleanfalse
long0Lchar‘\u0000’
short(short)0float0.0f
byte(byte)0double0.0d

如果类字段的属性表中存在ConstantValue属性,即用final修饰的静态变量,则这个变量会在初始化之前就进行赋值。

public static final int value = 123;

编译器会将value设置成ConstantValue属性,在准备阶段,虚拟机就会将这个值赋值为123。

解析

解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。
(1)符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义地定位到目标即可,符号引用与内存布局无关,引用的目标并不一定是已经加载到内存中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
(2)直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。直接引用和虚拟机内存布局直接相关,同一个符号引用在不停虚拟机实例上翻译出来的直接引用一般不同,有了直接引用,引用的目标必定已经在虚拟机的内存中存在。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
主要有以下四种:
(1)类或接口的解析
(2)字段解析
(3)类方法解析
(4)接口方法解析

初始化

在准备阶段我们已经为加载到jvm的类分配了内存空间并且为类变量赋予了初始值。而到了初始化阶段,才真正开始执行类中定义的java程序代码。
更直接的表示就是:初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
编译器收集的顺序是按照顺序自上而下运行类中的变量赋值语句和静态语句,并且只有类或接口被Java程序首次主动使用时才初始化他们。如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。

//静态语句块中只能访问到定义在语句块之前的静态变量,定义在之后的变量,只能赋值不能访问。
public class Test {
static {
	i = 0; //给变量赋值可以正确编译通过
	System.out.print(i);  //编译器会提示“非法前向引用”
	}
	static int i = 1;
}

类加载器

JAVA虚拟机把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机的外部的类加载器来完成。这样的好处在于,我们可以自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。系统提供的类加载器分为三种:

1、Bootstrap ClassLoader:启动类加载器,也叫根类加载器,它负责加载Java的核心类库,加载如(%JAVA_HOME%/lib)目录下的rt.jar(包含System、String这样的核心类)这样的核心类库。根类加载器非常特殊,它不是java.lang.ClassLoader的子类,它是JVM自身内部由C/C++实现的,并不是Java实现的(除此之外,其他的类加载器都是由java语言实现,继承自抽象类java,lang.ClassLoader)。

2、Extension ClassLoader:扩展类加载器,它负责加载扩展目录(%JAVA_HOME%/jre/lib/ext)下的jar包,用户可以把自己开发的类打包成jar包放在这个目录下即可扩展核心类以外的新功能。

3、System ClassLoader\APP ClassLoader:应用程序类加载器或称为系统类加载器,是加载CLASSPATH环境变量所指定的jar包与类路径。一般来说,如果没有自定义过自己的类加载器,用户自定义的类就是由APP ClassLoader加载的。

双亲委派模型

jdk 9 之前的Java应用都是由以上三种类加载器互相配合来完成加载的,如果用户认为有必要,还可以加入自定义的类加载器来进行拓展。这些类的协作关系通常会如下图:
双亲委派模型
上图展示的关系被称为“双亲委派模型”,该模型除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器的父子关系一般不是继承关系实现的,而是通常使用组合的关系来复用负载器的代码。

双亲委派模型的工作流程为:如果一个类加载器收到了类加载器的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此。因此所有的加载请求最终都会传送到最顶层的启动类加载器中。只有父类加载反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
双亲委派过程

双亲委派模型的优点:java类随着它的加载器一起具备了一种带有优先级的层次关系。可以避免重复加载,父类已经加载了,子类就不需要再次加载。它更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。

例如类java.lang.Object它存放在rt.jart之中,无论哪一个类加载器都要加载这个类,最终都是双亲委派模型最顶端的启动类加载器去加载。因此Object类在程序的各种类加载器环境中都是同一个类.相反.如果没有使用双亲委派模型.由各个类加载器自行去加载的话.如果用户编写了一个称为“java.lang.Object”的类,并存放在程序的ClassPath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也就无法保证,应用程序也将会一片混乱。.

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值