ClassLoader翻译过来就是类加载器,普通的java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。理解ClassLoader的加载机制,也有利于我们编写出更高效的代码。ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。本文的目的也是学习ClassLoader这种加载机制。

Class文件的认识

我们都知道在Java中程序是运行在虚拟机中,我们平常用文本编辑器或者是IDE编写的程序都是.java格式的文件,这是最基础的源码,但这类文件是不能直接运行的。如我们编写一个简单的程序HelloWorld.java

  1. public class HelloWorld{
  2. public static void main(String[] args){
  3. System.out.println("Hello world!");
  4. }
  5. }

如图:
java中的ClassLoader详解 - 图1
然后,我们需要在命令行中进行java文件的编译

  1. javac HelloWorld.java

java中的ClassLoader详解 - 图2
可以看到目录下生成了.class文件
我们再从命令行中执行命令

  1. java HelloWorld

java中的ClassLoader详解 - 图3

上面是基本代码示例,是所有入门JAVA语言时都学过的东西,这里重新拿出来是想让大家将焦点回到class文件上,class文件是字节码格式文件,java虚拟机并不能直接识别我们平常编写的.java源文件,所以需要javac这个命令转换成.class文件。另外,如果用C或者PYTHON编写的程序正确转换成.class文件后,java虚拟机也是可以识别运行的。更多信息大家可以参考这篇
了解了.class文件后,我们再来思考下,我们平常在Eclipse中编写的java程序是如何运行的,也就是我们自己编写的各种类是如何被加载到jvm(java虚拟机)中去的。

你还记得java环境变量吗?

初学java的时候,最害怕的就是下载JDK后要配置环境变量了,关键是当时不理解,所以战战兢兢地照着书籍上或者是网络上的介绍进行操作。然后下次再弄的时候,又忘记了而且是必忘。当时,心里的想法很气愤的,想着是–这东西一点也不人性化,为什么非要自己配置环境变量呢?太不照顾菜鸟和新手了,很多菜鸟就是因为卡在环境变量的配置上,遭受了太多的挫败感。
因为我是在Windows下编程的,所以只讲Window平台上的环境变量,主要有3个:JAVA_HOMEPATHCLASSPATH

JAVA_HOME

指的是你JDK安装的位置,一般默认安装在C盘,如

  1. C:\Program Files\Java\jdk1.8.0_91

PATH

将程序路径包含在PATH当中后,在命令行窗口就可以直接键入它的名字了,而不再需要键入它的全路径,比如上面代码中我用的到javac和java两个命令。
一般的

  1. PATH=%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;%PATH%;

也就是在原来的PATH路径上添加JDK目录下的bin目录和jre目录的bin.

CLASSPATH

  1. CLASSPATH=.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar

一看就是指向jar包路径。
需要注意的是前面的.;,.代表当前目录。

环境变量的设置与查看

设置可以右击我的电脑,然后点击属性,再点击高级,然后点击环境变量,具体不明白的自行查阅文档。
查看的话可以打开命令行窗口

  1. echo %JAVA_HOME%
  2. echo %PATH%
  3. echo %CLASSPATH%

JAVA类加载流程

Java语言系统自带有三个类加载器:

  • Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。
  • Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
  • Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类。

我们上面简单介绍了3个ClassLoader。说明了它们加载的路径。并且还提到了-Xbootclasspath和-D java.ext.dirs这两个虚拟机参数选项。

加载顺序?

我们看到了系统的3个类加载器,但我们可能不知道具体哪个先行呢?
我可以先告诉你答案

  1. Bootstrap CLassloder
  2. Extention ClassLoader
  3. AppClassLoader

为了更好的理解,我们可以查看源码。
sun.misc.Launcher,它是一个java虚拟机的入口应用。

  1. public class Launcher {
  2. private static Launcher launcher = new Launcher();
  3. private static String bootClassPath =
  4. System.getProperty("sun.boot.class.path");
  5. public static Launcher getLauncher() {
  6. return launcher;
  7. }
  8. private ClassLoader loader;
  9. public Launcher() {
  10. // Create the extension class loader
  11. ClassLoader extcl;
  12. try {
  13. extcl = ExtClassLoader.getExtClassLoader();
  14. } catch (IOException e) {
  15. throw new InternalError(
  16. "Could not create extension class loader", e);
  17. }
  18. // Now create the class loader to use to launch the application
  19. try {
  20. loader = AppClassLoader.getAppClassLoader(extcl);
  21. } catch (IOException e) {
  22. throw new InternalError(
  23. "Could not create application class loader", e);
  24. }
  25. //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
  26. Thread.currentThread().setContextClassLoader(loader);
  27. }
  28. /*
  29. * Returns the class loader used to launch the main application.
  30. */
  31. public ClassLoader getClassLoader() {
  32. return loader;
  33. }
  34. /*
  35. * The class loader used for loading installed extensions.
  36. */
  37. static class ExtClassLoader extends URLClassLoader {}
  38. /**
  39. * The class loader used for loading from java.class.path.
  40. * runs in a restricted security context.
  41. */
  42. static class AppClassLoader extends URLClassLoader {}
  43. }

