这回来分析一下OSGI的类加载机制。

先说一下OSGI能解决什么问题吧。

记得在上家公司的时候,经常参与上线。上线一般都是增加了一些功能或者修改了一些功能,然后将所有的代码重新部署。过程中要将之前的服务关掉,而且不能让客户访问。虽然每回的夜宵都不错,但还是感觉这个过程很麻烦,很别扭。

为什么明明只修改了一部分代码,却都要重新来一遍。

OSGI架构里面,很重要的一个理念就是分模块(bundle)。如果你只是修改了一个模块,就可以只热替换这个模块,不影响其它模块。想想就很有吸引力。要实现这种功能,类加载的委派模型必须大改。像AppClassLoader --》ExtClassLoader --》BootstrapClassLoader这种固定的树形结构,明显不能扩展,不能实现需求。

OSGI的规范要求每个模块都有自己的类加载器,而模块之间的依赖关系,就形成了各个类加载器之间的委派关系。这种委派关系是动态的,是自由恋爱,而不是指腹为婚。。。。。。

当然,委派是要依据规则的。这也好理解啊,谈婚论嫁时,女方的家长肯定会问,有房吗、有车吗、有几块腹肌啊。哎,又扯远了。

当一个模块(bundle)的类加载器遇到需要加载某个类或查找某个资源的请求时,规则步骤如下:

1)如果在以java.*开头的package中,那么这个请求需要委派给父类加载器

2)如果在父类委派清单所列明的package中,还是委派给父类加载器

3)如果在import-package标记描述的package中,委派给导出这个包的bundle的类加载器

4)如果在require-bundle导入的一个或多个bundle的包中,就好安装require-bundle指定的bundle清单顺序逐一委派给对应bundle的类加载器

5 )搜索bundle内部的classpath

6)搜索每个附加的fragment bundle的classpath

7)如果在某个bundle已经声明导出的package中,或者包含在已经声明导入(import-package或require-bundle)的package中,搜索终止

8)如果在某个使用dynamicimport-package声明导入的package中,尝试在运行时动态导入这个package

9)如果可以确定找到一个合适的完成动态导入的bundle,委派给该bundle的类加载器

上面这部分完全照抄周志明的著作《深入理解OSGI》。规则里面的父类加载器、bundle等概念,读者都可以从书中找到完整的讲解,我这里就不展开了。

根据这个规则,所有的bundle之间的类加载形成了错综复杂的网状结构,不再是一沉不变的单一的树状结构。

但是网状结构,会有一个致命的问题。在jdk1.6包括之前,ClassLoader的类加载方法是synchronized。

  1. protected synchronized Class<?> loadClass(String name, boolean resolve)

我们想象一个场景:bundle A 和 bundle B 互相引用了对方的package。这样在A加载B的包时,A在自己的类加载器的loadClass方法中,会最终调用到B的类加载器的loadClass方法。也就是说,A首先锁住自己的类加载器,然后再去申请B的类加载器的锁;当B加载A的包时,正好相反。这样,在多线程下,就会产生死锁。你当然可以让所有的类加载过程在单线程里按串行的方式完成,安全是安全,但是效率太低。

由此,引出了本文的另一个主题---并行类加载。

synchronized方法锁住的是当前的对象,在这种情况下,调用loadClass方法去加载一个类的时候,锁住的是当前的类加载器,也就不能再用这个类加载器去加载别的类。效率太低,而且容易出现死锁。

于是设计jdk的大牛,对这种模式进行了改进。大牛就是大牛!!!

看看jdk1.6之后的loadClass方法:

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // First, check if the class has already been loaded
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. long t0 = System.nanoTime();
  9. try {
  10. if (parent != null) {
  11. c = parent.loadClass(name, false);
  12. } else {
  13. c = findBootstrapClassOrNull(name);
  14. }
  15. } catch (ClassNotFoundException e) {
  16. // ClassNotFoundException thrown if class not found
  17. // from the non-null parent class loader
  18. }
  19.  
  20. if (c == null) {
  21. // If still not found, then invoke findClass in order
  22. // to find the class.
  23. long t1 = System.nanoTime();
  24. c = findClass(name);
  25.  
  26. // this is the defining class loader; record the stats
  27. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  28. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  29. sun.misc.PerfCounter.getFindClasses().increment();
  30. }
  31. }
  32. if (resolve) {
  33. resolveClass(c);
  34. }
  35. return c;
  36. }
  37. }

synchronized移到了方法内的代码块中,也就是说不再是简单的锁定当前类加载器,而是锁定一个生成的对象。

那么这个充当锁的对象是如何生成的?

  1. protected Object getClassLoadingLock(String className) {
  2. Object lock = this;
  3. if (parallelLockMap != null) {
  4. Object newLock = new Object();
  5. lock = parallelLockMap.putIfAbsent(className, newLock);
  6. if (lock == null) {
  7. lock = newLock;
  8. }
  9. }
  10. return lock;
  11. }

parallelLockMap是一个ConcurrentHashMap,putIfAbsent(K, V)方法查看K和V是否已经对应,是的话返回V,否则就将K,V对应起来,返回null。

