JVM专栏-类加载器和双亲委派机制
前言:在面试中,我们常被问及
JVM调优经验
、JVM内存区域知识
以及常用的JVM调优命令
。对于资深开发者而言,对JVM的不熟悉可能会影响高薪工作的获取。此外,JVM知识对于排查生产环境中的死锁
、内存溢出
、内存泄漏
等问题至关重要。本系列旨在从基础到深入,逐步加深对JVM的理解。相信坚持和收获总是成正比的,只愿今天的我比昨天的我更加努力一点,坚持的更久一点。
本篇是JVM专栏的第二篇,主要讲解以下内容:
- 类加载器的类型
- 双亲委派机制
- 如何打破双亲委派机制
- 自定义类加载器以及实际应用
1.类加载器
类加载器概述
当我们编写好的Java文件编译打包会生成一个Jar包或者War包,而类加载器负责将Jar包或者War包中的class文件加载到JVM虚拟机中,当然JVM把类加载到内存中然后再到方法调用是需要经过很多步骤的,我们一步一步去了解一下Class文件背后运行的原理。
在JAVA中,类加载器有四大分类:
-
引导类加载器:
负责加载支撑JVM运行的位于JRE的lib目录下的核心类库 ,比如rt.jar
、charsets.jar
等 -
扩展类加载器:
负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的Jar类包 -
应用类加载器:
负责加载ClassPath路径下的类包,主要就是加载我们自己写的那些类 -
自定义加载器:
负责加载用户自定义路径下的类
写个Demo打印出每个类的类加载器
java
public class TestJdkClassLoader {
public static void main(String[] args) {
/*String 位于jre的lib下*/
System.out.println(String.class.getClassLoader());
/*DESKeyFactory 位于jre的lib下的ext目录*/
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
/*classPath路径下*/
System.out.println(TestJdkClassLoader.class.getClassLoader().getClass().getName());
System.out.println();
//获取应用程序类加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
//扩展类加载器
ClassLoader extClassloader = appClassLoader.getParent();
//获取引来类加载器
ClassLoader bootstrapLoader = extClassloader.getParent();
System.out.println("the bootstrapLoader : " + bootstrapLoader);
System.out.println("the extClassloader : " + extClassloader);
System.out.println("the appClassLoader : " + appClassLoader);
System.out.println();
System.out.println("bootstrapLoader加载以下文件:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i]);
}
System.out.println();
System.out.println("extClassloader加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println();
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
运行结果:
java
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@4b67cf4d
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
bootstrapLoader加载以下文件:
file:/D:/environment/jdk1.8/jre/lib/resources.jar
file:/D:/environment/jdk1.8/jre/lib/rt.jar
file:/D:/environment/jdk1.8/jre/lib/sunrsasign.jar
file:/D:/environment/jdk1.8/jre/lib/jsse.jar
file:/D:/environment/jdk1.8/jre/lib/jce.jar
file:/D:/environment/jdk1.8/jre/lib/charsets.jar
file:/D:/environment/jdk1.8/jre/lib/jfr.jar
file:/D:/environment/jdk1.8/jre/classes
extClassloader加载以下文件:
D:\environment\jdk1.8\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
appClassLoader加载以下文件:
D:\environment\jdk1.8\jre\lib\charsets.jar;D:\environment\jdk1.8\jre\lib\deploy.jar;D:\environment\jdk1.8\jre\lib\ext\access-bridge-64.jar;D:\environment\jdk1.8\jre\lib\ext\cldrdata.jar;D:\environment\jdk1.8\jre\lib\ext\dnsns.jar;D:\environment\jdk1.8\jre\lib\ext\jaccess.jar;D:\environment\jdk1.8\jre\lib\ext\jfxrt.jar;D:\environment\jdk1.8\jre\lib\ext\localedata.jar;D:\environment\jdk1.8\jre\lib\ext\nashorn.jar;D:\environment\jdk1.8\jre\lib\ext\sunec.jar;D:\environment\jdk1.8\jre\lib\ext\sunjce_provider.jar;D:\environment\jdk1.8\jre\lib\ext\sunmscapi.jar;D:\environment\jdk1.8\jre\lib\ext\sunpkcs11.jar;D:\environment\jdk1.8\jre\lib\ext\zipfs.jar;D:\environment\jdk1.8\jre\lib\javaws.jar;D:\environment\jdk1.8\jre\lib\jce.jar;D:\environment\jdk1.8\jre\lib\jfr.jar;D:\environment\jdk1.8\jre\lib\jfxswt.jar;D:\environment\jdk1.8\jre\lib\jsse.jar;D:\environment\jdk1.8\jre\lib\management-agent.jar;D:\environment\jdk1.8\jre\lib\plugin.jar;D:\environment\jdk1.8\jre\lib\resources.jar;D:\environment\jdk1.8\jre\lib\rt.jar;D:\devTools\idea\workspace\jvm_study\target\classes;D:\devTools\idea\IntelliJ IDEA 2019.2.3\lib\idea_rt.jar
注意:
1.BootstrapLoader
是由c++语言实现的,所以会打印为null
2.虽然AppClassLoader
打印了jre/lib下的核心类库,但是它其实只加载class目录下的class类
类加载器的创建
我们知道程序运行的时候类加载器会加载我们编译的class文件,但是类加载器本身是由谁创建的呢?接下来我们跟随源码来一探究竟吧。
这里需要回顾下第一篇类加载子系统篇章的类是如何运行的。
当我们Java程序运行的时候,会创建一个引导类加载器(BootstrapLoader)
,再由这个引导类加载器
创建JVM启动器实例sun.misc.Launcher
,在Launcher类
构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)
和sun.misc.Launcher.AppClassLoader(应用程序类加载器)
。JVM默认使用Launcher
的getClassLoader()
方法返回的类加载器AppClassLoader
的实例加载我们的应用程序。
java
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
//静态new出来
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
//构造方法中创建类加载器
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//创建扩展类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//创建应用类加载器,注意这里把应用类加载赋值给loader属性,并将扩展类加载器作为参数传递给了
//getAppClassLoader方法
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()
中创建扩展类加载器,这里会调用到顶层ClassLoader类的构造方法,只不过这里扩展类加载器调用父类构造方法时传的
parent`为null
java
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
在Launcher.AppClassLoader.getAppClassLoader(var1)
中创建应用类加载器,这里会把ExtClassLoader
作为参数传入进来,注意,这里的两个类加载器不是类上的继承关系,只是AppClassLoader
的parent
属性指向ExtClassLoader
实例
java
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
顶层父类-ClassLoader
java
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
这里只有AppClassLoader
的parent
属性指向了ExtClassLoader
,而ExtClassLoader
并没有指向BootstrapLoader
,因为BootstrapLoader
是由C++编写的,我们JDK中是无法看到的,但是这里不会影响ExtClassLoader
委托BootstrapLoader
去加载类,这块会在双亲委派机制介绍parent属性的左右。
2.双亲委派机制
什么是双亲委派机制
当类加载器加载某个类时,会先检查自己加载过的类中是否存在,如果不存在会先委托父加载器寻找目标类,如果还是找不到则继续再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并加载目标类。

-
检查顺序是自底向上:加载过程中会先检查类是否被已加载,从
Custom ClassLoader
到BootStrapClassLoader
逐层检查,只要某个Classloader
已加载就视为已加载此类,保证此类只会被ClassLoader
加载一次。 -
加载的顺序是自顶向下:也就是由上层来逐层尝试加载此类。
我们追踪下源码ClassLoader.loadClass方法
来看看双亲委派的实现机制:
java
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) {
//parent属性不为空则调用父加载器加载类
c = parent.loadClass(name, false);
} else {
//如果parent为空,则调用引导类加载器加载
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);
}
}
return c;
}
}
走读核心代码逻辑:
findLoadedClass:
判断是否已经加载过此类,如果没有加载过走一下逻辑parent
不为空,则先由父类加载器加载,为空,则由BootstrapClassLoader
去加载(这也说明了为什么ExtAppClassLoader
的Parent
属性为空,也可先由BootstrapClassLoader
去加载)- 如果父类加载器加载不到,最后由自己调用
findClass
方法加载,需要注意的是findClass
是一个抽象方法,由子类实现。
双亲委派的优点
双亲委派机制的优点:
- 沙箱安全机制:自己写的
java.lang.String.class
类不会被加载,这样便可以防止核心API库
被随意篡改 - 避免类的重复加载:当父亲已经加载了该类时,就没有必要
子ClassLoader
再加载一次,保证被加载类的唯一性
我们可以自己试试自定义一个String
类,看看是否可以正常被加载
java
//包名也一样
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("******自定义String的Main方法");
}
}
运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
这是因为双亲委派机制的存在,当我们要加载java.lang.String的时候,应用程序类加载会向上委托,而我们的jre的lib下也有一个相同类路径的String类,此时会返回这个String类信息,但是这个String类是没有main方法的,就会出现以上错误。
全盘负责委托机制
"全盘委托"
是指当一个ClassLoder
装载一个类时,除非显示的使用另外一个ClassLoder
,该类所依赖及引用的类也由这个ClassLoder
载入(已经被加载过的类除外)。
3.创建自定义类加载器
自定义类加载器只需要继承 java.lang.ClassLoader
类,该类有两个核心方法,一个是loadClass(String, boolean)
,实现了双亲委派机制 ,还有一个方法是findClass
,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。
自定义类加载器
java
public class MyClassLoader extends ClassLoader {
private String classPath;
//加载路径
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
//把class文件加载成字节流
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数 //组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
创建一个测试类,作为我们外部类需要加载到项目中
java
package com.lx;
public class People {
public void say(){
System.out.println("加载成功");
}
}
编译后把原项目的class文件放在D盘的/test/com/lx目录下,

测试
java
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, InvocationTargetException {
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为 //应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
//D盘创建 test/com/lv 目录,将People.class丢入该目录
Class clazz = classLoader.loadClass("com.lx.People");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("printf", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
运行结果:
java
加载成功
org.bx.idgenerator.MyClassLoaderTest$MyClassLoader
4.双亲委派机制的打破
为什么需要打破双亲委派机制
在某些情况下,父类加载器需要加载的class文件
受到加载范围的限制,无法加载到需要的文件,这个时候就需要委托子类加载器进行加载。这种情况就打破了双亲委派模式。
举个例子:
以DriverManager 为例,DriverManager 定义在JDK中,其内部的数据库驱动实现由各个数据库的服务商来提供 ,如MySQL
、Oracle
、SQLServer
等等,都实现了该接口驱动接口,这些实现类都是以jar包的形式放到classpath 目录下。那么问题来了:DriverManager 基于SPI机制 加载各个实现了Driver 接口的实现类(在classpath下)进行管理,但是DriverManager 由启动类加载器 加载,只能加载JAVA_HOME 的lib 下文件,而其实现类是由服务商提供的,由应用类加载器 加载。这个时候,就需要扩展类加载器来委托子类来加载Driver 实现,这就破坏了双亲委派。类似情况还有很多,比如Tomcat 如何隔离不同应用所依赖jar包 ,Jrebel的热部署机制等等。
DriverManager 使用SPI机制
打破双亲委派机制
java
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
....
}
源码走读:
ServiceLoader.load
方法会加载我们META-INF/services/下文件指定的Driver接口的实现类
java
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
在这里重点看ClassLoader cl = Thread.currentThread().getContextClassLoader();
是从当前线程中拿到了一个上下文类加载器,这个类加载其实是在我们程序启动的时候会把AppClassLoader类加载放在线程的上下文中,参看Launcher类的构造方法

如何打破双亲委派机制
在我们自定义的类加载器中,其实只需要重新父类ClassLoader 的loadClass方法
java
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
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) {
// 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.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
我们重新把Person类放在工厂的类文件目录下,然后运行代码:
java.io.FileNotFoundException: D:\test\java\lang\Object.class (系统找不到指定的路径。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.(FileInputStream.java:138)
at java.io.FileInputStream.(FileInputStream.java:93)
at com.lx.MyClassLoaderTest
结果提示的找不到Object类,这是因为我们所有的类都继承于Object类,而自定义类加载器加载People类的时候找不到Object类,所以就会出现这个错误,这里我们可以怎么解决呢?我们需要修改下代码,自定义类加载器只加载自己想加载的类,而基础的类还遵循双亲委派机制
java
@Override
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//自定义包下的类由自定义类加载器加载
if(name.startsWith("com.lx")){
c = findClass(name);
}else {
c = this.getParent().loadClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
运行结果:
java
加载成功
org.bx.idgenerator.MyClassLoaderTest$MyClassLoader
通过深入理解类加载器和双亲委派机制,我们可以更好地掌握JVM的工作原理,这对于Java开发人员来说是一个不可或缺的技能。希望本文能够帮助你更深入地理解这些概念,并在实际开发中运用自如。