源码有精简,我们可以得到相关的信息。

  1. Launcher初始化了ExtClassLoader和AppClassLoader。
  2. Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty(“sun.boot.class.path”)得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。

我们可以先代码测试一下sun.boot.class.path是什么内容。

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

得到的结果是:

  1. C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;
  2. C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;
  3. C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;
  4. C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;
  5. C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;
  6. C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;
  7. C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;
  8. C:\Program Files\Java\jre1.8.0_91\classes

可以看到,这些全是JRE目录下的jar包或者是class文件。

ExtClassLoader源码

如果你有足够的好奇心,你应该会对它的源码感兴趣

  1. /*
  2. * The class loader used for loading installed extensions.
  3. */
  4. static class ExtClassLoader extends URLClassLoader {
  5. static {
  6. ClassLoader.registerAsParallelCapable();
  7. }
  8. /**
  9. * create an ExtClassLoader. The ExtClassLoader is created
  10. * within a context that limits which files it can read
  11. */
  12. public static ExtClassLoader getExtClassLoader() throws IOException
  13. {
  14. final File[] dirs = getExtDirs();
  15. try {
  16. // Prior implementations of this doPrivileged() block supplied
  17. // aa synthesized ACC via a call to the private method
  18. // ExtClassLoader.getContext().
  19. return AccessController.doPrivileged(
  20. new PrivilegedExceptionAction<ExtClassLoader>() {
  21. public ExtClassLoader run() throws IOException {
  22. int len = dirs.length;
  23. for (int i = 0; i < len; i++) {
  24. MetaIndex.registerDirectory(dirs[i]);
  25. }
  26. return new ExtClassLoader(dirs);
  27. }
  28. });
  29. } catch (java.security.PrivilegedActionException e) {
  30. throw (IOException) e.getException();
  31. }
  32. }
  33. private static File[] getExtDirs() {
  34. String s = System.getProperty("java.ext.dirs");
  35. File[] dirs;
  36. if (s != null) {
  37. StringTokenizer st =
  38. new StringTokenizer(s, File.pathSeparator);
  39. int count = st.countTokens();
  40. dirs = new File[count];
  41. for (int i = 0; i < count; i++) {
  42. dirs[i] = new File(st.nextToken());
  43. }
  44. } else {
  45. dirs = new File[0];
  46. }
  47. return dirs;
  48. }
  49. ......
  50. }

我们先前的内容有说过,可以指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径。这里我们通过可以编写测试代码。

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

结果如下:

  1. C:\Program Files\Java\jre1.8.0_91\lib\ext;C:\Windows\Sun\Java\lib\ext

AppClassLoader源码

  1. /**
  2. * The class loader used for loading from java.class.path.
  3. * runs in a restricted security context.
  4. */
  5. static class AppClassLoader extends URLClassLoader {
  6. public static ClassLoader getAppClassLoader(final ClassLoader extcl)
  7. throws IOException
  8. {
  9. final String s = System.getProperty("java.class.path");
  10. final File[] path = (s == null) ? new File[0] : getClassPath(s);
  11. return AccessController.doPrivileged(
  12. new PrivilegedAction<AppClassLoader>() {
  13. public AppClassLoader run() {
  14. URL[] urls =
  15. (s == null) ? new URL[0] : pathToURLs(path);
  16. return new AppClassLoader(urls, extcl);
  17. }
  18. });
  19. }
  20. ......
  21. }

可以看到AppClassLoader加载的就是java.class.path下的路径。我们同样打印它的值。

  1. System.out.println(System.getProperty("java.class.path"));

结果:

  1. D:\workspace\ClassLoaderDemo\bin

这个路径其实就是当前java工程目录bin,里面存放的是编译生成的class文件。
好了,自此我们已经知道了BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是查阅相应的环境属性sun.boot.class.path、java.ext.dirs和java.class.path来加载资源文件的。
接下来我们探讨它们的加载顺序,我们先用Eclipse建立一个java工程。
java中的ClassLoader详解 - 图4
然后创建一个Test.java文件。

  1. public class Test{}

然后,编写一个ClassLoaderTest.java文件。

  1. public class ClassLoaderTest {
  2. public static void main(String[] args) {
  3. // TODO Auto-generated method stub
  4. ClassLoader cl = Test.class.getClassLoader();
  5. System.out.println("ClassLoader is:"+cl.toString());
  6. }
  7. }

我们获取到了Test.class文件的类加载器,然后打印出来。结果是:

  1. ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93

也就是说明Test.class文件是由AppClassLoader加载的。
这个Test类是我们自己编写的,那么int.class或者是String.class的加载是由谁完成的呢?
我们可以在代码中尝试

  1. public class ClassLoaderTest {
  2. public static void main(String[] args) {
  3. // TODO Auto-generated method stub
  4. ClassLoader cl = Test.class.getClassLoader();
  5. System.out.println("ClassLoader is:"+cl.toString());
  6. cl = int.class.getClassLoader();
  7. System.out.println("ClassLoader is:"+cl.toString());
  8. }
  9. }

