classloader详解

基本知识

sun.misc.Lanuch 是虚拟机的入口,会设置主线程上下文的加载器为系统类加载器
( AppClassLoader),同时也会设置系统类加载器的父加载器为扩展类加载器。如果自己定
义的加载器不设置父加载器则默认为系统类加载器。线程上下文加载器如果没有设置加载
器则使用父线程的线程加载器。
2、 java 程序在 idea 启动,会在 java -cp 设置我们的 classpath,比如会设置我们依赖的
maven 仓库 jar 包位置等
3、 但是, jvm 启动的时候,并不会一次性加载所有的 class 文件,而是根据需要
去动态加载

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

CLassloader的认识

ClassLoader 这种加载机制。

Class 文件的认识

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

public class HelloWorld{

 public static void main(String[] args){
	System.out.println("Hello world!");
 }

然后,我们需要在命令行中进行 java 文件的编译
javac HelloWorld.java
可以看到目录下生成了.class 文件
我们再从命令行中执行命令:
java HelloWorld
上面是基本代码示例,是所有入门 JAVA 语言时都学过的东西,这里重新拿出
来是想让大家将焦点回到 class 文件上, class 文件是字节码格式文件, java 虚
拟机并不能直接识别我们平常编写的.java 源文件,所以需要 javac 这个命令转
换成.class 文件。另外,如果用 C 或者 PYTHON 编写的程序正确转换成.class
文件后, java 虚拟机也是可以识别运行的。更多信息大家可以参考这篇。了解了.class 文件后,我们再来思考下,我们平常在 Eclipse 中编写的 java 程
序是如何运行的,也就是我们自己编写的各种类是如何被加载到 jvm(java 虚拟
机)中去的。
Class 唯一性根据
在运行时,一个类或者接口并不是单单由它的名称确定的,而是由它的二进制
名称以及它的定义类加载器共同确定的。每个这样的类或者接口都属于一个运
行时包, 运行时包则由包名和定义类加载器共同确定。上述中所谓的定义类加
载器实际上就是 getClassLoader()的返回值,而这个概念的产生则跟类加载过
程中的“双亲委派机制” 有关。 当要加载一个类时, 第一个发起类加载的加载
器称之为“初始类加载器” ( initiating loader) ,但是根据“双亲委派机
制” ,它会先将加载委派给父加载器,如果父加载器加载失败,才会最终由自
己尝试加载。而无论哪个加载器加载成功,它就是该类的定义类加载器
( defining class loader)。
你还记得 java 环境变量吗?
初学 java 的时候,最害怕的就是下载 JDK 后要配置环境变量了,关键是当时
不理解,所以战战兢兢地照着书籍上或者是网络上的介绍进行操作。然后下次
再弄的时候,又忘记了而且是必忘。当时,心里的想法很气愤的,想着是–这东
西一点也不人性化,为什么非要自己配置环境变量呢?太不照顾菜鸟和新手
了,很多菜鸟就是因为卡在环境变量的配置上,遭受了太多的挫败感。因为我是在 Windows 下编程的,所以只讲 Window 平台上的环境变量,主要
有 3 个: JAVA_HOME、 PATH、 CLASSPATH。
JAVA_HOME
指的是你 JDK 安装的位置,一般默认安装在 C 盘,如
C:\Program Files\Java\jdk1.8.0_91
• PATH
将程序路径包含在 PATH 当中后,在命令行窗口就可以直接键入它的名字了,
而不再需要键入它的全路径,比如上面代码中我用的到 javac 和 java 两个命令。
一般的
PATH=%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;%PATH%;
• 也就是在原来的 PATH 路径上添加 JDK 目录下的 bin 目录和 jre 目录的
bin.
CLASSPATH
CLASSPATH=.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar
• 一看就是指向 jar 包路径。
需要注意的是前面的.;, .代表当前目录。
环境变量的设置与查看
设置可以右击我的电脑,然后点击属性,再点击高级,然后点击环境变量,具
体不明白的自行查阅文档。
查看的话可以打开命令行窗口

echo %JAVA_HOME%

echo %PATH%5

echo %CLASSPATH%

• 好了,扯远了,知道了环境变量,特别是 CLASSPATH 时,我们进入今天
的主题 Classloader.
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 个类加载器,但我们可能不知道具体哪个先行呢?
    我可以先告诉你答案
    Bootstrap CLassloder2. Extention ClassLoader
    AppClassLoader
    为了更好的理解,我们可以查看源码。
    看 sun.misc.Launcher,它是一个 java 虚拟机的入口应用。
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);
}
3 /*

  • Returns the class loader used to launch the main application.
    /
    public ClassLoader getClassLoader() {
    return loader;
    }
    /
  • The class loader used for loading installed extensions.
    */
    static class ExtClassLoader extends URLClassLoader {}

