JVM筆記11-類加載器和OSGI


一.JVM 類加載器:

一個類在使用前,如何通過類調用靜態字段,靜態方法,或者new一個實例對象,第一步就是需要類加載,然后是連接和初始化,最后才能使用。

  類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、准備(Preparation)、解析(Resolution)、初始化(Initialzation)、使用(Using)和卸載(Unloading)7 個階段。其中驗證、准備、解析 3 個部分統稱為連接(Linking),這 7 個階段的發生順序如下圖所示:

加載、驗證、准備、初始化和卸載這 5 個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持 Java 語言的運行時綁定(也稱為動態綁定或晚期綁定)。注意,這里筆者寫的是按部就班地 “開始”,而不是按部就班地 “進行” 或 “完成”,強調這點是因為這些階段通常都是互相交叉地混合式進行的,通常會在一個階段執行的過程中調用、激活另外一個階段。

        什么情況下需要開始類加載過程的第一個階段:加載?Java 虛擬機規范中並沒有進行強制約束,這點可以交給虛擬機的具體實現來自由把握。但是對於初始化階段,虛擬機規范則是嚴格規定了有且只有 5 種情況必須立即對類進行 “初始化”(而加載、驗證、准備自然需要在此之前開始):

  1.   遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這 4 條指令的最常見的 Java 代碼場景是:使用 new 關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  2.   使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  3.   當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4.   當虛擬機啟動時,用戶需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個類。
  5.   當使用 JDK 1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結構 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先觸發其初始化。

        對於這 5 種會觸發類進行初始化的場景,虛擬機規范中使用了一個很強烈的限定語:“有且只有”,這 5 種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用。

類加載器就是將 .java 代碼文件編譯成 .class 字節碼文件后,Java虛擬機的類加載器通過讀取此類的二進制流,轉換成目標類的實例。

除了Java會生成字節碼外,運行在JVM上的JRuby,Scala,Groovy同樣需要編譯成對應的 .class 文件,這里列舉了四種不同的字節碼,不單是Java才生成字節碼文件。

常用的類加載器有4種:

  1.Bootstrap ClassLoader:啟動類加載器,加載JAVA_HOME/lib 目錄下的類。如下圖選中的就是

 

 

  2.ExtClassLoader:擴張類加載器,加載JAVA_HOME/lib/ext 目錄下的類。

 

   3.AppClassLoader:應用程序類加載器,加載用戶指定的classpath(存放 src 目錄 Java 文件編譯之后的 class 文件和 xml、properties 等資源配置文件的 src/main/webapp/WEB-INF/classes 目錄)下的類

  4.UserClassLoader:用戶自定義的類加載器(只要繼承 ClassLoader並實現 findClass(String name) 方法),自定義加載路徑。

 

類加載時並不需要等到某個類被首次主動使用時再加載它,JVM類加載器會在預料某個類要使用時預先加載。雙親委派模型,如下圖:

 

Java 類加載基於雙親委派模型——當有類加載請求時,從下往上檢查類是否被加載,如果沒被加載,UserClassLoader 就委托父類 AppClassLoader 加載,AppClassLoader 繼續委托其父類 ExtClassLoader 加載,接着分派給 Bootstrap ClasssLoader 加載;

如果無法加載就返回到發起加載請求的類加載一直到由最開始發起加載請求的 UserClassLoader 加載,所有類最終都會去到頂層。Bootstrap ClasssLoader 開始加載,無法加載就返回子加載器處理,一直到最開始的加載器。

這樣子,就算用戶自定義了 java.lang.Object 類和系統的 java.lang.Object 類重復,也不會被加載,下面我們就來自定義自己的類加載器。

/**
 * Created by cong on 2018/8/2.
*/
public class MyClassLoader extends ClassLoader {
    public MyClassLoader() {
        super();

    }

    public MyClassLoader(ClassLoader parent) {
        super(parent);

    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // do something
        // 自己先不加載,先讓父類加載
        return super.findClass(name);
    }

    public static void main(String[] args) throws ClassNotFoundException {

        MyClassLoader myLoader = new MyClassLoader();
        // 打印當前類路徑
        System.out.println(System.getProperty("java.class.path"));
        
        // ClassPath路徑下並不存在Demo.class類,故拋出異常
        System.out.println(myLoader.loadClass("Demo").getClassLoader().getClass().getName());

    }
}

運行結果如下:

 

學習自定義類加載器后,我們看下源碼里雙親委派模型是怎么加載類的。源碼如下:

public abstract class ClassLoader {
    private final ClassLoader parent;

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 先檢查類是否已經被加載
            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異常被拋出則表明父類加載器加載失敗  
                }
                if (c == null) {
                    // 如果父類無法加載,就自己加載      
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);

                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);

                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}

我們看到上面 loadClass 類里有同步代碼塊 synchronized (getClassLoadingLock(name)),而在 JDK1.6 之前是方法 protected synchronized Class<?> loadClass(String name, boolean resolve) 上鎖,鎖住方法當前對象。

