JVM(三)类加载器

类的加载

类的加载是指将类的.class文件中二进制数据读入到内存中,而后将其放在运行时数据区的方法区内,而后在内存中建立爱你一个java.lang.Class对象java

规范并无说明Class对象应该存放在哪,HotSpot虚拟机将其放在方法区中,用来封装类在方法区内的数据结构mysql

加载.class文件的方式

  • 从本地系统中直接加载
  • 从网络下载.calss文件
  • 从zip,jar等归档文件中加载
  • 从专有数据库中提取
  • 将Java源文件动态编译为.class文件

servlet技术sql

类加载器

类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类的加载过程采用双亲委托机制,这种机制能保证Java平台的安全性.数据库

从源码文档中翻译应该称为父类委托模式bootstrap

类加载器并不须要等到某个类被首次主动使用时再加载它数组

  • JVM规范容许类加载器在预料某个类将要被使用时就预先加载它,若是在预先加载的过程当中遇到了.class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError)
  • 若是一个类一直没有被程序主动使用,那么累加载器就不会报告错误

JVM中的类加载器

根加载器(Bootstrap),

根加载器没有父加载器,主要负责虚拟机的核心类库,如java.lang.*等,java.lang.Object是由根类加载器加载的,根类加载器的实现依赖于底层操做系统,属于虚拟机实现第一部分,它并无继承java.lang.ClassLoader类. 启动类加载器是特定于平台的机器指令,它负责开启整个加载过程 启动类加载器还会负责加载JRE正常运行所需的基本组件.其中包括java.util,java.lang包中的类安全

扩展类加载器(Extension)

扩展类加载器的父加载器是根加载器,从java.ext.dirs系统属性指定的目录中加载类库,或者再jre\lib\ext子目录下加载类库,若是把用户建立的JAR文件放在这个目录下,会自动由扩展类加载器加载,扩展类加载器是纯Java类,是ClassLoader的子类bash

注意一点的是,拓展类加载器加载的是jar包内的class文件网络

系统(应用)类加载器(System/Application)

系统类加载器的父加载器为扩展类加载器,从环境变量classpath或者系统属性java.class.path所制定的目录加载类,它是用户自定义的类加载器的默认父加载器,系统类加载器是纯Java类,是ClassLoader的子类数据结构

用户自定义的类加载器

除了虚拟机自带的加载器外,用户能够定制本身的类加载器.Java提供了抽象类ClassLoader.全部用户自定义的加载器都应该继承ClassLoader

AppClassLoader和ExtClassLoader都是Java类,因此须要类加载器进行加载,而这两个类的类加载器就是bootstrapClassLoader

能够经过修改 System.getProperty(java.system.class.loader)对默认的SystemClassLoader进行修改

类加载器的层级关系

父亲委托机制

在父亲委托机制中,各个加载器按照父子关系造成树形结构,除了根加载器以外,其他的类加载器有且只有一个父加载器.

父亲委托机制

简单描述,就是一个类加载器要加载一个类,并非由自身进行直接加载,而是经过向上寻找父加载器,直到没有父加载器的类加载器,而后再从上至下尝试加载,直至找到一个能够正确加载的类加载器,通常状况下,系统类加载器就能加载普通的类.

并非全部的类加载器都必须遵照双亲委托的机制,具体实现能够根据须要进行改造

代码示例,查看类的加载器

public class Test08 {

