【Java】jvm类加载机制

一、类的生命周期

类从被加载到虚拟机内存当中,到卸载出内存位置,其生命周期包括七个阶段:

加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、运行(Using)、卸载(Unloading),其中验证、准备、解析三个阶段统称为连接(Linking)阶段。

1、加载、验证、准备、初始化和卸载5个阶段是确定的,类加载过程必须按照这5个步骤的先后顺序开始,但是其过程可以穿插,比如在加载过程的后面就开始激活验证阶段,而不用等到上一阶段完全结束才能开始。

2、解析阶段在运行时绑定的过程中可能会放在初始化阶段后面再开始。

 

二、加载(初始化)的时机

什么时候需要把类加载到虚拟机内存当中,虚拟机规范中并没有说明,但是对于初始化阶段则作了规定,而加载、验证、准备则就确定了什么时候需要开始了。

初始化的时机包括对一个类进行主动引用或者被动引用

主动引用规定了下列5种情况:

1、遇到new、getstatic、putstatic、invokestatic这4条字节码时,如果类没有被初始化过,则触发初始化过程。

常见场景:

new:使用new关键字实例化对象

getstatic:读取一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)

putstatic:设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)

invokestatic:调用一个类的静态方法

2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有被初始化过,则触发初始化过程。

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

4、虚拟机启动时,会初始化用户指定的主类(包含main())方法的类。

5、当使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且该方法句柄所对应的类没有初始化过的时候,会触发该类的初始化过程。

被动引用即所有引用类的方式都不会触发初始化:

1、通过子类引用父类的静态字段,不会触发子类初始化。

2、通过数组定义来引用类,不会触发此类的初始化。

3、常量在编译阶段会存放到常量池,本质上并没有直接引用到定义常量的类,而是放到使用该常量的类的常量池中,因此不会触发定义常量的类的初始化。

 

三、类加载的过程

1、加载

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

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

3)在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口(Class对象比较特殊,虽然是对象,但不存放在堆中,而是方法区里面,作为程序访问该对象的外部接口)

Tips:

数组类本身不通过类加载器创建,而是由虚拟机直接创建,但其去掉所有维度信息后的元素类型还是要靠类加载器创建,数组类创建过程遵循以下规则:

1)如果数组组件类型(数组去掉一个维度的类型)是引用类型,则递归采用普通类加载过程去加载这个组件类型,该数组与加载该类组建类型的类加载器的类名称空间上被标识

2)如果数组组建类型不是引用类型,则将该数组与引导类加载器关联

3)数组类的可见性与它的组件类型的可见性一致,组建类类型非引用类型时可见性默认为public

 

2、验证

验证大致分为以下几个步骤:

1)文件格式验证

检查字节流是否符合Class文件格式规范,以及能否被当前版本的虚拟机处理。

2)元数据验证

检查字节码描述的信息是否符合Java语言规范。

3)字节码验证

通过数据流和控制流分析,确定程序语义是否合法、合乎逻辑,同时对方法体进行校验分析,确保类方法不会危害虚拟机安全。

4)符号引用验证

发生在虚拟机将符号引用转化为直接饮用时,通常在解析阶段发生。用以对常量池中的各种符号引用进行匹配性校验。

 

3、准备

正式为类变量(非实例变量)分配内存并设置类变量初始值,存放在方法区中。

 

4、解析

将常量池中的符号引用替换为直接引用,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSATNT_MethodHandle_info、CONSTANT_InvokeDynamic_info这7种常量类型。

1)类或接口解析

(1)非数组类型:将符号引用的全限定名传递给该类的类加载器去加载。

(2)数组类型且数组的元素类型为对象:按第(1)点加载该数组的元素类型,并生成一个代表此数组维度和元素的数组对象。

(3)解析完成前进行符号引用验证,确认符号引用是否具备又对类或接口的访问权限。

2)字段解析

(1)如果字段所属的类包含了简单名称和字段描述符都与目标相匹配的字段,则返回该字段的直接引用。

(2)否则,如果在类中实现了接口,则按继承关系从下往上递归搜索各个接口和其父接口,并按第一步操作。

(3)否则,如果字段所属的类或接口不是java.lang.Objec的话,按继承关系从下往上递归搜索父类,并按第一步操作。