/**

  • The class loader used for loading from java.class.path.
  • runs in a restricted security context.
    /
    static class AppClassLoader extends URLClassLoader {}
    源码有精简,我们可以得到相关的信息。
    Launcher 初始化了 ExtClassLoader 和 AppClassLoader。
    Launcher 中并没有看见 BootstrapClassLoader,但通过
    System.getProperty(“sun.boot.class.path”)得到了字符串 bootClassPath,这
    个应该就是 BootstrapClassLoader 加载的 jar 包路径。
    我们可以先代码测试一下 sun.boot.class.path 是什么内容。
    System.out.println(System.getProperty(“sun.boot.class.path”));
    • 得到的结果是:
    C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;
    C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;
    C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;
    C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;
    C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;
    C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;
    C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;
    C:\Program Files\Java\jre1.8.0_91\classes
    • 可以看到,这些全是 JRE 目录下的 jar 包或者是 class 文件。ExtClassLoader 源码
    如果你有足够的好奇心,你应该会对它的源码感兴趣
    /
  • 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() {
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();
}
}
38. 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 的加载路径。这里我们通过可以编写测试代码。
System.out.println(System.getProperty(“java.ext.dirs”));
• 结果如下:
C:\Program Files\Java\jre1.8.0_91\lib\ext;C:\Windows\Sun\Java\lib\ext
• AppClassLoader 源码
/**

  • 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);
14.
return AccessController.doPrivileged(
new PrivilegedAction() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}


}
• 可以看到 AppClassLoader 加载的就是 java.class.path 下的路径。我们同
样打印它的值。
System.out.println(System.getProperty(“java.class.path”));
• 结果:
D:\workspace\ClassLoaderDemo\bin
• 这个路径其实就是当前 java 工程目录 bin,里面存放的是编译生成的
class 文件。
好了,自此我们已经知道了 BootstrapClassLoader、 ExtClassLoader、
AppClassLoader 实际是查阅相应的环境属性 sun.boot.class.path、 java.ext.dirs
和 java.class.path 来加载资源文件的。接下来我们探讨它们的加载顺序,我们先用 Eclipse 建立一个 java 工程。
然后创建一个 Test.java 文件。
public class Test{}
• 然后,编写一个 ClassLoaderTest.java 文件。

public class ClassLoaderTest {

public static void main(String[] args) {
// TODO Auto-generated method stub

ClassLoader cl = Test.class.getClassLoader();

System.out.println(“ClassLoader is:”+cl.toString());

}

}
• 我们获取到了 Test.class 文件的类加载器,然后打印出来。结果是:ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
• 也就是说明 Test.class 文件是由 AppClassLoader 加载的。
这个 Test 类是我们自己编写的,那么 int.class 或者是 String.class 的加载是由
谁完成的呢?
我们可以在代码中尝试
public class ClassLoaderTest {

public static void main(String[] args) {
// TODO Auto-generated method stub

ClassLoader cl = Test.class.getClassLoader();

System.out.println(“ClassLoader is:”+cl.toString());

cl = int.class.getClassLoader();

System.out.println(“ClassLoader is:”+cl.toString());

}

}
• 运行一下,却报错了
ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread “main” java.lang.NullPointerException
at ClassLoaderTest.main(ClassLoaderTest.java:15)
• 提示的是空指针,意思是 int.class 这类基础类没有类加载器加载?
当然不是!
int.class 是由 Bootstrap ClassLoader 加载的。要想弄明白这些,我们首先得
知道一个前提。
每个类加载器都有一个父加载器每个类加载器都有一个父加载器,比如加载 Test.class 是由 AppClassLoader
完成,那么 AppClassLoader 也有一个父加载器,怎么样获取呢?很简单,通
过 getParent 方法。比如代码可以这样编写:
ClassLoader cl = Test.class.getClassLoader();

System.out.println(“ClassLoader is:”+cl.toString());
System.out.println(“ClassLoader’s parent is:”+cl.getParent().toString());
• 运行结果如下:
ClassLoader is:sun.misc.Launcher A p p C l a s s L o a d e r @ 73 d 16 e 93 C l a s s L o a d e r ′ s p a r e n t i s : s u n . m i s c . L a u n c h e r AppClassLoader@73d16e93 ClassLoader's parent is:sun.misc.Launcher AppClassLoader@73d16e93ClassLoader′sparentis:sun.misc.LauncherExtClassLoader@15db9742
• 这个说明, AppClassLoader 的父加载器是 ExtClassLoader。那么
ExtClassLoader 的父加载器又是谁呢?
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());
• 运行如果:
ClassLoader is:sun.misc.Launcher A p p C l a s s L o a d e r @ 73 d 16 e 93 E x c e p t i o n i n t h r e a d " m a i n " C l a s s L o a d e r ′ s p a r e n t i s : s u n . m i s c . L a u n c h e r AppClassLoader@73d16e93 Exception in thread "main" ClassLoader's parent is:sun.misc.Launcher AppClassLoader@73d16e93Exceptioninthread"main"ClassLoader′sparentis:sun.misc.LauncherExtClassLoader@15db9742
java.lang.NullPointerException
at ClassLoaderTest.main(ClassLoaderTest.java:13)
• 又是一个空指针异常,这表明 ExtClassLoader 也没有父加载器。那么,
为什么标题又是每一个加载器都有一个父加载器呢?这不矛盾吗?为了解
释这一点,我们还需要看下面的一个基础前提。
父加载器不是父类
我们先前已经粘贴了 ExtClassLoader 和 AppClassLoader 的代码。
static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}• 可以看见 ExtClassLoader 和 AppClassLoader 同样继承自
URLClassLoader,但上面一小节代码中,为什么调用 AppClassLoader
的 getParent()代码会得到 ExtClassLoader 的实例呢?先从
URLClassLoader 说起,这个类又是什么?
先上一张类的继承关系图
URLClassLoader 的源码中并没有找到 getParent()方法。这个方法在
ClassLoader.java 中。
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;
…14. }
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;55. } else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
}
• 我们可以看到 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 的部分代码。
    public class Launcher {
    private static URLStreamHandlerFactory factory = new Factory();
    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;
13. 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 {
//将 ExtClassLoader 对象实例传递进去
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
“Could not create application class loader”, e);
}

public ClassLoader getClassLoader() {
return loader;
}
static class ExtClassLoader extends URLClassLoader {

/**

  • 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() {
public ExtClassLoader run() throws IOException {53. //ExtClassLoader 在这里创建
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}

/*

  • Creates a new ExtClassLoader for the specified directories.
    */
    public ExtClassLoader(File[] dirs) throws IOException {
    super(getExtURLs(dirs), null, factory);

}
}
}
• 我们需要注意的是
ClassLoader extcl;

