Java 虛擬機器經典六問

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

特意整理出了 6 個高頻問題,分享給大家,算做一篇加餐文。希望大家能繼續深耕 JVM,提升日常程式設計的效率,實現技術進階,挖掘到更多的寶藏。

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

Java 虛擬機器經典六問

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

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

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

異常捕獲是如何實現的?

Java 虛擬機器經典六問

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

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

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

反射呼叫為什麼慢?

Java 虛擬機器經典六問

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

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

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

相關文章:  < 方法內聯(下)>

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

Java 虛擬機器經典六問

目前 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 會將該鎖膨脹為輕量級鎖。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31545684/viewspace-2564214/,如需轉載,請註明出處,否則將追究法律責任。

相關文章