Java 类加载器和双亲委派机制原理剖析

一、类加载器(ClassLoader)

      JVM的类加载机制是按需加载的模式运行的,也就是代表着:所有类并不会在程序启动时全部加载,而是当需要用到某个类发现它未加载时,才会去触发加载的过程。

在这里插入图片描述

  • 引导/启动类加载器: Bootstrap ClassLoader,负责加载存放在jdk\jre\lib(jdk代表jdk的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类,比如java.lang.Integer均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

    注意:因为JVM是通过全限定名加载类库的,所以,如果你的文件名不被虚拟机识别,就算你把jar包丢入到lib目录下,引导类加载器也并不会加载它。出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类文件。

  • 扩展类加载器: Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

    这个类加载器是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$ExtClassLoader位置。它主要负责加载<JAVA_HOME>\lib\ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库。它可以直接被开发者使用。

  • 系统/应用程序类加载器: Application ClassLoader,也被称为应用程序类加载器,也是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$AppClassLoader位置。它负责加载系统类路径java -classpath或-D java.class.path指定路径下的类库,也就是经常用到的classpath路径。应用程序类加载器也可以直接被开发者使用。

    一般情况下,该类加载器是程序的默认类加载器,我们可以通过ClassLoader.getSystemClassLoader()方法可以直接获取到它。

  • 自定义类加载器: Application ClassLoader,也被称为应用程序类加载器,也是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$AppClassLoader位置。它负责加载系统类路径java -classpath或-D java.class.path指定路径下的类库,也就是经常用到的classpath路径。应用程序类加载器也可以直接被开发者使用。

二、四种类加载器之间的关系

如上分析的类加载器关系链如下:

Bootstrap引导类加载器 → Extension拓展类加载器 → Application系统类加载器 → User自定义类加载器

Bootstrap类加载器是在JVM启动时初始化的,它会负责加载ExtClassLoader,并将其父加载器设置为BootstrapClassLoader。BootstrapClassLoader加载完ExtClassLoader后会接着加载AppClassLoader系统类加载器,并将其父加载器设置为ExtClassLoader拓展类加载器。而自己定义的类加载器会由系统类加载器加载,加载完成后,AppClassLoader会成为它们的父加载器。

要注意的是:类加载器之间并不存在相互继承或包含关系,从上至下仅存在父加载器的层级引用关系。

例:

// 继承ClassLoader类,JDKClassLoaderDemo 相当于一个自定义类加载器
public class JDKClassLoaderDemo extends ClassLoader{
    
    
    public static void main(String[] args) {
    
    
        JDKClassLoaderDemo classLoader = new JDKClassLoaderDemo();
        System.out.println("自定义加载器:" + classLoader);
        System.out.println("自定义加载器的父类加载器:" + classLoader.getParent());
        System.out.println("Java程序系统默认的加载器:" + ClassLoader.getSystemClassLoader());
        System.out.println("系统类加载器的父加载器:" + ClassLoader.getSystemClassLoader().getParent());
        System.out.println("拓展类加载器的父加载器:" + ClassLoader.getSystemClassLoader().getParent().getParent());

        System.out.println();
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
    
    
            System.out.println(urls[i]);
        }

        System.out.println();
        System.out.println("extClassloader加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));

        System.out.println();
        System.out.println("appClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.class.path"));
    }
}

// 输出结果
/*
自定义加载器:com.kerwin.jvm.classloader.JDKClassLoaderDemo@74a14482
自定义加载器的父类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
Java程序系统默认的加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
系统类加载器的父加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
拓展类加载器的父加载器:null

bootstrapLoader加载以下文件:
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/classes

extClassloader加载以下文件:
C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

appClassLoader加载以下文件:
项目目录\target\classes;
...
*/

三、双亲委派机制

      类加载器在加载 class 文件的时候,遵从双亲委派原则,意思是加载依次由父加载器先执行加载动作,只有当父加载器没有加载到 class 文件时才由子类加载器进行加载。这种机制很好的保证了 Java API 的安全性,使得 JDK 的代码不会被篡改。
在这里插入图片描述

3.1、为什么要设计双亲委派机制

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

自定义String类运行测试:

package java.lang;

public class String {
    
    
    public static void main(String[] args) {
    
    
        System.out.println("**************My String Class**************");
    }
}

// 运行结果:
/*
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
 */

3.2、类加载器loadClass(String name) 源码解析

//ClassLoader的loadClass方法,里面实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
    
    synchronized (getClassLoadingLock(name)) {
    
    
        // 检查当前类加载器是否已经加载了该类,如果没有加载则委托父加载器加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
    
    
            long t0 = System.nanoTime();
            try {
    
    
                if (parent != null) {
    
     //如果当前加载器父加载器不为空则委托父加载器加载该类,如果父加载器也也没有加载了该类会一直向上委托,直到引导/启动类加载器
                    c = parent.loadClass(name, false);
                } else {
    
      //如果当前加载器父加载器为空则委托引导/启动类加载器加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
    
    
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
    
    
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name); //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
    
     //不会执行
            resolveClass(c);
        }
        return c;
    }
}

3.3、打破双亲委派机制

      尽管双亲委派机制有着诸多好处,但有时我们也需要打破它,比如在某些场景下需要加载自定义的类库,或者需要实现热部署等功能。这时,我们就需要自定义类加载器,并在其中打破双亲委派机制。

      以Java代码为例,我们可以通过继承ClassLoader类并重写loadClass方法来打破双亲委派机制。在loadClass方法中,我们可以根据需要自行加载类文件,并通过defineClass方法将其转换为Class对象,但是有一点需要注意,包名不能以java.开头(如:java.lang.String),在java.lang.ClassLoader.preDefineClass方法中会判断如果包名以java.开头会抛出java.lang.SecurityException: Prohibited package name: java

注意:同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。

  • 自定义一个User
package com.kerwin.jvm.classloader;

public class User {
    
    
    public void printClassLoad(){
    
    
        System.out.println("当前User类的类加载器为:"+this.getClass().getClassLoader());
    }
}
  • 打破双亲委派机制实现
import java.io.FileInputStream;
import java.lang.reflect.Method;

public class MyClassLoaderTest {
    
    
    static class MyClassLoader extends ClassLoader {
    
    
        private String classPath;

        public MyClassLoader(String classPath) {
    
    
            this.classPath = classPath;
        }

        private byte[] loadByte(String name) throws Exception {
    
    
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;

        }
        protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
            try {
    
    
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
    
    
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
        /**
         * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
         */
        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException {
    
    
            synchronized (getClassLoadingLock(name)) {
    
    
                Class<?> c = findLoadedClass(name);
                if (c == null) {
    
    
                    long t = System.nanoTime();
                    // 这里规定只有指定目录的类不走双亲委派加载,非自定义的类还是走双亲委派加载
                    if (!name.startsWith("com.kerwin.jvm.classloader")) {
    
    
                        c = this.getParent().loadClass(name);
                    } else {
    
    
                        c = findClass(name);
                    }
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
                if (resolve) {
    
    
                    resolveClass(c);
                }
                return c;
            }
        }
    }

    public static void main(String args[]) throws Exception {
    
    
        MyClassLoader classLoader = new MyClassLoader("D:\\classloader-example\\target\\classes");
        Class clazz = classLoader.loadClass("com.kerwin.jvm.classloader.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("printClassLoad", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());

        System.out.println();
        MyClassLoader classLoader1 = new MyClassLoader("D:\\classloader-example\\target\\classes");
        Class clazz1 = classLoader1.loadClass("com.kerwin.jvm.classloader.User");
        Object obj1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("printClassLoad", null);
        method1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader());
    }
}

// 输出结果
/*
当前User类的类加载器为:com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@7a7b0070
com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@7a7b0070

当前User类的类加载器为:com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@6ed3ef1
com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@6ed3ef1
*/

可以看到两个User所属类加载器内存地址不同,代表是两个不同的类加载器,那么这个User类信息也在JVM中存在2份,两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个,如果存在双亲委派那么类加载器应该是同一个,类加载器的内存地址应该也是相同的,这里不同则代表已经打破双亲委派。

猜你喜欢

转载自blog.csdn.net/weixin_44606481/article/details/134904288