运行一下,却报错了

  1. ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
  2. Exception in thread "main" java.lang.NullPointerException
  3. at ClassLoaderTest.main(ClassLoaderTest.java:15)

提示的是空指针,意思是int.class这类基础类没有类加载器加载?
当然不是!
int.class是由Bootstrap ClassLoader加载的。要想弄明白这些,我们首先得知道一个前提。

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

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

  1. ClassLoader cl = Test.class.getClassLoader();
  2. System.out.println("ClassLoader is:"+cl.toString());
  3. System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());

运行结果如下:

  1. ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
  2. ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742

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

  1. System.out.println("ClassLoader is:"+cl.toString());
  2. System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());
  3. System.out.println("ClassLoader\'s grand father is:"+cl.getParent().getParent().toString());

运行如果:

  1. ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
  2. Exception in thread "main" ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742
  3. java.lang.NullPointerException
  4. at ClassLoaderTest.main(ClassLoaderTest.java:13)

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

父加载器不是父类

我们先前已经粘贴了ExtClassLoader和AppClassLoader的代码。

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

可以看见ExtClassLoader和AppClassLoader同样继承自URLClassLoader,但上面一小节代码中,为什么调用AppClassLoader的getParent()代码会得到ExtClassLoader的实例呢?先从URLClassLoader说起,这个类又是什么?
先上一张类的继承关系图
java中的ClassLoader详解 - 图5
URLClassLoader的源码中并没有找到getParent()方法。这个方法在ClassLoader.java中。

  1. public abstract class ClassLoader {
  2. // The parent class loader for delegation
  3. // Note: VM hardcoded the offset of this field, thus all new fields
  4. // must be added *after* it.
  5. private final ClassLoader parent;
  6. // The class loader for the system
  7. // @GuardedBy("ClassLoader.class")
  8. private static ClassLoader scl;
  9. private ClassLoader(Void unused, ClassLoader parent) {
  10. this.parent = parent;
  11. ...
  12. }
  13. protected ClassLoader(ClassLoader parent) {
  14. this(checkCreateClassLoader(), parent);
  15. }
  16. protected ClassLoader() {
  17. this(checkCreateClassLoader(), getSystemClassLoader());
  18. }
  19. public final ClassLoader getParent() {
  20. if (parent == null)
  21. return null;
  22. return parent;
  23. }
  24. public static ClassLoader getSystemClassLoader() {
  25. initSystemClassLoader();
  26. if (scl == null) {
  27. return null;
  28. }
  29. return scl;
  30. }
  31. private static synchronized void initSystemClassLoader() {
  32. if (!sclSet) {
  33. if (scl != null)
  34. throw new IllegalStateException("recursive invocation");
  35. sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
  36. if (l != null) {
  37. Throwable oops = null;
  38. //通过Launcher获取ClassLoader
  39. scl = l.getClassLoader();
  40. try {
  41. scl = AccessController.doPrivileged(
  42. new SystemClassLoaderAction(scl));
  43. } catch (PrivilegedActionException pae) {
  44. oops = pae.getCause();
  45. if (oops instanceof InvocationTargetException) {
  46. oops = oops.getCause();
  47. }
  48. }
  49. if (oops != null) {
  50. if (oops instanceof Error) {
  51. throw (Error) oops;
  52. } else {
  53. // wrap the exception
  54. throw new Error(oops);
  55. }
  56. }
  57. }
  58. sclSet = true;
  59. }
  60. }
  61. }

我们可以看到getParent()实际上返回的就是一个ClassLoader对象parent,parent的赋值是在ClassLoader对象的构造方法中,它有两个情况:

  1. 由外部类创建ClassLoader时直接指定一个ClassLoader为parent。
  2. 由getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。直白的说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。