extcl = ExtClassLoader.getExtClassLoader();

loader = AppClassLoader.getAppClassLoader(extcl);
• 代码已经说明了问题 AppClassLoader 的 parent 是一个 ExtClassLoader
实例。
ExtClassLoader 并没有直接找到对 parent 的赋值。它调用了它的父类也就是
URLClassLoder 的构造方法并传递了 3 个参数。
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}
对应的代码1. public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
super(parent);
}
答案已经很明了了, 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 找到了,直接返回,如果没有找到,则一级一级返回,
最后到达自身去查找这些对象。这种机制就叫做双亲委托。
整个流程可以如下图所示:这张图是用时序图画出来的,不过画出来的结果我却自己都觉得不理想。
大家可以看到 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 路径下查找。找到就返回。如果没有找到就让子类找,如果没有
    子类会怎么样?抛出各种异常。上面的序列,详细说明了双亲委托的加载流程。 我们可以发现委托是从下向
    上,然后具体查找过程却是自上至下。
    我说过上面用时序图画的让自己不满意,现在用框图,最原始的方法再画一
    次。
    上面已经详细介绍了加载过程,但具体为什么是这样加载,我们还需要了解几
    个个重要的方法 loadClass()、 findLoadedClass()、 findClass()、
    defineClass()。重要方法
    loadClass()
    JDK 文档中是这样写的,通过指定的全限定类名加载 class,它通过同名的
    loadClass(String,boolean)方法。
    protected Class<?> loadClass(String name,
    boolean resolve)
    throws ClassNotFoundException
    上面是方法原型,一般实现这个方法的步骤是
  6. 执行 findLoadedClass(String)去检测这个 class 是不是已经加载过了。
  7. 执行父加载器的 loadClass 方法。如果父加载器为 null,则 jvm 内置的加载
    器去替代,也就是 Bootstrap ClassLoader。这也解释了 ExtClassLoader 的
    parent 为 null,但仍然说 Bootstrap ClassLoader 是它的父加载器。
  8. 如果向上委托父加载器没有加载成功,则通过 findClass(String)查找。
    如果 class 在上面的步骤中找到了,参数 resolve 又是 true 的话,那么
    loadClass()又会调用 resolveClass(Class)这个方法来生成最终的 Class 对象。
    我们可以从源代码看出这个步骤。
    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);13. } 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);

