Java-类加载器

目录

  • 1 类加载
  • 2 类加载过程
    • 2.1 类的初始化与主动使用和被动使用
    • new一个对象过程中发生了什么?
  • 3 类加载器
  • 4 JVM预定义的三种类加载器
    • 4.1 启动类加载器(引导类加载器,Bootstrap ClassLoader)
    • 4.2 扩展类加载器(Extension ClassLoader)
    • 4.3 应用程序类加载器(系统类加载器,AppClassLoader)
    • 4.4 用户自定义类加载器
    • 4.5 类加载器间的关系
    • 4.6 类的唯一性
    • 4.7 Launcher类 介绍
  • 5 特殊 线程上下文类加载器 (ThreadContextClassLoader)
  • 代码示例:通过一个类获取他们的类加载器
  • 代码示例:自定义一个类加载器
    • 继承ClassLoader
    • 继承URLClassLoader

1 类加载

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程就是类加载。

  • 类加载指的是将类 Class 文件读入内存,并为之创建一个 java.lang.Class 对象, class 文件被载入到了内存之后,才能被其它 class 所引用
  • jvm 启动的时候,并不会一次性加载所有的 class 文件,而是根据需要去动态加载
  • java 类加载器是 jre 的一部分,负责动态加载 java 类到 java 虚拟机的内存
  • 类的唯一性由类加载器和类共同决定

Java-类加载器_第1张图片

虚拟机规范并没有指明二进制字节流要从一个Class文件获取,或者说根本没有指明从哪里获取、怎样获取。这种开放使得Java在很多领域得到充分运用,例如:

  • 从ZIP包中读取,这很常见,成为JAR,EAR,WAR格式的基础
  • 从网络中获取,最典型的应用就是Applet
  • 运行时计算生成,最典型的是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
  • 有其他文件生成,最典型的JSP应用,由JSP文件生成对应的Class类
    ……

2 类加载过程

Java-类加载器_第2张图片

  • 加载:类加载过程的一个阶段,通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

  • 验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

  • 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

  • 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考周志明老师的《深入了解Java虚拟机》)。

  • 初始化:类加载最后阶段,若该类具有超类,则先对其超类进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

2.1 类的初始化与主动使用和被动使用

Java程序对类的使用方式分为:主动使用和被动使用。 主动使用:

  • 创建类的实例
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(比如:Class.forName(“com.test.HelloWorld”))
  • 初始化一个类的子类
  • Java虚拟机启动时被标明为启动类的类
  • JDK7开始提供的动态语言支持:
  • java.lang.invoke.MethodHandle实例的解析结果REF getStatic、REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化

除了以上情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化

new一个对象过程中发生了什么?

  1. 确认类元信息是否存在。当 JVM 接收到 new 指令时,首先在 metaspace 内检查需要创建的类元信息是否存在。 若不存在,那么在双亲委派模式下,使用当前类加载器以 ClassLoader + 包名+类名为 Key 进行查找对应的 class 文件。 如果没有找到文件,则抛出 ClassNotFoundException 异常 , 如果找到,则进行类加载(加载 - 验证 - 准备 - 解析 - 初始化),并生成对应的 Class 类对象。
  2. 分配对象内存。 首先计算对象占用空间大小,如果实例成员变量是引用变量,仅分配引用变量空间即可,即 4 个字节大小,接着在堆中划分—块内存给新对象。 在分配内存空间时,需要进行同步操作,比如采用 CAS (Compare And Swap) 失败重试、 区域加锁等方式保证分配操作的原子性。
    设定默认值。 成员变量值都需要设定为默认值, 即各种不同形式的零值。
  3. 设置对象头。设置新对象的哈希码、 GC 信息、锁信息、对象所属的类元信息等。这个过程的具体设置方式取决于 JVM 实现。
  4. 执行 init 方法。 初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

3 类加载器

在类加载的第一阶段“加载”过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器。

将 class 文件二进制数据放入方法区内,然后在堆内(heap)创建一个 java.lang.Class 对象,Class 对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内的数据结构的接口。

4 JVM预定义的三种类加载器

Java-类加载器_第3张图片

4.1 启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 使用C++语言实现的,嵌套在JVM内部,无法直接通过代码获取。
  • 不继承自java.lang.ClassLoader,没有父加载器。
  • 加载扩展类和应用程序类加载器,是他们的父类加载器。
  • 它用来加载Java的核心类库。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。

4.2 扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类,父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载。

4.3 应用程序类加载器(系统类加载器,AppClassLoader)

  • java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 派生于ClassLoader类,父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
  • 通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器

4.4 用户自定义类加载器

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。

为什么要自定义类加载器?

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄漏
  • 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。
  • 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。
  • 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。

自定义类加载器分为两步:

  1. 继承 java.lang.ClassLoader
  2. 重写父类的 findClass()方法

针对第 1 步,为什么要继承 ClassLoader 这个抽象类,而不继承 AppClassLoader 呢?因为它和 ExtClassLoader 都是 Launcher 的静态内部类,其访问权限是缺省的包访问权限。static class AppClassLoader extends URLClassLoader{...}

第 2 步,JDK 的 loadCalss() 方法在所有父类加载器无法加载的时候,会调用本身的 findClass()方法来进行类加载,因此我们只需重写 findClass() 方法找到类的二进制数据即可。

Java-类加载器_第4张图片

4.5 类加载器间的关系

