《深入理解jvm》读书笔记之——类加载器

类与类加载器

类加载器只用于实现类加载的动作,但是同时还有着确保类的唯一性的作用。也就是说:比较两个类是否相等,只有确保这两个类在同一个类加载器的前提下才是有意义的。否则即使两个类来源于同一个klass文件,被同一个jvm加载,只要他们的类加载器不同,那么这两个类就不相等(这里的相等代表klass对象的equals、isAssignableFrom、isInstance方法返回的结果)。

双亲委派模式

类加载器的种类

对于jvm来讲,类加载器分为两种:
一种是启动类加载器,使用c++实现,是jvm自身的一部分
另一种是所有其他的类加载器,使用java实现,独立于虚拟机外部,并且继承自ClassLoader。

然而对于java开发人员来讲,类加载器可以分成这三种:

  1. 启动类加载器
  2. 扩展类加载器
  3. 应用程序类加载器。这个类加载器也被称为系统类加载器,负责加载用户路径上的类库,开发者可以直接使用这个类加载器。

对于jvm加载一个类而言,是通过上面的三种类加载器相互配合加载的,他们之间的关系如下图:

img

上图中展示的这种一层层的层次加载关系,被称为类的双亲委派模式。的双亲委派要求除了顶层的类加载器外都有自己的父类加载器。这里的类加载器之间的父子关系一般都不会以继承的关系来实现,而是组合关系来复用类加载器的代码。

整个双亲委派模式的过程是:

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

为什么要使用双亲委派模式:因为这样加载一个类,java类随着他的类加载器一起具备了一种带有优先级的层次关系,例如java.lang.Object他放在rt.jar中。无论哪个类加载器加载这个类,最后都是委派给最顶端的启动类加载器去加载,这样就保证了java.lang.Object无论是由哪个类加载器加载的,在当前jvm下都是同一个类。

破坏双亲委派模型

  1. 第一次破坏是在jdk2之前,用户自定义的类加载器都是重写Classloader中的loadClass方法,这样就导致每个自定义的类加载器其实是在使用自己的loadClass方法中的加载机制来进行加载,这种模式当然是不符合双亲委派机制的,也是无法保证同一个类在jvm中的唯一性的,那么为了保证及时是由不同的类加载器(哪怕是用户自定义的类加载器加载)也是唯一的,java官方在Classloader中添加了findClass方法,用户只需要重新这个findClass方法,在loadClass方法的逻辑里,如果父类加载失败的时候,才会调用自己的findClass方法来完成类加载,这样就完成了符合双亲委派机制。

  2. 第二次的破坏是类似于jndi,jdbc这种服务,因为这种服务需要回调用户的代码,但是对于父类加载器而言是不认识用户的代码的。

    那么这时候java团队使用了一个不太优雅的设计:线程上下文类加载器。这个类加载器可以通过Thread类的setContextClassLoader方法进行设置,如果创建线程时还未设置,它就从父线程继承一个,如果在应用全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

利用这个线程上下文类加载器,jdni去加载需要的spi代码,也就是父类请求子类的加载器去加载。

  1. 第三次的破坏是因为用户对于程序的动态性追求,诸如:代码热替换,模块热部署。
    这时候就诞生了诸如jigsaw和osgi。对于现在的业界来讲,osgi赢得了java模块化的主导权,成为目前业界模块化的标准。而Osgi模块话的关键是他自己的类加载机制:每个程序模块(bundle)都有自己的类加载器,需要更换程序(bundle)的时候,连同类加载器一起替换,以实现代码的热部署

osgi和双亲委派模式不同,他是一个基于网状的互相组合依赖的加载。
Osgi的加载步骤是这样的:

  1. 如果类或者资源是在包java.*中,那么交由父级类加载器代理完成,否则,搜索过程进入第二步。如果父类级类加载器加载失败,那么查找过程结束,加载失败。
  2. 如果类或者资源在启动代理序列(org.osgi.framework.bootdelegation)中定义,那么交由父级代理完成,此时的父级代理有启动参数org.osgi.framework.bundle.parent指定,默认是引导类加载器(bootstrap class loader),如果找到了类或者资源,那么查找过程结束。
  3. 如果类或者资源所在的包是在Import-Package中指定的,或者是在此之前通过动态导入加载的了,那么将请求转发到导出bundle的类加载器,否则搜索继续进行下一步;如果该包在启动参数org.osgi.framework.system.packages.extra中,则将请求转发给osgi容器外部的类加载器(通常是系统类加载器)。如果将请求交由导出类加载器代理,而类或者资源又没有找到,那么查找过程中止,同时请求失败。
  4. 如果包中类或者和资源所在的包由其他bundle通过是使用Require-Bundle从一个或多个其他bundle进行导入的了,那么请求交由其他那些bundle的类加载器完成,按照根据在bundle的manifest中指定的顺序进行查找进行查找。如果没有找到类或者资源,搜索继续进行。
  5. 使用bundle本身的内部bundle类路径查找完毕之后,。如果类或者资源还没有找到,搜索继续到下一步。
  6. 查找每一个附加的fragment的内部类路径,fragment的查找根据bundle ID顺序升序查找。如果没有找到类或者资源的,查找过程继续下一步。
  7. 如果包中类或者资源所在的包由bundle导出,或者包由bundle导入(使用Import-Package或者Require-Bundle),查找结束,即类或者资源没有找到。
  8. 否则,如果类或者资源所在的包是通过使用DynamicImport-Package进行导入,那么试图进行包的动态导入。导出者exporter必须符合包约束。如果找到了合适的导出者exporter,然后建立连接,以后的包导入就可以通过步骤三进行。如果连接建立失败,那么请求失败。
  9. 如果动态导入建立了,请求交由导出bundle的类加载器代理。如果代理查找失败,那么查找过程中止,请求失败
Loading Disqus comments...
Table of Contents