JVM(三):类加载机制(类加载过程和类加载器)

1、为何要使用类加载器?
Java语言里,类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增长一些性能开销,可是会给java应用程序提供高度的灵活性。例如:
1.编写一个面向接口的应用程序,可能等到运行时再指定其实现的子类;
2.用户能够自定义一个类加载器,让程序在运行时从网络或其余地方加载一个二进制流做为程序代码的一部分;(这个是Android插件化,动态安装更新apk的基础)java

 

2、类加载的过程android

使用java编译器能够把java代码编译为存储字节码的Class文件,使用其余语言的编译器同样能够把程序代码翻译成Class文件,java虚拟机不关心Class的来源是何种语言。如图所示:程序员

在Class文件中描述的各类信息,最终都须要加载到虚拟机中才能运行和使用。那么虚拟机是如何加载这些Class文件的呢?
JVM把描述类数据的字节码.Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。面试

 

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称连接。数组


加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不必定;它在某些状况下能够在初始化以后再开始,这是为了运行时动态绑定特性(JIT例如接口只在调用的时候才知道具体实现的是哪一个子类)。值得注意的是:这些阶段一般都是互相交叉的混合式进行的,一般会在一个阶段执行的过程当中调用或激活另一个阶段。安全

 

1.加载:(重点)
加载阶段是“类加载机制”中的一个阶段,这个阶段一般也被称做“装载”,主要完成:
1.经过“类全名”来获取定义此类的二进制字节流网络

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

3.在java堆中生成一个表明这个类的java.lang.Class对象,做为方法区这些数据的访问入口多线程

相对于类加载过程的其余阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动做)是开发期可控性最强的阶段,由于加载阶段可使用系统提供的类加载器(ClassLoader)来完成,也能够由用户自定义的类加载器完成,开发人员能够经过定义本身的类加载器去控制字节流的获取方式。jvm

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。而后在java堆中实例化一个java.lang.Class类的对象,这个对象做为程序访问方法区中的这些类型数据的外部接口。

 

2.验证:(了解)

验证是连接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身安全。
验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

1.文件格式验证

 验证class文件格式规范,例如: class文件是否已魔术0xCAFEBABE开头 , 主、次版本号是否在当前虚拟机处理范围以内等

2.元数据验证

这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求。验证点可能包括:这个类是否有父类(除了java.lang.Object以外,全部的类都应当有父类)、这个类是否继承了不容许被继承的类(被final修饰的)、若是这个类的父类是抽象类,是否实现了起父类或接口中要求实现的全部方法。

3.字节码验证

 进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,这个阶段的任务是保证被校验类的方法在运行时不会作出危害虚拟机安全的行为。如:保证访法体中的类型转换有效,例如能够把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型、保证跳转命令不会跳转到方法体之外的字节码命令上。

4.符号引用验证

符号引用中经过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。

3.准备:(了解)

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一块儿分配在java堆中。其次是这里所说的初始值“一般状况”下是数据类型的零值,假设一个类变量定义为:

public static int value  = 12;

那么变量value在准备阶段事后的初始值为0而不是12,由于这时候还没有开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,因此把value赋值为12的动做将在初始化阶段才会被执行。

上面所说的“一般状况”下初始值是零值,那相对于一些特殊的状况,若是类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,建设上面类变量value定义为:

public static final int value = 123;

编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。

 

4.解析:(了解)
解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用是一组符号来描述所引用的目标对象,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不必定已经加载到内存中。

直接引用:直接引用能够是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不一样虚拟机实例上翻译出来的直接引用通常不会相同,若是有了直接引用,那引用的目标一定已经在内存中存在。

虚拟机规范并无规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操做符号引用的字节码指令以前,先对它们使用的符号引用进行解析,因此虚拟机实现会根据须要来判断,究竟是在类被加载器加载时就对常量池中的符号引用进行解析,仍是等到一个符号引用将要被使用前才去解析它。

解析的动做主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。

1.类、接口的解析

2.字段解析

3.类方法解析

4.接口方法解析

 

5.初始化:(了解)

类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员经过程序制定的主观计划去初始化类变量和其余资源,或者能够从另一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。在如下四种状况下初始化过程会被触发执行:

1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,若是类没有进行过初始化,则需先触发其初始化。生成这4条指令的最多见的java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。

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

3.当初始化一个类的时候,若是发现其父类尚未进行过初始化、则须要先出发其父类的初始化

4.jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类

在上面准备阶段 public static int value  = 12;  在准备阶段完成后 value的值为0,而在初始化阶调用了类构造器<clinit>()方法,这个阶段完成后value的值为12。

*类构造器<clinit>()方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在前面的静态语句快能够赋值,可是不能访问。