我们主要研究的是ExtClassLoader与AppClassLoader的parent的来源,正好它们与Launcher类有关,我们上面已经粘贴过Launcher的部分代码。

  1. public class Launcher {
  2. private static URLStreamHandlerFactory factory = new Factory();
  3. private static Launcher launcher = new Launcher();
  4. private static String bootClassPath =
  5. System.getProperty("sun.boot.class.path");
  6. public static Launcher getLauncher() {
  7. return launcher;
  8. }
  9. private ClassLoader loader;
  10. public Launcher() {
  11. // Create the extension class loader
  12. ClassLoader extcl;
  13. try {
  14. extcl = ExtClassLoader.getExtClassLoader();
  15. } catch (IOException e) {
  16. throw new InternalError(
  17. "Could not create extension class loader", e);
  18. }
  19. // Now create the class loader to use to launch the application
  20. try {
  21. //将ExtClassLoader对象实例传递进去
  22. loader = AppClassLoader.getAppClassLoader(extcl);
  23. } catch (IOException e) {
  24. throw new InternalError(
  25. "Could not create application class loader", e);
  26. }
  27. public ClassLoader getClassLoader() {
  28. return loader;
  29. }
  30. static class ExtClassLoader extends URLClassLoader {
  31. /**
  32. * create an ExtClassLoader. The ExtClassLoader is created
  33. * within a context that limits which files it can read
  34. */
  35. public static ExtClassLoader getExtClassLoader() throws IOException {
  36. final File[] dirs = getExtDirs();
  37. try {
  38. // Prior implementations of this doPrivileged() block supplied
  39. // aa synthesized ACC via a call to the private method
  40. // ExtClassLoader.getContext().
  41. return AccessController.doPrivileged(
  42. new PrivilegedExceptionAction<ExtClassLoader>() {
  43. public ExtClassLoader run() throws IOException {
  44. //ExtClassLoader在这里创建
  45. return new ExtClassLoader(dirs);
  46. }
  47. });
  48. } catch (java.security.PrivilegedActionException e) {
  49. throw (IOException) e.getException();
  50. }
  51. }
  52. /*
  53. * Creates a new ExtClassLoader for the specified directories.
  54. */
  55. public ExtClassLoader(File[] dirs) throws IOException {
  56. super(getExtURLs(dirs), null, factory);
  57. }
  58. }
  59. }

我们需要注意的是

  1. ClassLoader extcl;
  2. extcl = ExtClassLoader.getExtClassLoader();
  3. loader = AppClassLoader.getAppClassLoader(extcl);

代码已经说明了问题AppClassLoader的parent是一个ExtClassLoader实例。
ExtClassLoader并没有直接找到对parent的赋值。它调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数。

  1. public ExtClassLoader(File[] dirs) throws IOException {
  2. super(getExtURLs(dirs), null, factory);
  3. }

对应的代码

  1. public URLClassLoader(URL[] urls, ClassLoader parent,
  2. URLStreamHandlerFactory factory) {
  3. super(parent);
  4. }

答案已经很明了了,ExtClassLoader的parent为null。
上面张贴这么多代码也是为了说明AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是null。这符合我们之前编写的测试代码。
不过,细心的同学发现,还是有疑问的我们只看到ExtClassLoader和AppClassLoader的创建,那么BootstrapClassLoader呢?
还有,ExtClassLoader的父加载器为null,但是Bootstrap CLassLoader却可以当成它的父加载器这又是为何呢?
我们继续往下进行。

Bootstrap ClassLoader是由C++编写的。

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找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
整个流程可以如下图所示:
java中的ClassLoader详解 - 图6
这张图是用时序图画出来的,不过画出来的结果我却自己都觉得不理想。
大家可以看到2根箭头,蓝色的代表类加载器向上委托的方向,如果当前的类加载器没有查询到这个class对象已经加载就请求父加载器(不一定是父类)进行操作,然后以此类推。直到Bootstrap ClassLoader。如果Bootstrap ClassLoader也没有加载过此class实例,那么它就会从它指定的路径中去查找,如果查找成功则返回,如果没有查找成功则交给子类加载器,也就是ExtClassLoader,这样类似操作直到终点,也就是我上图中的红色箭头示例。
用序列描述一下:

  1. 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
  2. 递归,重复第1部的操作。
  3. 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
  4. Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
  5. ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

上面的序列,详细说明了双亲委托的加载流程。我们可以发现委托是从下向上,然后具体查找过程却是自上至下。
我说过上面用时序图画的让自己不满意,现在用框图,最原始的方法再画一次。
java中的ClassLoader详解 - 图7
上面已经详细介绍了加载过程,但具体为什么是这样加载,我们还需要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。

重要方法

loadClass()

JDK文档中是这样写的,通过指定的全限定类名加载class,它通过同名的loadClass(String,boolean)方法。

  1. protected Class<?> loadClass(String name,
  2. boolean resolve)
  3. throws ClassNotFoundException

上面是方法原型,一般实现这个方法的步骤是

  1. 执行findLoadedClass(String)去检测这个class是不是已经加载过了。
  2. 执行父加载器的loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
  3. 如果向上委托父加载器没有加载成功,则通过findClass(String)查找。

如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。 我们可以从源代码看出这个步骤。

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // 首先,检测是否已经加载
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. long t0 = System.nanoTime();
  9. try {
  10. if (parent != null) {
  11. //父加载器不为空则调用父加载器的loadClass
  12. c = parent.loadClass(name, false);
  13. } else {
  14. //父加载器为空则调用Bootstrap Classloader
  15. c = findBootstrapClassOrNull(name);
  16. }
  17. } catch (ClassNotFoundException e) {
  18. // ClassNotFoundException thrown if class not found
  19. // from the non-null parent class loader
  20. }
  21. if (c == null) {
  22. // If still not found, then invoke findClass in order
  23. // to find the class.
  24. long t1 = System.nanoTime();
  25. //父加载器没有找到,则调用findclass
  26. c = findClass(name);
  27. // this is the defining class loader; record the stats
  28. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  29. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  30. sun.misc.PerfCounter.getFindClasses().increment();
  31. }
  32. }
  33. if (resolve) {
  34. //调用resolveClass()
  35. resolveClass(c);
  36. }
  37. return c;
  38. }
  39. }

