虚拟机类加载机制

一、概述
1.类加载机制定义:虚拟机把Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

2.在java中类型的加载、连接和初始化过程都是在程序运行期完成,这种策略会令类加载时稍微增加一些性能消耗,但提高了java应用程序的灵活性。java的多态就是依赖运行期动态加载和动态连接实现的。

二、类加载的时机
1.类加载的生命周期:类从被加载到虚拟机内存开始,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段。其中验证、准备、解析3个部分统称为连接。其中加载、验证、准备、初始化和卸载5个阶段的顺序是确定的,类的加载过程必须按照这种顺序开始,而解析阶段则不一定,它在某些情况下可以在初始化之后再开始,这是为了支持java晚绑定机制。

2.加载、验证、准备后,有且只有5种情况必须立即对类进行初始化,这5种行为称对一个类进行主动引用,除此之外的引用类的方式被称为被动引用:
(1)遇到new、getstatic、putstatic、invokestatic4个字节码指令时,如果类没有进行过初始化,则需要触发其初始化。应用场景:使用new实例化对象、读取或者设置一个类的静态字段(被final修饰的static字段除外,因为在编译期已经把这个静态字段放入常量池)、调用一个类的静态方法。
(2)使用java.lang.Reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要触发其初始化。
(3)初始化一个类的时候发现其父类没有进行过初始化,则首先触发其父类的初始化。
(4)当虚拟机启动时,要首先初始化包含main方法的主类。
(5)使用jdk1.7动态语言时,java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要触发其初始化。

/**
 * 被动使用类字段演示一:
 * 通过子类引用父类的静态字段,不会导致子类初始化
 * 对于Sun HotSpot虚拟机来说,可通过-XX:+TraceClassLoading参数观察到此操作会导致子类的加载
 * @author shier
 *
 */
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!");
    }
}

/**
 * 被动使用类字段演示二:
 * 通过数组定义来引用类,不会触发此类的初始化
 * @author shier
 *
 */
public class NotInitialization{
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}
/**
 * 非主动使用类字段演示
 * @author shier
 *
 */
public class NotInitialization{
    public static void main(String[] args) {
        SuperClass[] sea = new SuperClass[10];
    }
}
/**
 * 被动使用类字段演示三:
 * 被final修饰的类常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,
 * 因此不会触发定义常量类的初始化
 * @author shier
 *
 */
public class ConstClass {
    static{
        System.out.println("ConstClass init!");
    }

    public static final String HELLOWORD  = "hello word";
}

/**
 * 非主动使用类字段演示
 * @author shier
 *
 */
public class NotInitialization{
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORD);
    }
}

三、类加载过程
1.加载
(1)类加载阶段,虚拟机要完成3件事情:通过一个类的全限定名来获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表这个类的java.lang.Class对象(Hotspot虚拟机中这个对象存放在方法区里面),作为方法区这个类的各种数据的访问接口。

(2)数组类(C)创建过程:如果数组的组件类型是引用类型,递归采用非数组类加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名空间上被标识;如果数组组件类型不是引用类型,java虚拟机将会把数组C标记为与引导类加载器关联;数组类的可见性和它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性默认为public。

(3)加载阶段和连接阶段部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,这两个阶段的开始时间仍然保持着固定的先后顺序。

2.验证
(1)验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。如果验证到输入的字节流不符合Class文件格式的约束,虚拟机就应抛出一个java.lang.VerfyError异常或其子类异常。

(2)验证阶段大致会完成下面4个阶段校验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。如是否以魔数开头、主、次版本号是否在当前虚拟机处理范围之内、常量池的常量中是否有不被支持的常量类型、只想常量的各种索引值中是否有指向不存在或不符合的常量、CONSTSNT_Utf8_info型的常量中是否有不符合UTF8编码的数据、Class文件中各个部分及文件本身是否有被删除或附加的其他信息。目的是保证输入的字节流能正确的解析并存储在方法区之内,这个阶段是基于二进制字节流进行的,后面的3个阶段全部是基于方法区的存储结构进行的。

元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。如这个类是否有父类、这个类的父类是否继承了不允许被继承的类、如果这个类不是抽象类,是否实现其父类或接口之中要求实现的所有方法、类中的字段,方法是否与父类产生矛盾,目的是对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。

