java 類加載,包衝突

JVM類生命週期

  1. 編譯器將 Robot.java 編譯成字節碼文件 Robot.class
  2. ClassLoader 將 Robot.class 轉換成 JVM 中的 Class 對象
  3. JVM 使用 Class 對象生成 Robot 實例

類何時被加載

類加載是一個按需的過程。
遇到 new,getstatic,putstatic,invokestatic 這四個字節碼指令時,若此時類還沒有被初始化, 則會觸發類的初始化。

new A(); // new 
String name = A.name; // getstatic
A.name = "aaa"; // putstatic
A.getName(); // invokestatic

ClassLoader

  1. ClassLoader 主要對類的請求提供服務,當 JVM 需要某類時,它根據名稱向 ClassLoader 要求這個類,然後由 ClassLoader 返回 這個類的 class 對象。
  2. ClassLoader 負責載入系統的所有 Resources(Class,文件,來自網絡的字節流 等)
  3. 每個 class 都有一個 reference,指向自己的 ClassLoader。Class.getClassLoader()
    參考:https://www.cnblogs.com/kabi/p/6124761.html

類加載機制

1. 虛擬機類加載機制

虛擬機把描述類的數據從class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型。

2. 加載的步驟

在這裏插入圖片描述

加載階段

主要完成以下3件事情:
1.通過“類全名”來獲取定義此類的二進制字節流
2.將字節流所代表的靜態存儲結構轉換爲方法區的運行時數據結構
3.在java堆中生成一個代表這個類的java.lang.Class對象,作爲方法區這些數據的訪問入口

驗證階段

這個階段目的在於確保 Class 文件的字節流中包含信息符合當前虛擬機要求,不會危害虛擬機自身安全。主要包括四種驗證:
1.文件格式驗證:基於字節流驗證,驗證字節流是否符合Class文件格式的規範,並且能被當前虛擬機處理。
2.元數據驗證:基於方法區的存儲結構驗證,對字節碼描述信息進行語義驗證。
3.字節碼驗證:基於方法區的存儲結構驗證,進行數據流和控制流的驗證。
4.符號引用驗證:基於方法區的存儲結構驗證,發生在解析中,是否可以將符號引用成功解析爲直接引用。

準備階段

僅僅爲類變量(即 static 修飾的字段變量)分配內存並且設置該類變量的初始值即零值,這裏不包含用 final 修飾的 static,因爲 final 在編譯的時候就會分配了,同時這裏也不會爲實例變量分配初始化。類變量會分配在方法區中,而實例變量是會隨着對象一起分配到 Java 堆中。

解析階段

解析主要就是將常量池中的符號引用替換爲直接引用的過程。符號引用就是一組符號來描述目標,可以是任何字面量,而直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。有類或接口的解析,字段解析,類方法解析,接口方法解析。這裏要注意如果有一個同名字段同時出現在一個類的接口和父類中,那麼編譯器一般都會拒絕編譯。
在這裏插入圖片描述

什麼是雙親委派

在這裏插入圖片描述

  • 如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。
  • 每一個層次的類加載器都是如此。因此,所有的加載請求最終都應該傳送到頂層的啓動類加載器中。
  • 只有當父加載器反饋自己無法完成這個加載請求時(搜索範圍中沒有找到所需的類),子加載器纔會嘗試自己去加載。

雙親委派有何意義

  • 使用雙親委派模型的好處在於Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類 java.lang.Object,它存在在 rt.jar 中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的Bootstrap ClassLoader 進行加載,因此 Object 類在程序的各種類加載器環境中都是同一個類。
  • 相反,如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶編寫了一個 java.lang.Object 的同名類並放在 ClassPath 中,那系統中將會出現多個不同的 Object 類,程序將混亂。因此,如果開發者嘗試編寫一個與 rt.jar 類庫中重名的 Java 類,可以正常編譯,但是永遠無法被加載運行。
  • 參考:https://www.imooc.com/article/34493

類是如何唯一確定的