這就導致一個問題,當 A 包依賴 B 包,A 在自己的類加載器的 loadClass 方法中,最終調用到 B 的類加載器的 loadClass 方法。A 先鎖住自己的類加載器,然后去申請 B 的類加載器的鎖,當 B 也依賴 A 包時,B 加載 A 的包時,過程相反,在多線程下,就容易產生死鎖。如果類加載器是單線程運行就會安全,但效率會很低 同步代碼塊 synchronized (getClassLoadingLock(name)) 鎖住的是一個特定對象。

   private final ConcurrentHashMap<String, Object> parallelLockMap;

   protected Object getClassLoadingLock(String className) {
        Object lock = this;
        // parallelLockMap是一個ConcurrentHashMap
        if (parallelLockMap != null) {
            // 鎖對象
            Object newLock = new Object();

            // putIfAbsent(K, V)方法查看K(className)和V(newLock)是否相互對應,
            // 是的就返回V(newLock),否則返回null
            // 每個className關聯一個鎖,並將這個鎖返回,縮小了鎖定粒度了,只要類名不同,就會匹配不同的鎖,
            // 就是並行加載,類似ConcurrentHashMap里面的分段鎖,
            // 不鎖住整個Map,而是鎖住一個Segment,每次只需要對Segment上鎖或解鎖,以空間換時間

            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                // 創建一個新鎖對象
                lock = newLock;
            }
        }
        return lock;

通過並行加載,可以提升加載效率,然后講下類加載的面試題,在 Java 反射中 Class.forName() 加載類和使用 ClassLoader 加載類是不一樣的。例子如下:

/**
 * Created by cong on 2018/8/5.
 */
public class MyCase {
    static {
        System.out.println("執行了靜態代碼塊");
    }

    private static String field = methodCheck();

    public static String methodCheck() {

        System.out.println("執行了靜態代方法");
        return "給靜態變量賦值";
    }
}

----------------------------------------------------

/**
 * Created by cong on 2018/8/5.
 */