    public static void main(String[] args) {
        try {
            Class<?> clzz = Class.forName("java.lang.String");
            //若是返回null,证实是由BootStrap加载器进行加载的
            System.out.println(clzz.getClassLoader());


            Class<?> customClass = Class.forName("com.r09er.jvm.classloader.Custom");
            System.out.println(customClass.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class Custom{

}
复制代码

输出

null
sun.misc.Launcher$AppClassLoader@18b4aac2
复制代码

String的类加载器为null,证实String是由Bootstrap类加载器加载,由于根加载器是由C++实现.因此会返回null.

Custom的类加载器是Launcher$AppClassLoader,这个类是不开源的.可是是默认的系统(应用)类加载器.

classLoader和初始化的时机

经过ClassLoader手动加载类,观察是否会触发类的初始化

public class Test12 {

    public static void main(String[] args) throws Exception {
        ClassLoader loader  = ClassLoader.getSystemClassLoader();
        Class<?> aClass = loader.loadClass("com.r09er.jvm.classloader.TestClassLoader");

        System.out.println(aClass);

        System.out.println("-------");

        aClass = Class.forName("com.r09er.jvm.classloader.TestClassLoader");

        System.out.println(aClass);

    }
}
class TestClassLoader{
    static {
        System.out.println("Test classloader");
    }
}
复制代码

输出

class com.r09er.jvm.classloader.TestClassLoader
-------
Test classloader
class com.r09er.jvm.classloader.TestClassLoader
复制代码

结论

明显能够看出,classLoader.load方法加载类,类并不会初始化,说明不是对类的主动使用,调用了Class.ForName才进行初始化

不一样的类加载器与加载动做分析

打印类加载器,因为根加载器由C++编写,因此就会返回null

public static void main(String[] args) {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        System.out.println(loader);
        //向上遍历父classLoader
        while (null != loader) {
            loader = loader.getParent();
            System.out.println(loader);
        }
    }
复制代码

输出结果

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@610455d6
null

复制代码

获取ClassLoader的途径

  • 经过类对象获取ClassLoader,clazz.getClassLoader()
  • 经过线程获取上限文ClassLoader,Thread.currentThread().getContextLoader()
  • 得到系统(应用)ClassLoader,ClassLoader.getSystemClassLoader()
  • 得到调用者的ClassLoader,DriverManager.getClassLoader()

ClassLoader源码分析

JavaDoc描述

类加载器是负责加载的对象,classLoader是抽象类.赋予类一个二进制名称,一个类加载器应当尝试定位生成数据,这些数据构成类的定义.一种典型的策略是将二进制名称转换为文件名,而后从文件系统中读取该名称的字节码文件

每个对象都包含定义该的classLoader引用(reference)

数组对应的class对象并非由类加载器建立的,而是由java虚拟机在须要时自动建立的.对于一个数组的类加载器,与这个数组元素的类加载器一致.若是数组是原生类型,那这个数组将没有classLoader

String[],则这个数组的类加载器是String的类加载器,使用的是Bootstrap类加载器 int[] ,这种基本类型的数组,是没有类加载器的.

应用实现classLoader的目的是为了拓展JVM动态加载类

ClassLoader使用了委托模型去寻找类的资源.ClassLoader的每个实例都有会一个关联的父ClassLoader,当须要寻找一个类的资源时,ClassLoader实例就会委托给父ClassLoader.虚拟机内建的ClassLoader称为BootstrapClassLoader,BootstrapClassLoader自己是没有父ClassLoader的,可是能够做为其余ClassLoader的父加载器

支持并发加载的类加载器称为并行类加载器,这种类加载器要求在类初始化期间经过ClassLoader.registerAsParallelCapable将自身注册上.默认状况下就是并行的,而子类须要须要并行,则须要调用该方法

在委托机制并非严格层次化的环境下,classLoader须要并行处理,不然类在加载过程当中会致使死锁,由于类加载过程当中是持有锁的

一般状况下,JVM会从本地的文件系统中加载类,这种加载与平台相关.例如在UNIX系统中,jvm会从环境变量中CLASSPATH定义的目录中加载类.

然而有些类并非文件,例如网络,或者由应用构建出来(动态代理),这种状况下,defineClass方法会将字节数组转换为Class实例,能够经过Class.newInstance建立类真正的对象 由类加载器建立的对象的构造方法和方法,可能会引用其余的类,因此JVM会调用loadClass方法加载其余引用的类

二进制名称BinaryNames,做为ClassLoader中方法的String参数提供的任何类名称,都必须是Java语言规范所定义的二进制名称。 例如

  • "java.lang.String",全限定类名
  • "javax.swing.JSpinner$DefaultEditor",内部类
  • "java.security.KeyStoreBuilderFileBuilder$1",匿名内部类
  • "java.net.URLClassLoader$3$1"

自定义类加载器

步骤

  • 1.继承CLassLoader
  • 2.重写loadClass方法
  • 3.在loadClass方法中实现加载class字节码的方法,返回byte[]
  • 4.调用super.defineClass(byte[])方法将Class对象返回给loadClass方法

源码示例

public class Test16 extends ClassLoader {

    private String classLoaderName;

    private String path;

    private final String fileExtension = ".class";


    public Test16(String classLoaderName) {
        //将systemClassLoader做为当前加载器的父加载器
        super();
        this.classLoaderName = classLoaderName;
    }

    public Test16(ClassLoader parent, String classLoaderName) {
        //将自定义的ClassLoader做为当前加载器的父加载器
        super(parent);
        this.classLoaderName = classLoaderName;
    }


    public void setPath(String path) {
        this.path = path;
    }

    public static void main(String[] args) throws Exception {
        Test16 loader1 = new Test16("loader1");
        //设置绝对路径,加载工程根目录下的com.r09er.jvm.classloader.Test01.class
        loader1.setPath("/Users/cicinnus/Documents/sources/jvm-learning/");
        Class<?> aClass = loader1.loadClass("com.r09er.jvm.classloader.Test01");
        //打印加载的类
        System.out.println("loader1 load class" + aClass.hashCode());
        Object instance = aClass.newInstance();
        System.out.println("instance1: " + instance);


        Test16 loader2 = new Test16("loader2");
        //设置绝对路径,加载工程根目录下的Test01.class
        loader2.setPath("/Users/cicinnus/Documents/sources/jvm-learning/");
        Class<?> aClass2 = loader2.loadClass("com.r09er.jvm.classloader.Test01");
        System.out.println("loader2 load class" + aClass2.hashCode());
        Object instance2 = aClass2.newInstance();
        System.out.println("instance2 : " + instance2);

        //todo ****
        //1.从新编译工程,确保默认的classPath目录下有Test01.class的字节码文件,而后运行main方法,观察输出
        //2.删除默认classpath目录下的Test01.class,运行main方法,观察输出

    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("invoke findClass");
        System.out.println("class loader name : " + this.classLoaderName);
        byte[] bytes = this.loadClassData(name);
        return super.defineClass(name, bytes, 0, bytes.length);
    }

    private byte[] loadClassData(String binaryName) {
        byte[] data = null;

        binaryName = binaryName.replace(".", "/");

        try (
                InputStream ins = new FileInputStream(new File(this.path + binaryName + this.fileExtension));
                ByteArrayOutputStream baos = new ByteArrayOutputStream();

        ) {
            int ch;
            while (-1 != (ch = ins.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

}

复制代码

执行两次main方法后,会发现类加载器真正生效的逻辑,由于默认的父加载器实际上是系统加载器(AppClassLoader),因此若是默认的classPath存在字节码文件,则会由AppClassLoader正确加载类,若是classPath中没有,则会向下使用自定义的类加载器加载类

若是构造函数传入两个不同的ClassLoaderName,会发现两个class对象并不一致,是因为命名空间NameSpace的缘由,由于两个类加载器定义的名称是不同的,若是改为相同的名称,则两个class对象一致

重写的是findClass方法,在调用时候,使用的是classLoader的loadClass方法,这个方法内部会调用findClass

还有一个重点,若是将class字节码文件放在根目录,则会抛出NoClassDefFoundError异常,由于binaryName不符合规范.

自定义类加载器加载类流程图

类加载器重要方法详解

findClass

实现本身的类加载器,最重要就是实现findClass,经过传入binaryName,将二进制名称加载成一个Class对象

defineClass

在实现findClass后,须要经过defineClass方法,将二进制数据交给defineClass方法转换成一个Class实例, 在defineClass内部会作一些保护和检验工做.

双亲委派机制解析

经过loadClass方法加载类,会有以下默认加载顺序

  • 1.调用findLoadedClass方法检查class是否被加载
  • 2.调用父加载器的loadClass方法,若是父加载器为null,则会调用JVM内建的类加载器.
  • 3.调用findClass方法找到类

在默认的loadClass方法中,类加载是同步

双亲委派机制优势

  • 1.能够确保Java核心类库的类型安全,若是这个加载过程由Java应用本身的类加载器完成,极可能会在JVM中存在多个版本的同一个类(包名,类名一致),

命名空间发挥的做用

  • 2.能够确保Java核心类库提供的类不会被自定义的类替代

由于优先加载的是类库中的class,会忽略掉自定义的类

  • 3.不一样的类加载器能够为相同名称(binaryName)的类建立额外的命名空间,相同名称的类能够并存在JVM中.

类的卸载

当类被加载,链接,初始化以后,它的生命周期就开始了.当表明类的Class对象再也不被引用,即不可触及时,Class对象就会结束生命周期,类在元空间内的数据也会被卸载,从而结束类的生命周期.

一个类什么时候结束生命周期,取决于表明它的Class对象什么时候结束生命周期

由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载.

用户自定义的类加载器所加载的类是能够被卸载的

类加载器加载的类路径

BootstrapClassLoader加载的路径

  • System.getProperty("sun.boot.class.path")

ExtClassLoader

  • System.getProperty("java.ext.dirs")

AppClassLoader

  • System.getProperty("java.class.path")

三个路径和JDK版本,操做系统都有关系

若是将编译好的class字节码文件放到根加载器的加载路径上,能够成功由BootstrapClassLoader加载

类加载器命名空间

  • 每一个类加载器都有本身的命名空间,命名空间由该加载器及全部父加载器所加载的类组成

即子加载器能访问父加载器加载的类,而父加载器不能访问子加载器加载的类.(相似于继承的概念)

  • 在同一个命名空间中,不会出现类的完整名字相同的两个类

一个Java类是由该类的全限定名称+用于加载该类的定义类加载器(defining loader)共同决定.

ClassLoader.getSystemClassLoader源码

返回用于委托的系统类加载器.是自定义类加载器的父加载器,一般状况下类会被系统类加载器加载. 该方法在程序运很早的时间就会被建立,而且会将系统类加载器设为调用线程的上下文类加载器(context class loader)

Launcher构造主要逻辑

1.初始化ExtClassLoader 2.初始化AppClassLoader,将初始化好的ExtClassLoader设置为AppClassLoader的父加载器 3.将AppClassLoader设置为当前线程的上下文类加载器

SystemClassLoaderAction逻辑

1.判断System.getProperty("java.system.class.loader")是否有设置系统类加载器 2.若是为空,直接返回AppClassLoader 3.若是不为空,经过反射建立classLoader,其中必须提供一个函数签名为ClassLoader的构造 4.将反射建立的自定义类加载器设置为上限为加载器. 5.返回建立好的类加载器

Class.ForName(name,initialize,classloader)解析

  • name,须要构造的类全限定名称(binaryName)

不能用于原生类型或者void类型 若是表示的是数组,则会加载数组中的元素class对象,可是不进行初始化

  • initialize,类是否须要初始化
  • classloader,加载此类的类加载器

线程上下文加载器(ContextClassLoader)实现与分析

CurrentClassLoader(当前类加载器)

  • 每个类都会尝试使用本身的ClassLoader去加载当前类引用的其余类

若是ClassA引用了ClassY,那么ClassA的类加载器会去加载ClassY,前提是ClassY未被加载

线程类加载器从JDK1.2开始引入,Thread类中的getContextClassLoadersetContextClassLoader分别用来获取和设置上下文加载器.若是没有手动进行设置,那么线程会继承其父线程的上下文加载器. java应用运行时的初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的类能够经过这个类加载器加载类与资源

由JDBC引出的问题

回顾一下JDBC操做

Class.forName("com.mysql.driver.Driver");
Connection conn = Driver.connect();
Statement stae = conn.getStatement();
复制代码

Driver,Connection,Statement都是由JDK提供的标准,而实现是由具体的DB厂商提供. 根据类加载的机制,JDK的rt包会被BootstrapClassLoader加载,而自定义的类会被AppClassLoader加载,同时由于命名空间的缘由,父加载器是没法访问子加载器加载的类的.因此父加载器会致使这个问题.

上下文加载器就是为了解决这种问题所存在的

父ClassLaoder可使用当前线程Thread.currentThread().getContextClassLoader()加载的类, 这就改变了父ClassLoader不能使用子ClassLoader或是其余没有直接父子关系的ClassLoader没法访问对方加载的class问题.

即改变了父亲委托模型

线程上下文加载器通常使用

使用步骤(获取 - 使用 - 还原)

  1. Thread.currentThread().getContextClassLoader()
  2. Thread.currentThread().setContextClassLoader(targetClassLoader) doSomentthing(); 3.Thread.currentThread().setContextClassLoader(originClassLoader);

ContextClassLoader的做用就是破坏Java的类加载委托机制

ServiceLoader

ServiceLoader是一个简单的服务提供者加载设施

加载基于JDK规范接口实现的具体实现类 实现类须要提供无参构造,用于反射构造出示例对象

服务提供者将配置文件放到资源目录的META-INF/services目录下,告诉JDK在此目录的文件内配置了须要加载的类,其中文件名称是须要加载的接口全限定名称,文件内容是一个或多个实现的类全限定名称.

总结

在双亲委托模型下,类加载时由下至上的.可是对于SPI机制来讲,有些接口是由Java核心库提供的,根据类加载的机制,JDK的rt包会被BootstrapClassLoader加载,而自定义的类会被AppClassLoader加载.这样传统的双亲委托模型就不能知足SPI的状况,就能够经过线程上下文加载器来实现对于接口实现类的加载.

相关文章
相关标签/搜索