代码解释了双亲委托。
另外,要注意的是如果要编写一个classLoader的子类,也就是自定义一个classloader,建议覆盖findClass()方法,而不要直接改写loadClass()方法。
另外

  1. if (parent != null) {
  2. //父加载器不为空则调用父加载器的loadClass
  3. c = parent.loadClass(name, false);
  4. } else {
  5. //父加载器为空则调用Bootstrap Classloader
  6. c = findBootstrapClassOrNull(name);
  7. }

前面说过ExtClassLoader的parent为null,所以它向上委托时,系统会为它指定Bootstrap ClassLoader。

自定义ClassLoader

不知道大家有没有发现,不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?
如果要这样做的话,需要我们自定义一个classloader。

自定义步骤

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

    defineClass()

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

    注意点:

    一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。
    上面说的是,如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。

    自定义ClassLoader示例之DiskClassLoader。

    假设我们需要一个自定义的classloader,默认加载路径为D:\lib下的jar包和资源。
    我们写编写一个测试用的类文件,Test.java

    Test.java

    ```java package com.frank.test;

public class Test {

  1. public void say(){
  2. System.out.println("Say Hello");
  3. }

}

  1. 然后将它编译过年class文件Test.class放到D:\lib这个路径下。
  2. <a name="UNQil"></a>
  3. #### DiskClassLoader
  4. 我们编写DiskClassLoader的代码。
  5. ```java
  6. import java.io.ByteArrayOutputStream;
  7. import java.io.File;
  8. import java.io.FileInputStream;
  9. import java.io.FileNotFoundException;
  10. import java.io.IOException;
  11. public class DiskClassLoader extends ClassLoader {
  12. private String mLibPath;
  13. public DiskClassLoader(String path) {
  14. // TODO Auto-generated constructor stub
  15. mLibPath = path;
  16. }
  17. @Override
  18. protected Class<?> findClass(String name) throws ClassNotFoundException {
  19. // TODO Auto-generated method stub
  20. String fileName = getFileName(name);
  21. File file = new File(mLibPath,fileName);
  22. try {
  23. FileInputStream is = new FileInputStream(file);
  24. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  25. int len = 0;
  26. try {
  27. while ((len = is.read()) != -1) {
  28. bos.write(len);
  29. }
  30. } catch (IOException e) {
  31. e.printStackTrace();
  32. }
  33. byte[] data = bos.toByteArray();
  34. is.close();
  35. bos.close();
  36. return defineClass(name,data,0,data.length);
  37. } catch (IOException e) {
  38. // TODO Auto-generated catch block
  39. e.printStackTrace();
  40. }
  41. return super.findClass(name);
  42. }
  43. //获取要加载 的class文件名
  44. private String getFileName(String name) {
  45. // TODO Auto-generated method stub
  46. int index = name.lastIndexOf('.');
  47. if(index == -1){
  48. return name+".class";
  49. }else{
  50. return name.substring(index+1)+".class";
  51. }
  52. }
  53. }

我们在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。

测试

现在我们要编写测试代码。我们知道如果调用一个Test对象的say方法,它会输出”Say Hello”这条字符串。但现在是我们把Test.class放置在应用工程所有的目录之外,我们需要加载它,然后执行它的方法。具体效果如何呢?我们编写的DiskClassLoader能不能顺利完成任务呢?我们拭目以待。

  1. import java.lang.reflect.InvocationTargetException;
  2. import java.lang.reflect.Method;
  3. public class ClassLoaderTest {
  4. public static void main(String[] args) {
  5. // TODO Auto-generated method stub
  6. //创建自定义classloader对象。
  7. DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
  8. try {
  9. //加载class文件
  10. Class c = diskLoader.loadClass("com.frank.test.Test");
  11. if(c != null){
  12. try {
  13. Object obj = c.newInstance();
  14. Method method = c.getDeclaredMethod("say",null);
  15. //通过反射调用Test类的say方法
  16. method.invoke(obj, null);
  17. } catch (InstantiationException | IllegalAccessException
  18. | NoSuchMethodException
  19. | SecurityException |
  20. IllegalArgumentException |
  21. InvocationTargetException e) {
  22. // TODO Auto-generated catch block
  23. e.printStackTrace();
  24. }
  25. }
  26. } catch (ClassNotFoundException e) {
  27. // TODO Auto-generated catch block
  28. e.printStackTrace();
  29. }
  30. }
  31. }

我们点击运行按钮,结果显示。
java中的ClassLoader详解 - 图8
可以看到,Test类的say方法正确执行,也就是我们写的DiskClassLoader编写成功。

