类加载器 ClassLoader即常说的类加载器,其功能是用于从Class文件加载所需的类,主要场景用于热部署、代码热替换等场景。 系统提供了3种类加载器:Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader。
Bootstrap ClassLoader 最顶层的加载器-启动类加载器,主要加载核心类库,%JRE_HOME%\lib
下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path
被指定的文件追加到默认的bootstrap路径中,Java程序无法直接引用该类加载器。
Extention ClassLoader 扩展类加载器,由Java实现,独立于虚拟机的外部,加载目录%JRE_HOME%\lib\ext
目录下的jar包和class文件,还可以加载-D java.ext.dirs
选项指定的目录。开发者可直接使用扩展类加载器。 该加载器是由sun.misc.Launcher$ExtClassLoader
实现。
Appclass Loader 应用程序类加载器,该加载器是由sun.misc.Launcher$AppClassLoader
实现,该类加载器负责加载用户类路径上所指定的类库。开发者可通过ClassLoader.getSystemClassLoader()
方法直接获取,故又称为系统类加载器。当应用程序没有自定义类加载器时,默认采用该类加载器。
加载顺序
Bootstrap CLassloder
Extention ClassLoader
AppClassLoader
Launcher分析 我们查看sun.misc.Launcher
类的部分源码,它是java虚拟机的入口应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class Launcher { private static Launcher launcher = new Launcher(); private static String bootClassPath = System.getProperty("sun.boot.class.path"); public static Launcher getLauncher() { return launcher; } private ClassLoader loader; public Launcher() { // Create the extension class loader ClassLoader extcl; try { extcl = ExtClassLoader.getExtClassLoader(); } catch (IOException e) { throw new InternalError( "Could not create extension class loader", e); } // Now create the class loader to use to launch the application try { loader = AppClassLoader.getAppClassLoader(extcl); } catch (IOException e) { throw new InternalError( "Could not create application class loader", e); } //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解 Thread.currentThread().setContextClassLoader(loader); }
根据上面部分源码我们可以知道:
Launcher在构造方法中初始化了ExtClassLoader
和AppClassLoader
。
Launcher定义了一个静态变量static String bootClassPath =System.getProperty("sun.boot.class.path");
,这个字符串”sun.boot.class.path”应该和BootstrapClassLoader
有关,猜测是BootstrapClassLoader
加载Jar包的路径。
我们可以输出一下这个值
1 System.out.println(System.getProperty("sun.boot.class.path"));
得到以下结果,可以看到,这些全是jre目录下的jar包或者是class文件
1 2 3 4 5 6 7 8 C:\Program Files\Java\jdk1.8.0_321\jre\lib\resources.jar; C:\Program Files\Java\jdk1.8.0_321\jre\lib\rt.jar; C:\Program Files\Java\jdk1.8.0_321\jre\lib\sunrsasign.jar; C:\Program Files\Java\jdk1.8.0_321\jre\lib\jsse.jar; C:\Program Files\Java\jdk1.8.0_321\jre\lib\jce.jar; C:\Program Files\Java\jdk1.8.0_321\jre\lib\charsets.jar; C:\Program Files\Java\jdk1.8.0_321\jre\lib\jfr.jar; C:\Program Files\Java\jdk1.8.0_321\jre\classes
ExtClassLoader分析 ExtClassLoader
部分源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 /* * The class loader used for loading installed extensions. */ static class ExtClassLoader extends URLClassLoader { static { ClassLoader.registerAsParallelCapable(); } /** * create an ExtClassLoader. The ExtClassLoader is created * within a context that limits which files it can read */ public static ExtClassLoader getExtClassLoader() throws IOException { final File[] dirs = getExtDirs(); try { // Prior implementations of this doPrivileged() block supplied // aa synthesized ACC via a call to the private method // ExtClassLoader.getContext(). return AccessController.doPrivileged( new PrivilegedExceptionAction<ExtClassLoader>() { public ExtClassLoader run() throws IOException { int len = dirs.length; for (int i = 0; i < len; i++) { MetaIndex.registerDirectory(dirs[i]); } return new ExtClassLoader(dirs); } }); } catch (java.security.PrivilegedActionException e) { throw (IOException) e.getException(); } } private static File[] getExtDirs() { String s = System.getProperty("java.ext.dirs"); File[] dirs; if (s != null) { StringTokenizer st = new StringTokenizer(s, File.pathSeparator); int count = st.countTokens(); dirs = new File[count]; for (int i = 0; i < count; i++) { dirs[i] = new File(st.nextToken()); } } else { dirs = new File[0]; } return dirs; } ...... }
在先前的内容有说过,可以指定-D java.ext.dirs
参数来添加和改变ExtClassLoader
的加载路径。这里我们编写测试代码。
1 System.out.println(System.getProperty("java.ext.dirs"));
得到以下结果
1 2 C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext; C:\Windows\Sun\Java\lib\ext
源码中通过ExtClassLoader$getExtDirs
获取到了”java.ext.dirs”的路径值,然后在
ExtClassLoader$getExtClassLoader
中被调用用来加载。
AppClassLoader分析 AppClassLoader部分源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /** * The class loader used for loading from java.class.path. * runs in a restricted security context. */ static class AppClassLoader extends URLClassLoader { public static ClassLoader getAppClassLoader(final ClassLoader extcl) throws IOException { final String s = System.getProperty("java.class.path"); final File[] path = (s == null) ? new File[0] : getClassPath(s); return AccessController.doPrivileged( new PrivilegedAction<AppClassLoader>() { public AppClassLoader run() { URL[] urls = (s == null) ? new URL[0] : pathToURLs(path); return new AppClassLoader(urls, extcl); } }); } ...... }
在源码中我们看到AppClassLoader
加载的就是java.class.path
下的路径。
加载顺序分析 前面了解到了BootstrapClassLoader
、ExtClassLoader
、AppClassLoader
实际是获取了相应的环境属性sun.boot.class.path
、java.ext.dirs
和java.class.path
来加载资源文件的。
获取Test.class文件的类加载器:
1 2 ClassLoader cl = Test.class.getClassLoader(); System.out.println("ClassLoader is:"+cl.toString());
输出结果如下
结果说明Test.class文件是由AppClassLoader
加载的,这个Test类是我们自己编写的,那我们再测试下一些基础类是由哪个加载器加载的
1 2 3 4 5 6 ClassLoader cl = Test.class.getClassLoader(); System.out.println("ClassLoader is:"+cl.toString()); cl = String.class.getClassLoader(); System.out.println("ClassLoader is:"+cl.toString()); cl = int.class.getClassLoader(); System.out.println("ClassLoader is:"+cl.toString());
运行报错,提示空指针
实际上int.class
等基础类是由Bootstrap ClassLoader
加载的
每个类加载器都有一个父加载器,比如加载Test.class
是由AppClassLoader
完成,那么AppClassLoader
也有一个父加载器,通过getParent
方法。代码如下:
1 2 3 ClassLoader cl = Test.class.getClassLoader(); System.out.println("ClassLoader is:"+cl.toString()); System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());
结果如下:
根据结果我们可以知道,AppClassLoader
的父加载器是ExtClassLoader
,因此我们可以在获取下ExtClassLoader
的父加载器
1 2 3 System.out.println("ClassLoader is:"+cl.toString()); System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString()); System.out.println("ClassLoader\'s grand father is:"+cl.getParent().getParent().toString());
运行结果如下:
一样报的空指针异常,这表明ExtClassLoader
没有父加载器?往下分析
ExtClassLoader
和AppClassLoader
继承同一个父类URLClassLoader
1 2 static class ExtClassLoader extends URLClassLoader {} static class AppClassLoader extends URLClassLoader {}
调用AppClassLoader
的getParent()
代码为什么会得到ExtClassLoader
的实例呢
先了解下URLClassLoader
的类继承图
在ClassLoader.java
中发现getParent()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public abstract class ClassLoader { // The parent class loader for delegation // Note: VM hardcoded the offset of this field, thus all new fields // must be added *after* it. private final ClassLoader parent; // The class loader for the system // @GuardedBy("ClassLoader.class") private static ClassLoader scl; private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; ... } protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent); } protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); } public final ClassLoader getParent() { if (parent == null) return null; return parent; } public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); if (scl == null) { return null; } return scl; } private static synchronized void initSystemClassLoader() { if (!sclSet) { if (scl != null) throw new IllegalStateException("recursive invocation"); sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); if (l != null) { Throwable oops = null; //通过Launcher获取ClassLoader scl = l.getClassLoader(); try { scl = AccessController.doPrivileged( new SystemClassLoaderAction(scl)); } catch (PrivilegedActionException pae) { oops = pae.getCause(); if (oops instanceof InvocationTargetException) { oops = oops.getCause(); } } if (oops != null) { if (oops instanceof Error) { throw (Error) oops; } else { // wrap the exception throw new Error(oops); } } } sclSet = true; } } }
可以看到getParent()
实际上返回的就是一个ClassLoader
对象
1 private final ClassLoader parent;
parent的赋值是在ClassLoader对象的构造方法中,根据上面的源码,我们可以发现
它可以通过两种方式赋值:
由外部类创建ClassLoader时直接指定一个ClassLoader为parent。
由getSystemClassLoader()
方法生成,也就是在sun.misc.Laucher
通过getClassLoader()
获取,也就是AppClassLoader
。也就是说,一个ClassLoader
创建时如果没有指定parent,那么它的parent默认就是AppClassLoader
。
现在探究下ExtClassLoader
与AppClassLoader
的parent的来源
从代码中可以看到AppClassLoader
的parent是一个ExtClassLoader
实例。ExtClassLoader
并没有直接找到对parent的赋值。它调用了它的父类也就是URLClassLoder
的构造方法并传递了3个参数。
在父类的构造方法中,我们可以看到传递的parent为null
综上,AppClassLoader
的parent是ExtClassLoader
,ExtClassLoader
的parent是null。
继续往下,BootstrapClassLoader
是如何创建的?
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
的父加载器。Bootstrap
没有父加载器,但是它却可以作用一个ClassLoader
的父加载器。比如ExtClassLoader
。这也可以解释之前通过ExtClassLoader
的getParent方法获取为Null的现象。
双亲委托 一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
流程图:
这个更具体一些
描述:
一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
递归,重复第1部的操作。
如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。
在了解加载过程时,需要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。
loadClass():加载具有指定二进制名称的类。此方法搜索类的方式与loadClass(String, boolean)
方法相同,Java虚拟机调用它来解析类引用。
findClass():根据名称或位置加载.class字节码,然后使用defineClass,通常由子类去实现。
defineClass():把字节码转化为Class。
findLoadedClass():判断该类是否已经加载过,加载过就返回Class对象,未加载过就返回null。
1 2 3 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
下面为loadClass(String, boolean)方法的源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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) { //父加载器不为空则调用父加载器的loadClass c = parent.loadClass(name, false); } else { //父加载器为空则调用Bootstrap Classloader 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(); //父加载器没有找到,则调用findclass 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() resolveClass(c); } return c; } }
大致的流程是:
执行findLoadedClass(String)
去检测这个class是不是已经加载过了。
执行父加载器的loadClass
方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader
。这也解释了ExtClassLoader
的parent为null,但仍然说Bootstrap ClassLoader
是它的父加载器。
如果向上委托父加载器没有加载成功,则通过findClass(String)
查找。
如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()
又会调用resolveClass(Class)
这个方法来生成最终的Class对象
如果要编写一个classLoader的子类,也就是自定义一个classloader,建议覆盖findClass()
方法,而不要直接改写loadClass()
方法。
自定义ClassLoader 不管是Bootstrap ClassLoader
还是ExtClassLoader
等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样的话,需要自定义一个ClassLoader。
步骤分为如下几步:
编写一个类继承自ClassLoader
抽象类。
复写它的findClass()
方法。
在findClass()
方法中调用defineClass()
。
注:如果自定义一个ClassLoader
,默认的parent父加载器是AppClassLoader
,因为这样就能够保证它能访问系统内置加载器成功加载class文件。
测试:自定义一个ClassLoader
,默认加载路径为D:\code\lib
下的jar包和资源。
Say.java
1 2 3 4 5 public class Say { public void say(){ System.out.println("Say Hello"); } }
编译成class文件后放到D:\code\lib
路径下。
DiskClassLoader.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import java.io.*; public class DiskClassLoader extends ClassLoader{ private String mLibPath; public DiskClassLoader(String mLibPath){ this.mLibPath = mLibPath; } @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) { // TODO Auto-generated method stub int index = name.lastIndexOf('.'); if(index == -1){ return name+".class"; }else{ return name.substring(index+1)+".class"; } } }
ClassLoaderTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ClassLoaderTest { public static void main(String[] args) { //创建自定义classloader对象。 DiskClassLoader diskLoader = new DiskClassLoader("D:\\code\\lib"); try { Class clazz = diskLoader.loadClass("Say"); if(clazz!=null){ Object o = clazz.newInstance(); Method say = clazz.getDeclaredMethod("say", null); say.invoke(o, null); } } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { e.printStackTrace(); } } }
运行结果如下:
Context ClassLoader 线程上下文类加载器 ContextClassLoader
其实只是一个概念,查看Thread.java
源码可以发现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Thread implements Runnable { /* The context ClassLoader for this thread */ private ClassLoader contextClassLoader; public void setContextClassLoader(ClassLoader cl) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new RuntimePermission("setContextClassLoader")); } contextClassLoader = cl; } public ClassLoader getContextClassLoader() { if (contextClassLoader == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { ClassLoader.checkClassLoaderPermission(contextClassLoader, Reflection.getCallerClass()); } return contextClassLoader; } }
contextClassLoader只是一个成员变量,通过setContextClassLoader()
方法设置,通过getContextClassLoader()
返回。
编写测试代码来加深理解,编写一个接口类ISpeak
1 2 3 4 public interface ISpeak { public void speak(); }
再编写2个SpeakTest.java
文件实现ISpeak
接口,一个源码是:
1 2 3 4 5 6 7 8 public class SpeakTest implements ISpeak { @Override public void speak() { // TODO Auto-generated method stub System.out.println("Test"); } }
它生成的SpeakTest.class
文件放置在D:\\code\\lib\\test
目录下。
然后,另一个SpeakTest.java
1 2 3 4 5 6 7 8 public class SpeakTest implements ISpeak { @Override public void speak() { // TODO Auto-generated method stub System.out.println("I am boy"); } }
它生成的SpeakTest.class
文件放置在D:\\code\\lib
目录下。
再编写DiskClassLoader1
和DiskClassLoader2
,代码和DiskClassLoader
基本一致,修改下加载路径和类路径即可。
DiskClassLoader1.java
的main方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static void main(String[] args) { DiskClassLoader1 diskLoader1 = new DiskClassLoader1("D:\\code\\lib\\test"); Class cls1 = null; try { //加载class文件 cls1 = diskLoader1.loadClass("SpeakTest"); System.out.println(cls1.getClassLoader().toString()); if(cls1 != null){ try { Object obj = cls1.newInstance(); //SpeakTest1 speak = (SpeakTest1) obj; //speak.speak(); Method method = cls1.getDeclaredMethod("speak",null); //通过反射调用Test类的speak方法 method.invoke(obj, null); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
运行结果如下:
DiskClassLoader2.java
的main方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public static void main(String[] args) { DiskClassLoader2 diskLoader2 = new DiskClassLoader2("D:\\code\\lib"); System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString()); new Thread(new Runnable() { @Override public void run() { System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString()); // TODO Auto-generated method stub try { //加载class文件 // Thread.currentThread().setContextClassLoader(diskLoader); //Class c = diskLoader.loadClass("SpeakTest"); ClassLoader cl = Thread.currentThread().getContextClassLoader(); Class c = cl.loadClass("SpeakTest"); // Class c = Class.forName("SpeakTest"); System.out.println(c.getClassLoader().toString()); if(c != null){ try { Object obj = c.newInstance(); //SpeakTest1 speak = (SpeakTest1) obj; //speak.speak(); Method method = c.getDeclaredMethod("speak",null); //通过反射调用Test类的say方法 method.invoke(obj, null); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }).start(); }
运行结果如下:
根据结果我们可以得出:
DiskClassLoader1
成功加载了SpeakTest.class
文件。
主线程和子线程的ContextClassLoader
是AppClassLoader
。
AppClassLoader
加载不了父线程当中已经加载的SpeakTest.class
内容。
修改代码,再子线程开头加上
1 Thread.currentThread().setContextClassLoader(diskLoader2);
结果如下:
可以看到子线程的ContextClassLoader
变成了DiskClassLoader
。
修改diskLoader2为diskLoader1,结果如下:
可以看到DiskClassLoader2
和DiskClassLoader1
分别加载了自己路径下的SpeakTest.class
文件。
参考 https://blog.csdn.net/briblue/article/details/54973413
http://gityuan.com/2016/01/24/java-classloader/