zoukankan      html  css  js  c++  java
  • 并行类加载与OSGI类加载

    这回来分析一下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。

    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方法:

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

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

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

        protected Object getClassLoadingLock(String className) {
            Object lock = this;
            if (parallelLockMap != null) {
                Object newLock = new Object();
                lock = parallelLockMap.putIfAbsent(className, newLock);
                if (lock == null) {
                    lock = newLock;
                }
            }
            return lock;
        }

    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,是为了向后兼容,因为以前的版本不支持并行。有疑问就看源码,

        // Maps class name to the corresponding lock object when the current
        // class loader is parallel capable.
        // Note: VM also uses this field to decide if the current class loader
        // is parallel capable and the appropriate lock object for class loading.
        private final ConcurrentHashMap<String, Object> parallelLockMap;
    
    
    
        private ClassLoader(Void unused, ClassLoader parent) {
            this.parent = parent;
            if (ParallelLoaders.isRegistered(this.getClass())) {
                parallelLockMap = new ConcurrentHashMap<>();
                package2certs = new ConcurrentHashMap<>();
                domains =
                    Collections.synchronizedSet(new HashSet<ProtectionDomain>());
                assertionLock = new Object();
            } else {
                // no finer-grained lock; lock on the classloader instance
                parallelLockMap = null;
                package2certs = new Hashtable<>();
                domains = new HashSet<>();
                assertionLock = this;
            }
        }

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

    ParallelLoaders是ClassLoader的内部类

        /**
         * Encapsulates the set of parallel capable loader types.
         */
        private static class ParallelLoaders {
            private ParallelLoaders() {}
    
            // the set of parallel capable loader types
            private static final Set<Class<? extends ClassLoader>> loaderTypes =
                Collections.newSetFromMap(
                    new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
            static {
                synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
            }
    
            /**
             * Registers the given class loader type as parallel capabale.
             * Returns {@code true} is successfully registered; {@code false} if
             * loader's super class is not registered.
             */
            static boolean register(Class<? extends ClassLoader> c) {
                synchronized (loaderTypes) {
                    if (loaderTypes.contains(c.getSuperclass())) {
                        // register the class loader as parallel capable
                        // if and only if all of its super classes are.
                        // Note: given current classloading sequence, if
                        // the immediate super class is parallel capable,
                        // all the super classes higher up must be too.
                        loaderTypes.add(c);
                        return true;
                    } else {
                        return false;
                    }
                }
            }
    
            /**
             * Returns {@code true} if the given class loader type is
             * registered as parallel capable.
             */
            static boolean isRegistered(Class<? extends ClassLoader> c) {
                synchronized (loaderTypes) {
                    return loaderTypes.contains(c);
                }
            }
        }

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

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

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

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

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

        static {
            // Register this base class loader as parallel capable on Java 7+ JREs
            Method getClassLoadingLockMethod = null;
            try {
                if (JreCompat.isJre7Available()) {
                    final Method registerParallel =
                            ClassLoader.class.getDeclaredMethod("registerAsParallelCapable");
                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
                        @Override
                        public Void run() {
                            registerParallel.setAccessible(true);
                            return null;
                        }
                    });
                    registerParallel.invoke(null);
                    getClassLoadingLockMethod =
                            ClassLoader.class.getDeclaredMethod("getClassLoadingLock", String.class);
                }
            } catch (Exception e) {
                // ignore
            }

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

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

  • 相关阅读:
    ntohs, ntohl, htons,htonl的比较和详解
    转 linux socket的select函数例子
    转 结构体中字节对齐问题(转载)
    C语言中volatile关键字的作用
    转 字符串模式匹配算法——BM、Horspool、Sunday、KMP、KR、AC算法
    转 常见hash算法的原理
    转 从头到尾彻底解析Hash表算法
    [CEOI2020 D2T1 / CF1403A] 权力药水The Potion of Great Power 题解
    [CEOI2020 D1T3 / CF1402C] 星际迷航Star Trek 题解
    [CEOI2020 D1T2 / CF1402B] 道路Roads 题解
  • 原文地址:https://www.cnblogs.com/cz123/p/6918708.html
Copyright © 2011-2022 走看看