掘金 后端 ( ) • 2024-04-02 10:44

theme: fancy

JVM—类加载子系统

JVM的类加载是通过ClassLoader及其子类来完成的。

有哪些类加载器

类加载器如下:

类加载器

  • 启动类加载器(BootStrap ClassLoader):负责加载JAVA_HOME\lib目录或通过-Xbootclasspath参数指定路径中的且被虚拟机认可(rt.jar)的类库;
  • 扩展类加载器(Extension ClassLoader):负责加载JAVA_HOME\lib\ext目录或通过java.ext.dirs系统变量指定路径中的类库;
  • 应用程序类加载器(Application ClassLoader):负责加载用户路径classpath上的类库;
  • 自定义类加载器(Custom ClassLoader):加载应用之外的类文件;

类加载器执行顺序

类加载器执行顺序如下图:

类加载器执行顺序

  1. 自底向上检查类是否已经加载:

    加载过程中会先检查类是否已被加载,从自定义加载器到BootStrap逐层检查,只要某个类加载器已加载某个类,就视为此类已加载,可以保证此类使得所有ClassLoader只加载一次;

  2. 自顶向下尝试加载类:由上层来逐层尝试加载此类。

类加载时机与过程

类加载的四个时机:

  1. 遇到new、getStatic、putStatic、invokeStatic四条指令;

    比如有如下类:

    public class MyTest {
        public static int hello;
        
        public static void testMethod(){
            
        }
    }
    

    当使用如下三种代码时,此类会被加载:

    //第一种
    MyTest.age;
    //第二种
    MyTest.testMethod();
    //第一种
    new MyTest();
    
  2. 使用java.lang.reflect包方法对类进行反射调用;

    比如:

    Class clazz = Class.forName("com.sjdwz.MyTest");
    
  3. 初始化一个类,发现其父类还没初始化,要先初始化其父类;

  4. 当虚拟机启动时,用户需要指定一个主类main,需要先将主类加载。

一个类的一生

一个类的一生如下:

一个类的一生

类加载做了什么

主要做了三件事:

  1. 根据类全限定名称,定位到class文件,以二进制字节流形式加载到内存中;
  2. 把字节流静态数据加载到方法区(永久代,元空间);
  3. 基于字节流静态数据,创建字节码Class对象。

类加载途径

类加载途径如下图:

类加载途径

自定义类加载器

我们可以自定义类加载器,来加载D:\sjdwzTest目录下的lib文件夹下的类。

步骤如下:

  1. 新建一个类MyTest.java

    package com.sjdwz.myclassloader;
    public class MyTest {
        public void sayHello(){
            System.out.println("hello world!");
        }
    }
    
  2. 使用javac MyTest.java命令,将生成的MyTest.class文件放到D:\sjdwzTest\lib\com\sjdwz\myclassloader文件夹下

    注意:包路径不能错。

    编译的位置

  3. 自定义类加载器,继承ClassLoader,重写findClass()方法 ,调用defineClass()方法:

    /**
     * @Description 自定义类加载器
     * @Created by 随机的未知
     */
    public class SjdwzClassLoader extends ClassLoader {
        private String classpath;
    
        public SjdwzClassLoader(String classpath) {
            this.classpath = classpath;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                //输入流,通过类的全限定名称加载文件到字节数组
                byte[] classDate = getData(name);
                if (classDate != null) {
                    //defineClass方法将字节数组数据 转为 字节码对象
                    return defineClass(name, classDate, 0, classDate.length);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return super.findClass(name);
        }
    
        /**
         * 加载类的字节码数据
         * @param className
         * @return
         * @throws IOException
         */
        private byte[] getData(String className) throws IOException{
            String path = classpath + File.separatorChar +
                    className.replace('.', File.separatorChar) + ".class";
            try (InputStream in = new FileInputStream(path);
                 ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                byte[] buffer = new byte[2048];
                int len = 0;
                while ((len = in.read(buffer)) != -1) {
                    out.write(buffer, 0, len);
                }
                return out.toByteArray();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
  4. 测试类如下:

    public class SjdwzClassLoaderTest {
        public static void main(String[] args) throws Exception {
            //自定义类加载器的记载路径
            SjdwzClassLoader sjdwzClassLoader = new SjdwzClassLoader("D:\\sjdwzTest\\lib");
            Class<?> testClazz = sjdwzClassLoader.loadClass("com.sjdwz.myclassloader.MyTest");
            if(testClazz != null){
                Object testObj = testClazz.newInstance();
                Method sayHelloMethod = testClazz.getMethod("sayHello", null);
                sayHelloMethod.invoke(testObj,null);
                System.out.println(testClazz.getClassLoader().toString());
            }
        }
    }
    

    输出如下:

    输出

双亲委派与打破双亲委派

什么是双亲委派

当一个类加载器收到类加载任务,会先交给其父类加载器去完成。 因此,最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,子类才会尝试加载任务。

为什么需要双亲委派

主要考虑安全因素,双亲委派可以避免重复加载核心的类,当父类加载器已经加载了该类时,子类加载器不会再去加载。

为什么还需要破坏双亲委派

在实际应用中,双亲委派解决了Java基础类统一加载的问题,但是存在着缺陷。JDK中的基础类的方法作为典型的API被用户类用户调用,但是也存在API调用用户代码的情况,比如:SPI代码。这种情况就需要打破双亲委派模式。

比如:数据库驱动DriverManager。以Driver接口为例,Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,由系统类加载器加载。这个时候就需要启动类加载器来委托子类来加载Driver实现,这就破坏了双亲委派。

如何破坏双亲委派

  1. 重写ClassLoader的loadClass方法;

    在JDK1.2之后,新加了一个findClass方法让用户重写;

  2. SPI,父类委托子类加载器加载Class;

  3. 热部署和不停机更新用到的OSGI技术。