虚拟机类加载机制(笔记)

 
 
 
 
                       连接<linking>
类的生命周期:加载->(验证->准备->解析)->初始化->使用->卸载  
 这个顺序是指开始的时刻点,激活后交叉混合进行
加载、验证、准备、初始化顺序固定    解析可在初始化前,也可在后(为了支持动态绑定或晚期绑定) 


有且只有以下5种情况必须立刻初始化类,当然会先执行加载。。。

1.遇到new getstatic putstatic invokestatic这4个字节码指令时,类未进行过初始化,则必须立刻

2.使用java.lang.reflect包的方法对类进行反射调用的时候

3.初始化一个类的时候,其父类未初始化,则需要先触发父类初始化

4.虚拟机启动的时候,需要指定一个主类,该类立刻初始化

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

加载:1.通过一个类的全限定名来获取定义此类的二进制字节流

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

         3.在内存中生成一个Class对象,作为方法区这个类的各种数据的访问入口

非数组类可控性最强,可自定义重写loadClass()

数组类:不通过类加载器创建,由Java虚拟机直接创建,但是数组的元素类型(数组去掉所有维度的类型)

最终是通过类加载器创建,创建过程规则如下

        1.如果数组的组件类型(数组去掉一个维度的类型)是引用类型,那么就递归采用定义

的加载过程去加载这个组件类型,数组C将加载该类组件类型的类加载器的类名称空间上被标识

(一个类必须与引导类加载器关联)

        2.如果数组的组件类型不是引用类型,Java虚拟机将会把数组C标记为与引导类加载器关联

        3.数组类的可见性与其他组件类型的可见性一致,若组件类型不是引用类型则默认为public

 
 

验证:确保Class文件字节流包含信息符合当前虚拟机的要求,并且不会危害jvm

   1.文件格式验证

       是否以魔数开头,主次版本号是否符合jvm要求,常量池中常量是否有不被支持的类型(检查tag标志),

指向常量的索引中是否存在是否有不符合类型的常量,CONSTANT_Utf8_info中是否有不符合utf-8编码的数据,

Class文件中各个部分是否有被删除或者附加的其他信息、、、

    2.元数据验证

        是否有父类、是否继承了不允许继承的类、若不是抽象类是否实现了父类或者接口种的抽象方法、

类中字段方法是否与父类产生了矛盾、、、、、

    3.字节码验证

        通过数据流和控制流分析确定程序语义是合法、符合逻辑,保证被校验类不会危害jvm。

    4.符号引用验证(在解析阶段中发生) 对类自身以外的信息进行匹配校验,确保解析动作能够正常完成

                 符号引用中通过字符串描述的全限定名是否能够找到对应类

                指定类中是否存在符合方法字段描述符以及简单名称所描述的方法和字段

                符号引用中类、字段、方法的访问性是否可以被当前类访问

                ................

准备    正式为类变量分配内存并设置初始值的阶段 在方法区中分配(static int i=123 这里初始化

为0,123在初始化阶段执行)

            当被final修饰的时候,初始化为123在这里执行

 

解析    jvm将常量池内符号引用替换为直接引用的过程,符号引用即一个表(包含名字,引用)

            符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要无歧义定位到

目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到内存中。

符号引用的字面量形式明确定义在Class文件中与jvm实现的内存布局无关。

        直接引用:可以是直接指向目标的指针、相对偏移量或间接定位的句柄。直接引用和虚拟机实现的内存布局相关,

不同虚拟机翻译出来的一般不同,有直接引用,引用的目标在内存中必然存在。

解析动作主要针对:类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符 分别对应常量池的7种表

        1.类或接口的解析

                假设当前代码所处类为D,若把从未解析的符号引用N解析为一个类或接口C的直接引用有如下操作步骤:

                    ①.若C不是数组类型,jvm把代表N的全限定名传递给D的类加载器去加载类C,在这个过程中,由于

元数据、字节   码验证的需要,可能触发其他相关类的加载动作,例如父类或者实现的接口,

