Java-类加载器(Classloader)

概念

Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中,用于加载系统、网络或者其他来源的类文件。Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。

类文件编译流程图

我们以下图为例子,比如我们创建一个ClassLoaderTest.java文件运行,经过javac编译,然后生成ClassLoaderTest.class文件。这个java文件和生成的class文件都是存储在我们的磁盘当中。但如果我们需要将磁盘中的class文件在java虚拟机内存中运行,需要经过一系列的类的生命周期(加载、连接(验证-->准备-->解析)和初始化操作,最后就是我们的java虚拟机内存使用自身方法区中字节码二进制数据去引用堆区的Class对象。

通过这个流程图,我们就很清楚地了解到类的加载就是由java类加载器实现的,作用将类文件进行动态加载到java虚拟机内存中运行。

建立TestHelloWorld.java

编译java文件为class文件 -> javac TestHelloWorld.java

生成编译好的TestHelloWorld.class文件

使用java 查看编译好的class文件内容

javap -c -p -l TestHelloWorld.class

JVM在执行TestHelloWorld之前会先解析class二进制内容,JVM执行的其实就是如上javap命令生成的字节码。

类加载器分类

引导类加载器(BootstrapClassLoader)

引导类加载器(BootstrapClassLoader),底层原生代码是C++语言编写,属于jvm一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar目录当中。(同时处于安全考虑,BootstrapClassLoader只加载包名为java、javax、sun等开头的类)。

扩展类加载器(ExtensionsClassLoader)

扩展类加载器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载java的扩展库。Java虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载java类。

App类加载器/系统类加载器(AppClassLoader)

App类加载器/系统类加载器(AppClassLoader),由sun.misc.Launcher$AppClassLoader实现,一般通过通过(java.class.path或者Classpath环境变量)来加载Java类,也就是我们常说的classpath路径。通常我们是使用这个加载类来加载Java应用类,可以使用ClassLoader.getSystemClassLoader()来获取它。

自定义类加载器(UserDefineClassLoader)

自定义类加载器(UserDefineClassLoader),除了上述java自带提供的类加载器,我们还可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。

双亲委派机制

双亲委派机制的概念

通常情况下,我们就可以使用JVM默认三种类加载器进行相互配合使用,且是按需加载方式,就是我们需要使用该类的时候,才会将生成的class文件加载到内存当中生成class对象进行使用,且加载过程使用的是双亲委派模式,及把需要加载的类交由父加载器进行处理。

如上图类加载器层次关系,我们可以将其称为类加载器的双亲委派模型。但注意的是,他们之间并不是"继承"体系,而是委派体系。当上述特定的类加载器接到加载类的请求时,首先会先将任务委托给父类加载器,接着请求父类加载这个类,当父类加载器无法加载时(其目录搜素范围没有找到所需要的类时),子类加载器才会进行加载使用。这样可以避免有些类被重复加载。

双亲委派机制的好处

1、这样就是能够实现有些类避免重复加载使用,直接先给父加载器加载,不用子加载器再次重复加载。

2、保证java核心库的类型安全。比如网络上传输了一个java.lang.Object类,通过双亲模式传递到启动类当中,然后发现其Object类早已被加载过,所以就不会加载这个网络传输过来的java.lang.Object类,保证我们的java核心API库不被篡改,出现类似用户自定义java.lang.Object类的情况。

核心方法

loadClass:加载指定的java类

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

在loadClass方法中,它先使用了findLoadedClass(String)方法来检查这个类是否被加载过。

接着使用父加载器调用loadClass(String)方法,如果父加载器为null,类加载器加载jvm内置的加载器。

之后就调用findClass(String) 方法装载类。

最后通过上述步骤我们找到了对应的类,并且接收到的resolve参数的值为true,那么就会调用resolveClass(Class)方法来处理类。

findCLass:查找指定的Java类

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

findLoadedClass:查找JVM已经加载过的类

protected final Class<?> findLoadedClass(String name) {
        if (!checkName(name))
            return null;
        return findLoadedClass0(name);
    }

defineClass:定义一个Java类,将字节码解析成虚拟机识别的Class对象。往往和findClass()方法配合使用

protected final Class<?> defineClass(byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(null, b, off, len, null);
    }

resolveClass:链接指定Java类

protected final void resolveClass(Class<?> c) {
        resolveClass0(c);
    }

    private native void resolveClass0(Class c);

ClassLoader类加载流程

理解Java类加载机制并非易事,这里我们以一个Java的HelloWorld来学习ClassLoader

ClassLoader加载com.tyut.TestHelloWorld类重要流程如下:

  1. ClassLoader会调用public Class<?> loadClass(String name)方法加载com.tyut.TestHelloWorld类。
  2. 调用findLoadedClass方法检查TestHelloWorld类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。
  3. 如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载TestHelloWorld类,否则使用JVM的Bootstrap ClassLoader加载。
  4. 如果上一步无法加载TestHelloWorld类,那么调用自身的findClass方法尝试加载TestHelloWorld类。
  5. 如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的com.tyut.TestHelloWorld类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。
  6. 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。
  7. 返回一个被JVM加载后的java.lang.Class类对象。

自定义类加载过程

定义被加载的类 TestHelloWorld

需要被加载的类 TestHelloWorld.java

//TestHelloWorld.java

package com.tyut;

public class TestHelloWorld {
    public String hello() {
        return "Hello World~";
    }
}

自定义加载器 TestClassLoader.java

使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入TestHelloWorld类的字节码的方式来向JVM中定义一个TestHelloWorld类,最后通过反射机制就可以调用TestHelloWorld类的hello方法了

//TestClassLoader.java

package com.tyut;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class TestClassLoader extends ClassLoader {
    // TestHelloWorld类名
    private static String testClassName = "com.tyut.TestHelloWorld";

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
//         只处理TestHelloWorld类
        if (name.equals(testClassName)) {
            byte[] classData = getClassData("com.tyut.TestHelloWorld");
            // 调用JVM的native方法向JVM定义TestHelloWorld类
            return defineClass(testClassName, classData, 0, classData.length);
        }

        return super.findClass(name);


    }

    public static byte[] getClassData(String testClassName) {
        InputStream is = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            is = new FileInputStream(testClassName);
            int temp = -1;
            while ((temp = is.read()) != -1) {
                baos.write(temp);


            }

            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (baos != null) {
                try {
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}


使用加载器调用类

创建一个主函数,使用自定义加载器调用我们的类

package com.tyut;


import java.lang.reflect.Method;

public class testmain {
    public static void main(String[] args) throws Exception {
        try {
            TestClassLoader clsload  = new TestClassLoader();
            // 使用自定义的类加载器加载TestHelloWorld类
            Class testClass = clsload.loadClass("com.tyut.TestHelloWorld");

            // 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
            Object testInstance = testClass.newInstance();

            // 反射获取hello方法
            Method method = testInstance.getClass().getMethod("hello");

            // 反射调用hello方法,等价于 String str = t.hello();
            String str = (String) method.invoke(testInstance);

            System.out.println(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    }


个人理解:

类加载器就是去加载一个我们需要的类,在双亲委派机制都无法加载的情况下,进行本地加载,重新定义 findClass,在方法里面定义我们所需要的内容,最后使用defineClass向JVM定义,我们就可以使JVM执行我们需要的方法了。

posted @ 2022-03-30 19:42  lalalaxiaoyuren  阅读(1164)  评论(0编辑  收藏  举报