Java虚拟机类加载机制中的ClassLoader类加载器详解以及如何自定义ClassLoader类加载器

1、Java虚拟机的类加载机制概述

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

类从被加载虚拟机内存中开始,到卸载出内存中为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。

加载是类加载过程中的一个阶段,不要混淆这两个看起很相似的名词,在加载阶段虚拟机会采用类加载器来加载Class文件。

2、Java虚拟机中的类加载器

站在Java虚拟机的角度来讲,只存在两种不同的类加载器:

  • 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是使用C++语言实现,是虚拟机自身的一部分。
  • 另外一种就是其他的类加载器,这些加载器都是由Java语言实现,独立于虚拟机外部,并且全部都继承于抽象类java.lang.ClassLoader。

站在Java开发人员角度来讲, 类加载器其实还可以分得更细致一些,绝大部分的Java程序都会使用到下面3种系统提供的类加载器。

  • 启动类加载器(Bootstrap
    ClassLoader),前面介绍过,这个类加载器负责将存放在<JAVA_HOME>\lib目录中的类库加载到虚拟机内存中
  • 扩展类加载器(Extension ClassLoader),这个类加载器负责加载<JAVA_HOME>\lib\ext目录中的类库。
  • 应用程序类加载器(Application
    ClassLoader),这个类加载器是ClassLoader中的getSystemLoader()方法的返回值,所以也称为系统类加载器。它负责加载ClassPath上所指定的类库。

2.1、查看类加载器加载的路径

通过下面代码可以看到类加载器加载的路径

2.1.1、查看启动类加载器

System.out.println(System.getProperty("sun.boot.class.path"));

结果:

D:\Program Files\Java\jdk\jre\lib\resources.jar;
D:\Program Files\Java\jdk\jre\lib\rt.jar;
D:\Program Files\Java\jdk\jre\lib\sunrsasign.jar;
D:\Program Files\Java\jdk\jre\lib\jsse.jar;
D:\Program Files\Java\jdk\jre\lib\jce.jar;
D:\Program Files\Java\jdk\jre\lib\charsets.jar;
D:\Program Files\Java\jdk\jre\lib\jfr.jar;
D:\Program Files\Java\jdk\jre\classes

2.1.2、查看扩展类加载器

System.out.println(System.getProperty("java.ext.dirs"));

结果:

D:\Program Files\Java\jdk\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext

3、类加载器之间的关系

接下来探讨它们的加载顺序,先创建一个 ClassLoaderTest 的Java文件。
先看看这个ClassLoaderTest类是被谁加载的:

public class ClassLoaderTest {
    public static void main(String[] args) {
    
        System.out.println(ClassLoaderTest.class.getClassLoader());
        
    }
}

结果:

sun.misc.Launcher$AppClassLoader@42a57993

也就是说明 ClassLoaderTest.class文件是由AppClassLoader加载的。

由于这个 ClassLoaderTest 类是我们自己编写的,那么int.class或者是String.class的加载是由谁完成的呢?
我们可以在代码中尝试:

public class ClassLoaderTest {
    public static void main(String[] args) {
    
        System.out.println(String.class.getClassLoader());
        System.out.println(int.class.getClassLoader());
        
    }
}

结果:

null
null

结果打印的是空null,意思是int.class和String.class这类基础类没有类加载器加载?

当然不是!
int.class和String.class这类基础类是由Bootstrap ClassLoader加载的,最直观的解释就是String类是在java.lang包里,这个包在rt.jar里面,而这个jar是由Bootstrap ClassLoader加载的。要彻底明白这些,我们首先得知道一个前提。

3.1、每个类加载器都有一个父加载器

每个类加载器都有一个父加载器,比如加载 ClassLoaderTest.class是由AppClassLoader完成,那么AppClassLoader也有一个父加载器,怎么样获取呢?很简单,通过getParent方法。就像这样:

public class ClassLoaderTest {
    public static void main(String[] args) {
    
        ClassLoader c =  ClassLoaderTest.class.getClassLoader();
        System.out.println(c.getParent());
        
    }
}

