Java的类加载器ClassLoader
前言
ClassLoader java的类加载器,其作用就是把编译好的class文件或者jar包中对应的类的元数据加载到jvm的方法区,在堆中创建一个Class对象并返回,调用这个这个Class对象的newInstance()方法就可以创建一个指定类对应的对象。熟悉反射的朋友应该很清楚,如果再在前面加一段通过Class.forName()方法来获取Class对象的话,这其实就是java“反射”获取对象的实现过程。
Class.forName()方法会调用forName0方法,这是一个jvm实现的native方法,里有一个重要的参数就是ClassLoader对象。Class.forName()方法是显示的获取一个Class对象的方法,jvm会调用根据参数里的ClassLoader对象来查询并加载一个Class对象。
Class.forName()方法是一种显示的获取Class对象的方法。这种方式一般是通过反射来创建java对象。我们平时用得更多的是使用new关键字来创建java对象,这个过程一个隐式创建java对象的过程:首先jvm会找到当前上下文的ClassLoader对象,通过这个ClassLoader对象来Load并创建一个Class对象;再通过Class对象的newInstance()方法实例化一个类对象。这个过程完全就jvm自己完成,所以称为“隐式加载”。通过这个过程,我们还可以发现,jvm是按需加载的,也就是说在new关键字被调用后,才会去加载对应的Class对象。否则如果一上来就把所有的jar包都加载到内存,势必一种内存空间的浪费;同时程序启动时间也会变长。
java自带的ClassLoader对象的创建过程
提示:本文中贴出的源码都是基于JDK1.8。
ClassLoader在java中定义的是一个抽象类,也说真实的ClassLoader对象是其子类对象。ClassLoader类中定义了一些通用的方法,比如需要一个ClassLoader时可以通过getSystemClassLoader()方法:
public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); //初始化ClassLoader对象 if (scl == null) { return null; } SecurityManager sm = System.getSecurityManager(); if (sm != null) {//权限检查 checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; }
这个方法没什么好说的关键就是调用initSystemClassLoader()方法 对ClassLoader对象进行初始化。initSystemClassLoader这方法不多说,其中核心部分是创建了一个Launcher对象:
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
java自带的类加载器对象就是在Launcher类中创建的。下面我们主要来看下Launcher类,这个类从整体上来看运用了一个 “类单例模式”,大致结构如下:
public class Launcher { private static Launcher launcher = new Launcher(); private ClassLoader loader; //省略其他成员变量 public static sun.misc.Launcher getLauncher() { return launcher; } //公有的构造方法,与典型的单例模式有点差别 public Launcher() { //省略方法体 } //省略其他方法 }
这是一种典型的饿汉式“单例模式”(除了构造方法是public),在ClassLoad的initSystemClassLoader()方法中就是通过getLauncher()方法来获取Launcher对象的,可以看到通过这个方法获取的对象,每次都是同一个。
Launcher的构造方法做了三件事:创建ExtClassLoader类加载器对象;创建AppClassLoader类加载器对象;创建SecurityManager安全策略对象。关于java的安全策略这里就不展开了,感兴趣的可以异步到这里https://www.cnblogs.com/yiwangzhibujian/p/6207212.html。下面是该构造方法源码:
public Launcher() { //创建ExtClassLoader类加载器对象 Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } //创建AppClassLoader类加载器对象 try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } //把AppClassLoader类加载器对象设置到上线文 Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); //创建SecurityManager安全策略 if(var2 != null) { SecurityManager var3 = null; if(!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) { ; } catch (InstantiationException var6) { ; } catch (ClassNotFoundException var7) { ; } catch (ClassCastException var8) { ; } } else { var3 = new SecurityManager(); } if(var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); } System.setSecurityManager(var3); } }
可以看到ExtClassLoader、AppClassLoader是二者都是Launcher的内部类,并且都继承自URLClassLoader;而URLClassLoader又都继承自前面提到的抽象类ClassLoader。二者的区别是AppClassLoader的父加载器是ExtClassLoader,而ExtClassLoader的父加载器为空(AppClassLoader的构造方法需要一个ExtClassLoader对象)。
Launcher的构造方法首次执行后,Launcher的单例对象就创建完成。下面继续返回ClassLoader抽象类。
当需要加载一个Class对象时:
1、首先通过ClassLoader的getSystemClassLoader()方法获取到到当前的ClassLoader对象(AppClassLoader对象);
2、然后调用该ClassLoader对象的LoadClass方法loadClass()方法进行加载。
下面就来看LoadClass方法的实现流程:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 检查该Class对象是否已经加载过 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //如果有父加载器就交给父加载器加载 //否则就交给BootstrapClassLoader 加载器加载 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 } //如果父加载器 以及BootstrapClassLoader加载器都没有加载,就自己加载 if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); //自己加载,如果加载不到就抛出ClassNotFoundException异常。 //该方式收抽象方法,交给子类实现 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方法的核心流程如下:
1、首先通过findLoadedClass方法,检查自己是否已经加载过该class对象。最终调用的是native方法findLoadedClass0进行检查,由于是jvm实现的看不到源码。可以猜测到已经该类加载器 加载过的class对象,应该是被缓存起来了。这里还可以猜测到“类加载器”和class对象之前相互持有对方的引用,这也就是为什么class对象 以及方法区很难被“垃圾回收”的原因,除非首先想办法回收掉“类加载器”或者切断二者之间的引用关系(要自己去实现很复杂,OSGI在回收Bundle时,应该就是这么做的)。
2、类加载器,首先不自己加载,而是交给自己的父类加载器加载。从前面的讲解我们可以得知getSystemClassLoader()方法获取的真实的ClassLoader是AppClassLoader,其父类加载器是ExtClassLoader;父加载器ExtClassLoader又会重复步骤1检查自己是否已经加载过,否则到步骤2,此时ExtClassLoader的父加载器为空,就会交给BootstrapClassLoader加载器加载。
3、如果步骤2中,父加载器ExtClassLoader、以及BootstrapClassLoader加载器都没有加载,则自己调用findClass方法进行加载。
在上述三个步骤中提到的三个类加载器:BootstrapClassLoader、ExtClassLoader、AppClassLoader,下一节再详细说明这三者的关系。先来看下findClass方法,在ClassLoader中是抽象方法,在UrlClassLoader中进行了实现,内容就比粘贴了,大致就是URI路径找到class文件,然后调用自己defineClass方法进行取去文件内容到ByteBuffer中,最后再调用父类ClassLoader中的defineClass方法进行加载,该方式是native的,由jvm实现。简易序列图如下:
简单的理解就是 loadClass方法-->调用findClass方法-->调用defineClass方法
findClass方法一般交由子类子类字节去实现从哪里找“class”文件,UrlClassLoader的实现是在一个目录下找。当然也可以在网络中去找,只要把文件内容读取到ByteBuffer中就行。
至此关于类加载器如何加载一个Class对象的流程就分析完毕。
java中自带的三个类加载器
在上一节中,可以看到java中自带的三个类加载器:BootstrapClassLoader、ExtClassLoader、AppClassLoader。准确的说BootstrapClassLoader应该是JVM自带的类加载器,ExtClassLoader、AppClassLoader是jdk api中定义的类加载器,他们都是ClassLoader的间接子类,并且是Launcher的内部类。
三个类加载器的最大区别就是 他们各自加载类的路径不同,也就是我们常说的ClassPath。它们的类路径在Launcher类中都可以看到:
BootstrapClassLoader:读取系统配置sun.boot.class.path,默认配置路径为:System.out.println(System.getProperty("sun.boot.class.path"));
ExtClassLoader:读取系统配置java.ext.dirs,默认配置路径为:System.out.println(System.getProperty("java.ext.dirs"));
AppClassLoader:读取系统配置java.class.path,默认配置路径为:System.out.println(System.getProperty("java.class.path"));
执行上面的System.out.println就可以看到你电脑中,三个类加载器的加载路径。简单的理解:
BootstrapClassLoader加载的是java的核心包,比如rt.jar。
ExtClassLoader加载的是扩展包 $JAVA_HOME/ lib/ext
AppClassLoader加载的是自己应用开发的jar包(或者class文件)。
java 类加载器的父优先原则
从第二节中,我们还可以看到java类加载器的父优先原则 即:在需要加载一个Class对象时,AppClassLoader判断自己是否加载过,如果没有,自己先不加载,而是交给ExtClassLoader;ExtClassLoader判断自己是否加载过,如果没有就交给BootstrapClassLoader进行加载。
反过来,如果BootstrapClassLoader发现这个class文件不在自己的类路径下,就交给ExtClassLoader;ExtClassLoader尝试在自己的类路径下找,如果还是没有找到,就交给AppClassLoader;AppClassLoader尝试在自己的类路径下找,如果还是没有找到,就抛出ClassNotFoundException。在上述三步中的任意一步中 如果找到,就由该类加载器加载,并被缓存起来。这就是java 类加载器的父优先原则(也有称做:双亲委派模型)。
为什么java的类加载要采用父优先原则呢?其中一个最重要的目的就是为了防止我们重新java中定义的类。比如:你在自己的项目中定义了一个java.lang.String类,在运行时是永远都加载不到你自己这个类的,因为每次加载时都由BootstrapClassLoader类加载器 加载自己类路径下的java.lang.String类。
自定义类加载器
通过前面几节的讲解,相信都对java的类加载器有了大致的了解,如果我们想定义自己的类加载器也是件很容易的时。无非就是继承ClassLoader类,重新其findClass方法,也就是指定类加载器到哪里去读Class文件。当然还有一个跟简单的方法就是,仿照ExtClassLoader和AppClassLoader,直接继承UrlClassLoader。这里我们采用后者来展示一个最简单的示例:
public class MyClassLoader extends URLClassLoader{ public MyClassLoader(URL[] urls,ClassLoader parent) { super(urls,parent); } } 写个main方法测试下: public static void main(String[] args) throws Exception{ URL temp = new URL("file:///D:/test/"); URL [] urls = {temp}; MyClassLoader myClassLoader = new MyClassLoader(urls,MainTest.class.getClassLoader().getParent()); Class c1 = myClassLoader.loadClass("com.sky.main.Hello"); Object obj1 = c1.newInstance(); System.out.println("obj1类名:"+obj1.getClass().getName()); Method sayhello = c1.getMethod("sayHello"); sayhello.invoke(obj1);//调用该对象的sayhello方法 System.out.println("##################"); Hello obj2 = new Hello(); System.out.println("obj2类名:"+obj2.getClass().getName()); if(obj1 instanceof Hello){ System.out.println("obj1是com.sky.main.Hello类型"); }else{ System.out.println("obj1不是com.sky.main.Hello类型"); } }
这里指定了自定义MyClassLoader的类路径为“D:/test/”目录,同时设定了他的父类加载器为ExtClassLoader。另外还创建了一个Hello类,这里首先使用ExtClassLoader进行显式的加载;然后是又在mian方法中创建了一个Hello类的对象,这里会使用默认的AppClassLoader再次加载Hello类。Hello类很简单:
package com.sky.main; /** * Created by gantianxing on 2018/2/10. */ public class Hello { public void sayHello(){ System.out.println("hello,xiaoming"); } }
我们把编译好的Hello.class放到“D:\test\com\sky\main\”目录下,即可运行main方法了,打印信息为:
obj1类名:com.sky.main.Hello hello,xiaoming ################## obj2类名:com.sky.main.Hello obj1不是com.sky.main.Hello类型
是不是发现了一个很奇怪的问题:使用我们自定义的ClassLoader加载的obj1对象的类名是com.sky.main.Hello,但instanceof检测时却发现不是com.sky.main.Hello类型。这是因为这里分别使用了两个类加载器,加载Hello类。
感兴趣的朋友还可以把上面测试代码的MyClassLoader myClassLoader = new MyClassLoader(urls,MainTest.class.getClassLoader().getParent());这行代码改为:
MyClassLoader myClassLoader = new MyClassLoader(urls,MainTest.class.getClassLoader());
即把MyClassLoader的父类加载器设置为AppClassLoader,再运行一次,运行结果会不同。具体为什么呢?答案就是java类加载器的父优先原则,自己摸索下。
这里的自定义ClassLoader很简单,当然你还可以根据自己的需要重写findClass方法。
总结
本文首先讲解了什么是类加载器,以及java的类加载器是如何创建的,并对整个创建过程,以及类加载过程源码进行了分析。然后讲解了java自带类加载器的父优先原则。最后展示了如何创建自己的类加载器。
创建自定义类加载器的,可以在运行时实时的加载新的class对象,并且可以通过一定的手段卸载指定的class对象,从而实现class对象的热更新。但要实现对一个jar包的热更新,使用自定义ClassLoader来实现就显得非常复杂,因为你不知道哪些Class已经加载,也就不知道要卸载哪些Class对象,除非把整个ClassLoader先卸载。这是一个复杂的过程,庆幸的是OSGI已经帮我们做了这些事情,如果想实现对jar包的热更新,选择OSGI就行了。
上一篇: jdk1.8--新日期和时间处理
下一篇: CentOS下安装NSM