类加载器与双亲委派模型及其验证

参考:blog.csdn.net/weixin_41

《深入理解java虚拟机》

类加载器

把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。这样的好处在于,我们可以自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。

JVM提供了3种类加载器: (加载的类的位置不同)

1、启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。 我的路径是D:\Program Files\Java\jdk1.8.0_91\jre\lib,是在jdk里面的jre里面的lib,而不是jdk里面的lib,也不是直接的jre里面的lib。

2、扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。

3、应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

通过下面这张图片我们就可以知道我们可以使用的类库(注意后面的路径,分别对应着bootstrap的加载路径和extension的加载路径)


类加载器也是java类,它要加载其他类,那么,谁来加载类加载器呢?就是Bootstrap加载器,这个不是java类,底层是用c++写的,他是第一个类加载器,嵌套在jvm中,jvm启动则这个类加载器也启动。

然后我们可以通过代码来进一步加深类加载器的认识:

public class ClassloaderTest {

public static void main(String[] args) {
System.out.println(ClassloaderTest.class.getClassLoader().getClass().getName());//先获取当前类的类对象,然后获取它的加载器,然后获取加载器的类对象,然后获取名字
System.out.println(System.class.getClassLoader());//获取System这个类的类加载器
System.out.println();
ClassLoader loader = ClassloaderTest.class.getClassLoader();
while (loader != null) {
System.out.println(loader.getClass().getName());
loader = loader.getParent();//不断地获取父加载器
}
System.out.println(loader);

}

}

输出为

misc.Launcher$AppClassLoader //应用类加载器
null //System这个类的类加载器是bootstrap,由于bootstrap不是java类,所以获取出来的为null

sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader//应用类的类加载器的父加载器为ext加载器
null ext加载器的父加载器为bootstrap

只要是一个类,那就肯定会有它的类加载器。然后就有下面这样的结构(图片取于上面的链接)

然后从上面的图片我们来提出新的知识点:双亲委派模型。

这个模型的工作过程是:如果一个类收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

所以说,bootstrap这位老爷爷是非常忙的,加载啥都要先看看它这里有没有先。没有就去找extension这位爸爸,如果爸爸没有才来找application这位儿子,再没有就看代码去找用户自定义的加载器。总的来说就是这样,然后大家可以验证一下:

验证过程:把上面那个文件export成一个jar文件,命名无所谓,jar文件就行。然后放到ext目录下,我那就是D:\Program Files\Java\jdk1.8.0_91\jre\lib\ext这个目录,然后重新运行那个程序,输出应该是:

sun.misc.Launcher$ExtClassLoader

null


sun.misc.Launcher$ExtClassLoader

null

也就是说现在这个类的类加载器就是ext类加载器了,但是它也在classpath路径下啊,所以这就是双亲委派模型在起作用了,ext加载到了这个类,就不用application这个儿子类加载器去加载了。(前面由于我默认使用的是jre1.8里面的lib,但是输出的jar在jdk的jre的ext里面,所以就没试出来,后面才发现由于自己刚换了工作空间,默认设置也换了,然后自己还自以为用的是jdk里面的,心理错觉,后面看源码时发现看不了源码,这才发现用的是jre里面的lib。)


然后说说这个双亲委托模型的好处:对于java程序的稳定运行很重要,保证了同一个类在内存中只有一个类对象。就比如说如果不是双亲委派模型,那么A类加载一个object,B类加载一个object,那么内存中就有两个object类了,又或者是你自己写一个java.lang.object类,然后放在classpath下,那么这时就会出现两个object类了,那么谁才是万物之父呢?这样一来整个java体系就被很不稳定了。

参考:关于Java类加载双亲委派机制的思考(附一道面试题) - Alexia(minmin) - 博客园 引出一道面试题:能不能自己写个类叫java.lang.System

答:通常不可以,但可以采取另类方法达到这个需求。
解释为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载

但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。


然后实现双亲委派模型的主要是在ClassLoader的loadClass方法里面实现,关于这个方法API上如是说:使用指定的二进制名称来加载类。此方法的默认实现将按以下顺序搜索类:

  1. 调用 findLoadedClass(String) 来检查是否已经加载类。
  2. 在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。
  3. 调用 findClass(String) 方法查找类。

如果使用上述步骤找到类,并且 resolve 标志为真,则此方法将在得到的 Class 对象上调用 resolveClass(Class) 方法。

鼓励用 ClassLoader 的子类重写 findClass(String),而不是使用此方法。

下面取出源码大致讲解一下:


protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded

//首先,检查请求的类是否已经被加载过,findLoadedClass是一个本地方法,底层应该是c++写的,API中如是说:如果 Java 虚拟机已将此加载器记录为具有给定二进制名称的某个类的启动加载器,则返回该二进制名称的类。否则,返回 null。其实也就是类加载器缓存有已经加载过的类。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);//调用父类的loadclass方法递归寻找父加载器是否能加载
} else {
c = findBootstrapClassOrNull(name);//不行就调用启动类加载器
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found

//如果父类加载器抛出ClassNotFoundException,就说明父类加载器无法完成加载请求
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order

//在父类加载器无法加载的情况下,再调用本身的findclass方法来进行类加载。也就是在classpath下去根据名字去加载类
// 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;
}
}


大家可以试试自己手写一个简单的类加载器,参考《深入理解java虚拟机》。Class.forName(String className)使用哪个类加载器? 这个链接里就有一个简单的类加载器的代码,可以试试。关键就是这里: MyClassLoader classLoader = new MyClassLoader(); 这个类加载器继承了classloader,重写了loadclass方法,也就是利用io流把二进制数据读取进内存的过程(回看前面的定义,类加载器其实就是通过一个类的全限定名来获取描述此类的二进制流)
然后 Class<?> clazz = classLoader.loadClass("org.zzj.UserService")

利用新建的类加载器去加载这个类。


欢迎交流讨论。

发布于 2018-08-31