第一个if判断里面的逻辑,一目了然:对每个className关联一个锁,并将这个锁返回。也就是说,将锁的粒度缩小了。只要类名不同,加载的时候就是完全并行的。这与ConcurrentHashMap实现里面的分段锁,目的是一样的。

我这里有2个问题希望读者思考一下:

1)为什么不直接用className这个字符串充当锁对象

2)为什么不是直接new一个Object对象返回,而是用一个map将className和锁对象缓存起来

上面的方法中还别有洞天,为什么要判断parallelLockMap是否为空,为什么还有可能返回this,返回this的话不就是又将当前类加载器锁住了吗。这里返回this,是为了向后兼容,因为以前的版本不支持并行。有疑问就看源码,

  1. // Maps class name to the corresponding lock object when the current
  2. // class loader is parallel capable.
  3. // Note: VM also uses this field to decide if the current class loader
  4. // is parallel capable and the appropriate lock object for class loading.
  5. private final ConcurrentHashMap<String, Object> parallelLockMap;
  6.  
  7. private ClassLoader(Void unused, ClassLoader parent) {
  8. this.parent = parent;
  9. if (ParallelLoaders.isRegistered(this.getClass())) {
  10. parallelLockMap = new ConcurrentHashMap<>();
  11. package2certs = new ConcurrentHashMap<>();
  12. domains =
  13. Collections.synchronizedSet(new HashSet<ProtectionDomain>());
  14. assertionLock = new Object();
  15. } else {
  16. // no finer-grained lock; lock on the classloader instance
  17. parallelLockMap = null;
  18. package2certs = new Hashtable<>();
  19. domains = new HashSet<>();
  20. assertionLock = this;
  21. }
  22. }

可见,对于parallelLockMap的处理一开始就分成了2种逻辑:如果将当前类加载器注册为并行类加载器,就为其赋值;否则就一直为null。

