1.背景
Java和类加载有关的八股文想必大家都背的滚瓜烂熟了,什么双亲委派等等.但是对于其中的细节,你又了解多少,这篇文章来深入理解Java ClassLoader机制
2.重要知识点
2.1 类加载器的父子关系非java类的继承关系extends
static class AppClassLoader extends URLClassLoader {
...
static class ExtClassLoader extends URLClassLoader {
...
从上面的代码块我们可以看到,AppClassLoader和ExtClassLoader类加载器都继承了URLClassLoader.那么为什么会说ExtClassLoader类加载器是AppClassLoader类加载器的父加载器呢?
URLClassLoader继承了SecureClassLoader, SecureClassLoader继承了ClassLoader抽象类,抽象类中有这样一个属性:
private final ClassLoader parent;
类加载器的基础关系如下图所示:
那么什么时候将AppClassLoader
的ClassLoader parent
属性进行设置的呢?首先从JVM入口应用sun.misc.Launcher
聊起,看下面一段代码.
public Launcher() {
ExtClassLoader localExtClassLoader;
try {
// 加载扩展类加载器
localExtClassLoader = ExtClassLoader.getExtClassLoader();
} catch (IOException localIOException1) {
throw new InternalError("Could not create extension class loader", localIOException1);
}
try {
// 加载应用类加载器
this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
} catch (IOException localIOException2) {
throw new InternalError("Could not create application class loader", localIOException2);
}
// 设置AppClassLoader为线程上下文类加载器
Thread.currentThread().setContextClassLoader(this.loader);
// ...
static class ExtClassLoader extends java.net.URLClassLoader
static class AppClassLoader extends java.net.URLClassLoader
}
重点看这一行代码AppClassLoader.getAppClassLoader(localExtClassLoader);
这里为什么初始化AppClassLoader
时要传入ExtClassLoader
实例?
答:初始化AppClassLoader
时传入的ExtClassLoader
实例最终设置为了AppClassLoader
(ClassLoader)的parent属性.
2.2 [ClassLoader
+类的全路径]会唯一确定一个类对象
看下面的一段代码
public static void main(String[] args) throws Throwable {
// Person类的类全路径
String className = "com.demo.example.Person";
// myClazz是自定义为URLClassLoader加载器加载
Class<?> myClazz = loadClass(new String[] { "file:/Users/zhangustb/Documents/code_projects/zhangustb_demo/zhangustb_demo-start/target/classes/" }, null, className);
System.out.println(myClazz.getClassLoader());
// appClazz1是系统类加载器appClassLoader加载
Class<?> appClazz1 = Class.forName(className);
// appClazz2是系统类加载器appClassLoader加载
Class<?> appClazz2 = Class.forName(className);
// 输出myClazz appClazz1 appClazz2三个Class对象的关系
System.out.println("myClazz == appClazz1 ? " + myClazz.equals(appClazz1));
System.out.println("appClazz1 == appClazz2 ? " + appClazz1.equals(appClazz2));
// 注意这里会出现类型转换异常
myClazz.newInstance();
}
public static Class<?> loadClass(String[] pathArray, ClassLoader parentClassLoader, String className) throws Throwable {
List<URL> urlList = new ArrayList<>();
for (String path : pathArray) {
URL url = new URL(path);
urlList.add(url);
}
URL[] urls = urlList.toArray(new URL[urlList.size()]);
URLClassLoader classLoader = new URLClassLoader(urls, parentClassLoader);
return classLoader.loadClass(className);
}
java.net.URLClassLoader@682a0b20
myClazz == appClazz1 ? false
appClazz1 == appClazz2 ? true
Exception in thread "main" java.lang.InstantiationException: com.demo.example.Person
at java.lang.Class.newInstance(Class.java:427)
at com.demo.example.TestZzx.main(TestZzx.java:29)
Caused by: java.lang.NoSuchMethodException: com.demo.example.Person.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 1 more
Process finished with exit code 1
从打印结果来看
Person类编译后在classpath目录下.
myClazz由自定义类加载器加载,
appClazz1、appClazz2由系统类加载器appClassLoader加载.
生成的myClazz与appClazz1两个class对象不相等,而appClazz1与appClazz2两对象是相等的。
这说明,相同路径下的.class文件,被不同类加载器加载到内存中会生成两个不同的Class对象。
2.3 Tomcat破坏双亲委派的原因
在初学时部署项目,我们是把war包放到tomcat的webapp下,这意味着一个tomcat可以运行多个Web应用程序.
那假设我现在有两个Web应用程序,它们都有一个类,叫做
Person
,并且它们的类全限定名都一样,比如都是com.demo.Person
,但是Person
类的实现逻辑是不同的.如果Tomcat没有破坏双亲委派机制,还是沿用Java的类加载机制,比如说应用程序A是先在Tomcat容器中部署运行了,Person类使用的时候会被appclassloader加载器进行加载.此时应用程序B部署运行,那么加载Person类的时候,发现
com.demo.Person
已经被加载过了,会直接使用Person类.但是很明显使用的Person
类是应用程序A的所以Tomcat是如何保证它们是不会冲突的呢?
答案就是,Tomcat给每个 Web 应用创建一个类加载器实例
WebAppClassLoader
,该加载器重写了loadClass
方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找