這回來分析一下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類的注冊方法。
類的加載能夠並行后,我們啟動應用的時候,肯定會更快。