JAVA虚拟机入门(2)------ 类加载机制(下)

来源:互联网 发布:大数据在医疗领域应用 编辑:程序博客网 时间:2024/06/01 08:09

大家好,今天是java虚拟机入门系列的最后一篇文章,希望没有看过前面的人能先看看前面的文章,这样衔接起来会比较容易。

JAVA虚拟机入门(2)——类加载机制(上)
JAVA虚拟机入门(2)—— 类加载机制(中)

今天我们讲解的是java虚拟机中一个极为重要的东西——类加载器!
之前我们在解析的时候说过,得知一个类的全限定名后,我们将这个全限定名传递给类加载器,由类加载器负责加载这个类。那个时候我们没有说明类加载器到底是什么,类加载器到底怎么工作,现在,让我们把这些问题弄清楚吧!

一:类加载器是什么

首先问一个问题,如果是你设计java虚拟机,你会将类加载的阶段放在java虚拟机内进行还是在java虚拟机外面进行呢?(提醒一下:类加载阶段提倡多种途径来加载类)如果你说在java虚拟机外面进行的话,恭喜你,因为java虚拟机团队就是这么干的。虚拟机设计团队将“根据类的全限定名来获取描述此类的二进制字节流”这个动作放在虚拟机外部实现,从而让应用程序自己决定去哪里获取所需要的类。
而执行这个动作的代码模块就是类加载器了!
也就是说,类加载器实际上是一串代码,用于根据全限定名加载特定的类。

那么有人就问了,怎么根据全限定名来加载特定的类呢?
这一切都是在loadClass函数中进行。
(1)根据全限定名获取后缀为”.class”的class文件名
(2)根据class文件名获取InputStream流
(3)根据InputStream流获取byte[]字节流
(4)调用defineClass()函数获得类

上代码!

