转载

JVM(三)类加载器

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

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

加载.class文件的方式

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

servlet技术

类加载器

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

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

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

程序首次主动

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的子类

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

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

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

用户自定义的类加载器

除了虚拟机自带的加载器外,用户可以定制自己的类加载器.Java提供了抽象类ClassLoader.所有用户自定义的加载器都应该继承 ClassLoader

AppClassLoader和ExtClassLoader都是Java类,所以需要类加载器进行加载,而这两个类的类加载器就是bootstrapClassLoader

可以通过修改 System.getProperty(java.system.class.loader)对默认的SystemClassLoader进行修改

JVM(三)类加载器

父亲委托机制

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

JVM(三)类加载器

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

并不是所有的类加载器都必须遵守双亲委托的机制,具体实现可以根据需要进行改造

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

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的途径

clazz.getClassLoader()
Thread.currentThread().getContextLoader()
ClassLoader.getSystemClassLoader()
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.KeyStore FileBuilder$1",匿名内部类
  • "java.net.URLClassLoader$3$1"

自定义类加载器

步骤

loadClass
loadClass
super.defineClass(byte[])

源码示例

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 不符合规范.

JVM(三)类加载器

类加载器重要方法详解

findClass

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

defineClass

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

双亲委派机制解析

通过 loadClass 方法加载类,会有如下默认加载顺序

findLoadedClass
loadClass
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 的情况,就可以通过线程上下文加载器来实现对于接口实现类的加载.

原文  https://juejin.im/post/5e881f3851882573c32a1339
正文到此结束
Loading...