回首

讲了这么大的篇幅,自定义ClassLoader才姗姗来迟。 很多同学可能觉得前面有些啰嗦,但我按照自己的思路,我觉得还是有必要的。因为我是围绕一个关键字进行讲解的。
关键字是什么?

关键字 路径

  • 从开篇的环境变量
  • 到3个主要的JDK自带的类加载器
  • 到自定义的ClassLoader

它们的关联部分就是路径,也就是要加载的class或者是资源的路径。
BootStrap ClassLoader、ExtClassLoader、AppClassLoader都是加载指定路径下的jar包。如果我们要突破这种限制,实现自己某些特殊的需求,我们就得自定义ClassLoader,自已指定加载的路径,可以是磁盘、内存、网络或者其它。
所以,你说路径能不能成为它们的关键字?
当然上面的只是我个人的看法,可能不正确,但现阶段,这样有利于自己的学习理解。

自定义ClassLoader还能做什么?

突破了JDK系统内置加载路径的限制之后,我们就可以编写自定义ClassLoader,然后剩下的就叫给开发者你自己了。你可以按照自己的意愿进行业务的定制,将ClassLoader玩出花样来。

玩出花之Class解密类加载器

常见的用法是将Class文件按照某种加密手段进行加密,然后按照规则编写自定义的ClassLoader进行解密,这样我们就可以在程序中加载特定了类,并且这个类只能被我们自定义的加载器进行加载,提高了程序的安全性。
下面,我们编写代码。

1.定义加密解密协议

加密和解密的协议有很多种,具体怎么定看业务需要。在这里,为了便于演示,我简单地将加密解密定义为异或运算。当一个文件进行异或运算后,产生了加密文件,再进行一次异或后,就进行了解密。

2.编写加密工具类

  1. import java.io.File;
  2. import java.io.FileInputStream;
  3. import java.io.FileNotFoundException;
  4. import java.io.FileOutputStream;
  5. import java.io.IOException;
  6. public class FileUtils {
  7. public static void test(String path){
  8. File file = new File(path);
  9. try {
  10. FileInputStream fis = new FileInputStream(file);
  11. FileOutputStream fos = new FileOutputStream(path+"en");
  12. int b = 0;
  13. int b1 = 0;
  14. try {
  15. while((b = fis.read()) != -1){
  16. //每一个byte异或一个数字2
  17. fos.write(b ^ 2);
  18. }
  19. fos.close();
  20. fis.close();
  21. } catch (IOException e) {
  22. // TODO Auto-generated catch block
  23. e.printStackTrace();
  24. }
  25. } catch (FileNotFoundException e) {
  26. // TODO Auto-generated catch block
  27. e.printStackTrace();
  28. }
  29. }
  30. }

我们再写测试代码

  1. FileUtils.test("D:\\lib\\Test.class");

java中的ClassLoader详解 - 图9
然后可以看见路径D:\lib\Test.class下Test.class生成了Test.classen文件。

编写自定义classloader,DeClassLoader

  1. import java.io.ByteArrayOutputStream;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.io.IOException;
  5. public class DeClassLoader extends ClassLoader {
  6. private String mLibPath;
  7. public DeClassLoader(String path) {
  8. // TODO Auto-generated constructor stub
  9. mLibPath = path;
  10. }
  11. @Override
  12. protected Class<?> findClass(String name) throws ClassNotFoundException {
  13. // TODO Auto-generated method stub
  14. String fileName = getFileName(name);
  15. File file = new File(mLibPath,fileName);
  16. try {
  17. FileInputStream is = new FileInputStream(file);
  18. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  19. int len = 0;
  20. byte b = 0;
  21. try {
  22. while ((len = is.read()) != -1) {
  23. //将数据异或一个数字2进行解密
  24. b = (byte) (len ^ 2);
  25. bos.write(b);
  26. }
  27. } catch (IOException e) {
  28. e.printStackTrace();
  29. }
  30. byte[] data = bos.toByteArray();
  31. is.close();
  32. bos.close();
  33. return defineClass(name,data,0,data.length);
  34. } catch (IOException e) {
  35. // TODO Auto-generated catch block
  36. e.printStackTrace();
  37. }
  38. return super.findClass(name);
  39. }
  40. //获取要加载 的class文件名
  41. private String getFileName(String name) {
  42. // TODO Auto-generated method stub
  43. int index = name.lastIndexOf('.');
  44. if(index == -1){
  45. return name+".classen";
  46. }else{
  47. return name.substring(index+1)+".classen";
  48. }
  49. }
  50. }

测试