public class DemoTest {
    public static void main(String[] args) {
        try {
            System.out.println("Class.forName開始執行:");
            //hjc是包名
            Class.forName("hjc.MyCase");
            System.out.println("ClassLoader開始執行:");
            ClassLoader.getSystemClassLoader().loadClass("hjc.MyCase");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

運行結果如下:

Class.forName 是加載 MyCase 類並完成初始化,給靜態代碼塊和靜態變量賦值,而 ClassLoader 只是將類加載進 JVM 虛擬機,並沒有初始化。

接下來我們進入Class.forName的源碼探究,源碼如下:

   @CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        return forName0(className, true,
                        ClassLoader.getClassLoader(Reflection.getCallerClass()));
    }

Class.forName 底層也是調用了 ClassLoader,只是第二個參數為 true,即加載類並初始化,默認就會初始化類,JDBC 連接就是用 Class.forName 加載驅動。所以注冊連接驅動會在靜態代碼塊執行,Sprng 里的 IOC 是通過 ClassLoader 來產生,可以控制 Bean 的延遲加載(首次使用才創建)。

 

二.OSGI 實戰:

  為了實現代碼熱替換,模塊化和動態化,就像鼠標一樣即插即用,雙親委派這種樹狀的加載器就難以勝任,於是出現了 OSGI 加載模型,OSGI 里每個程序模塊(Bundle,就是普通的 jar 包, 只是加入了特殊的頭信息,是最小的部署模塊)都會有自己的類加載器,當需要更換程序時,就連同 Bundle 和類加載器一起替換,是一種網狀的加載模型,Bundle 間互相委托加載,並不是層次化的。

Java 類加載機制的隔離是通過不同類加載器加載指定目錄來實現的,類加載的共享機制是通過雙親委派模型來實現,而 OSGI 實現隔離靠的是每個 Bundle 都自帶一個獨立的類加載器 ClassLoader。

OSGI 加載 Bundle 模塊的順序

  1. 首先檢查包名是否以 java.* 開頭,或者是否在一個特定的配置文件(org.osgi.framework.bootdelegation)中定義。如果是,則 bundle 類加載器立即委托給父類加載器(通常是 Application 類加載器),如果不是則進入 2
  2. 檢查是否在 Import-Package、Require-Bundle 委派列表里,如果是委托給對應 Bundle 類加載器,如果不是,進入 3
  3. 檢查是否在當前 Bundle 的 Classpath 里,如果是使用自己的類加載器加載,如果不是,進入 4
  4. 搜索可能附加在當前 bundle 上的 fragment 中的內部類,找到則委派給 Fragment bundle 類加載器加載,如果找不到,進入 5
  5. 查找動態導入列表里的 Bundle,委派給對應的類加載器加載,否則類加載失敗

如果用 Java 的結構的項目去部署,當項目復雜度提升時,每次上線,代碼只是增加或者修改了部分功能,但都得關掉服務,重新部署所有的代碼和配置,管理溝通成本都很高,很容產生線上事故,而 OSGI 的應用是一個模塊化的系統,避免了部署時 jar 或 classpath 錯綜復雜依賴管理,發布應用和更新應用都很強大,可以熱替換特定的 Bundle 模塊,提高部署可靠性。

接下來我們用IDE創建一個OSGI應用,首先要去 http://download.eclipse.org/equinox/ 下載最新的OSGI 框架enquinox

創建一個 OSGI 應用。打開 Eclipse,File->New->Project:

選擇 OSGI 框架 Equniox(Eclipse 強大的插件機制就是構建於 OSGI Bundle 之上,Eclipse 本身就包含了 Equniox) :

 

接下來,勾選創建 Activator 類,新建一個創Activator 類,每個 Bundle 啟動時都會調用 Bundle(模塊)里 Activator(類)的 start 方法,停止時調用 stop 方法,代碼如下:

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

/**
 * Created by cong on 2018/8/5.
 */
public class Activator implements BundleActivator {

    private static BundleContext context;

    static BundleContext getContext() {
        return context;
    }

    public void start(BundleContext bundleContext) throws Exception {
        Activator.context = bundleContext;
        //添加輸出This is OSGI Projcect
        System.out.println("This is OSGI Projcect");
    }

    public void stop(BundleContext bundleContext) throws Exception {
        Activator.context = null;
    }

}

接下來進行一下配置,Run->Run Configuration-> 雙擊 OSGI Framework 生成項目配置:如下圖:

 

然后點擊運行按鈕,可以看到控制台輸出 This is OSGI Projcect。在控制台我們輸入 ss ( short status) 查看服務狀態:

This is OSGI Projcect
osgi> ss
"Framework is launched."


id    State       Bundle
0    ACTIVE      org.eclipse.osgi_3.12.100.v20180210-1608
1    ACTIVE      org.apache.felix.gogo.runtime_0.10.0.v201209301036
2    ACTIVE      org.apache.felix.gogo.command_0.10.0.v201209301215

// ACTIVE表明 com.osgi.bundle.demo Bundle運行中 

3    ACTIVE      com.osgi.bundle.demo_1.0.0.qualifier
4    ACTIVE      org.apache.felix.gogo.shell_0.10.0.v201212101605
5    ACTIVE     org.eclipse.equinox.console_1.1.300.v20170512-2111
// 停止 com.osgi.bundle.demo Bundle  
osgi> stop com.osgi.bundle.demo
osgi> ss
"Framework is launched."


id    State       Bundle
0    ACTIVE      org.eclipse.osgi_3.12.100.v20180210-1608
1    ACTIVE      org.apache.felix.gogo.runtime_0.10.0.v201209301036
2    ACTIVE      org.apache.felix.gogo.command_0.10.0.v201209301215

// RESOLVED 表明 Bundle com.osgi.bundle.demo 停止了 

3    RESOLVED    com.osgi.bundle.demo_1.0.0.qualifier
4    ACTIVE      org.apache.felix.gogo.shell_0.10.0.v201212101605
5    ACTIVE      org.eclipse.equinox.console_1.1.300.v20170512-2111
// 通過close關閉整個應用框架
osgi> close
Really want to stop Equinox? (y/n; default=y)  y
osgi> 

 

一個 Bundle 包含 MANIFEST.MF,也就是 Bundle 的頭信息,Java 代碼以及配置文件(XML,Properties),其中 MANIFEST.MF 包含了下面的信息。如下所示:

/*版本號*/
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
/*名字*/
Bundle-Name: Demo
Bundle-SymbolicName: com.osgi.bundle.demo
Bundle-Version: 1.0.0.qualifier
/*Bundle類*/
Bundle-Activator: com.osgi.bundle.demo.Activator
Bundle-Vendor: OSGI
/*依賴環境*/
Bundle-RequiredExecutionEnvironment: JavaSE-1.7
/*導入的包*/
Import-Package: org.osgi.framework;version="1.3.0"
Bundle-ActivationPolicy: lazy

 

 

 

Equinox OSGi 命令列表

  1.控制框架

  1.launch 啟動框架

  2.shutdown 停止框架

  3.close 關閉、退出框架

  4.exit 立即退出,相當於 System.exit

  5.init 卸載所有 bundle(前提是已經 shutdown)

  6.setprop 設置屬性,在運行時進行

  2.控制 Bundle

  1.Install 安裝 uninstall 卸載

  2.Stop 停止

  3.Refresh 刷新

  4.Update 更新

  3.展示狀態

  1.Status 展示安裝的 bundle 和注冊的服務

  2.Ss 展示所有 bundle 的簡單狀態

  3.Services 展示注冊服務的詳細信息

  4.Packages 展示導入、導出包的狀態

  5.Bundles 展示所有已經安裝的 bundles 的狀態

  6.Headers 展示 bundles 的頭信息,即 MANIFEST.MF 中的內容

  7.Log 展示 LOG 入口信息

  4.其他

Exec 在另外一個進程中執行一個命令(阻塞狀態)

  1.Fork 和 EXEC 不同的是不會引起阻塞

  2.Gc 促使垃圾回收

  3.Getprop 得到屬性,或者某個屬性

  5.控制啟動級別

  1.Sl 得到某個 bundle 或者整個框架的 start level 信息

  2.Setfwsl 設置框架的 start level

  3.Setbsl 設置 bundle 的 start level

  4.setibsl 設置初始化 bundle 的 start level

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM