在面向对象编程实践中,咱们经过众多的类来组织一个复杂的系统,这些类之间相互关联、调用使他们的关系造成了一个复杂紧密的网络。当系统启动时,出于性能、资源利用多方面的考虑,咱们不可能要求 JVM 一次性将所有的类都加载完成,而是只加载可以支持系统顺利启动和运行的类和资源便可。那么在系统运行过程当中若是须要使用未在启动时加载的类或资源时该怎么办呢?这就要靠类加载器来完成了。java
类加载器(ClassLoader)就是在系统运行过程当中动态的将字节码文件加载到 JVM 中的工具,基于这个工具的整套类加载流程,咱们称做类加载机制。咱们在 IDE 中编写的都是源代码文件,之后缀名 .java
的文件形式存在于磁盘上,经过编译后生成后缀名 .class
的字节码文件,ClassLoader 加载的就是这些字节码文件。shell
Java 默认提供了三个 ClassLoader,分别是 AppClassLoader、ExtClassLoader、BootStrapClassLoader,依次后者分别是前者的「父加载器」。父加载器不是「父类」,三者之间没有继承关系,只是由于类加载的流程使三者之间造成了父子关系,下文会详细讲述。编程
BootStrapClassLoader 也叫「根加载器」,它是脱离 Java 语言,使用 C/C++ 编写的类加载器,因此当你尝试使用 ExtClassLoader 的实例调用 getParent()
方法获取其父加载器时会获得一个 null
值。bootstrap
// 返回一个 AppClassLoader 的实例
ClassLoader appClassLoader = this.getClass().getClassLoader();
// 返回一个 ExtClassLoader 的实例
ClassLoader extClassLoader = appClassLoader.getParent();
// 返回 null,由于 BootStrapClassLoader 是 C/C++ 编写的,没法在 Java 中得到其实例
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
复制代码
根加载器会默认加载系统变量 sun.boot.class.path
指定的类库(jar 文件和 .class 文件),默认是 $JRE_HOME/lib
下的类库,如 rt.jar、resources.jar 等,具体能够输出该环境变量的值来查看。数组
String bootClassPath = System.getProperty("sun.boot.class.path");
String[] paths = bootClassPath.split(":");
for (String path : paths) {
System.out.println(path);
}
// output
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/resources.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/sunrsasign.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jsse.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jce.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/charsets.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jfr.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/classes
复制代码
除了加载这些默认的类库外,也可使用 JVM 参数 -Xbootclasspath/a
来追加额外须要让根加载器加载的类库。好比咱们自定义一个 com.ganpengyu.boot.DateUtils
类来让根加载器加载。缓存
package com.ganpengyu.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtils {
public static void printNow() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()));
}
}
复制代码
咱们将其制做成一个名为 gpy-boot
的 jar 包放到 /Users/yu/Desktop/lib
下,而后写一个测试类去尝试加载 DateUtils。网络
public class Test {
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("com.ganpengyu.boot.DateUtils");
ClassLoader loader = clz.getClassLoader();
System.out.println(loader == null);
}
}
复制代码
运行这个测试类:app
java -Xbootclasspath/a:/Users/yu/Desktop/lib/gpy-boot.jar -cp /Users/yu/Desktop/lib/gpy-boot.jar:. Test
复制代码
能够看到输出为 true
,也就是说加载 com.ganpengyu.boot.DateUtils
的类加载器在 Java 中没法得到其引用,而任何类都必须经过类加载器加载才能被使用,因此推断出这个类是被 BootStrapClassLoader 加载的,也证实了 -Xbootclasspath/a
参数确实能够追加须要被根加载器额外加载的类库。ide
总之,对于 BootStrapClassLoader 这个根加载器咱们须要知道三点:工具
sun.boot.class.path
指定的类库-Xbootclasspath/a
参数追加根加载器的默认加载类库ExtClassLoader 也叫「扩展类加载器」,它是一个使用 Java 实现的类加载器(sun.misc.Launcher.ExtClassLoader
),用于加载系统所须要的扩展类库。默认加载系统变量 java.ext.dirs
指定位置下的类库,一般是 $JRE_HOME/lib/ext
目录下的类库。
public static void main(String[] args) {
String extClassPath = System.getProperty("java.ext.dirs");
String[] paths = extClassPath.split(":");
for (String path : paths) {
System.out.println(path);
}
}
// output
// /Users/leon/Library/Java/Extensions
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/ext
// /Library/Java/Extensions
// /Network/Library/Java/Extensions
// /System/Library/Java/Extensions
// /usr/lib/java
复制代码
咱们能够在启动时修改java.ext.dirs
变量的值来修改扩展类加载器的默认类库加载目录,但一般并不建议这样作。若是咱们真的有须要扩展类加载器在启动时加载的类库,能够将其放置在默认的加载目录下。总之,对于 ExtClassLoader 这个扩展类加载器咱们须要知道两点:
java.ext.dirs
参数的值来修改默认加载目录,若有须要,能够将要加载的类库放到这个默认目录下。AppClassLoader 也叫「应用类加载器」,它和 ExtClassLoader 同样,也是使用 Java 实现的类加载器(sun.misc.Launcher.AppClassLoader
)。它的做用是加载应用程序 classpath
下全部的类库。这是咱们最常打交道的类加载器,咱们在程序中调用的不少 getClassLoader()
方法返回的都是它的实例。在咱们自定义类加载器时若是没有特别指定,那么咱们自定义的类加载器的默认父加载器也是这个应用类加载器。总之,对于 AppClassLoader 这个应用类加载器咱们须要知道两点:
classpath
下的类库。除了上述三种 Java 默认提供的类加载器外,咱们还能够经过继承 java.lang.ClassLoader
来自定义一个类加载器。若是在建立自定义类加载器时没有指定父加载器,那么默认使用 AppClassLoader 做为父加载器。关于自定义类加载器的建立和使用,咱们会在后面的章节详细讲解。
上文已经提到过 BootStrapClassLoader 是一个使用 C/C++ 编写的类加载器,它已经嵌入到了 JVM 的内核之中。当 JVM 启动时,BootStrapClassLoader 也会随之启动并加载核心类库。当核心类库加载完成后,BootStrapClassLoader 会建立 ExtClassLoader 和 AppClassLoader 的实例,两个 Java 实现的类加载器将会加载本身负责路径下的类库,这个过程咱们能够在 sun.misc.Launcher
中窥见。
咱们将 Launcher 类的构造方法源码精简展现以下:
public Launcher() {
// 建立 ExtClassLoader
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
// 建立 AppClassLoader
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 设置线程上下文类加载器
Thread.currentThread().setContextClassLoader(this.loader);
// 建立 SecurityManager
}
复制代码
能够看到当 Launcher 被初始化时就会依次建立 ExtClassLoader 和 AppClassLoader。咱们进入 getExtClassLoader()
方法并跟踪建立流程,发现这里又调用了 ExtClassLoader 的构造方法,在这个构造方法里调用了父类的构造方法,这即是 ExtClassLoader 建立的关键步骤,注意这里传入父类构造器的第二个参数为 null。接着咱们去查看这个父类构造方法,它位于 java.net.URLClassLoader
类中:
URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory)
复制代码
经过这个构造方法的签名和注释咱们能够明确的知道,第二个参数 parent
表示的是当前要建立的类加载器的父加载器。结合前面咱们提到的 ExtClassLoader 的父加载器是 JVM 内核中 C/C++ 开发的 BootStrapClassLoader,且没法在 Java 中得到这个类加载器的引用,同时每一个类加载器又必然有一个父加载器,咱们能够反证出,ExtClassLoader 的父加载器就是 BootStrapClassLoader。
理清了 ExtClassLoader 的建立过程,咱们来看 AppClassLoader 的建立过程就清晰不少了。跟踪 getAppClassLoader()
方法的调用过程,能够看到这个方法自己将 ExtClassLoader 的实例做为参数传入,最后仍是调用了 java.net.URLClassLoader
的构造方法,将 ExtClassLoader 的实例做为父构造器 parent
参数值传入。因此这里咱们又能够肯定,AppClassLoader 的父构造器就是 ExtClassLoader。
将一个 .class
字节码文件加载到 JVM 中成为一个 java.lang.Class
实例须要加载这个类的类加载器及其全部的父级加载器共同参与完成,这主要是遵循「双亲委派原则」。
当咱们要加载一个应用程序 classpath
下的自定义类时,AppClassLoader 会首先查看本身是否已经加载过这个类,若是已经加载过则直接返回类的实例,不然将加载任务委托给本身的父加载器 ExtClassLoader。一样,ExtClassLoader 也会先查看本身是否已经加载过这个类,若是已经加载过则直接返回类的实例,不然将加载任务委托给本身的父加载器 BootStrapClassLoader。
BootStrapClassLoader 收到类加载任务时,会首先检查本身是否已经加载过这个类,若是已经加载则直接返回类的实例,不然在本身负责的加载路径下搜索这个类并尝试加载。若是找到了这个类,则执行加载任务并返回类实例,不然将加载任务交给 ExtClassLoader 去执行。
ExtClassLoader 一样也在本身负责的加载路径下搜索这个类并尝试加载。若是找到了这个类,则执行加载任务并返回类实例,不然将加载任务交给 AppClassLoader 去执行。
因为本身的父加载器 ExtClassLoader 和 BootStrapClassLoader 都没能成功加载到这个类,因此最后由 AppClassLoader 来尝试加载。一样,AppClassLoader 会在 classpath
下全部的类库中查找这个类并尝试加载。若是最后仍是没有找到这个类,则抛出 ClassNotFoundException
异常。
综上,当类加载器要加载一个类时,若是本身曾经没有加载过这个类,则层层向上委托给父加载器尝试加载。对于 AppClassLoader 而言,它上面有 ExtClassLoader 和 BootStrapClassLoader,因此咱们称做「双亲委派」。可是若是咱们是使用自定义类加载器来加载类,且这个自定义类加载器的默认父加载器是 AppClassLoader 时,它上面就有三个父加载器,这时再说「双亲」就不太合适了。固然,理解了加载一个类的整个流程,这些名字就无关痛痒了。
「双亲委派机制」最大的好处是避免自定义类和核心类库冲突。好比咱们大量使用的 java.lang.String
类,若是咱们本身写的一个 String 类被加载成功,那对于应用系统来讲彻底是毁灭性的破坏。咱们能够尝试着写一个自定义的 String 类,将其包也设置为 java.lang
:
package java.lang;
public class String {
private int n;
public String(int n) {
this.n = n;
}
public String toLowerCase() {
return new String(this.n + 100);
}
}
复制代码
咱们将其制做成一个 jar 包,命名为 thief-jdk
,而后写一个测试类尝试加载 java.lang.String
并使用接收一个 int 类型参数的构造方法建立实例。
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("java.lang.String");
System.out.println(clz.getClassLoader() == null);
Constructor<?> c = clz.getConstructor(int.class);
String str = (String) c.newInstance(5);
str.toLowerCase();
}
}
复制代码
运行测试程序
java -cp /Users/yu/Desktop/lib/thief/thief-jdk.jar:. Test
复制代码
程序抛出 NoSuchMethodException 异常,由于 JVM 不可以加载咱们自定义的 java.lang.String
,而是从 BootStrapClassLoader 的缓存中返回了核心类库中的 java.lang.String
的实例,且核心类库中的 String 没有接收 int 类型参数的构造方法。同时咱们也看到 Class 实例的类加载器是 null
,这也说明了咱们拿到的 java.lang.String
的实例确实是由 BootStrapClassLoader 加载的。
总之,「双亲委派」机制的做用就是确保类的惟一性,最直接的例子就是避免咱们自定义类和核心类库冲突。
「双亲委派」机制用来保证类的惟一性,那么 JVM 经过什么条件来判断惟一性呢?其实很简单,只要两个类的全路径名称一致,且都是同一个类加载器加载,那么就判断这两个类是相同的。若是同一份字节码被不一样的两个类加载器加载,那么它们就不会被 JVM 判断为同一个类。
Person 类
public class Person {
private Person p;
public void setPerson(Object obj) {
this.p = (Person) obj;
}
}
复制代码
setPerson(Object obj)
方法接收一个对象,并将其强制转换为 Person 类型赋值给变量 p。
测试类
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) {
CustomClassLoader classLoader1 = new CustomClassLoader("/Users/yu/Desktop/lib");
CustomClassLoader classLoader2 = new CustomClassLoader("/Users/yu/Desktop/lib");
try {
Class c1 = classLoader1.findClass("Person");
Object instance1 = c1.newInstance();
Class c2 = classLoader2.findClass("Person");
Object instance2 = c2.newInstance();
Method method = c1.getDeclaredMethod("setPerson", Object.class);
method.invoke(instance1, instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
CustomClassLoader 是一个自定义的类加载器,它将字节码文件加载为字符数组,而后调用 ClassLoader 的 defineClass()
方法建立类的实例,后文会详细讲解怎么自定义类加载器。在测试类中,咱们建立了两个类加载器的实例,让他们分别去加载同一份字节码文件,即 Person 类的字节码。而后在实例一上调用 setPerson()
方法将实例二传入,将实例二强制转型为实例一。
运行程序会看到 JVM 抛出了 ClassCastException
异常,异常信息为 Person cannot be cast to Person
。从这咱们就能够知道,同一份字节码文件,若是使用的类加载器不一样,那么 JVM 就会判断他们是不一样的类型。
「全盘负责」是类加载的另外一个原则。它的意思是若是类 A 是被类加载器 X 加载的,那么在没有显示指定别的类加载器的状况下,类 A 引用的其余全部类都由类加载器 X 负责加载,加载过程遵循「双亲委派」原则。咱们编写两个类来验证「全盘负责」原则。
Worker 类
package com.ganpengyu.full;
import com.ganpengyu.boot.DateUtils;
public class Worker {
public Worker() {
}
public void say() {
DateUtils dateUtils = new DateUtils();
System.out.println(dateUtils.getClass().getClassLoader() == null);
dateUtils.printNow();
}
}
复制代码
DateUtils 类
package com.ganpengyu.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtils {
public void printNow() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()));
}
}
复制代码
测试类
import com.ganpengyu.full.Worker;
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("com.ganpengyu.full.Worker");
System.out.println(clz.getClassLoader() == null);
Worker worker = (Worker) clz.newInstance();
worker.say();
}
}
复制代码
运行测试类
java -Xbootclasspath/a:/Users/yu/Desktop/lib/worker.jar Test
复制代码
运行结果
true
true
2018-09-16 22:34:43
复制代码
咱们将 Worker 类和 DateUtils 类制做成名为worker
的 jar 包,将其设置为由根加载器加载,这样 Worker 类就必然是被根加载器加载的。而后在 Worker 类的 say()
方法中初始化了 DateUtils 类,而后判断 DateUtils 类是否由根加载器加载。从运行结果看到,Worker 和其引用的 DateUtils 类都被跟加载器加载,符合类加载的「全盘委托」原则。
「全盘委托」原则实际是为「双亲委派」原则提供了保证。若是不遵照「全盘委托」原则,那么同一份字节码可能会被 JVM 加载出多个不一样的实例,这就会致使应用系统中对该类引用的混乱,具体能够参考上文「JVM 怎么判断两个类是相同的」这一节的示例。
除了使用 JVM 预约义的三种类加载器外,Java 还容许咱们自定义类加载器以让咱们系统的类加载方式更灵活。要自定义类加载器很是简单,一般只须要三个步骤:
java.lang.ClassLoader
类,让 JVM 知道这是一个类加载器findClass(String name)
方法,告诉 JVM 在使用这个类加载器时应该按什么方式去寻找 .class
文件defineClass(String name, byte[] b, int off, int len)
方法,让 JVM 加载上一步读取的 .class
文件import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class CustomClassLoader extends ClassLoader {
private String classpath;
public CustomClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classFilePath = getClassFilePath(name);
byte[] classData = readClassFile(classFilePath);
return defineClass(name, classData, 0, classData.length);
}
public String getClassFilePath(String name) {
if (name.lastIndexOf(".") == -1) {
return classpath + "/" + name + ".class";
} else {
name = name.replace(".", "/");
return classpath + "/" + name + ".class";
}
}
public byte[] readClassFile(String filepath) {
Path path = Paths.get(filepath);
if (!Files.exists(path)) {
return null;
}
try {
return Files.readAllBytes(path);
} catch (IOException e) {
throw new RuntimeException("Can not read class file into byte array");
}
}
public static void main(String[] args) {
CustomClassLoader loader = new CustomClassLoader("/Users/leon/Desktop/lib");
try {
Class<?> clz = loader.loadClass("com.ganpengyu.demo.Person");
System.out.println(clz.getClassLoader().toString());
Constructor<?> c = clz.getConstructor(String.class);
Object instance = c.newInstance("Leon");
Method method = clz.getDeclaredMethod("say", null);
method.invoke(instance, null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
示例中咱们经过继承 java.lang.ClassLoader
建立了一个自定义类加载器,经过构造方法指定这个类加载器的类路径(classpath)。重写 findClass(String name)
方法自定义类加载的方式,其中 getClassFilePath(String filepath)
方法和 readClassFile(String filepath)
方法用于找到指定的 .class
文件并加载成一个字符数组。最后调用 defineClass(String name, byte[] b, int off, int len)
方法完成类的加载。
在 main()
方法中咱们测试加载了一个 Person 类,经过 loadClass(String name)
方法加载一个 Person 类。咱们自定义的 findClass(String name)
方法,就是在这里面调用的,咱们把这个方法精简展现以下:
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) {
c = parent.loadClass(name, false);
} else {
// 全部父加载器都没法加载,使用根加载器尝试加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
// 全部父加载器和根加载器都没法加载
// 使用自定义的 findClass() 方法查找 .class 文件
c = findClass(name);
}
}
return c;
}
}
复制代码
能够看到 loadClass(String name)
方法内部是遵循「双亲委派」机制来完成类的加载。在「双亲」都没能成功加载类的状况下才调用咱们自定义的 findClass(String name)
方法查找目标类执行加载。
自定义类加载器的用处有不少,这里简单列举一些常见的场景。
类加载器是 Java 中很是核心的技术,本文仅对类加载器进行了较为粗浅的分析,若是须要深刻更底层则须要咱们打开 JVM 的源码进行研读。「Java 有路勤为径,JVM 无涯苦做舟」,与君共勉。