我们可以在ClassLoaderTest.java中的main方法中如下编码:

  1. DeClassLoader diskLoader = new DeClassLoader("D:\\lib");
  2. try {
  3. //加载class文件
  4. Class c = diskLoader.loadClass("com.frank.test.Test");
  5. if(c != null){
  6. try {
  7. Object obj = c.newInstance();
  8. Method method = c.getDeclaredMethod("say",null);
  9. //通过反射调用Test类的say方法
  10. method.invoke(obj, null);
  11. } catch (InstantiationException | IllegalAccessException
  12. | NoSuchMethodException
  13. | SecurityException |
  14. IllegalArgumentException |
  15. InvocationTargetException e) {
  16. // TODO Auto-generated catch block
  17. e.printStackTrace();
  18. }
  19. }
  20. } catch (ClassNotFoundException e) {
  21. // TODO Auto-generated catch block
  22. e.printStackTrace();
  23. }

查看运行结果是:
java中的ClassLoader详解 - 图10
可以看到了,同样成功了。现在,我们有两个自定义的ClassLoader:DiskClassLoader和DeClassLoader,我们可以尝试一下,看看DiskClassLoader能不能加载Test.classen文件也就是Test.class加密后的文件。
我们首先移除D:\lib\Test.class文件,只剩下一下Test.classen文件,然后进行代码的测试。

  1. DeClassLoader diskLoader1 = new DeClassLoader("D:\\lib");
  2. try {
  3. //加载class文件
  4. Class c = diskLoader1.loadClass("com.frank.test.Test");
  5. if(c != null){
  6. try {
  7. Object obj = c.newInstance();
  8. Method method = c.getDeclaredMethod("say",null);
  9. //通过反射调用Test类的say方法
  10. method.invoke(obj, null);
  11. } catch (InstantiationException | IllegalAccessException
  12. | NoSuchMethodException
  13. | SecurityException |
  14. IllegalArgumentException |
  15. InvocationTargetException e) {
  16. // TODO Auto-generated catch block
  17. e.printStackTrace();
  18. }
  19. }
  20. } catch (ClassNotFoundException e) {
  21. // TODO Auto-generated catch block
  22. e.printStackTrace();
  23. }
  24. DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
  25. try {
  26. //加载class文件
  27. Class c = diskLoader.loadClass("com.frank.test.Test");
  28. if(c != null){
  29. try {
  30. Object obj = c.newInstance();
  31. Method method = c.getDeclaredMethod("say",null);
  32. //通过反射调用Test类的say方法
  33. method.invoke(obj, null);
  34. } catch (InstantiationException | IllegalAccessException
  35. | NoSuchMethodException
  36. | SecurityException |
  37. IllegalArgumentException |
  38. InvocationTargetException e) {
  39. // TODO Auto-generated catch block
  40. e.printStackTrace();
  41. }
  42. }
  43. } catch (ClassNotFoundException e) {
  44. // TODO Auto-generated catch block
  45. e.printStackTrace();
  46. }
  47. }

运行结果:
java中的ClassLoader详解 - 图11
我们可以看到。DeClassLoader运行正常,而DiskClassLoader却找不到Test.class的类,并且它也无法加载Test.classen文件。

Context ClassLoader 线程上下文类加载器

前面讲到过Bootstrap ClassLoader、ExtClassLoader、AppClassLoader,现在又出来这么一个类加载器,这是为什么?
前面三个之所以放在前面讲,是因为它们是真实存在的类,而且遵从”双亲委托“的机制。而ContextClassLoader其实只是一个概念。
查看Thread.java源码可以发现

  1. public class Thread implements Runnable {
  2. /* The context ClassLoader for this thread */
  3. private ClassLoader contextClassLoader;
  4. public void setContextClassLoader(ClassLoader cl) {
  5. SecurityManager sm = System.getSecurityManager();
  6. if (sm != null) {
  7. sm.checkPermission(new RuntimePermission("setContextClassLoader"));
  8. }
  9. contextClassLoader = cl;
  10. }
  11. public ClassLoader getContextClassLoader() {
  12. if (contextClassLoader == null)
  13. return null;
  14. SecurityManager sm = System.getSecurityManager();
  15. if (sm != null) {
  16. ClassLoader.checkClassLoaderPermission(contextClassLoader,
  17. Reflection.getCallerClass());
  18. }
  19. return contextClassLoader;
  20. }
  21. }

contextClassLoader只是一个成员变量,通过setContextClassLoader()方法设置,通过getContextClassLoader()设置。
每个Thread都有一个相关联的ClassLoader,默认是AppClassLoader。并且子线程默认使用父线程的ClassLoader除非子线程特别设置。
我们同样可以编写代码来加深理解。
现在有2个SpeakTest.class文件,一个源码是

  1. package com.frank.test;
  2. public class SpeakTest implements ISpeak {
  3. @Override
  4. public void speak() {
  5. // TODO Auto-generated method stub
  6. System.out.println("Test");
  7. }
  8. }

它生成的SpeakTest.class文件放置在D:\lib\test目录下。
另外ISpeak.java代码

  1. package com.frank.test;
  2. public interface ISpeak {
  3. public void speak();
  4. }