全限定名 + ClassLoader 名唯一確定一個類。
不同的 ClassLoader 加載同一個 jar 包,是不同的類。

如何寫一個最簡ClassLoader

https://github.com/qianyiwen2019/learn-java/tree/master/hsfdemo/src/main/java/classLoaderDemo

public class TestClassLoader extends ClassLoader {
    private String path;

    public TestClassLoader(String path) {
        this.path = path;
    }

    @Override
    public Class findClass(String name) {
        return loadClass(name);
    }

    @Override
    public Class<?> loadClass(String name) {
        try {
            // 自己不加載自己,TestClassLoader 本身由 AppClassLoader 加載
            if (StringUtils.equals(name, this.getClass().getName())) {
                return getParent().loadClass(name);
            }

            // 目的:替換 AppClassLoader 爲 TestClassLoader,原本由 AppClassLoader 加載的類(用戶自己定義的類)改成由 TestClassLoader 加載
            // 其他系統類(如 Object, String) 仍由 ExtClassLoader 和 BootstrapClassLoader 加載
            // 默認情況下,當前 classLoader 的 parent 是 AppClassLoader,
            // 而當前 classLoader 的 parent 的 parent 纔是 ExtClassLoader
            return getParent().getParent().loadClass(name);
        } catch (Exception e) {
            System.out.println("class " + name + " is not laoded by parent");
        }

        byte[] b = null;
        try {
            b = loadClassData(name);
        } catch (IOException e) {
            e.printStackTrace();
        }

        Class clazz = defineClass(name, b, 0, b.length);
        return clazz;
    }

    private byte[] loadClassData(String name) throws IOException {
        String tmpPath = "";
        for (String tmp: name.split("\\.")) {
            tmpPath += "/" + tmp;
        }
        String namePath = path + tmpPath + ".class";

        InputStream in = null;
        ByteArrayOutputStream out = null;

        try {
            in = new FileInputStream(new File(namePath));
            out = new ByteArrayOutputStream();
            int i = 0;
            while ((i = in.read()) != -1) {
                out.write(i);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            in.close();
            out.close();
        }
        return out.toByteArray();
    }
}

類衝突是怎麼產生的

  1. 類衝突都是在運行時產生的
  2. 類衝突類型
  • NoClassDefFoundError
    缺少 jar 包或 App 加載了錯誤版本
  • NoSuchMethodError
    App 加載了錯誤版本,通常是 App 中不同組件依賴了同一個庫的不同版本。
  • ClassCastException
    典型情況:同一個類由不同的加載器加載的,兩個類對象互相轉化時,就會報此錯。只會出現在多加載器場景 ( demo )
  • LinkageError
  1. 如果App 不同組件依賴同一個庫不同版本,maven 怎麼確定執行時使用哪個版本?
    答:不能確定。

類衝突如何避免

1. exclustion

通過 mvn dependency:treee 查看找到衝突的包 (logger.api) 是被誰引入的然後通過 exclusion 去除 <dependency>

 <groupId>com.aaa.bbb.ccc</groupId>
 <artifactId>ccc-client</artifactId>
 <exclusions>
     <exclusion>
         <groupId>com.aaa.bbb</groupId>
         <artifactId>logger.api</artifactId>
     </exclusion>
 </exclusions>
</dependency>

maven-shade-plugin

maven-shade-plugin基本功能:

  • 將依賴的jar包打包到當前jar包(常規打包是不會將所依賴jar包打進來的)
  • 對依賴的jar包進行重命名(用於類的隔離);
    Java 工程經常會遇到第三方 Jar 包衝突,使用 maven shade plugin 解決 jar 或類的多版本衝突。 maven-shade-plugin 在打包時,可以將項目中依賴的 jar 包中的一些類文件打包到項目構建生成的 jar 包中,在打包的時候把類重命名。
    對於中間件來說,尤其有用,發佈出 shade 包,可以讓客戶不產生衝突。
    在這裏插入圖片描述
    參考:https://maven.apache.org/plugins/maven-shade-plugin/

其他類隔離容器

如 Pandora

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章