Java虛擬機之對象創(chuàng)建過程與類加載機制及雙親委派模型
一、對象的創(chuàng)建過程:
1、對象的創(chuàng)建過程:
對象的創(chuàng)建過程一般是從 new 指令(JVM層面)開始的,整個創(chuàng)建過程如下:
(1)首先檢查 new 指令的參數(shù)是否能在常量池中定位到一個類的符號引用;
(2)如果沒有,說明類還沒有被加載,則須先執(zhí)行相應的類加載、解析和初始化等類加載過程,該過程的具體步驟詳見下文
(3)如果有,虛擬機將在堆中為新生對象分配內(nèi)存。分配內(nèi)存方式有:指針碰撞和空閑列表,具體選擇哪種分配方式由 Java 堆是否規(guī)整決定,而 Java 堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
指針碰撞:如果Java堆是絕對規(guī)整的,所有用過的內(nèi)存都放在一邊,所有沒用過的內(nèi)存存放在另一邊,中間存放一個指針作為分界點指示器。分配內(nèi)存時,將指針從用過的內(nèi)存區(qū)域向空閑內(nèi)存區(qū)域移動等距離區(qū)域。
空閑列表:如果Java不是規(guī)整的,這時,虛擬機就必須維護一張列表,列表上記錄了可用的內(nèi)存塊,在分配內(nèi)存時,從列表上找到一個足夠大的連續(xù)內(nèi)存塊分配給對象,并更新列表上的記錄。
在分配對象內(nèi)存空間的過程中,需要考慮在并發(fā)情況下,線程是否安全的問題。因為創(chuàng)建對象在虛擬機中是非常頻繁的行為,可能出現(xiàn)正在給對象A分配內(nèi)存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內(nèi)存的情況。因此必須要保證線程安全,解決這個問題有兩種方案:
- CAS以及失敗重試(比較和交換機制):對分配內(nèi)存空間的操作進行同步處理,實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性。CAS操作需要輸入兩個數(shù)值,一個舊值(操作前期望的值)和一個新值,在操作期間先比較舊值有沒有發(fā)送變化,如果沒有變化,才交換成新值,否則不進行交換。
- TLAB(Thread Local Allocation Buffer,本地線程分配緩存):把內(nèi)存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊私有內(nèi)存,也就是本地線程分配緩沖。TLAB的目的是在為新對象分配內(nèi)存空間時,讓每個Java應用線程能在使用自己專屬的分配指針來分配空間,減少同步開銷。
(4)內(nèi)存分配完成后,虛擬機需要將分配到的內(nèi)存空間都初始化為零值,保證了對象實例的字段在 Java 代碼中可以不賦初始值就可以直接使用;
(5)對對象進行必要的設置,例如是哪個對象的實例、如何才能找到類元信息、對象的哈希碼、GC 分代年齡等信息,這些信息存放在對象頭中。
(6)執(zhí)行 init 方法,把對象按照程序員意愿進行初始化。
至此,一個對象就被創(chuàng)建完畢,同時會在Java棧中分配一個引用指向這個對象,通過棧上面的引用可以訪問堆中的具體對象,訪問對象主要有兩種方式:通過句柄訪問對象和直接指針訪問對象。
2、對象的訪問方式:
(1)通過句柄訪問對象:
在Java堆中劃出一塊內(nèi)存專門作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的地址地址信息。
(2)通過直接指針訪問對象:
(3)優(yōu)劣對比:
① 使用句柄,reference中存儲的是穩(wěn)定的句柄地址,在對象被移動時只會改變句柄中的實例數(shù)據(jù)指針,而reference本身不需要修改。
② 直接指針,速度快,節(jié)省一次指定定位的開銷。
二、類加載機制:
Java 文件中的代碼在編譯后,就會生成JVM能夠識別的二進制字節(jié)流 class 文件,class 文件中描述的各種信息,都需要加載到虛擬機中才能被運行和使用。
類加載機制,就是虛擬機把類的數(shù)據(jù)從class文件加載到內(nèi)存,并對數(shù)據(jù)進行校檢,轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型的過程。類從加載到虛擬機內(nèi)存開始,到卸載出內(nèi)存結束,整個生命周期包括七個階段,如下圖(每個過程作用見文章第四部分):
2.1、加載階段:
這階段的虛擬機需要完成三件事情:
(1)通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
(2)將這個字節(jié)流所代表的靜態(tài)存儲結構轉化為方法區(qū)的運行時數(shù)據(jù)結構。
(3)在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
2.2、驗證階段:
這階段是為了確保class文件的字節(jié)流包含的信息符合當前虛擬機的要求,不會危害虛擬機自身的安全。分為4個校檢動作:
(1)文件格式驗證:驗證字節(jié)流是否符合class文件格式的規(guī)范,并且能被當前版本的虛擬機處理,通過該階段后,字節(jié)流會進入內(nèi)存的方法區(qū)中進行儲存。
(2)元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語言分析,對類的元數(shù)據(jù)信息進行語義校驗,確保其描述的信息符合java語言規(guī)范要求。
(3)字節(jié)碼驗證:通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。這個階段對類的方法進行校驗分析,保證類的方法在運行時不會做出危害虛擬機安全的事件。
(4)符號引用驗證:對類自身以外的信息(常量池中各種符號引用)的信息進行校檢,確保解析動作能正常執(zhí)行(該動作發(fā)生在解析階段中)
2.3、準備階段:
正式為類變量分配內(nèi)存空間并設置數(shù)據(jù)類型零值。這個階段進行內(nèi)存分配的僅包括類變量(static修飾),不包括實例變量,實例變量會在對象實例化時隨對象一起分配在java堆。
public static int value= 123 ; //變量value在準備階段過后的初始值是0,不是123. public static final int value = 123 ; //特殊情況:會生成ConstantValue屬性,在準備階段初始值是123.
final、static、static final修飾的字段賦值的區(qū)別:
(1)static修飾的字段在準備階段被初始化為0或null等默認值,然后在初始化階段(觸發(fā)類構造器)才會被賦予代碼中設定的值,如果沒有設定值,那么它的值就為默認值。
(2)static final修飾的字段在Javac時生成ConstantValue屬性,在類加載的準備階段根據(jù)ConstantValue的值為該字段賦值,它沒有默認值,必須顯式地賦值,否則Javac時會報錯??梢岳斫鉃樵诰幾g期即把結果放入了常量池中。
(3)final修飾的字段在運行時被初始化(可以直接賦值,也可以在實例構造器中()賦值),一旦賦值便不可更改。
2.4、解析階段:
將常量池的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄、和調(diào)用限定符7類符號引用。
對同一符號引用進行多次解析請求是很常見的事情,除invokedynamic指令以外,虛擬機可以對第一次解析的結果進行緩存,從而避免解析動作重復進行。invokedynamic對應的引用稱為“動態(tài)調(diào)用限定符”,必須等到程序?qū)嶋H運行到這條指定的時候,解析動作才能進行。因此,當碰到由前面的invokedynamic指令觸發(fā)過的解析的符號引用時,并不意味著這個解析結果對其他的invokedynamic指令也同樣生效。
(1)符號引用:符號引用是以一組符號來描述所引用的目標,符號可以是任何字面量,只要使用時無歧義定位到目標即可。符號引用與虛擬機的內(nèi)存布局無關,引用的目標并不一定已經(jīng)加載到內(nèi)存中。各種虛擬機實現(xiàn)的內(nèi)存布局可以不相同,但是他們能接受的符號引用必須都是一致的,符號引用的字面量形式明確地定義在java虛擬機規(guī)范的calss文件格式中。
(2)直接引用:直接引用是可以直接定位到目標的指針、相對偏移量或是一個能間接定位目標的句柄。直接引用是與虛擬機的內(nèi)存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經(jīng)在內(nèi)存中存在。
2.5、初始化:
初始化階段才真正執(zhí)行類中定義的java代碼。執(zhí)行類構造器()方法的過程,并按程序的設置去初始類變量和其他資源。
2.5.1、類的主動引用:
在初始化階段,有且只有5種場景必須立即對類進行“初始化”,稱為主動引用:
(1)遇到new、getstatic、putstatic、invokestatic這4條指定時。對應的場景是:使用new關鍵字實例化對象的時候,讀取或設置一個類的靜態(tài)字段(被final修飾、已經(jīng)在編譯期把結果放入常量池的靜態(tài)字段除外),以及調(diào)用一個類的靜態(tài)方法的時候。
(2)使用java.lang.reflect包的方法對類進行發(fā)射調(diào)用的時候。
(3)當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒進行初始化,則必須對父類進行初始化。(與接口的區(qū)別:接口在初始化時,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候,才會初始化)
(4)當虛擬機啟動時,用戶指定的要執(zhí)行的主類(包含main方法的類)。
(5)java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個句柄所對應的類沒有進行過初始化,則需要觸發(fā)其初始化。
2.5.2、類的被動引用:
除了主動引用,其他引用類的方式都不會觸發(fā)初始化,稱為被動引用:
(1)對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化,通過其子類來引用父類中定義的靜態(tài)字段,只會觸發(fā)其父類的初始化而不會觸發(fā)子類的初始化。
//父類 public class SuperClass { //靜態(tài)變量value public static int value =123456; //靜態(tài)塊,父類初始化時會調(diào)用 static{ System.out.println("父類初始化!"); } } //子類 public class SubClass extends SuperClass{ //靜態(tài)塊,子類初始化時會調(diào)用 static{ System.out.println("子類初始化!"); } } //主類、測試類 public class InitTest { public static void main(String[] args){ System.out.println(SubClass.value); //輸出結果:父類初始化! 123456 } }
(2)通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化。
//父類 public class SuperClass { //靜態(tài)變量value public static int value = 123456; //靜態(tài)塊,父類初始化時會調(diào)用 static{ System.out.println("父類初始化!"); } } //主類、測試類 public class InitTest { public static void main(String[] args){ SuperClass[] test = new SuperClass[10]; //輸出結果:沒有任何輸出結果 } }
(3)常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量的類的初始化。
//定義常量的類 public class ConstClass { static{ System.out.println("常量類初始化!"); } public static final String HELLOWORLD = "hello world!"; } //主類、測試類 public class InitTest { public static void main(String[] args){ System.out.println(ConstClass.HELLOWORLD); //輸出結果:hello world } }
2.5.3、()方法的特點:
(1)()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的,收集的順序是由語句在源文件中出現(xiàn)的順序決定的。靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊中可以賦值,但是不可以訪問。
public class Test { static{ i=0; //給變量賦值可以正常編譯通過 System.out.println(i); //編譯器會提示非法向前引用 } static int i=1; }
(2)()方法與實例構造器()方法不同,它不需要顯示調(diào)用父類構造器,虛擬機會保證子類的()方法執(zhí)行之前,父類的()方法已經(jīng)執(zhí)行完畢,所以父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作,虛擬機中第一個被執(zhí)行的()方法的類是java.lang.Object。
(3)()方法對于類或接口并不是必需的,如果一個類中沒有靜態(tài)語句塊,也就沒有對變量的賦值操作,那么編譯器可以不為這個類生成()方法。
(4)接口中不能使用靜態(tài)語句塊,仍然有變量初始化操作,因此仍然會生成()方法,與類不同的是,執(zhí)行接口中的()方法不需要先執(zhí)行父接口的()方法。只有父接口中定義的變量被使用是,才需要初始化父接口,同時,接口實現(xiàn)類在初始化時也不會執(zhí)行接口的()方法。
(5)()方法在多線程環(huán)境中被正確的加鎖、同步,多個線程同時去初始化一個類,只會有一個線程執(zhí)行()方法,其他線程則需要阻塞等待,直到活動線程執(zhí)行()方法完畢,活動線程執(zhí)行完畢后,其他線程喚醒后被不會再次進入()方法,因為同一個類加載器下,一個類型只會被初始化一次。
三、類加載器與雙親委派模型:
3.1、JVM 的類加載器:
類加載機制生命周期的第一階段,即加載階段需要由類加載器來完成的,類加載器根據(jù)一個類的全限定名讀取類的二進制字節(jié)流到JVM中,然后生成對應的java.lang.Class對象實例,
在虛擬機默認提供了3種類加載器,啟動類加載器(Bootstrap ClassLoader)、擴展類加載器(Extension ClassLoader)、應用類加載器(Application ClassLoader),如果有必要還可以加入自己定義的類加載器。
(1)啟動類加載器(Bootstrap ClassLoader):負責加載 在\lib目錄 和 被-Xbootclasspath參數(shù)所指定的路徑中的類庫
(2)擴展類加載器(Extension ClassLoader):負責加載 \lib\ext目錄 和 被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫
(3)應用程序類加載器(Application ClassLoader):負責加載用戶類路徑classPath所指定的類庫,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
(4)自定義加載器(CustomClassLoader):由應用程序根據(jù)自身需要自定義,如 Tomcat、Jboss 都會根據(jù) j2ee 規(guī)范自行實現(xiàn)。
任意一個類在 JVM 中的唯一性,是由加載它的類加載器和類的全限定名一共同確定的。因此,比較兩個類是否“相等”的前提是這兩個類是由同一個類加載器加載的,否則,即使兩個類來源于同一個 Class 文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這兩個類就必定不相等。JVM 的類加載機制,規(guī)定一個類有且只有一個類加載器對它進行加載。而如何保證這個只有一個類加載器對它進行加載呢?則是由雙親委派模型來實現(xiàn)的。
3.2、雙親委派模型:
雙親委派模型要求除了頂層的啟動類加載器外,其余類加載器都應該有自己的父類加載器。(類加載器之間的父子關系不是以繼承的關系實現(xiàn),而是使用組合關系來復用父加載器的代碼)
3.2.1、雙親委派模型的工作原理:
如果一個類加載器收到了類加載的請求,他首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層級的類加載器都是如此,因此所有請求最終都會被傳到最頂層的啟動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載。因此,加載過程可以看成自底向上檢查類是否已經(jīng)加載,然后自頂向下加載類。
3.2.2、雙親委派模型的優(yōu)點:
(1)使用雙親委派模型來組織類加載器之間的關系,Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關系。
(2)避免類的重復加載,當父類加載器已經(jīng)加載了該類時,子類加載器就沒必要再加載一次。
(3)解決各個類加載器的基礎類的統(tǒng)一問題,越基礎的類由越上層的加載器進行加載。避免Java核心API中的類被隨意替換,規(guī)避風險,防止核心API庫被隨意篡改。
例如類 java.lang.Object,它存在在 rt.jar 中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的 Bootstrap ClassLoader 進行加載,因此 Object 類在程序的各種類加載器環(huán)境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶編寫了一個 java.lang.Object 的同名類并放在 ClassPath 中,那系統(tǒng)中將會出現(xiàn)多個不同的 Object 類,程序?qū)⒒靵y。因此,如果開發(fā)者嘗試編寫一個與 rt.jar 類庫中重名的 Java 類,可以正常編譯,但是永遠無法被加載運行。
3.3、類加載器源碼:loadClass()
實現(xiàn)雙親委派的代碼都集中在 java.lang.ClassLoader 的 loadClass() 方法之中,下面我們簡單看下 loadClass() 的源碼是怎么樣的:
public abstract class ClassLoader { // 每個類加載器都有一個父加載器 private final ClassLoader parent; public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先,檢查該類是否已經(jīng)加載過了 Class<?> c = findLoadedClass(name); // 如果沒有加載過 if (c == null) { if (parent != null) { // 先委托給父加載器去加載,注意這是個遞歸調(diào)用 c = parent.loadClass(name, false); } else { // 如果父加載器為空,查找 Bootstrap 加載器是不是加載過了 c = findBootstrapClassOrNull(name); } // 如果父加載器沒加載成功,調(diào)用自己的 findClass 去加載 if (c == null) { c = findClass(name); } } return c; } } // ClassLoader 中 findClass 方式需要被子類覆蓋,下面這段代碼就是對應代碼 protected Class<?> findClass(String name){ //1. 根據(jù)傳入的類名 name,到在特定目錄下去尋找類文件,把 .class 文件讀入內(nèi)存 ... //2. 調(diào)用 defineClass 將字節(jié)數(shù)組轉成 Class 對象 return defineClass(buf, off, len); } // 將字節(jié)碼數(shù)組解析成一個 Class 對象,用 native 方法實現(xiàn) protected final Class<?> defineClass(byte[] b, int off, int len){ } }
3.4、如何破壞雙親委派模型:
雙親委派過程是在 loadClass() 方法中實現(xiàn)的,如果想要破壞這種機制,那么就自定義一個類加載器(繼承自 ClassLoader),重寫其中的 loadClass() 方法,使其不進行雙親委派即可。ClassLoader 中和類加載有關的方法有很多,除了前面提到了 loadClass(),除此之外,還有 findClass() 和 defineClass() 等,那么這幾個方法有什么區(qū)別呢:
- loadClass() 就是主要進行類加載的方法,默認的雙親委派機制就實現(xiàn)在這個方法中
- findClass() 根據(jù)名稱或位置加載 .class 字節(jié)碼
- definclass() 把字節(jié)碼轉化為 Class 對象
findClass() 是 JDK1.2 之后 ClassLoader 新添加的方法,在 JDK1.2 之后已不提倡用戶直接覆蓋 loadClass() 方法,而是建議把自己的類加載邏輯實現(xiàn)到 findClass() 方法中,因為在 loadClass() 方法的邏輯里,如果父類加載器加載失敗,則會調(diào)用自己的 findClass() 方法來完成加載。而同樣如果你想定義一個自己的類加載器,并且要遵守雙親委派模型,那么可以繼承 ClassLoader,并且在 findClass() 中實現(xiàn)你自己的加載邏輯即可
對于雙親委派模型的破壞,最經(jīng)典例子就是 Tomcat 容器的類加載機制了,它實現(xiàn)了自己的類加載器 WebApp ClassLoader,并且打破了雙親委派模型,在每個應用在部署后,都會創(chuàng)建一個唯一的類加載器,對此感興趣的讀者可以閱讀后面這篇文章:Tomcat 的類加載機制
總結
到此這篇關于Java虛擬機之對象創(chuàng)建過程與類加載機制及雙親委派模型的文章就介紹到這了,更多相關Java虛擬機對象創(chuàng)建過程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
如何利用 Either 和 Option 進行函數(shù)式錯誤處理
這篇文章主要介紹了如何利用 Either 和 Option 進行函數(shù)式錯誤處理。在 Java 中,錯誤的處理在傳統(tǒng)上由異常以及創(chuàng)建和傳播異常的語言支持進行。但是,如果不存在結構化異常處理又如何呢?,需要的朋友可以參考下2019-06-06淺談System.getenv()和System.getProperty()的區(qū)別
這篇文章主要介紹了System.getenv()和System.getProperty()的區(qū)別,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06