新聞中心
說說 JVM 的類加載機(jī)制『非專業(yè)』
作者:程序鍋 2021-04-29 11:18:14
云計(jì)算
虛擬化 類是在運(yùn)行期間第一次使用時動態(tài)加載的,而不是一次性加載所有類。因?yàn)槿绻淮涡约虞d,那么會占用很多的內(nèi)存。

創(chuàng)新互聯(lián)堅(jiān)持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時代的海林網(wǎng)站設(shè)計(jì)、移動媒體設(shè)計(jì)的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!
[[396813]]
類加載機(jī)制
類是在運(yùn)行期間第一次使用時動態(tài)加載的,而不是一次性加載所有類。因?yàn)槿绻淮涡约虞d,那么會占用很多的內(nèi)存。
類的生命周期
包括以下 7 個階段:
- 「加載(Loading)」
- 「驗(yàn)證(Verification)」
- 「準(zhǔn)備(Preparation)」
- 「解析(Resolution)」
- 「初始化(Initialization)」
- 使用(Using)
- 卸載(Unloading)
類加載過程 --- new 一個對象的過程
包含加載、驗(yàn)證、準(zhǔn)備、解析和初始化這 5 個階段。
1.加載
加載過程完成以下三件事:
其中二進(jìn)制字節(jié)流可以從以下方式中獲?。?/p>
- 從 ZIP 包讀取,成為 JAR、EAR、WAR 格式的基礎(chǔ)。
- 從網(wǎng)絡(luò)中獲取,最典型的應(yīng)用是 Applet。
- 運(yùn)行時計(jì)算生成,例如動態(tài)代理技術(shù),在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理類的二進(jìn)制字節(jié)流。
- 由其他文件生成,例如由 JSP 文件生成對應(yīng)的 Class 類。
- 通過類的完全限定名稱獲取定義該類的二進(jìn)制字節(jié)流。
- 將該字節(jié)流表示的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時存儲結(jié)構(gòu)。
- 在內(nèi)存中生成一個代表該類的 Class 對象,作為方法區(qū)中該類各種數(shù)據(jù)的訪問入口。
2.驗(yàn)證
格式驗(yàn)證:驗(yàn)證是否符合class文件規(guī)范 語義驗(yàn)證:檢查一個被標(biāo)記為final的類型是否包含子類;檢查一個類中的final方法是否被子類進(jìn)行重寫;確保父類和子類之間沒有不兼容的一些方法聲明(比如方法簽名相同,但方法的返回值不同) 操作驗(yàn)證:在操作數(shù)棧中的數(shù)據(jù)必須進(jìn)行正確的操作,對常量池中的各種符號引用執(zhí)行驗(yàn)證(通常在解析階段執(zhí)行,檢查是否可以通過符號引用中描述的全限定名定位到指定類型上,以及類成員信息的訪問修飾符是否允許訪問等)
3.準(zhǔn)備
類變量是被 static 修飾的變量,準(zhǔn)備階段為類變量分配內(nèi)存并設(shè)置初始值,使用的是方法區(qū)的內(nèi)存。
- public static int value = 123;
如果類變量是常量,那么它將初始化為表達(dá)式所定義的值而不是 0。例如下面的常量 value 被初始化為 123 而不是 0。
- public static final int value = 123;
實(shí)例變量不會在這階段分配內(nèi)存,它會在對象實(shí)例化時隨著對象一起被分配在堆中。
4.解析
將常量池中的符號引用轉(zhuǎn)為直接引用(得到類或者字段、方法在內(nèi)存中的指針或者偏移量,以便直接調(diào)用該方法),這個可以在初始化之后再執(zhí)行,可以支持 Java 的動態(tài)綁定。
以上2、3、4三個階段又合稱為鏈接階段,鏈接階段要做的是將加載到JVM中的二進(jìn)制字節(jié)流的類數(shù)據(jù)信息合并到JVM的運(yùn)行時狀態(tài)中。
5.初始化
初始化階段是虛擬機(jī)執(zhí)行類構(gòu)造器 () 方法的過程,是真正開始執(zhí)行類中定義的 Java 程序代碼。在前面的準(zhǔn)備階段,類變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段,主要根據(jù)程序員通過程序制定的主觀計(jì)劃去初始化類變量和其它資源。
() 是由編譯器自動收集類中所有類變量的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的,編譯器收集的順序由語句在源文件中出現(xiàn)的順序決定。特別注意的是,靜態(tài)語句塊只能訪問到定義在它之前的類變量,定義在它之后的類變量只能賦值,不能訪問。
虛擬機(jī)會保證一個類的 () 方法在多線程環(huán)境下被正確的加鎖和同步,如果多個線程同時初始化一個類,只會有一個線程執(zhí)行這個類的\ () 方法,其它線程都會阻塞等待,直到活動線程執(zhí)行\(zhòng) () 方法完畢。如果在一個類的 () 方法中有耗時的操作,就可能造成多個線程阻塞,在實(shí)際過程中此種阻塞很隱蔽。
上述步驟簡單來說就是分為以下兩步:
- 類變量的賦值操作
- 執(zhí)行static代碼塊。static代碼塊只有jvm能夠調(diào)用。如果是多線程需要同時初始化一個類,僅僅只能允許其中一個線程對其執(zhí)行初始化操作,其余線程必須等待,只有在活動線程執(zhí)行完對類的初始化操作之后,才會通知正在等待的其他線程。
最終,方法區(qū)會存儲當(dāng)前類的類信息,包括類的靜態(tài)變量、類初始化代碼(定義靜態(tài)變量時的賦值語句和靜態(tài)初始化代碼塊)、實(shí)例變量定義、實(shí)例初始化代碼(定義實(shí)例變量時的賦值語句實(shí)例代碼塊和構(gòu)造方法)和實(shí)例方法,還有父類的類信息引用。
創(chuàng)建對象
假設(shè)是第一次使用一個類的話,那么需要經(jīng)過上述的類加載的過程,之后才是創(chuàng)建對象。
「1、在堆區(qū)分配對象需要的內(nèi)存」
分配的內(nèi)存包括本類和父類的所有實(shí)例變量,但不包括任何靜態(tài)變量
「2、對所有實(shí)例變量賦默認(rèn)值」
將方法區(qū)內(nèi)對實(shí)例變量的定義拷貝一份到堆區(qū),然后賦默認(rèn)值
「3、執(zhí)行實(shí)例初始化代碼」
初始化順序是先初始化父類再初始化子類,初始化時先執(zhí)行實(shí)例代碼塊然后是構(gòu)造方法。(第一執(zhí)行類中的靜態(tài)代碼,包括靜態(tài)成 員變量的初始化和靜態(tài)語句塊的執(zhí)行;第二執(zhí)行類中的非靜態(tài)代碼,包括非靜態(tài)成員變量的初始化和非靜態(tài)語句塊的執(zhí)行,最后執(zhí) 行構(gòu)造函數(shù)。在繼承的情況下,會首先執(zhí)行父類的靜態(tài)代碼,然后執(zhí)行子類的靜態(tài)代碼;之后執(zhí)行父類的非靜態(tài)代碼和構(gòu)造函數(shù); 最后執(zhí)行子類的非靜態(tài)代碼和構(gòu)造函數(shù))
「4、如果有類似于Child c = new Child()形式的c引用的話,在棧區(qū)定義Child類型引用變量c,然后將堆區(qū)對象的地址賦值給它」
需要注意的是,「每個子類對象持有父類對象的引用」,可在內(nèi)部通過super關(guān)鍵字來調(diào)用父類對象,但在外部不可訪問。
存在繼承的情況下,初始化順序?yàn)椋?/p>
- 父類(靜態(tài)變量、靜態(tài)語句塊)
- 子類(靜態(tài)變量、靜態(tài)語句塊)
- 父類(實(shí)例變量、普通語句塊)
- 父類(構(gòu)造函數(shù))
- 子類(實(shí)例變量、普通語句塊)
- 子類(構(gòu)造函數(shù))
類初始化的情況
主動引用
虛擬機(jī)規(guī)范中并沒有強(qiáng)制約束何時進(jìn)行加載,但是規(guī)范嚴(yán)格規(guī)定了有且只有下列五種情況必須對類進(jìn)行初始化(加載、驗(yàn)證、準(zhǔn)備都會隨之發(fā)生):
- 遇到 new、getstatic、putstatic、invokestatic 這四條字節(jié)碼指令時,如果類沒有進(jìn)行過初始化,則必須先觸發(fā)其初始化。最常見的生成這 4 條指令的場景是:使用 new 關(guān)鍵字實(shí)例化對象的時候;讀取或設(shè)置一個類的靜態(tài)字段(被 final 修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候;以及調(diào)用一個類的靜態(tài)方法的時候。
- 使用 java.lang.reflect 包的方法對類進(jìn)行反射調(diào)用的時候,如果類沒有進(jìn)行初始化,則需要先觸發(fā)其初始化。
- 當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。
- 當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類(包含 main() 方法的那個類),虛擬機(jī)會先初始化這個主類。
被動引用
以上的行為稱為對一個類進(jìn)行主動引用。除此之外,所有引用類的方式都不會觸發(fā)初始化,稱為被動引用。被動引用的常見例子包括:
- 通過子類引用父類的靜態(tài)字段,不會導(dǎo)致子類初始化。
- System.out.println(SubClass.value); // value 字段在 SuperClass 中定義
通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化。該過程會對數(shù)組類進(jìn)行初始化,數(shù)組類是一個由虛擬機(jī)自動生成的、直接繼承自 Object 的子類,其中包含了數(shù)組的屬性和方法。
- SuperClass[] sca = new SuperClass[10];
- 常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量的類的初始化。
- System.out.println(ConstClass.HELLOWORLD);
類與類加載器
兩個類相等,需要類本身相等,并且使用同一個類加載器進(jìn)行加載。這是因?yàn)槊恳粋€類加載器都擁有一個獨(dú)立的類名稱空間。那么最終的相等包括了類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結(jié)果為 true,也包括使用 instanceof 關(guān)鍵字做對象所屬關(guān)系判定結(jié)果為 true。
從 Java 虛擬機(jī)的角度來講,只存在以下兩種不同的類加載器:
- 啟動類加載器(Bootstrap ClassLoader),使用 C++ 實(shí)現(xiàn),是虛擬機(jī)自身的一部分;
- 所有其它類的加載器,使用 Java 實(shí)現(xiàn),獨(dú)立于虛擬機(jī),繼承自抽象類 java.lang.ClassLoader。
那么上述又可以分為以下三種類加載器:BootstrapClassLoader、ExtensionClassLoader、App ClassLoader
- 啟動類加載器(BootstrapClassLoader)是嵌在JVM內(nèi)核中的加載器,該加載器是用C++語言寫的,主要負(fù)載加載JAVA_HOME/lib下的類庫,或者被 -Xbootclasspath 參數(shù)所指定的路徑中的,并且是虛擬機(jī)識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被加載)。
啟動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給啟動類加載器,直接使用 null 代替即可。
- 擴(kuò)展類加載器(ExtensionClassLoader)是用JAVA編寫,由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實(shí)現(xiàn)的。它負(fù)責(zé)將 /lib/ext 或者被 java.ext.dir 系統(tǒng)變量所指定路徑中的所有類庫加載到內(nèi)存中,開發(fā)者可以直接使用擴(kuò)展類加載器。
它的父類加載器是Bootstrap。
- 應(yīng)用程序類加載器(Application ClassLoader)這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實(shí)現(xiàn)的。一般負(fù)責(zé)加載應(yīng)用程序classpath目錄下的所有jar和class文件。
開發(fā)者可以直接使用這個類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。
它的父加載器為ExteClassLoader。
雙親委派模型
應(yīng)用程序是由三種類加載器互相配合從而實(shí)現(xiàn)類加載,除此之外還可以加入自己定義的類加載器。下圖展示了類加載器之間的層次關(guān)系,稱為雙親委派模型(Parents Delegation Model)。這里的父子關(guān)系一般通過委托來實(shí)現(xiàn),而不是繼承關(guān)系(Inheritance)。
- 工作流程
如果一個類加載器收到了一個類加載請求,它不會自己去嘗試加載這個類,而是把這個請求轉(zhuǎn)交給父類加載器去完成。每一個層次的類加載器都是如此。因此所有的類加載請求都應(yīng)該傳遞到最頂層的啟動類加載器中,只有到父類加載器反饋?zhàn)约簾o法完成這個加載請求(在它的搜索范圍沒有找到這個類)時,子類加載器才會嘗試自己去加載。
- 好處
使得 Java 類隨著它的類加載器一起具有一種帶有優(yōu)先級的層次關(guān)系,從而使得基礎(chǔ)類得到統(tǒng)一。
例如 java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 并放到 ClassPath 中,程序可以編譯通過。由于雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優(yōu)先級更高,這是因?yàn)?rt.jar 中的 Object 使用的是啟動類加載器,而 ClassPath 中的 Object 使用的是應(yīng)用程序類加載器。rt.jar 中的 Object 優(yōu)先級更高,那么程序中所有的 Object 都是這個 Object。
- demo
以下是抽象類 java.lang.ClassLoader 的代碼片段,其中的 loadClass() 方法運(yùn)行過程如下:先檢查類是否已經(jīng)加載過,如果沒有則讓父類加載器去加載。當(dāng)父類加載器加載失敗時拋出 ClassNotFoundException,此時嘗試自己去加載。
- public abstract class ClassLoader {
- // The parent class loader for delegation
- private final ClassLoader parent;
- public Class> loadClass(String name) throws ClassNotFoundException {
- return loadClass(name, false);
- }
- 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) {
- 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.
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
- protected Class> findClass(String name) throws ClassNotFoundException {
- throw new ClassNotFoundException(name);
- }
- }
- 自定義類加載器實(shí)現(xiàn)
以下代碼中的 FileSystemClassLoader 是自定義類加載器,繼承自 java.lang.ClassLoader,用于加載文件系統(tǒng)上的類。它首先根據(jù)類的全名在文件系統(tǒng)上查找類的字節(jié)代碼文件(.class 文件),然后讀取該文件內(nèi)容,最后通過 defineClass() 方法來把這些字節(jié)代碼轉(zhuǎn)換成 java.lang.Class 類的實(shí)例。
java.lang.ClassLoader 的 loadClass() 實(shí)現(xiàn)了雙親委派模型的邏輯,自定義類加載器一般不去重寫它,但是需要重寫 findClass() 方法。
- public class FileSystemClassLoader extends ClassLoader {
- private String rootDir;
- public FileSystemClassLoader(String rootDir) {
- this.rootDir = rootDir;
- }
- protected Class> findClass(String name) throws ClassNotFoundException {
- byte[] classData = getClassData(name);
- if (classData == null) {
- throw new ClassNotFoundException();
- } else {
- return defineClass(name, classData, 0, classData.length);
- }
- }
- private byte[] getClassData(String className) {
- String path = classNameToPath(className);
- try {
- InputStream ins = new FileInputStream(path);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- int bufferSize = 4096;
- byte[] buffer = new byte[bufferSize];
- int bytesNumRead;
- while ((bytesNumRead = ins.read(buffer)) != -1) {
- baos.write(buffer, 0, bytesNumRead);
- }
- return baos.toByteArray();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return null;
- }
- private String classNameToPath(String className) {
- return rootDir + File.separatorChar
- + className.replace('.', File.separatorChar) + ".class";
- }
- }
巨人的肩膀
程序鍋春招筆記的摘記
https://github.com/CyC2018/CS-Notes
本文轉(zhuǎn)載自微信公眾號「多選參數(shù)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系多選參數(shù)公眾號。
網(wǎng)站標(biāo)題:說說JVM的類加載機(jī)制『非專業(yè)』
URL鏈接:http://m.5511xx.com/article/dpcehho.html


咨詢
建站咨詢
