這回來分析一下OSGI的類加載機制。java
先說一下OSGI能解決什麼問題吧。緩存
記得在上家公司的時候,常常參與上線。上線通常都是增長了一些功能或者修改了一些功能,而後將全部的代碼從新部署。過程當中要將以前的服務關掉,並且不能讓客戶訪問。雖然每回的夜宵都不錯,但仍是感受這個過程很麻煩,很彆扭。安全
爲何明明只修改了一部分代碼,卻都要從新來一遍。多線程
OSGI架構裏面,很重要的一個理念就是分模塊(bundle)。若是你只是修改了一個模塊,就能夠只熱替換這個模塊,不影響其它模塊。想一想就頗有吸引力。要實現這種功能,類加載的委派模型必須大改。像AppClassLoader --》ExtClassLoader --》BootstrapClassLoader這種固定的樹形結構,明顯不能擴展,不能實現需求。架構
OSGI的規範要求每一個模塊都有本身的類加載器,而模塊之間的依賴關係,就造成了各個類加載器之間的委派關係。這種委派關係是動態的,是自由戀愛,而不是指腹爲婚。。。。。。app
固然,委派是要依據規則的。這也好理解啊,談婚論嫁時,女方的家長確定會問,有房嗎、有車嗎、有幾塊腹肌啊。哎,又扯遠了。dom
當一個模塊(bundle)的類加載器遇到須要加載某個類或查找某個資源的請求時,規則步驟以下:ide
1)若是在以java.*開頭的package中,那麼這個請求須要委派給父類加載器ui
2)若是在父類委派清單所列明的package中,仍是委派給父類加載器this
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類的註冊方法。
類的加載可以並行後,咱們啓動應用的時候,確定會更快。