字节码验证:目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会作出危害虚拟机的安全事件。如保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作、保证跳转指令不会跳转到方法体以外的字节码指令上、保证方法体中的类型转换是有效的。由于数据流验证的高复杂性,jdk1.6后给方法体的Code属性的属性表增加了一项名为“StackMapTable”的属性,这样在字节码验证期间,不需要根据程序推导这些状态的合法性,只需要检查StackMapTable属性中记录是否合法即可。

符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段-解析阶段发生,是对类自身以外的信息进行匹配性校验。如符号引用中通过字符串描述的全限定名是否能找到对应的类、在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段、符号引用中的类、字段、方法的访问性是否可被当前类访问。

(3)对于虚拟机类加载机制来说,验证阶段是一个非常重要、但不一定必要的阶段。如果运行的全部代码都已经被反复使用和验证过,那么实施阶段就可以考虑使用-Xverify:none参数来关闭大部分类验证措施,以缩短虚拟机类加载时间。

3.准备
(1)准备阶段是正式为类变量分配内存并设置类变量初始值(通常情况下,类变量的初始值是数据类型的零值,putstatic指令是程序编译后存放在类构造器()方法之中,然后初始化阶段才会调用赋值 public static int value = 123;特殊情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段会直接赋值 public static final int value = 123)的阶段,这些变量所使用的内存都将在方法区中进行分配。

4.解析
(1)解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:以一组符号来描述引用的目标,符号可以是任何形式的字面量,与虚拟机的内存布局无关,引用目标不一定加载到内存中。虚拟机接受的符号引用必须一致。
直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,与虚拟机的内存布局有关,引用的目标肯定在内存中存在,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同。

(2)除invokedynamic指令以外,虚拟机可以对第一次解析的结构进行缓存,虚拟机需要保证同一个实体中,一个符号引用之前被解析成功过,后续的引用解析就应当一直成功;如果解析失败,则其他指令也应该得到相同的异常。对于invokedynamic指令,它的对应引用称为“动态调用点限定符”,必须程序实际运行到这条指令时候,解析动作才能进行。其余触发解析的指令都是“静态的”,可以在刚刚完成加载阶段,还没开始执行代码时进行解析。

(3)解析动作主要针对:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号引用进行

5.初始化
(1)初始化阶段是执行类构造器clinit()方法的过程。
(2)静态语句块中只能访问定义在静态语句之前的变量,定义在它之后的变量可以赋值,但不可访问
(3)父类的clinit()方法一定在子类的()方法之前执行完
(4)父类中定义的静态语句块优先于子类的变量赋值操作
(5)如果一个类中没有静态语句块和类变量,那么就可以不产生clinit()方法
(6)接口中只有当父接口中变量被调用时,才会调用父接口的clinit()方法
(7)虚拟机会保证多线程环境中,只有一个线程执行clinit()方法,其他线程阻塞等待,如果等待时间过长,这样会造成进程阻塞。

四、类加载器
1.Java虚拟机的类加载是通过类加载器实现的, Java中的类加载器体系结构如下:
这里写图片描述

(1).BootStrap ClassLoader:启动类加载器,负责加载存放在%JAVA_HOME%\lib目录中的,或者通被-Xbootclasspath参数所指定的路径中的,并且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)类库到虚拟机的内存中,启动类加载器无法被java程序直接引用。

(2).Extension ClassLoader:扩展类加载器,(由sun.misc.Launcher$ExtClassLoader实现,负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库),开发者可以直接使用扩展类加载器。

(3).Application ClassLoader:应用程序类加载器,(由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径classpath上所指定的类库),是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者可以直接使用应用程序类加载器,如果程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器。

注意:上述三个JDK提供的类加载器虽然是父子类加载器关系,但是没有使用继承,而是使用了组合关系。
从JDK1.2开始,java虚拟机规范推荐开发者使用双亲委派模式(ParentsDelegation Model)进行类加载,其加载过程如下:
(1).如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成。
(2).每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器。
(3).如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。

2.双亲委派 模式的类加载机制的优点是java类它的类加载器一起具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。双亲委派模式的实现:

protected synchronized Class<?> loadClass(String name, Boolean resolve) throws ClassNotFoundException{  
    //首先检查请求的类是否已经被加载过  
    Class c = findLoadedClass(name);  
    if(c == null){  
    try{  
        if(parent != null){//委派父类加载器加载  
    c = parent.loadClass(name, false);  
}  
else{//委派启动类加载器加载  
    c = findBootstrapClassOrNull(name);   
}  
}catch(ClassNotFoundException e){  
    //父类加载器无法完成类加载请求  
}  
if(c == null){//本身类加载器进行类加载  
    c = findClass(name);  
}  
}  
if(resolve){  
    resolveClass(c);  
}  
return c;  
}  

若要实现自定义类加载器,只需要继承java.lang.ClassLoader 类,并且重写其findClass()方法即可。java.lang.ClassLoader 类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class 类的一个实例。除此之外,ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等,ClassLoader 中与加载类相关的方法如下:

这里写图片描述

注意:在JDK1.2之前,类加载尚未引入双亲委派模式,因此实现自定义类加载器时常常重写loadClass方法,提供双亲委派逻辑,从JDK1.2之后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass方法,而推荐重写findClass方法。
在Java中,任意一个类都需要由加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类来源于同一个Class类文件,只要加载它的类加载器不相同,那么这两个类必定不相等(这里的相等包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof关键字的结果)。例子代码如下:

package com.test;  

public class ClassLoaderTest {  
    public static void main(String[] args)throws Exception{  
        //匿名内部类实现自定义类加载器  
    ClassLoader myClassLoader = new ClassLoader(){  
    protected Class<?> findClass(String name)throws ClassNotFoundException{  
        //获取类文件名  
    String filename = name.substring(name.lastIndexOf(“.”) + 1) + “.class”;  
    InputStream in = getClass().getResourceAsStream(filename);  
    if(in == null){  
    throw RuntimeException(“Could not found class file:” + filename);  
}  
byte[] b = new byte[in.available()];  
return defineClass(name, b, 0, b.length);  
}catch(IOException e){  
    throw new ClassNotFoundException(name);  
}  
};  
Object obj = myClassLoader.loadClass(“com.test.ClassLoaderTest”).newInstance();  
System.out.println(obj.getClass());  
System.out.println(obj instanceof com.test. ClassLoaderTest);  
}  
}  

输出结果如下:
com.test.ClassLoaderTest
false
之所以instanceof会返回false,是因为com.test.ClassLoaderTest类默认使用Application ClassLoader加载,而obj是通过自定义类加载器加载的,类加载不相同,因此不相等

3.类加载器双亲委派模型是从JDK1.2以后引入的,并且只是一种推荐的模型,不是强制要求的,因此有一些没有遵循双亲委派模型的特例:
(1).在JDK1.2之前,自定义类加载器都要覆盖loadClass方法去实现加载类的功能,JDK1.2引入双亲委派模型之后,loadClass方法用于委派父类加载器进行类加载,只有父类加载器无法完成类加载请求时才调用自己的findClass方法进行类加载,因此在JDK1.2之前的类加载的loadClass方法没有遵循双亲委派模型,因此在JDK1.2之后,自定义类加载器不推荐覆盖loadClass方法,而只需要覆盖findClass方法即可。
(2).双亲委派模式很好地解决了各个类加载器的基础类统一问题,越基础的类由越上层的类加载器进行加载,但是这个基础类统一有一个不足,当基础类想要调用回下层的用户代码时无法委派子类加载器进行类加载。为了解决这个问题JDK引入了ThreadContext线程上下文,通过线程上下文的setContextClassLoader方法可以设置线程上下文类加载器。
JavaEE只是一个规范,sun公司只给出了接口规范,具体的实现由各个厂商进行实现,因此JNDI,JDBC,JAXB等这些第三方的实现库就可以被JDK的类库所调用。线程上下文类加载器也没有遵循双亲委派模型。
(3).近年来的热码替换,模块热部署等应用要求不用重启java虚拟机就可以实现代码模块的即插即用,催生了OSGi技术,在OSGi中类加载器体系被发展为网状结构。OSGi也没有完全遵循双亲委派模型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值