这里的四者之间是包含关系,不是上层和下层,也不是继承关系 。

  • 启动类加载器(Bootstrap ClassLoader),由C++实现,没有父类加载器。

  • 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为Bootstrap ClassLoader

  • 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

  • 自定义类加载器,父类加载器为AppClassLoader。

4.6 类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。

也就是说,在JVM中表示两个class对象是否为同一个类存在两个必要条件:

  • 类的全限定名称一样
  • 加载这个类的类加载器必须相同。

即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。

这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof关键字做对象所属关系判定等情况。

4.7 Launcher类 介绍

sun.misc.Launcher类是java的入口,在启动java应用的时候会首先创建Launcher类,创建Launcher类的时候,会创建应用程序运行中所需要的类加载器。

Launcher的构造器的源码

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
	...
    }

Launcher在创建的时候,创建了ExtClassLoader,然后用ExtClassLoader创建了 AppClassLoader。

5 特殊 线程上下文类加载器 (ThreadContextClassLoader)

线程上下文类加载器 是从 JDK 1.2 开始引入的。我们知道JVM虚拟机采用双亲委派模式来加载类,而且在类加载的整个过程中只有在加载阶段可以让程序员操作,加载器通过类的全限定名在class文件的二进制流中加载类,并创建类的唯一一个class对象,作为类的全局访问点。我们知道为了实现程序的动态性,我们可以自定义类加载器,通过重写findClas()方法来实现自定义类加载器,再通过重写loadClass()方法打破双亲委派机制,这是一次打破双亲委派,那么还有一种就是使用线程上下文类加载器来打破双亲委派机制。

为解决基础类无法调用类加载器加载用户提供代码的问题,Java 引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器默认就是 Application 类加载器,并且可以通过 java.lang.Thread.setContextClassLoaser()方法进行设置。

这里只是简单介绍一下它,不进行深入探讨,总结就是线程上下文类加载器,可以打破双亲委派机制,实现逆向调用类加载器来加载当前线程中类加载器加载不到的类。

代码示例:通过一个类获取他们的类加载器

public class ClassLoaderTest {
    public static void main(String[] args) {
        // 系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系统类加载器:" + systemClassLoader);

        // 扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println("扩展类加载器:" + extClassLoader);

        // 启动类加载器
        ClassLoader bootstrpClassLoader = extClassLoader.getParent();
        System.out.println("启动类加载器:" + bootstrpClassLoader);

        // 当前类的类加载器
        ClassLoader curClassLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println("当前类的类加载器:" + curClassLoader);

        // java.lang.String类的加载器
        ClassLoader stringClassLoader = String.class.getClassLoader();
        System.out.println("String类的类加载器:" + stringClassLoader);

        /**
         系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
         扩展类加载器:sun.misc.Launcher$ExtClassLoader@68f7aae2
         启动类加载器:null
         当前类的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
         String类的类加载器:null
         */
    }
}

得到的结果,从结果可以看出启动类加载器无法直接通过代码获取,同时目前用户代码所使用的加载器为系统类加载器。同时我们通过获取String类型的加载器,发现是null,那么说明String类型是通过启动类加载器进行加载的。

代码示例:自定义一个类加载器

Java 默认 ClassLoader 只加载指定目录下的 class,如果需要动态加载类到内存,例如要从远程网络下来类的二进制,然后调用这个类中的方法实现我的业务逻辑,如此,就需要自定义 ClassLoader。

如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

继承ClassLoader

先写一个基本测试类

public class TestClass {
    @Override
    public String toString() {
        return " this is TestClass ~";
    }
}

自己写一个继承ClassLoader 的类加载器

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 加载指定目录下指定类名的class文件
        String property = getClassPath(name);
        byte[] classData = getClassData(property);

        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    // 读取字节流的方法
    private byte[] getClassData(String path) {
        try (InputStream ins = new FileInputStream(path);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()
        ) {

            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    // 获取class字节码文件绝对路径
    public static String getClassPath(String className){
        return System.getProperty("user.dir") + File.separator + "com"+ File.separator + "qiuwen"
                + File.separator + className + ".class";

    }

    public static void main(String[] args) throws Exception {
        // 指定类加载器加载调用
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> aClass = classLoader.loadClass("com.qiuwen.TestClass");
        System.out.println(aClass.newInstance().toString());

        // 输出
        // this is TestClass ~
    }
}

继承URLClassLoader

public class MyURLClassLoader extends URLClassLoader {

    public MyURLClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public MyURLClassLoader(URL[] urls) {
        super(urls);
    }

    public MyURLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }

    // 获取class字节码文件绝对路径
    public static String getClassPath(String className) {
        return System.getProperty("user.dir") + File.separator + "com" + File.separator + "qiuwen"
                + File.separator + className + ".class";
    }

    public static void main(String[] args) throws Exception {
        String classPath = getClassPath("com.qiuwen.TestClass");

        //创建自定义文件类加载器
        File file = new File(classPath);

        //File to URI
        URI uri= file.toURI();
        URL[] urls= {uri.toURL()};

        URLClassLoader myURLClassLoader = new MyURLClassLoader(urls);
        Class<?> aClass = myURLClassLoader.loadClass("com.qiuwen.TestClass");
        System.out.println(aClass.newInstance().toString());

		// 输出
		// this is TestClass ~
    }
}

你可能感兴趣的:(Java,jvm,java,类)