结果:

sun.misc.Launcher$ExtClassLoader@28d93b30

这个说明,AppClassLoader的父加载器是ExtClassLoader。那么ExtClassLoader的父加载器又是谁呢?

public class ClassLoaderTest {
    public static void main(String[] args) {
    
        ClassLoader c =  ClassLoaderTest.class.getClassLoader();
        System.out.println(c.getParent().getParent());
        
    }
}

结果:

null

又是一个空,这表明ExtClassLoader没有父加载器。那么,为什么标题又是每一个加载器都有一个父加载器呢?这不矛盾吗?为了解释这一点,我们还需要看下面的一个基础前提。

3.2、父加载器不是父类

我们我们可以看一下ExtClassLoader和AppClassLoader的源码。(源码是精简过的,只展示了关键部分)

...
static class AppClassLoader extends URLClassLoader{...}
...
static class ExtClassLoader extends URLClassLoader{...}
...

可以看见ExtClassLoader和AppClassLoader同样继承自URLClassLoader,但上面一小节代码中,为什么调用AppClassLoader的getParent()代码会得到ExtClassLoader的实例呢?

可以看sun.misc.Launcher.class的部分关键源码:

public Launcher() {
 ...
	Launcher.ExtClassLoader var1;
	try {
		// 创建ExtClassLoader 实例。没有传参数,注意这个细节。下面解释
		var1 = Launcher.ExtClassLoader.getExtClassLoader();
	} catch (IOException var10) {
		throw new InternalError("Could not create extension class loader", var10);
	}

	try {
		// 创建AppClassLoader实例。将ExtClassLoader 实例作为参数,注意这个细节。下面解释
		this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
	} catch (IOException var9) {
		throw new InternalError("Could not create application class loader", var9);
	}
...

AppClassLoader.class的部分关键源码:

 static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null?new File[0]:Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
                    // 创建AppClassLoader时,传进来的参数var0,继续往AppClassLoader方法传。
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
        // 传进来的 参数 ClassLoader var2。继续传往父类构造器,也就是URLClassLoader类的构造器
        AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
        ...

ExtClassLoader.class的部分关键源码:

  static class ExtClassLoader extends URLClassLoader {
        private static volatile Launcher.ExtClassLoader instance;
        // 在创建ExtClassLoader 是调用了父类构造器,也就是URLClassLoader类的构造器,第二个参数直接写了为null
        // 这是为什么呢?下面 URLClassLoader 源码中揭晓
        public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }	
        ...

java.net.URLClassLoader的部分关键源码:

// 注意看这个构造器的第二个参数,传进来的是 parent!
 public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        acc = AccessController.getContext();
        ucp = new URLClassPath(urls, factory, acc);
    }	
    ...

需要注意的是sun.misc.Launcher.class的部分关键源码中的这几句话:

Launcher.ExtClassLoader var1;

var1 = Launcher.ExtClassLoader.getExtClassLoader();

this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

结合AppClassLoader.class的部分关键源码和java.net.URLClassLoader的部分关键源码得知:

Launcher.AppClassLoader.getAppClassLoader(var1)这个方法中的var1参数最终会传到URLClassLoder的构造方法中去,这个参数就表示了parent。

代码已经说明了问题,AppClassLoader的parent是一个ExtClassLoader实例。

ExtClassLoader并没有直接找到对parent的赋值。它调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数。和AppClassLoader的方法一样,在创建ExtClassLoader 时,并没有传任何参数,而在调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数时,第二个参数显示的写明了为(ClassLoader)null。看下方代码

源码两个关键部分:
ExtClassLoader.class的源码:

public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }
       ...

java.net.URLClassLoader的源码:

public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(parent);
           ...

答案已经很明了了,ExtClassLoader的parent为null。

上面张贴这么多代码也是为了说明AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是null。这符合我们之前编写的测试代码。

3.3、再讲Bootstrap ClassLoader

Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。

4、双亲委派模式

双亲委派模式的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它搜索的范围没有找到所需的类),子加载器才会尝试自己取加载。