ParallelLoaders是ClassLoader的内部类

  1. /**
  2. * Encapsulates the set of parallel capable loader types.
  3. */
  4. private static class ParallelLoaders {
  5. private ParallelLoaders() {}
  6.  
  7. // the set of parallel capable loader types
  8. private static final Set<Class<? extends ClassLoader>> loaderTypes =
  9. Collections.newSetFromMap(
  10. new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
  11. static {
  12. synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
  13. }
  14.  
  15. /**
  16. * Registers the given class loader type as parallel capabale.
  17. * Returns {@code true} is successfully registered; {@code false} if
  18. * loader's super class is not registered.
  19. */
  20. static boolean register(Class<? extends ClassLoader> c) {
  21. synchronized (loaderTypes) {
  22. if (loaderTypes.contains(c.getSuperclass())) {
  23. // register the class loader as parallel capable
  24. // if and only if all of its super classes are.
  25. // Note: given current classloading sequence, if
  26. // the immediate super class is parallel capable,
  27. // all the super classes higher up must be too.
  28. loaderTypes.add(c);
  29. return true;
  30. } else {
  31. return false;
  32. }
  33. }
  34. }
  35.  
  36. /**
  37. * Returns {@code true} if the given class loader type is
  38. * registered as parallel capable.
  39. */
  40. static boolean isRegistered(Class<? extends ClassLoader> c) {
  41. synchronized (loaderTypes) {
  42. return loaderTypes.contains(c);
  43. }
  44. }
  45. }

原来,一个类加载器想要成为一个并行类加载器,是需要自己注册的,看看注册方法

  1. @CallerSensitive
  2. protected static boolean registerAsParallelCapable() {
  3. Class<? extends ClassLoader> callerClass =
  4. Reflection.getCallerClass().asSubclass(ClassLoader.class);
  5. return ParallelLoaders.register(callerClass);
  6. }

最终还是调用了内部类的注册方法。源码在上面,可以看到,一个类加载器要想注册,它的父类必须已经注册了,也就是说从继承路径上的所有父类都必须是并行类加载器。而且一开始,就把ClassLoader这个类注册进去了。

我有个疑问,这里有父类的什么事呢,光注册自己这个类就好了呀。想了半天,还是不明白,是有关于安全吗?哎,大牛就是大牛,哈哈。读者如有明白的,请直言相告。

最后,来看看并行类加载在Tomcat上的应用。原本WebappClassLoader没有注册,只能串行加载类。后来,是阿里意识到了这个问题,解决方案被Tomcat采纳。

  1. static {
  2. // Register this base class loader as parallel capable on Java 7+ JREs
  3. Method getClassLoadingLockMethod = null;
  4. try {
  5. if (JreCompat.isJre7Available()) {
  6. final Method registerParallel =
  7. ClassLoader.class.getDeclaredMethod("registerAsParallelCapable");
  8. AccessController.doPrivileged(new PrivilegedAction<Void>() {
  9. @Override
  10. public Void run() {
  11. registerParallel.setAccessible(true);
  12. return null;
  13. }
  14. });
  15. registerParallel.invoke(null);
  16. getClassLoadingLockMethod =
  17. ClassLoader.class.getDeclaredMethod("getClassLoadingLock", String.class);
  18. }
  19. } catch (Exception e) {
  20. // ignore
  21. }

这段代码出现在WebappClassLoader类的父类WebappClassLoaderBase里,通过反射调用了ClassLoader类的注册方法。

类的加载能够并行后,我们启动应用的时候,肯定会更快。

并行类加载与OSGI类加载的更多相关文章

  1. OSGi类加载流程

    思路 OSGi每个模块都有自己独立的classpath.如何实现这一点呢?是因为OSGi采取了不同的类加载机制: OSGi为每个bundle提供一个类加载器,该加载器能够看到bundle Jar文件内 ...

  2. JVM,Tomcat与OSGi类加载机制比较

    首先一个思维导图来看下Tomcat的类加载机制和JVM类加载机制的过程 类加载 在JVM中并不是一次性把所有的文件都加载到,而是一步一步的,按照需要来加载. 比如JVM启动时,会通过不同的类加载器加载 ...

  3. OSGi类加载问题

    项目中遇到的JVM难点 ——启动OSGi容器时,出现永久代内存不够.内存泄露 ——OSGi找不到类路径问题. ——线程死锁问题.   问题一:OSGi类内存问题         其次,从内存用量来看, ...

  4. Tomcat 应用中并行流带来的类加载问题

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/f-X3n9cvDyU5f5NYH6mhxQ作者:肖铭轩.王道环 随着 Java8 的不断流行, ...

  5. 【JVM进阶之路】十四:类加载器和类加载机制

    在上一章里,我们已经学习了类加载的过程,我们知道在加载阶段需要"通过一个类的全限定名来获取描述该类的二进制字节流",而来完成这个工作的就是类加载器(Class Loader). 1 ...

  6. java类加载器-Tomcat类加载器

    在上文中,已经介绍了系统类加载器以及类加载器的相关机制,还自定制类加载器的方式.接下来就以tomcat6为例看看tomat是如何使用自定制类加载器的.(本介绍是基于tomcat6.0.41,不同版本可 ...

  7. java类加载器-系统类加载器

    系统类加载器 系统类加载器可能都耳详能熟,但是为了完整点,还是先简单的说说系统的类加载器吧. public class Test { public static void main(String[] ...

  8. tomcat6类加载器与类加载顺序

    tomcat6.0.32 com.dyyx.ShareUtils//返回系统当前时间public static String now(); package com.dyyx;import java.t ...

  9. Java自定义类加载和ClassPath类加载器

    1 自定义类加载器: 实现规则: 自定义类加载器,需要重写findClass,然后通过调用loadClass进行类加载(loadClass通过递归实现类的双亲委派加载) package com.dax ...

随机推荐

  1. Redis数据类型之字符串String

    String类型是Redis中最基本也最简单的一种数据类型 首先演示一些常用的命令 一.SET key value 和GET key SET key value 和 GET key  设置键值和获取值 ...

  2. 一些CSS/JS小技巧

    CSS部分 1.文本框不可点击 .inputDisabled{ background-color: #eee;cursor: not-allowed;} 2.禁止复制粘贴 onpaste=" ...

  3. Bug 笔记

    1.页面返回 400 Bag request: 原因:使用Spring  MVC  controller的时候,查询数据库:当数据库的数据类型是int型时,Spring MVC在查询的数据匹配给实体类 ...

  4. android设备使用usb串口传输数据

    首先介绍两个开源项目一个是Google的开源项目:https://code.google.com/archive/p/android-serialport-api/ 另一个是我们这次介绍的开源项目:h ...

  5. 通过修改CoreCLR中的ClrHost实现自托管程序

    上一篇我们讲了如何在windows和Linux上编译CoreClr的问题 虽然文章使用的是windows 10 (Bash)环境,但是也可以做为ubuntu环境的参考. 成功编译CoreCLR的源代码 ...

  6. git工具使用的简单介绍

    百度百科 写道 Git是一款免费.开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目. Git的读音为/gɪt/. Git是一个开源的分布式版本控制系统,用以有效.高速的处理从很小到非常大 ...

  7. 用Rvm安装Ruby,Rails运行环境及常见错误解决方法

    一.安装Rvm 1.下载安装Rvm $ curl -L https://get.rvm.io | bash -s stable 此时可能出现错误:"gpg: 无法检查签名:找不到公钥&quo ...

  8. [Linux] PHP程序员玩转Linux系列-telnet轻松使用邮箱

    1.PHP程序员玩转Linux系列-怎么安装使用CentOS 2.PHP程序员玩转Linux系列-lnmp环境的搭建 3.PHP程序员玩转Linux系列-搭建FTP代码开发环境 4.PHP程序员玩转L ...

  9. STM32实战应用(一)——1602蓝牙时钟1液晶的显示测试

    前言 从51到STM32F4学习这么久了,总算找到点头绪了,目前学习了GPIO,中断,定时器,看门狗的基本使用,所以想试着看看能不能做个什么东西,就是想复习一下最近学习的知识.正好上学期单片机课程设计 ...

  10. highcharts的多级下钻以及图形形态转换

    <script src="https://img.hcharts.cn/jquery/jquery-1.8.3.min.js"></script> < ...