Java 虛擬機經典六問

Java 虛擬機經典六問

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

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

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

Java 虛擬機經典六問


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

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

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

2、異常捕獲是如何實現的?

Java 虛擬機經典六問


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

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

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

3、反射調用為什麼慢?

Java 虛擬機經典六問


默認情況下,反射調用首先會被委派給 native 方法來進行。可想而知,其運行效率低下。當某個反射調用的調用次數達到 15 之後,JDK 代碼斷定該調用屬於熱點調用。繼而,JDK 將動態生成直接調用目標方法的字節碼,並將反射調用的委派對象由原本的 native 方法實現切換至該動態生成的實現。這種方式的運行效率相對於 native 方法來說要高很多。

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

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

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

Java 虛擬機經典六問


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

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

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

5、如何理解 Java 內存模型?

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

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

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

6、JVM 如何應對對象鎖的各種場景?

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

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

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

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

相關推薦

推薦中...