类加载器双亲委派模型:
在这里插入图片描述

上面已经详细介绍了加载过程,但具体为什么是这样加载,我们还需要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。

4.1、重要方法

4.1.1、loadClass()

通过指定的全限定类名加载class,它通过重载的loadClass(String,boolean)方法,它的源码是这样的:

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

源码描述的加载步骤是:

  1. 执行 findLoadedClass(name) 方法,检查这个class有没有被加载过,如果加载过了,直接返回,如果没有就进行加载。
  2. 判断父加载器是否为null,如果不是,就交给父加载去尝试加载。如果为null就交给Bootstrap ClassLoader去尝试加载。

填坑:这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器的原因。

另外代码解释了双亲委托的加载机制。

4.1.2、findClass()

根据名称或位置加载.class字节码

4.1.3、defineClass()

这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。

5、自定义ClassLoader

如果在某种情况下,我们需要动态加载一些东西,比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?

如果要这样做的话,需要我们自定义一个ClassLoader。

步骤描述

  1. 编写一个类继承自ClassLoader抽象类。
  2. 复写它的findClass()方法。
  3. 在findClass()方法中调用defineClass()。

注意:一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。

5.1、普通Java类

我们写编写一个测试用的类文件,Test.java

public class Test {
	
	public void say(){
		System.out.println("Hello World");
	}

}

然后将它编译过年class文件Test.class放到F:\这个路径下。

5.2、自定义的ClassLoader

我们编写自定义的ClassLoader的代码。

public class CustomClassLoader extends ClassLoader {

    private String mLibPath;

    public CustomClassLoader(String path) {
        mLibPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getFileName(name);

        File file = new File(mLibPath,fileName);

        try {
            FileInputStream is = new FileInputStream(file);

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] data = bos.toByteArray();
            is.close();
            bos.close();

            return defineClass(name,data,0,data.length);

        } catch (IOException e) {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    //获取要加载 的class文件名
    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if(index == -1){
            return name+".class";
        }else{
            return name.substring(index+1)+".class";
        }
    }
}

5.3、编写测试类

现在编写测试代码。如果调用Test对象的say方法,它会输出"Say Hello"这条字符串。但现在是我们把Test.class放置在应用工程所有的目录之外,我们需要加载它,然后执行它的方法。具体效果如何呢?

public class ClassLoaderTest {

    public static void main(String[] args) {

        //创建自定义classloader对象。
        CustomClassLoader diskLoader = new CustomClassLoader("F:\\");
        try {
            //加载class文件
            Class c = diskLoader.loadClass("com.learn.classLoader.Test");

            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say");
                    //通过反射调用Test类的say方法
                    method.invoke(obj);
                } catch (InstantiationException | IllegalAccessException
                        | NoSuchMethodException
                        | SecurityException |
                        IllegalArgumentException |
                        InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

结果:

Say Hello

可以看到,Test类的say方法正确执行,也就是我们自定义写的CustomClassLoader 编写成功。

6、另外一个类加载器:线程上下文类加载器

6.1、概念

线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个。Java 应用运行的初始线程的上下文类加载器是应用程序类加载器(AppClassLoader)。

6.2、绕过双亲委派模式

有了这个类加载器,父类加载器就可以请求子类加载器去完成类加载的动作,这种行为实际上就是打破了双亲委派模型的层次结构来逆向使用类加载器
最典型的例子就是:JNDI、JDBC。

7、总结

  • Java虚拟机类加载机制
  • Java的几种类加载器
  • 各个加载器扫描的范围
  • 各个加载器之间的关系
  • 如何自定义一个ClassLoader类加载器

8、拓展之Class解密类加载器

常见的用法是将Class文件按照某种加密手段进行加密,然后按照规则编写自定义的ClassLoader进行解密,这样我们就可以在程序中加载特定了类,并且这个类只能被我们自定义的加载器进行加载,提高了程序的安全性。
详情请参照该文章:Java实现自定义classLoader类加载器动态解密class文件


技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章