Java 虛擬機器經典六問

Java大蝸牛發表於2019-01-19

大家好,我是鄭雨迪。很榮幸,我開設的《深入拆解 Java 虛擬機器》專欄得到了大家的青睞,有了20000+的訂閱。很顯然,現在越來越多的程式設計師意識到了Java虛擬機器的重要性,渴望去了解底層,迫切想通過系統性的學習深入Java虛擬機器,達到“知其然且知其所以然”的目的。

在專欄開更到完結期間,我收到了不下幾千條問題,儘量都做了解答。現特意整理出了6個高頻問題,分享給大家,算做一篇加餐文。希望大家能繼續深耕JVM,提升日常程式設計的效率,實現技術進階,挖掘到更多的寶藏。

Java是如何在保證可移植性的前提下提供高執行效率的?

\"\"
Java程式最為常見的執行方式,是預先編譯為一種名為Java位元組碼的中間程式碼格式。這種程式碼格式無法直接執行在CPU之上,而是需要藉助JVM來執行。換句話說,只要某個平臺提供了合乎JVM規範的實現,它便能執行這份Java位元組碼。這也就是我們經常說的“一次編寫,到處執行”。

主流的OpenJDK/OracleJDK中所提供的JVM叫做HotSpot。它同時採用瞭解釋執行和即時編譯。解釋執行就好比同聲傳譯,JVM一邊理解輸入的位元組碼一邊向CPU發出指令序列;即時編譯則是“磨刀不誤砍柴工”,JVM會在執行過程中將熱點程式碼編譯成為可直接執行的二進位制程式碼。

這種混合執行模式是建立在程式符合二八定律的假設上,即百分之二十的程式碼佔據了百分之八十的計算資源。對於不常用程式碼,我們無需耗費時間將其編譯成二進位制程式碼,而是採取解釋執行的方式執行;另一方面,對於僅佔據小部分的熱點程式碼,JVM則會花費時間將其編譯為二進位制程式碼,以達到理想的執行效率。

異常捕獲是如何實現的?

\"\"

在編譯生成的Java位元組碼中,每個方法都附帶一個異常表。異常表中的每一行均定義了一條異常執行路徑,其中包括規定捕獲範圍的起始位元組碼索引、終止(不包含)位元組碼索引,異常處理程式碼的起始位元組碼索引,以及所捕獲的異常型別。

當程式觸發異常時,JVM會從上至下遍歷異常表中的所有條目。當觸發異常的位元組碼的索引值在某行異常表條目的捕獲範圍內,JVM會判斷所丟擲的異常和該條目想要捕獲的異常是否匹配。如果匹配,JVM會將控制流轉移至該條目所指向的異常處理程式碼。

上述異常捕獲機制還被用於finally從句的實現。通常,Java程式的編譯器javac會複製多份finally程式碼塊,放置於生成的Java位元組碼之中,然後通過生成多行異常表條目,來實現完整的finally邏輯。

反射呼叫為什麼慢?

\"\"
預設情況下,反射呼叫首先會被委派給native方法來進行。可想而知,其執行效率低下。當某個反射呼叫的呼叫次數達到15之後,JDK程式碼斷定該呼叫屬於熱點呼叫。繼而,JDK將動態生成直接呼叫目標方法的位元組碼,並將反射呼叫的委派物件由原本的native方法實現切換至該動態生成的實現。這種方式的執行效率相對於native方法來說要高很多。

之所以JDK不從一開始便採用動態生成位元組碼的方式,主要是因為生成過程需要耗費一定的時間。對於那些整個生命週期中僅執行數次的反射呼叫,動態生成位元組碼將得不償失。

然而,即便是直接呼叫目標方法的動態實現,其峰值效能也無法跟真正的直接呼叫相媲美。這背後涉及到即時編譯中的虛方法內聯。

相關文章:\u0026lt; 方法內聯(下)\u0026gt;

垃圾回收的基礎思想是什麼?

\"\"
目前JVM的主流垃圾回收器採取的都是可達性分析演算法。該演算法的實質是將一系列被稱為GC Roots的物件作為初始的存活物件合集,然後從該合集出發探索所有能夠被該集合引用到的物件,並標記為存活物件。當標記階段結束之後,未被標記到的物件便是可以清除的。

傳統的垃圾回收演算法在標記、清除過程中需要中止其他應用執行緒,即所謂的Stop-The-World。新型的垃圾回收演算法,如CMS、G1以及ZGC,儘可能地實現併發標記、清除,從而讓Stop-The-World的時間長度可控。

垃圾回收的另一基礎思想則是分代回收。JVM會將新生成的物件劃為新生代,而將在多次垃圾回收中存活下來的物件劃為老年代。JVM會為不同的分代設定不同的回收演算法,從而達到新生代多收集、快收集,老年代少收集、全收集的目標。

如何理解Java記憶體模型?

現代計算機多為對稱多處理器的體系架構。每個處理器均有獨立的暫存器組和快取(這在Java記憶體模型中被抽象為工作記憶體);多個處理器可同時執行同一程式中的不同執行緒。

在Java程式中,不同執行緒可能訪問同一變數或物件。如果任由編譯器或處理器對這些訪問進行優化,則很可能出現在單執行緒執行思維下無法想象的問題。因此,Java語言規範引入了Java記憶體模型,通過定義多項規則對編譯器和處理器進行限制。

這些規則所體現的最為重要的屬性便是可見性,即對某一變數的訪問能否被同一執行緒的其他操作,或者不同執行緒所觀測到。Java記憶體模型引入了多種happens-before關係,以實現上述可見性。以volatile欄位為例,對其的寫操作happens before這之後的讀操作,也就是說,我們總能讀到volatile欄位的最新值。

JVM如何應對物件鎖的各種場景?

重量級鎖是最為基礎、最為低效的物件鎖實現。JVM會阻塞加鎖失敗的執行緒,並且在目標鎖被釋放的時候,喚醒這些執行緒。我們用等紅燈作類比。Java執行緒進入阻塞狀態相當於熄火停車,再次點火啟動必然耗費時間。JVM會在進入阻塞狀態之前進行自旋,也就是怠速停車。如果目標鎖能夠在短時間內被釋放出來,該執行緒便能夠不進入阻塞狀態,直接獲取該鎖。

重量級鎖針對的是多個執行緒同時競爭同一把鎖的場景。在現實中,多個執行緒可能在不同時間段持有同一把鎖。為了應對這種沒有鎖競爭的情況,JVM採用了輕量級鎖機制。在加鎖時,JVM將在鎖物件處做標記,指向當前執行緒的棧上;在解鎖時,上述標記會被清除。如果某執行緒在請求鎖時,發現該鎖為輕量級鎖,並且指向另一執行緒所對應的棧,那麼它會將該鎖膨脹為重量級鎖。

偏向鎖所應對的場景則更為樂觀:至始至終只有一個執行緒請求某把鎖。JVM採取的做法是在第一次加鎖時為鎖物件做標記,使其指向當前執行緒的地址;在解鎖時則不做任何操作。如果下一次請求該鎖的仍是同一執行緒,便直接跳過標記過程;否則,JVM會將該鎖膨脹為輕量級鎖。

文章出自極客時間《深入拆解Java 虛擬機器》專欄。

相關文章