.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//调用 resolveClass()
resolveClass©;
}
return c;
}
}
代码解释了双亲委托。
另外,要注意的是如果要编写一个 classLoader 的子类,也就是自定义一个
classloader,建议覆盖 findClass()方法,而不要直接改写 loadClass()方法。
另外
if (parent != null) {2. //父加载器不为空则调用父加载器的 loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用 Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
前面说过 ExtClassLoader 的 parent 为 null,所以它向上委托时,系统会为它
指定 Bootstrap ClassLoader。
自定义 ClassLoader
不知道大家有没有发现,不管是 Bootstrap ClassLoader 还是 ExtClassLoader
等,这些类加载器都只是加载指定的目录下的 jar 包或者资源。如果在某种情
况下,我们需要动态加载一些东西呢?比如从 D 盘某个文件夹加载一个 class
文件,或者从网络上下载 class 主内容然后再进行加载,这样可以吗?
如果要这样做的话,需要我们自定义一个 classloader。
自定义步骤
编写一个类继承自 ClassLoader 抽象类。
复写它的 findClass()方法。
在 findClass()方法中调用 defineClass()。
defineClass()
这个方法在编写自定义 classloader 的时候非常重要,它能将 class 二进制内容
转换成 Class 对象,如果不符合要求的会抛出各种异常。
注意点:一个 ClassLoader 创建时如果没有指定 parent,那么它的 parent 默认就是
AppClassLoader。
上面说的是,如果自定义一个 ClassLoader,默认的 parent 父加载器是
AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的
class 文件。
自定义 ClassLoader 示例之 DiskClassLoader。
假设我们需要一个自定义的 classloader,默认加载路径为 D:\lib 下的 jar 包和资
源。
我们写编写一个测试用的类文件, Test.java
Test.java
package com.frank.test;

public class Test {

public void say(){
System.out.println(“Say Hello”);
}

}
然后将它编译过年 class 文件 Test.class 放到 D:\lib 这个路径下。
DiskClassLoader
我们编写 DiskClassLoader 的代码。
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;6.

public class DiskClassLoader extends ClassLoader {

private String mLibPath;

public DiskClassLoader(String path) {
// TODO Auto-generated constructor stub
mLibPath = path;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub

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) {
// TODO Auto-generated catch block
e.printStackTrace();
}48.
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";
}
}

}
我们在 findClass()方法中定义了查找 class 的方法,然后数据通过 defineClass()
生成了 Class 对象。
测试
现在我们要编写测试代码。我们知道如果调用一个 Test 对象的 say 方法,它会
输出” Say Hello”这条字符串。但现在是我们把 Test.class 放置在应用工程所
有的目录之外,我们需要加载它,然后执行它的方法。具体效果如何呢?我们
编写的 DiskClassLoader 能不能顺利完成任务呢?我们拭目以待。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTest {

public static void main(String[] args) {
// TODO Auto-generated method stub

//创建自定义 classloader 对象。10. DiskClassLoader diskLoader = new DiskClassLoader(“D:\lib”);
try {
//加载 class 文件
Class c = diskLoader.loadClass(“com.frank.test.Test”);

if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod(“say”,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();
}

}

}
我们点击运行按钮,结果显示。
可以看到, Test 类的 say 方法正确执行,也就是我们写的 DiskClassLoader 编
写成功。回首
讲了这么大的篇幅,自定义 ClassLoader 才姗姗来迟。 很多同学可能觉得前
面有些啰嗦,但我按照自己的思路,我觉得还是有必要的。因为我是围绕一个
关键字进行讲解的。
关键字是什么?
关键字 路径
• 从开篇的环境变量
• 到 3 个主要的 JDK 自带的类加载器
• 到自定义的 ClassLoader
它们的关联部分就是路径,也就是要加载的 class 或者是资源的路径。
BootStrap ClassLoader、 ExtClassLoader、 AppClassLoader 都是加载指定
路径下的 jar 包。如果我们要突破这种限制,实现自己某些特殊的需求,我们
就得自定义 ClassLoader,自已指定加载的路径,可以是磁盘、内存、网络或
者其它。
所以,你说路径能不能成为它们的关键字?
当然上面的只是我个人的看法,可能不正确,但现阶段,这样有利于自己的学
习理解。
自定义 ClassLoader 还能做什么?突破了 JDK 系统内置加载路径的限制之后,我们就可以编写自定义
ClassLoader,然后剩下的就叫给开发者你自己了。你可以按照自己的意愿进
行业务的定制,将 ClassLoader 玩出花样来。
玩出花之 Class 解密类加载器
常见的用法是将 Class 文件按照某种加密手段进行加密,然后按照规则编写自
定义的 ClassLoader 进行解密,这样我们就可以在程序中加载特定了类,并且
这个类只能被我们自定义的加载器进行加载,提高了程序的安全性。
下面,我们编写代码。
1.定义加密解密协议
加密和解密的协议有很多种,具体怎么定看业务需要。在这里,为了便于演
示,我简单地将加密解密定义为异或运算。当一个文件进行异或运算后,产生
了加密文件,再进行一次异或后,就进行了解密。
2.编写加密工具类
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileUtils {

public static void test(String path){
File file = new File(path);
try {
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(path+“en”);15. int b = 0;
int b1 = 0;
try {
while((b = fis.read()) != -1){
//每一个 byte 异或一个数字 2
fos.write(b ^ 2);
}
fos.close();
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}
我们再写测试代码
FileUtils.test(“D:\lib\Test.class”);
然后可以看见路径 D:\lib\Test.class 下 Test.class 生成了 Test.classen 文
件。
编写自定义 classloader, DeClassLoader
import java.io.ByteArrayOutputStream;
import java.io.File;3. import java.io.FileInputStream;
import java.io.IOException;

public class DeClassLoader extends ClassLoader {

private String mLibPath;

public DeClassLoader(String path) {
// TODO Auto-generated constructor stub
mLibPath = path;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub

String fileName = getFileName(name);

File file = new File(mLibPath,fileName);

try {
FileInputStream is = new FileInputStream(file);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
byte b = 0;
try {
while ((len = is.read()) != -1) {
//将数据异或一个数字 2 进行解密
b = (byte) (len ^ 2);
bos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}

byte[] data = bos.toByteArray();
is.close();
bos.close();
44. return defineClass(name,data,0,data.length);

} catch (IOException e) {
// TODO Auto-generated catch block
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+".classen";
}else{
return name.substring(index+1)+".classen";
}
}

}
测试
我们可以在 ClassLoaderTest.java 中的 main 方法中如下编码:
DeClassLoader diskLoader = new DeClassLoader(“D:\lib”);
try {
//加载 class 文件
Class c = diskLoader.loadClass(“com.frank.test.Test”);

if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod(“say”,null);
//通过反射调用 Test 类的 say 方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException14. | SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
查看运行结果是:
可以看到了,同样成功了。现在,我们有两个自定义的
ClassLoader:DiskClassLoader 和 DeClassLoader,我们可以尝试一下,看看
DiskClassLoader 能不能加载 Test.classen 文件也就是 Test.class 加密后的文
件。
我们首先移除 D:\lib\Test.class 文件,只剩下一下 Test.classen 文件,然后
进行代码的测试。
DeClassLoader diskLoader1 = new DeClassLoader(“D:\lib”);
try {
//加载 class 文件
Class c = diskLoader1.loadClass(“com.frank.test.Test”);

if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod(“say”,null);10. //通过反射调用 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();
}

DiskClassLoader diskLoader = new DiskClassLoader(“D:\lib”);
try {
//加载 class 文件
Class c = diskLoader.loadClass(“com.frank.test.Test”);

if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod(“say”,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();49. }

}
运行结果:
我们可以看到。 DeClassLoader 运行正常,而 DiskClassLoader 却找不到
Test.class 的类,并且它也无法加载 Test.classen 文件。
Context ClassLoader 线程上下文类加载器
前面讲到过 Bootstrap ClassLoader、 ExtClassLoader、 AppClassLoader,
现在又出来这么一个类加载器,这是为什么?
前面三个之所以放在前面讲,是因为它们是真实存在的类,而且遵从”双亲委
托“的机制。而 ContextClassLoader 其实只是一个概念。
查看 Thread.java 源码可以发现
public class Thread implements Runnable {

/* The context ClassLoader for this thread */4. 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()设置。
每个 Thread 都有一个相关联的 ClassLoader,默认是 AppClassLoader。并
且子线程默认使用父线程的 ClassLoader 除非子线程特别设置。
我们同样可以编写代码来加深理解。
现在有 2 个 SpeakTest.class 文件,一个源码是
package com.frank.test;

public class SpeakTest implements ISpeak {

@Override
public void speak() {
// TODO Auto-generated method stub
System.out.println(“Test”);
}10.
}
它生成的 SpeakTest.class 文件放置在 D:\lib\test 目录下。
另外 ISpeak.java 代码
package com.frank.test;

public interface ISpeak {
public void speak();

}
然后,我们在这里还实现了一个 SpeakTest.java
package com.frank.test;

public class SpeakTest implements ISpeak {

@Override
public void speak() {
// TODO Auto-generated method stub
System.out.println(“I’ frank”);
}

}
它生成的 SpeakTest.class 文件放置在 D:\lib 目录下。
然后我们还要编写另外一个 ClassLoader, DiskClassLoader1.java 这个
ClassLoader 的代码和 DiskClassLoader.java 代码一致,我们要在
DiskClassLoader1 中加载位置于 D:\lib\test 中的 SpeakTest.class 文件。
测试代码:
DiskClassLoader1 diskLoader1 = new DiskClassLoader1(“D:\lib\test”);
Class cls1 = null;
try {
//加载 class 文件
cls1 = diskLoader1.loadClass(“com.frank.test.SpeakTest”);6. 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();
}

DiskClassLoader diskLoader = new DiskClassLoader(“D:\lib”);
System.out.println("Thread “+Thread.currentThread().getName()+” classloader:
hread.currentThread().getContextClassLoader().toString());
new Thread(new Runnable() {

@Override
public void run() {
System.out.println("Thread “+Thread.currentThread().getName()+” classloader:
hread.currentThread().getContextClassLoader().toString());

// TODO Auto-generated method stub
try {
//加载 class 文件
// Thread.currentThread().setContextClassLoader(diskLoader);
//Class c = diskLoader.loadClass(“com.frank.test.SpeakTest”);
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class c = cl.loadClass(“com.frank.test.SpeakTest”);44. // Class c = Class.forName(“com.frank.test.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();
结果如下:
我们可以得到如下的信息:

  1. DiskClassLoader1 加载成功了 SpeakTest.class 文件并执行成功。2. 子线程的 ContextClassLoader 是 AppClassLoader。
  2. AppClassLoader 加载不了父线程当中已经加载的 SpeakTest.class 内容。
    我们修改一下代码,在子线程开头处加上这么一句内容。
    Thread.currentThread().setContextClassLoader(diskLoader1);
    结果如下:
    可以看到子线程的 ContextClassLoader 变成了 DiskClassLoader。
    继续改动代码:
    Thread.currentThread().setContextClassLoader(diskLoader);
    结果:
    可以看到 DiskClassLoader1 和 DiskClassLoader 分别加载了自己路径下的
    SpeakTest.class 文件,并且它们的类名是一样的 com.frank.test.SpeakTest,但
    是执行结果不一样,因为它们的实际内容不一样。
    Context ClassLoader 的运用时机
    其实这个我也不是很清楚,我的主业是 Android,研究 ClassLoader 也是为了
    更好的研究 Android。网上的答案说是适应那些 Web 服务框架软件如Tomcat 等。主要为了加载不同的 APP,因为加载器不一样, 同一份 class 文
    件加载后生成的类是不相等的。如果有同学想多了解更多的细节,请自行查阅
    相关资料。
    总结
  3. ClassLoader 用来加载 class 文件的。
  4. 系统内置的 ClassLoader 通过双亲委托来加载指定路径下的 class 和资
    源。
  5. 可以自定义 ClassLoader 一般覆盖 findClass()方法。
  6. ContextClassLoader 与线程相关,可以获取和设置,可以绕过双亲委托
    的机制。
    下一步
  7. 你可以研究 ClassLoader 在 Web 容器内的应用了,如 Tomcat。
上一篇:类加载与ClassLoader的理解


下一篇:Android 类加载器