JVM类加载器源码剖析

744 阅读7分钟

类加载运行的过程

以windows为例,当程序启动时,java.exe主程序会调用底层的jvm.dll文件创建虚拟机并创建一个引导类加载器

loadClass(类加载)的步骤

  • 加载: 只有被使用到的类才会被加载,加载过程中会生成一个Class对象,作为方法区的访问入口。
  • 验证: 校验class文件格式内容等的正确性。
  • 准备: 为加载的类的静态变量分配内存,并赋予默认值。
  • 解析: 将符号引用替换为直接引用。
  • 初始化:为类的静态变量初始化指定的值, 并执行静态代码块。

类加载器

类加载的过程是通过类加载器实现的。

类加载器的类型:

  • 引导类加载器(BootstrapClassLoader): 负责加载JRE中lib目录下的核心类库, 如rt.jar等
  • 扩展类加载器(ExtClassLoader): 负责加载JRE中lib目录下ext目录下的扩展类库。
  • 应用程序加载器(AppClassLoader): 负责加载classpath路径下的类包, 引用的jar包或自己的类。
  • 自定义加载器: 负责自定义路径下的类包。

输出类加载器

System.out.println(String.class.getClassLoader()); //引用加载器由C++创建, 所以java无法直接获取
System.out.println(DESKeyFactory.class.getClassLoader());
System.out.println(TestJDKClassLoader.class.getClassLoader());

ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassLoader.getParent();
System.out.println("bootStrapLoader : " + bootstrapLoader);
System.out.println("extClassLoader : " + extClassLoader);
System.out.println("appClassLoader : " + appClassLoader);

//输出结果
null
sun.misc.Launcher$ExtClassLoader@404b9385
sun.misc.Launcher$AppClassLoader@18b4aac2
bootStrapLoader : null
extClassLoader : sun.misc.Launcher$ExtClassLoader@404b9385
appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2

由于String由引导类加载器负责加载, 而引导类加载器由C++底层实现, 所以输出为null。

bootstrapLoader同理。

类加载的初始化

当java底层创建jvm时会创建JVM启动器, 实例为sun.misc.Launcher

Launcher初始化使用了单例模式

1616380019196.png

初始化时Launcher调用构造方法创建了两个类加载器ExtClassLoader和AppClassLoader, 并默认调用getClassLoader()方法返回创建的加载器(AppClassLoader)

为什么是AppClassLoader呢?

不懂就看源码!

Launcher调用getClassLoader直接返回的是loader属性(ClassLoader)

1616380505028.png

当运行Launcher的构造方法时Launcher首先创建了ExtClassLoader(扩展类加载器), 随后又创建了AppClassLoader并将生成好的ExtClassLoader传入到AppClassLoader的创建方法参数中, 并将生成的AppClassLoader返回给了this.loader

看清楚了, AppClassLoader返回给了this.loader

1616380199997.png

那么问题又来了, ExtClassLoader传入getAppClassLoader又是做了什么呢?

继续看源码!

最终只是初始化了一个AppClassLoader

1616381061834.png

那么来看一下父类(URLClassLoader)又做了什么吧

1616381202981.png

咦, 发现这个参数居然起名为parent, 并且继续往上走。

1616381296128.png

发现走到最上层的父类是ClassLoader类, 原来是把老祖宗的parent设置为了ExtClassLoader。