(4)否则,查找失败,抛出java.lang.NoSuchFieldError异常。

(5)查找成功时对该字段进行权限验证,失败则抛出java.lang.IllegalAccessError异常。

3)类方法解析

(1)如果在类方法表中发现索引的类是一个接口的话,直接抛出java.lang.IncompatibleClassChangeError异常。

(2)否则,说明索引的是一个类,则在该类中查找是否有简单名称和描述符都与目标相匹配的方法,有则返回该方法的直接引用。

(3)否则,在索引的类的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,有则返回该方法的直接引用。

(4)否则,在索引的类实现的接口及它们的父接口之中中递归查找是否有简单名称和描述符都与目标相匹配的方法,有则说明该类是一个抽象类,抛出java.lang.AbstarctMethodError异常

(5)否则,方法查找失败,抛出java.lang.NoSuchMethodError异常。

(6)查找成功时对该方法进行权限验证,失败则抛出java.lang.IllegalAccessError异常。

4)接口方法解析

(1)如果在接口方法表中发现索引的类或接口是一个类的话,直接抛出java.lang.IncompatibleClassChangeError异常。

(2)否则,说明索引的是一个接口,则在该接口中查找是否有简单名称和描述符都与目标相匹配的方法,有则返回该方法的直接引用。

(3)否则,在索引的接口的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,知道java.lang.Object类,有则返回该方法的直接引用。

(5)否则,方法查找失败,抛出java.lang.NoSuchMethodError异常。

(6)由于接口所有方法默认为public,所以不存在权限验证问题,不会抛出java.lang.IllegalAccessError异常。

 

5、初始化

前面的过程除了在加载阶段而可以自定义类加载器之外,其余过程都是完全由虚拟机完成,初始化过程才真正开始执行类中定义的Java程序代码。

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

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生,顺序由语句在源代码中的出现顺序决定。
  • <clinit>()方法与类的构造函数不同,不需要显式调用父类构造器,虚拟机会保证子类<clinit>()方法执行之前父类的<clinit>()方法先执行。
  • 父类静态语句块优于子类静态语句块执行。
  • <clinit>()方法对于类或接口非必需,如果没有类变量或静态语句块,则不会生成<clinit>()方法。
  • 接口不能使用静态语句块,但仍有变量初始化的赋值操作,因此也会生成<clinit>()方法,但执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,而是父接口定义的变量使用时才会执行父接口的<clinit>()方法。
  • <clinit>()方法在多线程环境下会上锁、同步,因此<clinit>()方法如果有耗时较长的操作可能会阻塞进程。

 

四、类加载器

类加载器用于实现类的加载动作,对于任意一个类,都需要加载它的类加载器和这个类本身才能确定这个类在虚拟机中的唯一性。如果有两个类是来自同一个Class文件,而由不同的类加载器加载,那么这两个类就一定不相等。

Java虚拟机中存在3种类加载器:

1、启动类加载器(Bootstrap ClassLoader)

负责加载<JAVA_HOME>\lib或被-Xbootclasspath参数所指定的路径中的被虚拟机识别的类库,无法被开发者直接使用,但可以委派给引导类加载器。

2、扩展类加载器(Extension ClassLoader)

负责加载<JAVA_HOME>\lib\ext或被java.ext.dirs系统变量所制定路径的所有类库,开发者可直接使用。

3、应用程序类加载器(Application ClassLoader)

负责加载用户类路径(ClassPath)上所制定的类库,开发者可直接使用(未自定义类加载器时的默认类加载器)。

 

五、双亲委派模型(Parents Delegation Model)

双亲委派模型用来展示类加载器之间的层次关系,要求除了顶层的启动类加载器外,其余的类加载器都要有自己的父类加载器,通常由组合(Composition)关系来实现。

工作过程:

如果一个类加载器收到类加载请求,它先不会尝试加载这个类,而是把这个请求委派给父类去完成,每个层次都是如此,因此所有加载请求最终会传送到启动类加载器中,只有父类加载器无法完成这个加载请求的时候,子加载器才会自己尝试加载。

优点:双亲委派模型对于Java类来说具备了一种带有优先级的层次关系,确保了出现用户编写的与JDK源码里面的类的类名相重合时能够加载到正确的类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值