然后,我们在这里还实现了一个SpeakTest.java

  1. package com.frank.test;
  2. public class SpeakTest implements ISpeak {
  3. @Override
  4. public void speak() {
  5. // TODO Auto-generated method stub
  6. System.out.println("I\' frank");
  7. }
  8. }

它生成的SpeakTest.class文件放置在D:\lib目录下。
然后我们还要编写另外一个ClassLoader,DiskClassLoader1.java这个ClassLoader的代码和DiskClassLoader.java代码一致,我们要在DiskClassLoader1中加载位置于D:\lib\test中的SpeakTest.class文件。
测试代码:

  1. DiskClassLoader1 diskLoader1 = new DiskClassLoader1("D:\\lib\\test");
  2. Class cls1 = null;
  3. try {
  4. //加载class文件
  5. cls1 = diskLoader1.loadClass("com.frank.test.SpeakTest");
  6. System.out.println(cls1.getClassLoader().toString());
  7. if(cls1 != null){
  8. try {
  9. Object obj = cls1.newInstance();
  10. //SpeakTest1 speak = (SpeakTest1) obj;
  11. //speak.speak();
  12. Method method = cls1.getDeclaredMethod("speak",null);
  13. //通过反射调用Test类的speak方法
  14. method.invoke(obj, null);
  15. } catch (InstantiationException | IllegalAccessException
  16. | NoSuchMethodException
  17. | SecurityException |
  18. IllegalArgumentException |
  19. InvocationTargetException e) {
  20. // TODO Auto-generated catch block
  21. e.printStackTrace();
  22. }
  23. }
  24. } catch (ClassNotFoundException e) {
  25. // TODO Auto-generated catch block
  26. e.printStackTrace();
  27. }
  28. DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
  29. System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
  30. new Thread(new Runnable() {
  31. @Override
  32. public void run() {
  33. System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
  34. // TODO Auto-generated method stub
  35. try {
  36. //加载class文件
  37. // Thread.currentThread().setContextClassLoader(diskLoader);
  38. //Class c = diskLoader.loadClass("com.frank.test.SpeakTest");
  39. ClassLoader cl = Thread.currentThread().getContextClassLoader();
  40. Class c = cl.loadClass("com.frank.test.SpeakTest");
  41. // Class c = Class.forName("com.frank.test.SpeakTest");
  42. System.out.println(c.getClassLoader().toString());
  43. if(c != null){
  44. try {
  45. Object obj = c.newInstance();
  46. //SpeakTest1 speak = (SpeakTest1) obj;
  47. //speak.speak();
  48. Method method = c.getDeclaredMethod("speak",null);
  49. //通过反射调用Test类的say方法
  50. method.invoke(obj, null);
  51. } catch (InstantiationException | IllegalAccessException
  52. | NoSuchMethodException
  53. | SecurityException |
  54. IllegalArgumentException |
  55. InvocationTargetException e) {
  56. // TODO Auto-generated catch block
  57. e.printStackTrace();
  58. }
  59. }
  60. } catch (ClassNotFoundException e) {
  61. // TODO Auto-generated catch block
  62. e.printStackTrace();
  63. }
  64. }
  65. }).start();

结果如下:
java中的ClassLoader详解 - 图12
我们可以得到如下的信息:

  1. DiskClassLoader1加载成功了SpeakTest.class文件并执行成功。
  2. 子线程的ContextClassLoader是AppClassLoader。
  3. AppClassLoader加载不了父线程当中已经加载的SpeakTest.class内容。

我们修改一下代码,在子线程开头处加上这么一句内容。

  1. Thread.currentThread().setContextClassLoader(diskLoader1);

结果如下:
java中的ClassLoader详解 - 图13
可以看到子线程的ContextClassLoader变成了DiskClassLoader。
继续改动代码:

  1. Thread.currentThread().setContextClassLoader(diskLoader);

结果:
java中的ClassLoader详解 - 图14
可以看到DiskClassLoader1和DiskClassLoader分别加载了自己路径下的SpeakTest.class文件,并且它们的类名是一样的com.frank.test.SpeakTest,但是执行结果不一样,因为它们的实际内容不一样。

Context ClassLoader的运用时机

其实这个我也不是很清楚,我的主业是Android,研究ClassLoader也是为了更好的研究Android。网上的答案说是适应那些Web服务框架软件如Tomcat等。主要为了加载不同的APP,因为加载器不一样,同一份class文件加载后生成的类是不相等的。如果有同学想多了解更多的细节,请自行查阅相关资料。

总结

  1. ClassLoader用来加载class文件的。
  2. 系统内置的ClassLoader通过双亲委托来加载指定路径下的class和资源。
  3. 可以自定义ClassLoader一般覆盖findClass()方法。
  4. ContextClassLoader与线程相关,可以获取和设置,可以绕过双亲委托的机制。

    下一步

  5. 你可以研究ClassLoader在Web容器内的应用了,如Tomcat。

  6. 可以尝试以这个为基础,继续学习Android中的ClassLoader机制。

原文链接:https://blog.csdn.net/briblue/article/details/54973413