JVM類生命週期
- 編譯器將 Robot.java 編譯成字節碼文件 Robot.class
- ClassLoader 將 Robot.class 轉換成 JVM 中的 Class 對象
- JVM 使用 Class 對象生成 Robot 實例
類何時被加載
類加載是一個按需的過程。
遇到 new,getstatic,putstatic,invokestatic 這四個字節碼指令時,若此時類還沒有被初始化, 則會觸發類的初始化。
new A(); // new
String name = A.name; // getstatic
A.name = "aaa"; // putstatic
A.getName(); // invokestatic
ClassLoader
- ClassLoader 主要對類的請求提供服務,當 JVM 需要某類時,它根據名稱向 ClassLoader 要求這個類,然後由 ClassLoader 返回 這個類的 class 對象。
- ClassLoader 負責載入系統的所有 Resources(Class,文件,來自網絡的字節流 等)
- 每個 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();
}
}
類衝突是怎麼產生的
- 類衝突都是在運行時產生的
- 類衝突類型
- NoClassDefFoundError
缺少 jar 包或 App 加載了錯誤版本 - NoSuchMethodError
App 加載了錯誤版本,通常是 App 中不同組件依賴了同一個庫的不同版本。 - ClassCastException
典型情況:同一個類由不同的加載器加載的,兩個類對象互相轉化時,就會報此錯。只會出現在多加載器場景 ( demo ) - LinkageError
- 如果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