所以说AppClassLoader、ExtClassLoader、BootstrapClassLoader之间是存在父子关系的! (注意, 这里的父子关系绝不是继承的关系!

那么为什么要存在这种父子关系呢? 重头戏来了!


双亲委派机制

刚才我们看到的这种加载方式其实就存在双亲委派机制, 当加载某个类时会先委托父加载器寻找目标类, 找不到再继续委托上层父加载器, 如果所有父加载器都没有找到目标类, 则在自己的类加载路径中查找并加载。

说简单点就是有事先找爹!

那么jvm是怎么实现双亲委派机制的?

不废话还是直接看源码!

以下代码为Launcher类中的AppClassLoader类中的loadClass方法

前面的代码我们暂时忽略掉, 重点看红框框这里的代码!

这里直接调用父类的loadClass()方法, 父类是谁? URLClassLoader

1616381984408.png

我们去瞜一眼!

1616382152481.png

还是一直在往上走! 最终又到了老祖宗这里 ClassLoader类

重头戏来了!

最终这个方法大概逻辑:

  1. 首先检查指定的类是否已经加载过了, 如果加载就不需要加载直接返回该类。
  2. 如果没有加载过那么判断是否有父加载器, 如果有则由父加载器加载
  3. 如果父加载器及bootstrap加载器都没有找到指定类, 那么就调用当前类加载器的findClass方法来加载。
//这是ClassLoader类中的loadClass方法
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //首先, 检查当前类加载器是否已经加载了该类, 调用底层的native方法
            Class<?> c = findLoadedClass(name);
            if (c == null) {//当前类没有被加载过
                long t0 = System.nanoTime();
                try {
                	//如果存在父加载器则调用父加载器的loadClass方法(ClassLoader)
                	//实际父加载器还是ClassLoader类, 还会递归调用该方法!
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    	//从引导类加载器中寻找该类, 没找到会返回null
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

				//如果在父加载器和引导类加载器中都没有找到该类, 那么调用自身的findClass方法来加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // 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;
        }
    }

双亲委派机制有什么作用?

  • 沙箱安全机制: 防止核心API库被随意篡改

  • 避免类的重复加载: 当父加载器加载类后就没有必要让子加载器再加载一次, 保证被加载类的唯一性

    看一个示例

    package java.lang;
    
    public class String {
        public static void main(String[] args) {
            System.out.println("自定义String类");
        }
    }
    
    //程序报错!
    错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
       public static void main(String[] args)
    否则 JavaFX 应用程序类必须扩展javafx.application.Application
    

    因为, String类是由**引导类加载器(BootstrapClassLoader)**加载的,核心类库中的String并没有main方法

自定义类加载

自定义类加载只需要继承老祖宗java.lang.ClassLoaderL类, 通过以上原发分析得知, 该类有两个核心方法, 一个是 loadClass(), 该方法实现了双亲委派机制, 还有一个方法就是findClass, 默认实现是空方法, 所以自定义类加载主要是重写findClass方法!

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 fileInputStream = new FileInputStream(classPath + "/" + name + ".class");
            int len = fileInputStream.available();
            byte[] data = new byte[len];
            fileInputStream.read(data);
            fileInputStream.close();
            return data;
        }

        //重写findClass方法
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);//获取类的字节数组
                return defineClass(name, data, 0, data.length);//将字节数组转换为Class对象
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

    }

    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("E:/test");
        Class<?> aClass = myClassLoader.loadClass("com.wf.User");
        System.out.println(aClass.getClassLoader());
    }
}

//最终执行结果
sun.misc.Launcher$AppClassLoader@18b4aac2

有没有惊奇的发现为什么我们使用自己的MyClassLoader后User类居然还是AppClassLoader加载的?

1616384871313.png

如果到这里你还是不明白, 那说明你没有真正的理解双亲委派机制

首先我们自定义加载器会找到父加载器AppClassLoader加载器, 之后再继续向上找, 最终由AppClassLoader加载!

那么就是想让我们自己定义的MyClassLoader加载怎么办?

很简单, 我们只需要把项目中的User类删掉, 注意target下的class文件

1616385108576.png

搞定!

打破双亲委派机制

双亲委派机制的具体实现是在ClassLoader类中的loadClass方法中实现的, 那么想打破这个规则思路就很简单了! 重写loadClass方法不就行了嘛。

去掉双亲委派机制的核心代码

protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                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);

                        // 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;
            }
        }

搞定! 运行测试!

什么? 报错了???

java.io.FileNotFoundException: E:\test\java\lang\Object.class (系统找不到指定的路径。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at java.io.FileInputStream.<init>(FileInputStream.java:93)
	at com.wf.jvm.MyClassLoaderTest$MyClassLoader.loadByte(MyClassLoaderTest.java:15)
	at com.wf.jvm.MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:25)
	at com.wf.jvm.MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:45)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at com.wf.jvm.MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:26)
	at com.wf.jvm.MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:45)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at com.wf.jvm.MyClassLoaderTest.main(MyClassLoaderTest.java:64)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at com.wf.jvm.MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:26)
	at com.wf.jvm.MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:45)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at com.wf.jvm.MyClassLoaderTest.main(MyClassLoaderTest.java:64)

Process finished with exit code 1

没关系! 报错了我们就找问题所在!

看一下报错信息, FileNotFoundException 说没有找到Object.class文件!

为什么会出现这个错误呢? 因为每一个类都有个超级父类它的名字叫Object

我们的加载器只加载了User类, 而我们的父加载器被我们屏蔽掉了, 所以加载不到Object类

protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    //在这里定义如果不是自定义加载器加载的路径都交给父加载器加载
                    if(!name.startsWith("com.wf")){
                        c = super.loadClass(name, resolve);
                    }else{
                        if (c == null) {
                            // If still not found, then invoke findClass in order
                            // to find the class.
                            long t1 = System.nanoTime();
                            c = findClass(name);

                            // 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;
            }
        }

问题解决, 项目中存在User依然使用我们自定义加载器加载我们指定磁盘目录的类文件!

1616389808508.png

为什么要打破双亲委派机制?

例如tomcat, Tomcat是一个web容器,但是可能需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,因此要保证每个应用程序的类库都是独立的, 保证互相隔离