类的加载是需要类加载器完成的,最早的类加载器是满足java applet 需求开发的,但是现在类加载器在热部署、类层次划分、OSGi等领域大放光彩,成为java技术体系中重要的一块。
然而类加载器其实也是一个类(hotspot bootstrap classloader 是C++实现),那么这个类在什么时候被加载呢?会被那个类加载器来加载呢?下面来看下类加载器是如何工作的。
从虚拟机的角度来说,存在2种类加载器:一种是启动类加载器,作为虚拟机的一部分,由c++语言实现。另一种就是所有的其他类加载器,这些类加载器是由java语言实现,独立于虚拟机外部,并且都需要继承ClassLoader抽象类。
但是在开发角度而言,可以分为以下三种系统提供的类加载器。
启动类加载器: BootStrap ClassLoader 作为虚拟机中的一部分,负载加载
/lib 目录下且虚拟机能识别的类库(如rt.jar等)。也通过Xbootclasspath去修改路径。它是不能被java程序直接引用的。
扩展类加载器:Extension ClassLoader 是Launcher$ExtensionClassLoader类实现,主要负责加载
/lib/ext目录下的(或者被java.ext.dirs系统变量指定的目录)所有的类库,如脚本引擎的nashorn.jar,负责加解密的sunjce_provider.jar等,和系统类加载器一样可以被开发者直接使用!
系统类加载器:Application ClassLoader 是由Launcher$AppClassLoader类实现,主要负责加载classpath下面的类库,一般情况下这个是程序中默认的类加载器。
我们的应用程序都是都这3类加载器相互配合进行加载的,如果有需要还可以自定义类加载器,下面是这些类加载器关系:
上图展示了类加载器之间的层级关系,这种关系就是双亲委派模型。
双亲委派模型的工作机制是:一个类加载器收到了类加载的请求,首先不会自己加载这个类,而是把这个请求委派给父类加载器去完成(顶层除外),每个层级的类加载器都是这样,因为最终会传到顶层的类加载器(BootstrapClassLoader)中,如果当父类加载器无法完成这加载请求时,字加载器尝试这自己去加载。我们可以看下Luncher中加载的相关代码片段。如下:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
// 代码简化。。
// 如果已经被加载,则直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
//委托父类加载,BootstrapClassLoader不能为程序直接引用,直接用null代替即可!
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
//父类没有找到,则自己尝试加载
if (c == null) {
c = findClass(name);
}
}
return c;
}
但是这些类加载器的父子关系并不是以继承的方式去体现的,而是以组合关系类复用父加载器的代码。
那么这2个java类的类加载器是什么加载的呢?BootStrapClassLoader是虚拟机的一部分,在虚拟机启动的时候就会产生。刚才也说了BootStrapClassLoader会加载JAVA_HOME/lib的类库的,而我们另外的2个java类的类加载器ExtClassLoader和AppClassLoader都在Launch类中,Launch类所在的rt.jar就是lib下面的一个类库。
双亲委派模型对于保证java程序运行的稳定非常重要,当然好处也是非常多的,如下:
可以确保Java核心类库所提供的类不会被自定义的类所替代,安全性。
不同类加载器的命名空间不同,为tomcat 的部署多个应用提供了主要依赖等等。
每一个类加载器都有一个独立的命名空间,和继承一样,子类加载器命名空间是有该类加载器本身所加载的类以及其父类加载器所加载的类所组成,父类的类加载器是无法看到子类加载器命名空间的类。
1、那么在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
2、在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
下面有个简单的代码片段看下不同类加载器加载相同的类会产生几个class对象:
public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
try {
InputStream is = getClass().getResourceAsStream(fileName);
if (is != null) {
byte[] b = new byte[is.available()];
is.read(b);
return super.defineClass(name, b, 0, b.length);
}
} catch (Exception e) {
//ignore
}
return super.loadClass(name);
}
}
public class Launch {
public static void main(String[] args) throws ClassNotFoundException {
CustomClassLoader loader = new CustomClassLoader();
Class clazz = loader.loadClass("com.pepsi.classloader.Launch");
System.out.println(clazz.getClassLoader());
System.out.println(Launch.class.getClassLoader());
}
}
最终输出:
com.pepsi.classloader.CustomClassLoader@4b1210ee
sun.misc.Launcher$AppClassLoader@18b4aac2
这段代码没有用到双亲委托机制逻辑,只是简单的使用不同的类加载器加载同一个类的时候会产生2个不同的class对象。
双亲委托模型并不是强制性的约束模型,大部分类加载的时候都会遵循这个模型,但是也有例外的情况。
1、spi 模式下产生的一个问题,首先 JDK 提供很多相应的接口标准(rt.jar),这些接口都会被 BootStrapClassLoader 加载。但是不同的接口标准实现的厂商不同。需要调用厂商实现的并部署在classpath下的接口实现者。但是 BootStrapClassLoader 可不认识在classpath下的这个接口实现者的类,这里java团队引入了一个设计:线程上下文类加载器。可以通过线程中获取到一个AppClassLoader。如果创建线程时还没有设置,它将会从父线程中继承一个。其实在 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 {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
} Thread.currentThread().setContextClassLoader(this.loader);
}
2、tomcat 中的设计以及热部署也会破坏双亲委托模型的。
本文介绍了一下类加载器的工作模型。