*类构造器<clinit>()方法与类的构造函数(实例构造函数<init>()方法)不一样,它不须要显式调用父类构造,虚拟机会保证在子类<clinit>()方法执行以前,父类的<clinit>()方法已经执行完毕。所以在虚拟机中的第一个执行的<clinit>()方法的类确定是java.lang.Object。

*因为父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句快要优先于子类的变量赋值操做。

*<clinit>()方法对于类或接口来讲并非必须的,若是一个类中没有静态语句,也没有变量赋值的操做,那么编译器能够不为这个类生成<clinit>()方法。

*接口中不能使用静态语句块,但接口与类不太可以的是,执行接口的<clinit>()方法不须要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()方法。

*虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步,若是多个线程同时去初始化一个类,那么只会有一个线程执行这个类的<clinit>()方法,其余线程都须要阻塞等待,直到活动线程执行<clinit>()方法完毕。若是一个类的<clinit>()方法中有耗时很长的操做,那就可能形成多个进程阻塞。

 

 

3、类加载器

JVM设计者把类加载阶段中的“经过'类全名'来获取定义此类的二进制字节流”这个动做放到Java虚拟机外部去实现,以便让应用程序本身决定如何去获取所须要的类。实现这个动做的代码模块称为“类加载器”。

 

1.类与类加载器

对于任何一个类,都须要由加载它的类加载器和这个类来确立其在JVM中的惟一性。也就是说,两个类来源于同一个Class文件,而且被同一个类加载器加载,这两个类才相等。

2.双亲委派模型

从虚拟机的角度来讲,只存在两种不一样的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另一种就是全部其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,而且所有继承自抽象类java.lang.ClassLoader。

 

从Java开发人员的角度来看,大部分Java程序通常会使用到如下三种系统提供的类加载器:
1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中而且能被虚拟机识别的类库到JVM内存中,若是名称不符合的类库即便放在lib目录中也不会被加载。该类加载器没法被Java程序直接引用。
2)扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\,该加载器能够被开发者直接使用。
3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者能够直接使用该类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。

咱们的应用程序都是由这三类加载器互相配合进行加载的,咱们也能够加入本身定义的类加载器。这些类加载器之间的关系以下图所示:

如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其他的类加载器都应当有本身的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是经过组合(Composition)关系来复用父加载器的代码。


双亲委派模型的工做过程为:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的加载器都是如此,所以全部的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈本身没法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试本身去加载。


使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系。例如java.lang.Object类,不管哪一个类加载器去加载该类,最终都是由启动类加载器进行加载,所以Object类在程序的各类类加载器环境中都是同一个类。不然的话,若是不使用该模型的话,若是用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。若是咱们自定义一个rt.jar中已有类的同名Java类,会发现JVM能够正常编译,但该类永远没法被加载运行。
在rt.jar包中的java.lang.ClassLoader类中,咱们能够查看类加载实现过程的代码,具体源码以下:

protected synchronized Class loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // 首先检查该name指定的class是否有被加载
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    // 若是parent不为null,则调用parent的loadClass进行加载
                    c = parent.loadClass(name, false);
                } else {
                    // parent为null,则调用BootstrapClassLoader进行加载
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
                // 若是仍然没法加载成功,则调用自身的findClass进行加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

经过上面代码能够看出,双亲委派模型是经过loadClass()方法来实现的,根据代码以及代码中的注释能够很清楚地了解整个过程其实很是简单:先检查是否已经被加载过,若是没有则调用父加载器的loadClass()方法,若是父加载器为空则默认使用启动类加载器做为父加载器。若是父类加载器加载失败,则先抛出ClassNotFoundException,而后再调用本身的findClass()方法进行加载。

 

3.自定义类加载器

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

 
方法                                 说明
getParent()  返回该类加载器的父类加载器。

loadClass(String name) 加载名称为 二进制名称为name 的类,返回的结果是 java.lang.Class 类的实例。

findClass(String name) 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。

findLoadedClass(String name) 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。

resolveClass(Class<?> c) 连接指定的 Java 类。


注意:在JDK1.2以前,类加载还没有引入双亲委派模式,所以实现自定义类加载器时经常重写loadClass方法,提供双亲委派逻辑,从JDK1.2以后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不须要在本身写双亲委派的逻辑,所以不鼓励重写loadClass方法,而推荐重写findClass方法。

在Java中,任意一个类都须要由加载它的类加载器和这个类自己一同肯定其在java虚拟机中的惟一性,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,不然,即便这两个类来源于同一个Class类文件,只要加载它的类加载器不相同,那么这两个类一定不相等(这里的相等包括表明类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof关键字的结果)。例子代码以下:

/**
	 * 1、ClassLoader加载类的顺序
	 *  1.调用 findLoadedClass(String) 来检查是否已经加载类。
	 *  2.在父类加载器上调用 loadClass 方法。若是父类加载器为 null,则使用虚拟机的内置类加载器。
	 *  3.调用 findClass(String) 方法查找类。
	 * 2、实现本身的类加载器
	 *  1.获取类的class文件的字节数组
	 *  2.将字节数组转换为Class类的实例
	 * @author lei 2011-9-1
	 */
	public class ClassLoaderTest {
	    public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
	        //新建一个类加载器
	        MyClassLoader cl = new MyClassLoader("myClassLoader");
	        //加载类,获得Class对象
	        Class<?> clazz = cl.loadClass("classloader.Animal");
	        //获得类的实例
	        Animal animal=(Animal) clazz.newInstance();
	        animal.say();
	    }
	}
	class Animal{
	    public void say(){
	        System.out.println("hello world!");
	    }
	}
	class MyClassLoader extends ClassLoader {
	    //类加载器的名称
	    private String name;
	    //类存放的路径
	    private String path = "E:\\workspace\\Algorithm\\src";
	    MyClassLoader(String name) {
	        this.name = name;
	    }
	    MyClassLoader(ClassLoader parent, String name) {
	        super(parent);
	        this.name = name;
	    }
	    /**
	     * 重写findClass方法
	     */
	    @Override
	    public Class<?> findClass(String name) {
	        byte[] data = loadClassData(name);
	        return this.defineClass(name, data, 0, data.length);
	    }
	    public byte[] loadClassData(String name) {
	        try {
	            name = name.replace(".", "//");
	            FileInputStream is = new FileInputStream(new File(path + name + ".class"));
	            ByteArrayOutputStream baos = new ByteArrayOutputStream();
	            int b = 0;
	            while ((b = is.read()) != -1) {
	                baos.write(b);
	            }
	            return baos.toByteArray();
	        } catch (Exception e) {
	            e.printStackTrace();
	        }
	        return null;
	    }
	}

 

类加载器双亲委派模型是从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也没有彻底遵循双亲委派模型。

4.动态加载Jar && ClassLoader 隔离问题

动态加载Jar:

Java 中动态加载 Jar 比较简单,以下:

URL[] urls = new URL[] {new URL("file:libs/jar1.jar")};
URLClassLoader loader = new URLClassLoader(urls, parentLoader);

表示加载 libs 下面的 jar1.jar,其中 parentLoader 就是上面1中的 parent,能够为当前的 ClassLoader。


ClassLoader 隔离问题:

你们以为一个运行程序中有没有可能同时存在两个包名和类名彻底一致的类?
JVM 及 Dalvik 对类惟一的识别是 ClassLoader id + PackageName + ClassName,因此一个运行程序中是有可能存在两个包名和类名彻底一致的类的。而且若是这两个”类”不是由一个 ClassLoader 加载,是没法将一个类的示例强转为另一个类的,这就是 ClassLoader 隔离。 如 Android 中碰到以下异常

android.support.v4.view.ViewPager can not be cast to android.support.v4.view.ViewPager

当碰到这种问题时能够经过 instance.getClass().getClassLoader(); 获得 ClassLoader,看 ClassLoader 是否同样。

 

加载不一样 Jar 包中公共类:

如今 Host 工程包含了 common.jar, jar1.jar, jar2.jar,而且 jar1.jar 和 jar2.jar 都包含了 common.jar,咱们经过 ClassLoader 将 jar1, jar2 动态加载进来,这样在 Host 中实际是存在三份 common.jar,以下图:

https://farm4.staticflickr.com/3872/14301963930_2f0f0fe8aa_o.png

咱们怎么保证 common.jar 只有一份而不会形成上面3中提到的 ClassLoader 隔离的问题呢,其实很简单,在生成 jar1 和 jar2 时把 common.jar 去掉,只保留 host 中一份,以 host ClassLoader 为 parentClassLoader 便可。

 

最后:

一道面试题

能不能本身写个类叫java.lang.System?

答案:一般不能够,但能够采起另类方法达到这个需求。
解释:为了避免让咱们写System类,类加载采用委托机制,这样能够保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算本身重写,也老是使用Java系统提供的System,本身写的System类根本没有机会获得加载。

可是,咱们能够本身定义一个类加载器来达到这个目的,为了不双亲委托机制,这个类加载器也必须是特殊的。因为系统自带的三个类加载器都加载特定目录下的类,若是咱们本身的类加载器放在一个特殊的目录,那么系统的加载器就没法加载,也就是最终仍是由咱们本身的加载器加载。