一:ClassLoader
从JVM结构图中可以看到,类加载器的作用是将Java类文件加载到Java虚拟机。
只有当类被加载进虚拟机内存,才能使用对应的类。
在Java中,类加载过程大概分为以下几步:
- 通过全限类名获取类文件字节数组。可来自本地文件、jar包、网络等。
- 在方法区/元空间保存类的描述信息、静态属性。
- 在JVM堆中生成一个对应的java.lang.Class对象。
理解Java的类加载机制,对理解JVM有很大帮助。
二:Java默认的类加载器
Java默认提供三个类加载器,分别为:
- Bootstrap ClassLoader
- Extension ClassLoader
- App ClassLoader
Bootstrap ClassLoader 负责加载Java基础类,主要是 %JRE_HOME%/lib/ 目录下的rt.jar、resources.jar、charsets.jar等。
Extension ClassLoader 负责加载Java扩展类,主要是 %JRE_HOME%/lib/ext 目录下的jar。
App ClassLoader 负责加载当前应用的ClassPath中的所有类。
三个ClassLoader所负责加载的类,可以通过以下方式进行查看。
public class ClassPath {
public static void main(String[] args) {
System.out.println("Bootstrap ClassLoader path: ");
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println("----------------------------");
System.out.println("Extension ClassLoader path: ");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("----------------------------");
System.out.println("App ClassLoader path: ");
System.out.println(System.getProperty("java.class.path"));
System.out.println("----------------------------");
}
}
具体原因,在源码分析章节说明。
其中Bootstrap ClassLoader是JVM级别的,由C++撰写。
Extension ClassLoader和App ClassLoader都是Java类。
JVM启动Bootstrap ClassLoader,然后初始化sun.misc.Launcher。
接着,Launcher初始化Extension ClassLoader和App ClassLoader。
三:源码分析
sun.misc.Launcher类是Java程序的入口。
其构造器如下:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
……
}
其中有两行比较重要的代码:
Launcher.ExtClassLoader.getExtClassLoader();
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
第一行初始化了ExtClassLoader,但没有指定其parent。
一些文章表示ExtClassLoader的父加载器是Bootstrap ClassLoader,这个说法其实并不完全准确。
第二行初始化了AppClassLoader,指定ExtClassLoader作为其父加载器。并将AppClassLoader作为系统类加载器。
AppClassLoader将会成为自定义ClassLoader的默认父加载器。
具体逻辑可按照以下顺序查看源代码:
- Launcher类的getClassLoader()方法。
- ClassLoader类的initSystemClassLoader()方法。
- ClassLoader类的getSystemClassLoader()方法。
- ClassLoader类的ClassLoader()方法。
其中getSystemClassLoader()方法的注释为:
/**
* Returns the system class loader for delegation. This is the default
* delegation parent for new ClassLoader instances, and is
* typically the class loader used to start the application.
**/
ExtClassLoader和AppClassLoader都继承了URLClassLoader类。
URLClassLoader支持从文件目录和jar包加载class。
ExtClassLoader和AppClassLoader都调用了父类的构造函数。
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory)
URLClassLoader类中有个属性为ucp,表示该ClassLoader负责搜索的路径。
ExtClassLoader和AppClassLoader最大的不同,即它们负责的路径不同。
/* The search path for classes and resources */
private final URLClassPath ucp;
查看源码可得:
ExtClassLoader负责搜索的路径为:
String var0 = System.getProperty("java.ext.dirs");
AppClassLoader负责搜索的路径为:
String var1 = System.getProperty("java.class.path");
所以,上一节可以通过这两个方法获取不同ClassLoader所负责加载的目录。
此外,Bootstrap ClassLoader负责搜索的路径为:
String bootClassPath = System.getProperty("sun.boot.class.path");
ClassLoader源码
ClassLoader是一个抽象类,几个主要的方法如下:
defineClass(String name, byte[] b, int off, int len)把字节数组b中的内容转换成Java类,返回的结果是java.lang.Class类的实例。
findClass(String name)查找名称为name的类,返回的结果是java.lang.Class类的实例。
loadClass(String name)加载名称为name的类,返回的结果是java.lang.Class类的实例。
resolveClass(Class> c)链接指定的Java 类。
其中,loadClass方法是最常涉及的一个。
其代码如下:
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
该方法主要的步骤如下:
- 指定全限类名进行加载,首先调用findLoadedClass(name)判断当前类加载器是否已经加载该类。
- 如果没有被加载。则判断当前ClassLoader的父加载器是否为null。如果不为null,则委托其父加载器进行加载。如果为null,则使用Bootstrap ClassLoader进行加载。
- 如果父加载器或Bootstrap ClassLoader都无法加载,则调用findClass(name)方法寻找需要加载的类。
此外,loadClass方法还涉及加锁的过程,使用ConcurrentHashMap对不同的全限类名进行加锁。
具体可查看getClassLoadingLock方法。
四:双亲委托模式
Java类加载机制使用双亲委托模式。
一个ClassLoader加载一个类时,首先需要将任务委托给其父加载器,直到Bootstrap ClassLoader。
如果父加载器未加载该类,则逐层返回给委托发起者即当前ClassLoader进行加载。
在正常应用中,用户不自定义类加载器。
类加载工作首先由App ClassLoader发起,然后委托给Extension ClassLoader,最后委托给Bootstrap ClassLoader。
首先,通过一个例子了解三个ClassLoader所负责加载的类和双亲委托模式。
新建一个jar包,名为acai-cl.jar,包中有个简单的Person类。
写一个简单的程序输出person对象所对应的ClassLoader。
import com.acai.Person;
public class TestClassLoader {
public static void main(String[] args) {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
}
}
测试一:将jar包引入项目
对应输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
可以看到,位于ClassPath的类,是由App ClassLoader负责加载。
测试二:将jar包复制到%JRE_HOME%/lib/ext目录
对应输出:
sun.misc.Launcher$ExtClassLoader@4cc77c2e
可以得出,Extension ClassLoader负责加载%JRE_HOME%/lib/ext目录下的类。
加载Person类时,会首先尝试使用App ClassLoader进行加载。
由于双亲委托模式,最终委托到Extension ClassLoader,而其负责的目录%JRE_HOME%/lib/ext下存在Person类,则进行了类加载操作。
测试三:将jar包追加到Bootstrap ClassLoader加载路径上
使用参数:-Xbootclasspath/a:d:\acai-cl.jar,将jar包追加到Bootstrap ClassLoader加载路径。
对应输出:
null
可以看出,Person类的加载工作,最终被委托到了Bootstrap ClassLoader。
注:Bootstrap ClassLoader由C++撰写。由Bootstrap ClassLoader负责加载的类,其getClassLoader()方法输出为null。
可以尝试输出String类的类加载器。
System.out.println(String.class.getClassLoader());
接下来,再通过debug来验证双亲委托模式。
还是原来那个简单的demo。
import com.acai.Person;
public class Test {
public static void main(String[] args) {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
}
}
在ClassLoader类的loadClass方法上打断点。
可以看出,类的加载过程符合从下到上委托,最终会被委托到Bootstrap ClassLoader。
同时符合从上到下加载,每一层ClassLoader都会尝试进行加载。最终由App ClassLoader加载了Person类。
接着,尝试加载一个特殊的类:Splash.class。
Splash类位于jfxrt.jar,这个jar包在%JRE_HOME%/lib/ext目录下。
import com.sun.javafx.applet.Splash;
public class ExtTest {
public static void main(String[] args) {
Splash splash = new Splash(null);
System.out.println(splash.getClass().getClassLoader());
}
}
对应输出:
sun.misc.Launcher$ExtClassLoader@330bedb4
毫无疑问,Splash类应该由Extension ClassLoader进行加载。
但其加载过程,仍然会从默认的系统类加载器App ClassLoader开始。
可以通过debug进行查看。
Splash类加载的过程会被委托到Bootstrap ClassLoader,但Bootstrap ClassLoader并不负责加载%JRE_HOME%/lib/ext目录下的类。最终由Extension ClassLoader进行加载。
很多文章在阐述三个ClassLoader之间的关系时候,会给出一个getParent操作的demo。
并且认为Bootstrap ClassLoader是Extension ClassLoader的父加载器。
Extension ClassLoader是App ClassLoader的父加载器。
App ClassLoader是自定义类加载器的父加载器。
这样的解释基本正确,但Bootstrap ClassLoader和Extension ClassLoader之间的关系需要额外解释。
由于Bootstrap ClassLoader并不是使用Java编写,故无法指定Extension ClassLoader的parent为Bootstrap ClassLoader。
这一层关系在ClassLoader的loadClass方法中做了弥补。
在加载类时,会判断当前ClassLoader的父加载器是否为null,为null则使用Bootstrap ClassLoader进行加载。
在Java提供的三个默认类加载器中,父加载器为null的只有Extension ClassLoader。
该过程可参考ClassLoader的loadClass方法。
为什么使用双亲委托模式?
网上很多例子是关于String类。假设自己写一个java.lang.String类,使用双亲委托模式可以防止这个问题。
但其实双亲委托模式可以被打破,而真正阻止自定义java.lang.String的是“安全机制”。
这里尝试自定义java.lang.String类,并使用自定义ClassLoader进行加载。
package java.lang;
public class String {
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class StringClassLoader extends ClassLoader {
@Override
public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if ("java.lang.String".equals(name)) {
return findClass(name);
} else {
return super.loadClass(name);
}
}
@Override
public Class> findClass(String s) throws ClassNotFoundException {
try {
byte[] classBytes = Files.readAllBytes(Paths.get("d:/String.class"));
return defineClass(s, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(s);
}
}
public static void main(String[] args) throws ClassNotFoundException {
StringClassLoader stringClassLoader = new StringClassLoader();
Class clazz = stringClassLoader.loadClass("java.lang.String", false);
System.out.println(clazz.getClassLoader());
}
}
该自定义类加载器破坏了双亲委托机制,具体方式将在下个章节说明。
输出结果为:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
可以看到,在被findClass方法调用的defineClass中有这么一段:
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
它会检查当前加载类的全限类名是否以java.开头,这也是一种安全机制。
如果按照网上的说法,java.lang.String被Bootstrap ClassLoader加载,demo中自定义的类加载器会被略过,不会输出异常。
所以说,双亲委托模式的作用只是防止类重复加载。
五:自定义ClassLoader
多数情况下,Java默认的三个类加载器已经可以满足需求。
自定义类加载器则可以实现额外的需求,例如:
- 从网络文件加载类。
- 从任意目录加载类。
- 对字节码文件做加密处理,由自定义类加载器做解密。
实现自定义类加载器的主要步骤为:
- 继承ClassLoader类。如果只是从目录或者jar包加载类,也可以选择继承URLClassLoader类。
- 重写findClass方法。
- 在重写的findClass方法中,无论用何种方法,获取类文件对应的字节数组,然后调用defineClass方法转换成类实例。
自定义类加载器真正好玩的是打破双亲委托机制,也是很多面试官会问到的问题。
上文提到类加载双亲委托模式实现位于ClassLoader的loadClass方法,想要破坏这个机制,则需要重写该方法。
打破双亲委托模式的确有一定的实用价值。
比如有两个class文件,或者两个jar包。
其中两个类的全限类名都一样,如果需要同时使用这两个类,则需要打破双亲委托模式。
有两个Person类,它们的全限类名均为com.acai.Person,唯一的区别是sayHello()方法输出的内容略有不同。
package com.acai;
import lombok.Data;
@Data
public class Person {
private String name;
private Integer age;
public void sayHello() {
System.out.println("Hello, this is Person in acai-cl");
}
}
package com.acai;
import lombok.Data;
@Data
public class Person {
private String name;
private Integer age;
public void sayHello() {
System.out.println("Hello, this is Person in acai-cl2");
}
}
将两个Person所在的项目打成jar包。
常规操作是,把两个jar包都引进项目。
写一个小小的demo。
import com.acai.Person;
public class Main {
public static void main(String[] args) throws Exception {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
person.sayHello();
}
}
对应输出为:
sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl
可以看到,demo中默认使用了acai-cl.jar中的Person类。
如果想要使用acai-cl2.jar中的Person类,则想到新建一个ClassLoader。
需要从jar包加载类,则优先想到URLClassLoader。
import com.acai.Person;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws Exception {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
person.sayHello();
URL url = new File("d:/acai-cl2.jar").toURI().toURL();
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Thread.currentThread().setContextClassLoader(loader);
Class> clazz = loader.loadClass("com.acai.Person");
System.out.println(clazz.getClassLoader());
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(clazz.newInstance());
}
}
对应输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-clsun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl
可以看出,即使是指定了使用acai-cl2.jar,输出的仍然是acai-cl.jar中Person的sayHello。
原因是由于两个Person类拥有一样的全限类名。
加载第二个Person的时候,发现自定义类加载器的父类加载器App ClassLoader已经加载了com.acai.Person。
所以直接返回该类,即为acai-cl.jar中的Person类。
于是想到,新建ClassLoader,并且破坏双亲委托机制,重新loadClass方法。
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandlerFactory;
public class MyClassLoader extends URLClassLoader {
public MyClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public MyClassLoader(URL[] urls) {
super(urls);
}
public MyClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
if (name.equals("com.acai.Person")) {
return super.findClass(name);
} else {
return super.loadClass(name);
}
}
}
在MyClassLoader中重写了loadClass方法,当加载的类名等于com.acai.Person时,直接调用findClass方法,绕过双亲委托机制。
这里需要一个if判断,表示只有在加载com.acai.Person时才破坏双亲委托。
因为在加载一个类时,会同时加载它的父类。
Person的父类为java.lang.Object。
直接用自定义类加载器加载Object类,会抛出SecurityException异常。
于是,写一个demo。
import com.acai.Person;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws Exception {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
person.sayHello();
URL url = new File("d:/acai-cl2.jar").toURI().toURL();
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Thread.currentThread().setContextClassLoader(loader);
Class> clazz = loader.loadClass("com.acai.Person");
System.out.println(clazz.getClassLoader());
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(clazz.newInstance());
URL url2 = new File("d:/acai-cl2.jar").toURI().toURL();
MyClassLoader myClassLoader = new MyClassLoader(new URL[]{url2});
Class> clazz2 = myClassLoader.loadClass("com.acai.Person");
System.out.println(clazz2.getClassLoader());
Method method2 = clazz2.getDeclaredMethod("sayHello");
method2.invoke(clazz2.newInstance());
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-clsun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-clMyClassLoader@5e2de80c
Hello, this is Person in acai-cl2
可以看到,acai-cl2.jar中的Person类被正确加载。
得出,可以通过自定义ClassLoader,重写loadClass,破坏双亲委托机制。
六:参考资料
[1] Java Garbage Collection Basics
[2] java classloader是怎么加载自身到内存里面执行的?
[3] 详细深入分析 Java ClassLoader 工作机制
[4] 深入分析Java ClassLoader原理
[5] 深入探讨 Java 类加载器
[6] 深度分析Java的ClassLoader机制(源码级别)
[7] Java类加载原理与ClassLoader使用总结
[8] 实现java classloader 动态加载jar包
[9] ClassLoader的基础详解