ClassLoader classLoader = new ClassLoader(){            @Override            public Class<?> loadClass(String name) throws ClassNotFoundException{                try{                    //第一步                    String classFileName = name.substring(name.lastIndexOf(".") + 1) + ".class";                                                //第二步                    InputStream is = getClass().getResourceAsStream(classFileName);                    if(is == null){                        return super.loadClass(name);                    }                    byte[] b = new byte[is.available()];                    //第三步                    is.read(b);                    is.close();                    return defineClass(name,b,0,b.length);//第四步                }catch(Exception e){                    throw new ClassNotFoundException();                }            }        };

二:类和类加载器的关系

在知道类加载器到底是什么后,我们来说说类和类加载器的关系。
问题又来了。请问,如何判断两个类是否相等?只要简单名称相同就行了吗?你可能会说,不是的,要全限定名相等。很好,说明你知道了类的标志是全限定名。但是这样就够了吗?

我们来做个实验,来加载一个类javaLearning.DynamicUrlExample,代码是在上面的代码基础上加工。

public class ClassLoaderExample {    public static void main(String[]args) throws IOException, InstantiationException, IllegalAccessException, ClassNotFoundException{        ClassLoader classLoader = new ClassLoader(){            @Override            public Class<?> loadClass(String name) throws ClassNotFoundException{                try{                    String classFileName = name.substring(name.lastIndexOf(".") + 1) + ".class";                    InputStream is = getClass().getResourceAsStream(classFileName);                    if(is == null){                        return super.loadClass(name);                    }                    byte[] b = new byte[is.available()];                    is.read(b);                    is.close();                    return defineClass(name,b,0,b.length);                }catch(Exception e){                    throw new ClassNotFoundException();                }            }        };        Object inputStreamInstance = classLoader.loadClass("javaLearning.DynamicUrlExample").newInstance();        boolean isEqual = inputStreamInstance instanceof javaLearning.DynamicUrlExample;        System.out.println(isEqual);    }}

这段代码是将同一个类javaLearning.DynamicUrlExample使用不同的加载器加载,一个使用自定义的类加载加载,另一个使用默认的类加载器加载。如果按照前面所说,如果全限定名相同,就是同一个类,那么输出应该是true,然而,输出是:

false

这就说明类相等不仅要类的全限定名相同,还要加载这个类的加载器相同。

注意:这里说的类的相等,指:
1、equals()
2、isAssignableFrom()
3、isInstance()
4、instanceof

三:类加载器的层次结构

在认识到类相等是由类的全限定名和类的加载器共同决定之后,我们就有必要认识类到底是怎么由类加载器加载了。咦,我们在第一点的时候不是已经说了吗?不不不,第一点说的是在一个类加载器中怎么根据一个全限定名加载类(注意到我们是在一个类加载的loadClass中完成加载的),而我们现在要说的是到底应该由哪个加载器加载的问题,这两者是不一样的,要区分清楚。

那么这里就涉及到类加载器的双亲委派模式了!

(一)双亲委派模式

双亲委派模式是什么呢?用一张图来表示:

双亲委派模式

我们来慢慢说明这里面的每一个加载器到底是什么?

1、启动类加载器(Bootstrap ClassLoader)

这个加载器负责将<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath所指定的路径中的,并且能够被虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库放在lib目录下也不会加载)类库加载到java虚拟机中。启动类加载器无法被应用程序直接引用。

2、扩展类加载器(Extension ClassLoader)

它负责加载<JAVA-HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
注意:和启动类加载器不同,扩展类加载器能够加载目录下的所有类库,而不仅仅是特定文件名的类库。

3、应用程序类加载器(Application ClassLoader)

这个类加载器也被称为系统类加载器,因为是getSystemClassLoader()的返回值。它负责加载用户类路径(ClassPath)上的指定类库。开发者也可以直接使用这个加载器。
注意:应用程序类加载器一般是程序默认的加载器。

4、自定义类加载器(User ClassLoader)

这个类加载器是开发者自定义的类加载器,用于实现自己的加载类,比如上面我们的自定义加载器,就是属于这里的加载器。

在认识所有的类加载器后,我们来看看双亲代理模式到底是什么?

简单来说,双亲代理模式就是“有事找爹娘,爹娘摆不平自己解决”。
什么意思呢?在一个类加载接受到一个加载类的请求后,它并不会立刻加载这个类,而是将这个类请求委派给父类加载器去完成,每个层次的类加载器都是这样子的,因此类请求最终会汇聚到启动类加载器。只有当父类加载器无法加载这个类的时候(比如启动类加载器在指定的自己可以加载的目录下没有找到对应的类),子类才会尝试自己去加载。

那么这种双亲委派模式怎么实现呢?实际上十分简单,看代码:

 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {    // First, check if the class has already been loaded   Class c = findLoadedClass(name);   if (c == null) {        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.      c = findClass(name);     }   }   if (resolve) {     resolveClass(c);  }  return c;}

双亲委派模式可以描述如下:
(1)搜索这个类是否曾经加载过,如果已经加载过,则没必要重复加载,返回
(2)否则,让父类加载器尝试加载这个类,如果加载成功,返回
(3)否则,自己尝试加载这个类,返回。

理解完双亲委派模式后,来想一想,为什么我们需要双亲委派模式?为什么不能直接由子类来加载类,而是要先尝试让父类加载?

那么就来假设吧,如果由子类来加载类,那么如果我自己随便写了一个java.lang.Object,并且将它放在ClassPath中(也就是系统类加载器可以成功加载),这样系统中就出现了两个Object(包括系统自己的java.lang.Object)。我们也知道,java.lang.Object是全部类需要继承的最基础的类,但是现在出现了两个java.lang.Object,我们该继承哪一个呢?如果连这个类都无法保证,那么java系统不就凌乱了么?

但是如果使用双亲委派模式的话,不管你加载的类是什么,先由最顶层的启动类加载器加载,就这个例子而言,因为java.lang.Object存在于rt.jar中,因此启动类加载器能够成功加载这个类,子类就不会去加载这个类了,这样整个系统就只有启动类加载器加载的java.lang.Object这个唯一的类了,世界又恢复了往日的平静。

(二)破坏双亲委派模式

虽然java设计者建议使用双亲委派模式,但是由于各种原因,还是会采取违背双亲委派模式的方法,其中较大规模的破坏双亲委派模式有三种:

1、双亲委派模式出现之前用户重写loadClass()

在双亲委派模式出现之前,用户继承ClassLoader的唯一目的就是重写loadClass()函数。还记得这个函数吗?没错,在我们讲解怎么实现双亲委派模式的时候将loadClass()函数放出来过,然而loadClass()在双亲委派模式出现之前是用户自己重写的。在双亲委派模式出现后,loadClass()不再建议被用户重写,但是为了满足用户对于ClassLoader自定义的需要,java设计者添加了一个新的方法findClass(),请看看loadClass()的源代码,我们发现findClass()只有在父类加载失败的情况下才会调用,因此保证了双亲委派模式。
总而言之,在自定义ClassLoader的时候,应该重写findClass()而不是loadClass(),否则会破坏双亲委派模式。

2、妥协基础类对于调用用户代码的需要

JNDI服务是java的标准服务,它的任务是对资源进行集中管理和查找,需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI)的代码。但是JNDI服务的代码由启动类加载器加载,这不就麻烦了吗?为什么呢?请听我解释。

JNDI要调用SPI的代码,但是请注意SPI的代码是在应用程序的ClassPath中的,还记得吗?启动类加载器只会加载rt.jar下的类,不是rt.jar下的类,即使路径是<JAVA-HOME>\lib也不会被启动类加载器加载,也就是说SPI的代码根本没法加载!

那应该怎么办呢?java设计者做出妥协—-使用线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以由java.lang.Thread类下的setContextClassLoader(ClassLoader cl)的方法设置。如果创建线程时还没设置,它将从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。

有了线程类加载器,父类加载器可以请求子类加载器完成类加载的动作,加载完后再供父类使用。

总而言之,线程类加载器本质上还是一个加载器,默认是系统类加载器,可以使得应用在当前线程中不遵循双亲委派模式,从而达到自己的目的。

3、用户对程序动态性的追求

在实际工作中,我们经常会需要根据已有的bug来修复原有的代码,如果每次修复都要求用户将原来的应用程序卸载,将最新的版本安装,这样显然用户是不乐意的。那怎么办呢?这个时候就出现了代码热替换,模块热部署的概念。这两者能够让用户只需下载需要更新的部分来替换有bug的部分,而不需要将整个应用卸载,这显然是更有利于留住客户的。而动态性指的就是代码热替换和模块热部署等这些“热”概念。

那怎么实现动态性呢?
这就需要java的模块化标准“OSGi“出场了,它的自定义类加载机制正能实现这种动态性。

那OSGi的自定义类加载机制的规则是怎样的呢?

(1)将以java.*开头的类,委派给父类加载器加载;
(2)否则,将委派列表名单(org.osgi.framework.bootdelegation)内的类,委派给父类加载器加载;
(3)否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载;
(4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载;
(5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
(6)否则,查找Dynamic Import(动态导入)列表的Bundel,委派给对应Bundle的类加载加载
(7)否则,类查找失败

通过上面的描述我们清楚地知道,除了(1)、(2)满足双亲委派模式外,其余都是在同级的类加载器中进行。

由于OSGi过于复杂,在这里就不展开说明了,感兴趣的同学可以到OSGi中文社区入门中继续深入了解。

总结一下,这一节我们主要讲解了:
1、类加载器是什么?

2、比较两个类需要考虑什么?

3、类的双亲委派模式是什么?它因为什么而存在?

4、类的破坏双亲委派模式是什么?有几种情况?为什么会出现这种破坏模式?

如果你将上述问题都解决了,恭喜你,这篇博客已经达到它的目的了;如果没有,没关系,沿着问题在博客中找到答案,你会收获很多。

好了,到现在为止,我们的类加载机制已经全部学习完毕,感谢大家一直以来的支持,接下来我还会继续沿着这条主线学习下去,希望大家一如既往地支持我,谢谢。

0 0
原创粉丝点击