參考鏈接: JVM如何工作–JVM體系結構
JVM簡介? ? ? ? JVM是Java程序得以運行的平臺,也是Java程序可以跨平臺的底層支撐,從整體上來看,JVM的主要功能可以分為加載和執行兩大塊。其中類加載器負責.class文件的尋址與加載,執行引擎負責字節碼指令執行及內存的管理等等。下面是JVM一個經典的體系結構圖?
類加載系統:關于類加載體系的詳細說明在另一博客https://blog.csdn.net/w1673492580/article/details/81838835?
運行時數據區宏觀角度:? ? ? ? ?從宏觀角度來看,JVM運行時數據區的各部分劃分很明顯,方法區用于存儲類數據,堆用于存儲Java程序在運行過程中創建的所有對象,棧和PC寄存器屬于線程獨有,棧代表線程執行過程中所有方法調用信息(比如比如入參、局部變量、中間結果,返回信息等等),PC寄存器即計數器,代表指令在主存中的地址,每執行一條指令后PC+1,即指向下一條指令。本地方法棧即通過Java調用本地方法時的相關信息,但與調用Java方法不一樣,調用Java方法時JVM會當前Java棧中壓入一個新的棧楨,而調用本地方法則不會修改Java棧,從這個角度看,本地方法棧可以理解成JVM運行時數據區的一個擴展。?
微觀角度:堆:Java程序在運行過程中(加載也是運行的一部分)創建的所有對象都位于堆內存中,且JVM中堆內存在設計上是多線程共享的,所以堆中數據的訪問也必須進行安全控制;? ? ? ? 由于運行期間很可能會創建大量的對象,而且大部分對象都是小而短的(占用空間小及生命周期短),所以堆內存的管理也尤其重要,但JVM中并沒有釋放對象的指令,這表示開發中不能通過代碼去管理對象的釋放,所以JVM內置了垃圾回收器來管理,堆也是垃圾回收主要集中的地方(對于棧由于棧楨的大小可以在編譯期就根據類結構數據確定,所以這部分的回收具有一定的確定性)。? ? ? ?在Java代碼中,通過obj.getClass()獲取對象所屬的類,也可以通過new? ClassName()創建一個對象。這是因為在JVM中對象數據包含了一個指向方法區中對應類型信息的指針,可以通過該指針獲取對應類信息。反過來JVM也可以根據方法區中類信息創建該類對象,甚至知道該類對象應該占用多少空間(但實際分配大小依賴于JVM實現)。同樣通過class.getClassLoader()可以獲取當前類加載器也是同樣的原理。? ? ? ? 還有個比較有意思的就是對象鎖synchronized(obj),Java中每個對象都可以作為鎖,同樣也是因為對象本身包含了一個指向鎖數據的指針,但由于絕大部分對象的鎖都用不到,所以大部分JVM的實現,都只在第一個線程嘗試獲取對象鎖時,才給該對象分配對應的鎖數據。? ? ? ?關于堆空間本身的設計依賴具體的實現,下面是兩種完全不同的可能設計,第一種把堆內存劃分為句柄池和對象池兩部分,對對象的引用指向句柄池,句柄池中每個元素又由兩部分組成,一個指向方法區中的存放類數據的地址,一個指向對象池中的對象。這樣的優勢體現在,當垃圾回收器 回收內存并重新劃分導致對象內存地址發生變化時,不需要更新所有引用的指向,而只需要更新句柄池中指向對象的指針,缺點就是中間需要額外經過一次查找。?
第二種設計引用直接指向堆中的對象,這樣的優缺點和第一種設計剛好相反,不需要額外的查找,但對象地址發生變化時,需要更新所有引用。?
方法區:方法區在設計上也是所有線程共享,主要存儲類相關信息(如字段/方法信息、常量池信息、對當前ClassLoader和Class的引用等等),在JVM加載某個類時,會抽取出對應.class文件中類相關的信息并以某種結構(依賴于JVM實現)存到方法區中,當程序運行時,JVM則會到方法區中去查找使用對應類信息(比如創建對象)。? ? ? ? 值得注意的一點是由于所有線程共享方法區中的數據,所以方法區中數據的訪問必須被設計成線程安全的,比如說多個線程并發加載同一類等等。另外方法區雖然也被稱為“永久代”,但實際上其中的數據也是可以被垃圾回收器回收的,回收內容主要包括常量池中無用的常量、無用的類(具體判斷依據請參考垃圾回收篇)。? ? ? ? ?雖然類信息具體的存儲結構依賴于具體JVM實現,但為了提高方法的檢索效率,部分JVM實現會為每個非抽象類生成一個方法表(方法表雖然加快了檢索速度,但本身也會占用一定的內存空間,算是以空間換時間),方法表是一種數組結構,每個元素代表一個方法實例(從Java角度來說,每個元素就相當于一個Method對象)。這種情況下,對象不再直接指向方法區中的存放類信息的地址,而是指向方法表,通過方法表來間接關聯對象與類型信息,從JVM的角度來看其基本指向如下圖?
Java棧與PC寄存器:JVM中每個線程都有自己的PC寄存器和Java棧,PC寄存器即計數器,表示指令在主存中的地址,每執行一條指令后PC+1,即指向下一條指令。Java棧代表線程在執行過程中的所有方法調用信息,JVM對棧只有壓入棧楨和彈出棧楨兩個操作,“棧”由"棧楨“組成,線程每調用一次方法JVM即會為它產生并壓入一個“棧楨”,方法執行完畢即會彈出對應的棧楨,棧楨本質上就是一個內存片,用來存儲方法局部變量和計算的中間結果,其中用于存儲中間結果的部分又稱為“操作數棧”,所以操作的數據即可能是“操作數棧”中的數據,也可能是“棧楨”中的數據。 值得注意的是局部變量和操作數棧的大小在編譯時計算出來,并放置到class文件中,然后JVM就可以知道方法棧楨的大小,當調用一個方法時jvm將壓入一個適當大小的棧楨至棧中,但棧在jvm中是有深度限制的,當線程調用的棧深度超出該限制時將拋出StockOverflowErro異常,如果jvm在擴展棧時無法獲取更多內存則會拋出OutofMemoryErro。下面關于Java棧的圖文描述及驗證代碼? ?
public static void main(String[] args) throws InterruptedException {
//? ? ? ? ?testStackOverflowError();
? ? ? ? testOutOfMemoryError();
? ? }
? ??
? ? public static void testStackOverflowError(){
? ? ? ? testStackOverflowError();
? ? }
? ??
? ? public static void testOutOfMemoryError(){
? ? ? ? byte[] bytes = new byte[2000000000];
? ? }?
本地方法棧:? ? ? ? 本地方法棧即代表了線程在執行過程中調用本地方法的一系列信息,與Java棧不同的地方在于,本地方法并不受JVM的限制,對本地方法的調用不會導致JVM往Java棧中壓入棧楨。關于本地方法與Java方法的調用可以簡單的假設一下,假設某個線程在執行Java方法過程中調用了本地方法C1,且本地方法C1最終又調用了某Java方法,則在這個過程中JVM會先由Java棧進入本地方法棧最終又回到Java棧中,下圖簡單的描述了這種情況?
執行引擎:? ? ? ? 執行引擎負責字節碼指令的執行,方法的字節碼流由一系列有序指令組成,指令又由一個單字節的操作碼 + 0個或多個操作數組成。操作碼表示需要執行的操作,操作數表示操作的數據,一般來源于當前棧楨中的局部變量或當前Java棧楨中操作數棧的頂部,至于操作數的個數,由操作碼決定(操作碼本身就決定了它是否需要操作數,以及操作數的形式等等)。? ? ? ? 不同的JVM中執行引擎也可能不同,最簡單同時效率也最低的執行引擎是一次性解釋字節碼,它在每次運行方法時都把字節碼翻譯成本地代碼再執行; 其次是即時編譯(JITC),它在第一次執行方法時,會把對應的字節碼翻譯成本地機器代碼并緩存,后續調用就可以重用緩存的本地機器代碼;另外一種是自適應優化器(特殊的即時編譯器), JVM一開始也會解釋字節碼,但它會監視程序的活動,并記錄活動過程中使用最頻繁的代碼,然后把這些代碼編譯成本地代碼,而其它代碼則繼續采用解釋的方式。下面是javap -c com.alibaba.fastjson.JSONObject.class反匯編后的部分信息?
?public static java.lang.String valueToString(java.lang.Object) throws org.zend.sdklib.internal.utils.json.JSONException;
? ?Code:
? ? ? 0: aload_0
? ? ? 1: ifnull? ? ? ? 12
? ? ? 4: aload_0
? ? ? 5: aconst_null
? ? ? 6: invokevirtual #304? ? ? ? ? ? ? ? // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
? ? ? 9: ifeq? ? ? ? ? 16
? ? ?12: ldc_w? ? ? ? ?#261? ? ? ? ? ? ? ? // String null
? ? ?15: areturn
? ? ?16: aload_0
? ? ?17: instanceof? ? #635? ? ? ? ? ? ? ? // class org/zend/sdklib/internal/utils/json/JSONString
? ? ?20: ifeq? ? ? ? ? 83
? ? ?23: aload_0
? ? ?24: checkcast? ? ?#635? ? ? ? ? ? ? ? // class org/zend/sdklib/internal/utils/json/JSONString
? ? ?27: invokeinterface #637,? 1? ? ? ? ? // InterfaceMethod org/zend/sdklib/internal/utils/json/JSONString.toJSONString:()Ljava/lang/String;
? ? ?32: astore_1
? ? ?33: goto? ? ? ? ? 46
? ? ?36: astore_2
? ? ?37: new? ? ? ? ? ?#52? ? ? ? ? ? ? ? ?// class org/zend/sdklib/internal/utils/json/JSONException
? ? ?40: dup
? ? ?41: aload_2
? ? ?42: invokespecial #640? ? ? ? ? ? ? ? // Method org/zend/sdklib/internal/utils/json/JSONException."<init>":(Ljava/lang/Throwable;)V
? ? ?45: athrow
? ? ?46: aload_1
? ? ?47: instanceof? ? #92? ? ? ? ? ? ? ? ?// class java/lang/String
? ? ?50: ifeq? ? ? ? ? 58
? ? ?53: aload_1
? ? ?54: checkcast? ? ?#92? ? ? ? ? ? ? ? ?// class java/lang/String
? ? ?57: areturn
? ? ?58: new? ? ? ? ? ?#52? ? ? ? ? ? ? ? ?// class org/zend/sdklib/internal/utils/json/JSONException
? ? ?61: dup?
aload_0表示將第一個局部變量壓入到當前操作數棧中,aload指令后跟著的操作數必須是對象引用,這里第一個局部變量也是引用,即參數Object。 ifnull 12表示彈出棧頂對象(即剛壓入的參數obj),判斷是否為null, 為null則跳轉到偏移量為12的分支處,關于指令相關的更多信息這里就不說了