oracle 官方文档
Class Loaders In Java - Baeldung
Demystifying class loading problems
浅谈双亲委派模型
tomcat classloader violates delegating policy
What is JAR Hell?
Spring Boot Classloader and Class Overriding
JVM 类加载机制也是一道常见的暖场题, 令人感到厌烦的是, 这个类加载机制的翻译就和“套接字”一样令人感到窒息。
大部分的计算机英文术语在命名时, 都会尽可能做到直白易懂, 体现技术概念的本质。 但是中文翻译中往往因为翻译者水平有限,导致这种信息的丢失, 使得原本直白的概念变得晦涩难懂,容易误解。
双亲委派模型 就是一个典型的例子。
大部分程序员第一眼看到这个术语, 脑子中必定会浮现这样一种画面:
上面这个第一印象, 再加上百度 “双亲委派模型” 最常见的如下配图, 基本上就足以误导 80 % 80\% 80% 的读者
oracle 官方文档关于 jvm 类加载机制所用的描述是:
The Java platform uses
a delegation model
for loading classes. The basic idea is thatevery class loader has a "parent" class loader.
When loading a class, a class loaderfirst "delegates" the search for the class to its parent class loader
before attempting to find the class itself.
翻译过来就是:
java 平台使用
委派模型
来加载类。 基本思想就是,每一个类加载器都有一个父加载器
, 当需要加载一个 class
时,首先把该 class 的查询和加载优先委派给父加载器进行
, 如果父加载器无法加载该 class, 再去尝试自行加载这个 class
下面是 jdk 中 java.lang.ClassLoader
的 loadClass
方法具体代码逻辑, 较为清晰的展现了父加载模型的逻辑。
下面是源码摘录 java.lang.ClassLoader.java
( jdk1.8.0_101)
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loadCed
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;
}
}
细心的同学应该会有疑问, 加载一个类时, 发现该类还继承其他的类,或者方法定义中用到了别的类作为参数或者返回值, 会发生什么。
这就涉及到了类的具体加载过程, 如下图, 类的加载过程被从左到右划分为 3 大阶段:
从上文类加载的详细过程可以看出, 类有两种方式被加载进来
看过 ClassLoader
的源码以后, 会意识到所谓的父加载器, 只是一个简单的成员变量引用 parent
, 该引用在构造 ClassLoader
时, 由外部传递
正如先前所展示的, jdk 默认提供了三类内建的类加载器。
public void printClassLoaders() throws ClassNotFoundException {
System.out.println("Classloader of this class:"
+ PrintClassLoader.class.getClassLoader());
System.out.println("Classloader of Logging:"
+ Logging.class.getClassLoader());
System.out.println("Classloader of ArrayList:"
+ ArrayList.class.getClassLoader());
}
输出内容:
Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null
上面输出了三种结果 AppClassLoader
, ExtClassLoader
, null
(其实是 BootStrapClassLoader
)
AppClassLoader
负责加载 classpath
下的文件ExtClassLoader
负责加载 java 核心类的扩展类, 通常是搜索 $JAVA_HOME/lib/ext
中的文件或是任意定义在 java.ext.dirs
属性中的文件夹下的文件予以加载BootStrapClassLoader
负责加载 java 核心类, 例如 ArrayList
.但是, 我们看到, ArrayList
加载类的输出内容为 null
, 这是因为 BootStrapClassLoader 是用平台原生语言( 可能是 C,C++ 或其他平台相关语言) , 而 getClassLoader()
返回的是 java 类, 所以这项输出只能为空
AppClassLoader
, ExtClassLoader
是由 sun.misc.Launcher
初始化的, 查看源码中的构造方法可以发现
源码为 IDE 反编译获得, 所以变量名可读性较弱, 但不影响理解
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// var1 是 ExtClassLoader 引用变量
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// var1 作为入参, 传入了 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);
// ... 省略部分代码
沿着 getAppClassLoader()
方法, 最后可以追踪到 ClassLoader
的构造方法中, 可以看到 getAppClassLoader(var1)
中传入的参数 var1
最终被保存在 parent
成员变量中
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
// 省略部分代码
}
综上, 可以看到 AppClassLoader
与 ExtClassLoader
的父子关系由 Launcher
保证
那 BootStrapClassLoader
是如何成为 ExtClassLoader
的父加载器呢?
其实上文提到过的 ClassLoader
的源码逻辑提供了答案
注意到, 在加载类的过程中, 找不到 parent 的时候, 会首先调用 findBootStrapClassOrNull(name)
去尝试返回由 BootStrapClassLoader
加载的 java 核心类。 这种机制便保证了 bootStrapClassLoader
是所有 ClassLoader
的父加载器
使用了父委派模型后, **类随着它的类加载器, 一起具备了一种层级关系
如果将父加载器的层级视为更高层级的加载器(如上图所示), 那么由于父加载器总是拥有优先加载一个类的机会, 那么当不同的 child class loader
试图加载一个属于更高层级的parent class loader
加载范围的 class
时, 该请求总会被转发给对应的最高层级的父加载器, 返回一致的结果。
例如应用层级 classpath
中的代码, 是由 AppClassLoader
负责加载的, 但是如果有懵懂或邪恶的程序员定义了与 jdk 中的核心类同名的类, 如 sun.applet.Main
会发生什么呢, 是否会导致项目里面其他使用了这个sun.applet.Main
错误访问到这个由程序员自行定义的类,导致行为异常呢
答案是不会
可以尝试运行下面自定义的这个类的 main 方法
package sun.applet;
public class Main {
public Main() {
}
public static void main(String[] args) {
System.out.println("this sun.applet.Main class defined by an ignorant programmer");
}
}
得到输出
用法: appletviewer url
其中, 包括:
-debug 在 Java 调试器中启动小应用程序查看器
-encoding 指定 HTML 文件使用的字符编码
-J 将参数传递到 java 解释器
-J 选项是非标准选项, 如有更改, 恕不另行通知。
这说明 jvm 并没有加载运行我们自行定义的 sun.applet.Main
, 这也是父委派模型的好处, 当 AppClassLoader 试图加载我们自行定义的 sun.applet.Main
时, 最终将这个请求委派给了 Bootstrap Class Loader
, 并执行了 jdk 中所定义的 sun.applet.Main
类的 main 方法。
使用了父委派模型的另一个影响是, 一个类加载器只能看到由他自己或是由其父辈加载的类, 它自己是看不到更低层级加载器所负责加载的类。
例如, 如果父加载器(ExtClassLoader
)需要加载的类 $JAVA_HOME/jre/ext/xx.jar#Class A
引用了存在于更低层级加载器AppClassLoader
负责范围($class_path
)中才存在的类, 那么在加载过程就会报错。
当这种需求出现的时候, 可以使用 JDK 提供的另一种类加载器 ContextClassLoader
予以解决, 这里不做展开描述, 有兴趣的同学请自行查阅资料
CustomClassLoader
继承自 ClassLoader
public class CustomClassLoader extends ClassLoader {
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassFromFile(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassFromFile(String fileName) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
fileName.replace('.', File.separatorChar) + ".class");
byte[] buffer;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
try {
while ( (nextValue = inputStream.read()) != -1 ) {
byteStream.write(nextValue);
}
} catch (IOException e) {
e.printStackTrace();
}
buffer = byteStream.toByteArray();
return buffer;
}
}
注意到我们重写了 ClassLoader 中的 findClass(String name)
方法, 里面自行实现了读取 class 文件为 byte 数组, 调用 defineClass
方法将 byte 数组解析加载为类。
由于我们并未重写 loadClass(String name)
方法 , 所以 CustomClassLoader
依旧会遵从 loadClass(String name)
中定义的父委派模型加载方法。
以为我们之前自行定义的 sun.applet.Main 为例, 如果我们就是想让这个自定义的类加载到 JVM 中, 并得以执行自定义 main 方法, 该如何自定义一个类加载器完成该操作?
package sun.applet;
public class Main {
public Main() {
}
public static void main(String[] args) {
System.out.println("this sun.applet.Main class defined by an ignorant programmer");
}
}
把 ide 编译出的 Main.class
文件放到 ./out/production/classes/sun/applet/
目录下
然后自定义类加载器如下
public class UnDelegationClassLoader extends ClassLoader {
private String classpath;
public UnDelegationClassLoader(String classpath) {
super(null);
this.classpath = classpath;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clz = findLoadedClass(name);
if (clz != null) {
return clz;
}
// jdk 目前对"java."开头的包增加了权限保护,这些包我们仍然交给 jdk 加载
if (name.startsWith("java.")) {
return ClassLoader.getSystemClassLoader().loadClass(name);
}
return findClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream is = null;
try {
String classFilePath = this.classpath + name.replace(".", "/") + ".class";
is = new FileInputStream(classFilePath);
byte[] buf = new byte[is.available()];
is.read(buf);
return defineClass(name, buf, 0, buf.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
throw new IOError(e);
}
}
}
}
public static void main(String[] args)
throws ClassNotFoundException, IllegalAccessException, InstantiationException,
MalformedURLException, FileNotFoundException, NoSuchMethodException, InvocationTargetException {
sun.applet.Main main1 = new sun.applet.Main();
FileInputStream file = new FileInputStream("./out/production/classes/sun/applet/Main.class");
UnDelegationClassLoader cl = new UnDelegationClassLoader("./out/production/classes/");
String name = "sun.applet.Main";
Class<?> clz = cl.loadClass(name);
Object main2 = clz.newInstance();
Method mainMehthod = clz.getMethod("main",String[].class);
String params[] = null;
mainMehthod.invoke(null,(Object)params);
System.out.println("main1 class: " + main1.getClass());
System.out.println("main2 class: " + main2.getClass());
System.out.println("main1 classloader: " + main1.getClass().getClassLoader());
System.out.println("main2 classloader: " + main2.getClass().getClassLoader());
System.out.println( );
}
}
输出:
this sun.applet.Main class defined by an ignorant programmer
main1 class: class sun.applet.Main
main2 class: class sun.applet.Main
main1 classloader: null
main2 classloader: sun.applet.UnDelegationClassLoader@36baf30c
注意到为了打破父委派模型, 我们重写 loadClass(String name)
方法, 在该方法中, java.
开头的类, 我们还是调用 jdk 提供的加载器去加载。因为这些核心类 jdk 做了权限保护, 如果直接尝试加载一个自定义的 java.
开头的核心类, 例如 java.lang.Object
的话, 在执行 defineClass 时会得到如下报错
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at sun.applet.UnDelegationClassLoader.findClass(UnDelegationClassLoader.java:36)
at sun.applet.UnDelegationClassLoader.loadClass(UnDelegationClassLoader.java:24)
at sun.applet.UnDelegationClassLoader.main(UnDelegationClassLoader.java:58)
这是 jdk 对于 java. 开头的类加载的一种权限保护, 确保用户没办法错误或恶意的加载自定义的核心 java 类 。
目前有不少框架都会自行实现 classLoader 满足一些特定需求, 其中就有一些框架会在一定程度上违背父加载模型, 例如 Tomcat, JNDI、OSGi .
这里分析一下 Tomcat 什么如何违背父委派模型, 以及为什么违背
首先 tomcat 文档中描述了其自定义的类加载器层级关系:
When Tomcat is started, it creates a set of class loaders that are organized into the following parent-child relationships, where the parent class loader is above the child class loader:
Bootstrap ( $JAVA_HOME/jre/lib/ ; $JAVA_HOME/jre/lib/ext )
|
System ( $CATALINA_HOME/bin/bootstrap.jar ;$CATALINA_BASE/bin/tomcat-juli.jar or $CATALINA_HOME/bin/tomcat-juli.jar ; $CATALINA_BASE/bin/tomcat-juli.jar ;CATALINA_HOME/bin/commons-daemon.jar
|
Common (deafult: $CATALINA_BASE/lib)
/ \
webapp1 webapp2
Tomcat 作为一个服务器容器, 需要有能力同时运行多个 war 包, 而每个 war 包中都拥有各自的依赖 lib 库(WEB-INF/lib
) 以及各自的项目代码(WEB-INF/classes
), 为了保证每个 web 项目可以共同运行, 互不干扰, Tomcat 为每个项目都创建一个单独 webapp classloader, 它会负责加载对应的 web 项目下 WEB-INF/classes
的 class 文件和资源以及 WEB-INF/lib
下的jar 包中所包含的 class 文件和资源文件, 使得这些被加载的内容仅对该 web 项目可见, 对其他 web 项目不可见。
在上述过程中, 每一个 webapp classloader 在加载类时, 会优先在 WEB-INF/classes
和 WEB-INF/lib
中搜索并尝试加载, 而不是优先委托给父加载器尝试加载,
这样做的好处是它允许不同的 web 项目去重载 Tomcat 提供的 lib 包(如$CATALINA_HOME/lib/
中的 jar 包)。
这极大程度上保证了不同 web 项目的独立性和自由度。
应该很多人会疑问, 作为普通程序员, 为什么有必要理解类的加载过程 ? 我平时又没有需求要开发自己的类加载器
这里简单举几个用处
JAR Hell 是一个术语, 用于描述由 java 类加载机制特性而引发的一系列问题。
NoClassDefFoundError
findLoadedClass( myClassName)
, 如果加载过, 就会直接返回上文提到, 全局限定名相同的类只会被同一个类加载器加载一次。 这容易引发问题, 但也可以用来实现对第三方类库的修改。
想象你引用了一个第三方 jar 包, 但是发现有一点小问题, 你希望简单地修改这个 jar 包中的某一个类。 但是这个 jar 包其他项目也在引用, 你无权或不便修改。 但你的项目又确实需要进行这种修改。
此时最为便捷的方式, 是把 jar 包中的类拷贝到你的项目中,包路径及类名和 jar 中的完全相同, 然后直接进行修改,如果类加载能直接优先加载项目源码中, 你所定义的 class 文件,而不再使用 jar 包中的那个类文件, 不是极好的吗?
以笔者使用的 gradle 构建的 spring boot 单体 jar 包为例, 由于 gradle 构建出的 spring boot 单体 jar 包中,在 BOOT-INF 文件夹下, 将项目文件 classes 目录放置与 lib 目录之前, 而 spring boot 应用启动时, 又会按照 BOOT-INF 中文件夹组织顺序去加载类文件, 这就确保了笔者可以方便的对 lib 中所引用的第三方 jar 包进行类的替换 --》 想替换或修改哪个类, 就在项目下面自定义一个包路径相同的同名类, 自由修改。 jvm 运行时, 只会加载这个我们自定义的类, 忽略 jar 包中原始的那个类。