一旦出现异常,就失败。

                    ②.若C是数组类型,并且数组的元素类型为对象,即类似“[Ljava/lang/Integer”形式,则按第一

点的规则加载数   组元素类型,接着jvm生成一个代表此数组维度和数组元素的数组对象

                    ③.如果以上步骤未出现异常,那么C已在jvm中成为一个有效的类或接口,但是解析完成以后还要

进行符号引用验   证,确认D是否对C有访问权限,不具备会抛出异常。

        2.字段的解析

                要解析一个未被解析的字段符号的引用,会先对字段表内class_index项中索引的

CONSTANT_Class_info符号引用进行解析,即字段所属类或者接口的符号的引用,若解析失败,

会导致字段符号解析失败,如果解析完成,这个字段所属类或者接口用C表示,jvm要求对C进行后续字段的搜索

            ①如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。

            ②否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,

如果接口中包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,查找结束。

            ③否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索类。。。。。。

            ④否则查找失败抛出异常   

            若成功,则会对该字段进行权限验证,若不具备对字段的访问权限也会抛出异常

     3.类方法解析

            第一步与字段解析相同会先对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,

即字段所属类或者接口的符号的引用,若解析失败,会导致字段符号解析失败,如果解析完成,这个字段所属

类或者接口用C表示,jvm要求对C进行后续字段的搜索

            ①类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法class_index中索引的C是一个接口,

抛出异常

            ②如果通过了第一步,在类C中查找与其相同的简单名称与描述符相同的方法,返回直接引用,结束

            ③否则,在类C的父类中。。。。。。

           ④否则在C的接口以及父接口中。。。。存在匹配,则C是一个抽象类,查找结束,抛出异常。

            ⑤否则查找失败

            若成功返回了直接引用,将会进行权限验证

        4.接口方法的解析

            同样解析出接口方法表的class_index项索引的方法所属的类或者接口的符号引用,解析成功C表示,有如下步

        骤

            ①如果接口方法中索引C是一个类而不是接口,则抛出异常

            ②否则在接口C中查找。。。。。。找到返回直接引用,结束

            ③否则,在父接口中。。。。。直到object类为止,找到返回引用,结束。

            ④否则失败,抛出异常。


 
 

初始化

     类初始化阶段是类加载的最后一个阶段,前面的类加载过程中,除了加载阶段用户可以通过自定义类加载器参与外

其余动作都由jvm解决,到了初始化阶段,才真正开始执行类中定义的Java程序代码。

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

    该方法由编译器自动收集类中所以类变量的赋值动作和静态语句块中语句合并而成,按java文件顺序,其中静态语

句块中只能访问定义在该块之前的变量,在后面的只能赋值,不能访问。

public class Test{

static{

i=10;

sout(i); //报错

}

static int i = 1;

}

    <clinit>()方法不需要显式调用父类构造器,jvm会保证在子类的<clinit>()方法执行之前,父类的<clinit>()

会执行完毕,因此第一个被执行的<clinit>()方法肯定是Object类的。

    父类<clinit>()优先于子类的执行,因此父类中定义的静态语句块优先于子类的变量赋值操作执行。

    <clinit>()对接口或者类非必须,当没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成该

方法。

    接口中不能使用静态语句块,执行接口的<clinit>()方法不用先执行父接口的该方法,只有父接口中定义的变量使用

的时候才会初始化,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

    jvm会保证一个类的<clinit>()方法在多线程中只被执行一次

Ps:同一个类加载器下,一个类型只会被初始化一次

 类加载器
 
 

    通过一个类的全限定名来获取描述此类的二进制节流

    同一个class文件,通过不同的两个类加载器加载,这俩个类必然不相等(equals,isAssignableForm,

isInstance.instenseof)!!!

    双亲委派模型

    在jvm的角度而言,只存在两种类加载器:1.启动类加载器(Bootstrap ClassLoader)这个类加载器由C艹实现是

jvm的一部分,2.所有其他的类加载器,由java语言实现,独立于jvm外部全都继承自抽象类java.lang.ClassLoader。

    在程序员的角度而言可以分为三种:

    1.启动类加载器(Bootstrap ClassLoader),负责将放在<JAVA_HOME>\lib目录中,或者被-Xbootclasspath参数

指定的路径中的,并且是jvm识别的类库加载到虚拟机内存中,启动类加载器无法被Java程序直接使用,用户在编写自定

类加载器的时候,如果需要把加载请求委派给引导类加载器,那么直接使用null代替。

    2.扩展类加载器(Extension ClassLoader),由sun.misc.Lancher$ExtClassLoader实现,负责

加载<JAVA_HOME>\ext目录中的或者被java.ext.dirs系统变量指定的目录的所有类库,开发者可以直接使用。

    3.应用程序类加载器(Application ClassLoader),sun.misc.Lancher$AppClassLoader实现。由于这个类加载器

是ClassLoader中getSystemClassLoader()方法的返回值,一般也称为系统类加载器,负责加载用户类路径

(ClassPath)上所指定的类库,开发者直接使用,若没有自定义类加载器,这个就是默认的类加载器。

    双亲委派模型除了启动类加载器外,其余的都应该有父类,父子关系一般用组合在实现。

工作流程为:一个类加载器接收到类加载请求,先检测是否被加载过,首先给父类加载器loadClass(),若父类加载器

为空则默认使用启动类加载器作为父类加载器,失败则抛出异常,并且再调用自己的findClass()进行加载。

    JSR-291(OSGi R4.2)

    在OSGi环境下类加载顺序为

    1.将以java.*开头的类委派给父类加载器

    2.否则,将委派列表名单内的类给父类加载器

    3.否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载

    4.否则,查找当前Bundle的ClassPath,使用自己的类加载器

    5.否则,查找类是否在自己的Fragment Bundle中,若在,交给他的类加载器

    6.否则,查找Dynamic Import列表的Bundle,委派给对应Bundle类的类加载器

    7.否则失败。

    






猜你喜欢

转载自blog.csdn.net/qmylzx/article/details/80995173