本文所有內(nèi)容來于:http://stuq.com/a/100ww
java虛擬機會解釋執(zhí)行java字節(jié)碼,并且對于熱點代碼會采用即時編譯(Just-In-Time compilation,,JIT),即將一個方法中包含的所有字節(jié)碼編譯成機器碼后再執(zhí)行。如下圖所示:
Java 虛擬機將運行時內(nèi)存區(qū)域劃分為五個部分,分別為方法區(qū)、堆、PC 寄存器、Java 方法棧和本地方法棧。如下圖所示:
??java引用類型分為四種:類、接口、數(shù)組類和泛型參數(shù)。其中泛型參數(shù)會在編譯過程中被擦除。因此 Java 虛擬機實際上只有前三種。在類、接口和數(shù)組類中,數(shù)組類是由 Java 虛擬機直接生成的,其他兩種則有對應(yīng)的字節(jié)流(接口,類)。
??Java 虛擬機識別方法的關(guān)鍵在于類名、方法名、方法的參數(shù)類型以及返回類型。在同一個類中,如果同時出現(xiàn)多個名字相同且描述符也相同的方法,那么 Java 虛擬機會在類的驗證階段報錯。
??Java 虛擬機與 Java 語言不同,它并不限制名字與參數(shù)類型相同,但返回類型不同的方法出現(xiàn)在同一個類中,對于調(diào)用這些方法的字節(jié)碼來說,由于字節(jié)碼所附帶的方法描述符包含了返回類型,因此 Java 虛擬機能夠準確地識別目標方法。
??Java 虛擬機中關(guān)于方法重寫的判定同樣基于方法描述符。也就是說,如果子類定義了與父類中非私有、非靜態(tài)方法同名的方法,那么只有當(dāng)這兩個方法的參數(shù)類型以及返回類型一致,Java 虛擬機才會判定為重寫。對于 Java 語言中重寫而 Java 虛擬機中非重寫的情況,編譯器會通過生成橋接方法來實現(xiàn) Java 中的重寫語義。
??Java 虛擬機中的靜態(tài)綁定指的是在解析時便能夠直接識別目標方法的情況,而動態(tài)綁定則指的是需要在運行過程中根據(jù)調(diào)用者的動態(tài)類型來識別目標方法的情況。
??Java 字節(jié)碼中與調(diào)用相關(guān)的指令共有五種:
1:invokestatic:用于調(diào)用靜態(tài)方法,編譯期就可以確定調(diào)用的方法。
2:invokespecial:用于調(diào)用私有實例方法、構(gòu)造器,以及使用 super 關(guān)鍵字調(diào)用父類的實例方法或構(gòu)造器,和所實現(xiàn)接口的默認方法。編譯期就可以確定調(diào)用的方法。
3:invokevirtual:用于調(diào)用非私有實例方法,需要在運行期確定需要調(diào)用的方法。
4:invokeinterface:用于調(diào)用接口方法,需要在運行期確定需要調(diào)用的方法。
5:invokedynamic:用于調(diào)用動態(tài)方法。
??在編譯過程中,我們并不知道目標方法的具體內(nèi)存地址。因此,Java 編譯器會暫時用符號引用來表示該目標方法。這一符號引用包括目標方法所在的類或接口的名字,以及目標方法的方法名和方法描述符。符號引用存儲在 class 文件的常量池之中。根據(jù)目標方法是否為接口方法,這些引用可分為接口符號引用和非接口符號引用。如果虛方法(invokevirtual)調(diào)用指向一個標記為 final 的方法,那么Java虛擬機也可以靜態(tài)綁定該虛方法調(diào)用的目標方法。
??Java 虛擬機中采取了一種用空間換取時間的策略來實現(xiàn)動態(tài)綁定。它為每個類生成一張方法表(類加載的鏈接階段實現(xiàn)),用以快速定位目標方法。方法表分為虛方法表(invokevirtual調(diào)用)與接口方法表(invokeinterface)調(diào)用。方法表本質(zhì)上是一個數(shù)組,每個數(shù)組元素指向一個當(dāng)前類及其祖先類中非私有的實例方法。方法表滿足兩個特質(zhì):
??方法調(diào)用指令中的符號引用會在執(zhí)行之前解析成實際引用。對于靜態(tài)綁定的方法調(diào)用而言,實際引用將指向具體的目標方法。對于動態(tài)綁定的方法調(diào)用而言,實際引用則是方法表的索引值(實際上并不僅是索引值)。在執(zhí)行過程中,Java 虛擬機將獲取調(diào)用者的實際類型,并在該實際類型的虛方法表中,根據(jù)索引值獲得目標方法。這個過程便是動態(tài)綁定。Java 虛擬機中的即時編譯器會使用內(nèi)聯(lián)緩存來加速動態(tài)綁定。Java 虛擬機所采用的單態(tài)內(nèi)聯(lián)緩存將紀錄調(diào)用者的動態(tài)類型,以及它所對應(yīng)的目標方法。當(dāng)碰到新的調(diào)用者時,如果其動態(tài)類型與緩存中的類型匹配,則直接調(diào)用緩存的目標方法。否則,Java 虛擬機將該內(nèi)聯(lián)緩存劣化為超多態(tài)內(nèi)聯(lián)緩存,在今后的執(zhí)行過程中直接使用方法表進行動態(tài)綁定。
??拋出異??煞譃轱@式和隱式兩種。顯式拋異常的主體是應(yīng)用程序,它指的是在程序中使用“throw”關(guān)鍵字,手動將異常實例拋出。隱式拋異常的主體則是Java 虛擬機,它指的是 Java 虛擬機在執(zhí)行過程中,碰到無法繼續(xù)執(zhí)行的異常狀態(tài),自動拋出異常。
??異常實例的構(gòu)造十分昂貴。這是由于在構(gòu)造異常實例時,Java 虛擬機需要生成該異常的棧軌跡(stack trace)。該操作會逐一訪問當(dāng)前線程的 Java 棧幀,并且記錄下各種調(diào)試信息,包括棧幀所指向方法的名字,方法所在的類名、文件名,以及在代碼中的第幾行觸發(fā)該異常。
??在編譯生成的字節(jié)碼中,每個方法都附帶一個異常表。異常表中的每一個條目代表一個異常處理器,并且由 from 指針、to 指針、target 指針以及所捕獲的異常類型構(gòu)成。這些指針的值是字節(jié)碼索引(bytecode index,bci),用以定位字節(jié)碼。
??當(dāng)程序觸發(fā)異常時,Java 虛擬機會從上至下遍歷異常表中的所有條目。當(dāng)觸發(fā)異常的字節(jié)碼的索引值在某個異常表條目的監(jiān)控范圍內(nèi),Java 虛擬機會判斷所拋出的異常和該條目想要捕獲的異常是否匹配。如果匹配,Java 虛擬機會將控制流轉(zhuǎn)移至該條目 target 指針指向的字節(jié)碼。如果遍歷完所有異常表條目,Java 虛擬機仍未匹配到異常處理器,那么它會彈出當(dāng)前方法對應(yīng)的 Java 棧幀,并且在調(diào)用者(caller)中重復(fù)上述操作。在最壞情況下,Java 虛擬機需要遍歷當(dāng)前線程 Java 棧上所有方法的異常表。
??finally 代碼塊的編譯比較復(fù)雜。當(dāng)前版本 Java 編譯器的做法,是復(fù)制 finally 代碼塊的內(nèi)容,分別放在 try-catch 代碼塊所有正常執(zhí)行路徑以及異常執(zhí)行路徑的出口中。
??通過 new 指令新建出來的對象(分存在堆中),它的內(nèi)存其實涵蓋了所有父類中的實例字段。也就是說,雖然子類無法訪問父類的私有實例字段,或者子類的實例字段隱藏了父類的同名實例字段,但是子類的實例還是會為這些父類實例字段分配內(nèi)存的。
??在 Java 虛擬機中,每個 Java 對象都有一個對象頭(object header),這個由標記字段(Mark Word)和類型指針所構(gòu)成。其中,標記字段用以存儲 Java 虛擬機有關(guān)該對象的運行數(shù)據(jù),如哈希碼、GC 信息以及鎖信息,而類型指針則指向該對象的類。
??在 64 位的 Java 虛擬機中,對象頭的標記字段占 64 位,而類型指針又占了 64 位。也就是說,每一個 Java 對象在內(nèi)存中的額外開銷就是 16 個字節(jié)。為了盡量較少對象的內(nèi)存使用量,64 位 Java 虛擬機引入了壓縮指針 的概念(對應(yīng)虛擬機選項 -XX:+UseCompressedOops,默認開啟),將堆中原本 64 位的 Java 對象類型指針壓縮成 32 位,這樣對象頭就只占用 12位(原來占用16位)。
??默認情況下,Java 虛擬機堆中對象的起始地址需要對齊至 8 的倍數(shù)。如果一個對象用不到 8N 個字節(jié),那么空白的那部分空間就浪費掉了。這些浪費掉的空間我們稱之為對象間的填充(padding)。
&essp;?內(nèi)存對齊不僅存在于對象與對象之間,也存在于對象中的字段之間。比如說,Java 虛擬機要求 long 字段、double 字段,以及非壓縮指針狀態(tài)下的引用字段地址為 8 的倍數(shù)。
??在默認情況下,Java 虛擬機中的32 位壓縮指針可以尋址到 2 的 35 次方個字節(jié),也就是 32GB 的地址空間(超過 32GB 則會關(guān)閉壓縮指針)。
具體的內(nèi)存布局可以參考:https://www.jianshu.com/p/3d38cba67f8b
??目前 Java 虛擬機的主流垃圾回收器采取的是可達性分析算法。這個算法的實質(zhì)在于將一系列 GC Roots 作為初始的存活對象合集(live set),然后從該合集出發(fā),探索所有能夠被該集合引用到的對象,并將其加入到該集合中,這個過程我們也稱之為標記(mark)。最終,未被探索到的對象便是死亡的,是可以回收的。GC Roots 包括(但不限于)如下幾種:
1:Java 方法棧楨中的局部變量;
2:已加載類的靜態(tài)變量;
3:JNI handles;
4:已啟動且未停止的 Java 線程。
??Java 虛擬機中的 Stop-the-world 是通過安全點(safepoint)機制來實現(xiàn)的。當(dāng) Java 虛擬機收到 Stop-the-world 請求,它便會等待所有的線程都到達安全點,才允許請求 Stop-the-world 的線程進行獨占的工作。安全點的初始目的并不是讓其他線程停下,而是找到一個穩(wěn)定的執(zhí)行狀態(tài)。在這個執(zhí)行狀態(tài)下,Java 虛擬機的堆棧不會發(fā)生變化。這么一來,垃圾回收器便能夠“安全”地執(zhí)行可達性分析。
??回收死亡對象的內(nèi)存共有三種方式,分別為:會造成內(nèi)存碎片的清除、性能開銷較大的壓縮、以及堆使用效率較低的復(fù)制。
??Java 虛擬機將堆劃分為新生代和老年代。其中,新生代又被劃分為 Eden 區(qū),以及兩個大小相同的 Survivor 區(qū)。如下圖所示:
??即時編譯器(和處理器)需要保證程序能夠遵守 as-if-serial 屬性。通俗地說,就是在單線程情況下,要給程序一個順序執(zhí)行的假象。即經(jīng)過重排序的執(zhí)行結(jié)果要與順序執(zhí)行的結(jié)果保持一致。但這在多線程執(zhí)行的情況下,就有可能出現(xiàn)意想不到的結(jié)果。
??Java 內(nèi)存模型通過定義了一系列的 happens-before 操作,讓應(yīng)用程序開發(fā)者能夠輕易地表達不同線程的操作之間的內(nèi)存可見性。
Java 內(nèi)存模型還定義了下述線程間的 happens-before 關(guān)系。
1:解鎖操作 happens-before 之后(這里指時鐘順序先后)對同一把鎖的加鎖操作。
2:volatile 字段的寫操作 happens-before 之后(這里指時鐘順序先后)對同一字段的讀操作。
3:線程的啟動操作(即 Thread.starts()) happens-before 該線程的第一個操作。
4:線程的最后一個操作 happens-before 它的終止事件(即其他線程通過 Thread.isAlive() 或 Thread.join() 判斷該線程是否中止)。
5:線程對其他線程的中斷操作 happens-before 被中斷線程所收到的中斷事件(即被中斷線程的 InterruptedException 異常,或者第三個線程針對被中斷線程的 Thread.interrupted 或者 Thread.isInterrupted 調(diào)用)。
6:構(gòu)造器中的最后一個操作 happens-before 析構(gòu)器的第一個操作。
??在遵守 Java 內(nèi)存模型的前提下,即時編譯器以及底層體系架構(gòu)能夠調(diào)整內(nèi)存訪問操作,以達到性能優(yōu)化的效果。如果開發(fā)者沒有正確地利用 happens-before 規(guī)則,那么將可能導(dǎo)致數(shù)據(jù)競爭。
??Java 內(nèi)存模型是通過內(nèi)存屏障來禁止重排序的。對于即時編譯器來說,內(nèi)存屏障將限制它所能做的重排序優(yōu)化。對于處理器來說,內(nèi)存屏障會導(dǎo)致緩存的刷新操作。
基本類型如下圖:
??在默認情況下,方法的反射調(diào)用為委派實現(xiàn),委派給本地實現(xiàn)來進行方法調(diào)用。在調(diào)用超過 15 次之后(可以通過 -Dsun.reflect.inflationThreshold= 來調(diào)整),委派實現(xiàn)便會將委派對象切換至動態(tài)實現(xiàn)。這個動態(tài)實現(xiàn)的字節(jié)碼是自動生成的,它將直接使用 invoke 指令來調(diào)用目標方法。動態(tài)實現(xiàn)和本地實現(xiàn)相比,其運行效率要快上 20 倍 。這是因為動態(tài)實現(xiàn)無需經(jīng)過 Java 到 C++ 再到 Java 的切換,但由于生成字節(jié)碼十分耗時,僅調(diào)用一次的話,反而是本地實現(xiàn)要快上 3 到 4 倍。反射調(diào)用的 Inflation 機制是可以通過參數(shù)(-Dsun.reflect.noInflation=true)來關(guān)閉的。這樣一來,在反射調(diào)用一開始便會直接生成動態(tài)實現(xiàn),而不會使用委派實現(xiàn)或者本地實現(xiàn)。
??方法的反射調(diào)用會帶來不少性能開銷,原因主要有三個:變長參數(shù)方法導(dǎo)致的 Object 數(shù)組,基本類型的自動裝箱、拆箱,還有最重要的方法內(nèi)聯(lián)。
??當(dāng)聲明 synchronized 代碼塊時,編譯而成的字節(jié)碼將包含 monitorenter 和 monitorexit 指令。這兩種指令均會消耗操作數(shù)棧上的一個引用類型的元素(也就是 synchronized 關(guān)鍵字括號里的引用),作為所要加鎖解鎖的鎖對象。
??關(guān)于 monitorenter 和 monitorexit 的作用,我們可以抽象地理解為每個鎖對象擁有一個鎖計數(shù)器和一個指向持有該鎖的線程的指針。當(dāng)執(zhí)行 monitorenter 時,如果目標鎖對象的計數(shù)器為 0,那么說明它沒有被其他線程所持有。在這個情況下,Java 虛擬機會將該鎖對象的持有線程設(shè)置為當(dāng)前線程,并且將其計數(shù)器加 1。在目標鎖對象的計數(shù)器不為 0 的情況下,如果鎖對象的持有線程是當(dāng)前線程,那么 Java 虛擬機可以將其計數(shù)器加 1,否則需要等待,直至持有線程釋放該鎖。當(dāng)執(zhí)行 monitorexit 時,Java 虛擬機則需將鎖對象的計數(shù)器減 1。當(dāng)計數(shù)器減為 0 時,那便代表該鎖已經(jīng)被釋放掉了。HotSpot 虛擬機中具體的鎖實現(xiàn)分為:
??對于 Java 語言中重寫而 Java 虛擬機中非重寫的情況,編譯器會通過生成橋接方法來實現(xiàn) Java 中的重寫語義。下機的圖可以通過字節(jié)碼看出是如何實現(xiàn)的:
方法內(nèi)聯(lián)是指:在編譯過程中遇到方法調(diào)用時,將目標方法的方法體納入編譯范圍之中,并取代原方法調(diào)用的優(yōu)化手段。以 getter/setter 為例,如果沒有方法內(nèi)聯(lián),在調(diào)用 getter/setter 時,程序需要保存當(dāng)前方法的執(zhí)行位置,創(chuàng)建并壓入用于 getter/setter 的棧幀、訪問字段、彈出棧幀,最后再恢復(fù)當(dāng)前方法的執(zhí)行。而當(dāng)內(nèi)聯(lián)了對 getter/setter 的方法調(diào)用后,上述操作僅剩字段訪問。
??通常而言,代碼會先被 Java 虛擬機解釋執(zhí)行,之后反復(fù)執(zhí)行的熱點代碼則會被即時編譯成為機器碼,直接運行在底層硬件之上。即時編譯器有C1,C2,Grral。
??逃逸分析將判斷新建的對象是否逃逸。即時編譯器判斷對象是否逃逸的依據(jù),一是對象是否被存入堆中(靜態(tài)字段或者堆中對象的實例字段),二是對象是否被傳入未知代碼中。
當(dāng)發(fā)現(xiàn)一個對象只在某個方法里,或者這個方法的內(nèi)聯(lián)方法里,則可以認為這個對象是逃逸的。主要的優(yōu)化有:1:鎖消除,對逃逸的對象加鎖是沒有意義的。2:采用標量替換的技術(shù)將需要分配在堆上的對象直接在棧上采用變量的方式進行替換。