Java 虛擬機經典六問

大家好,我是鄭雨迪。很榮幸,我開設的《深入拆解 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不從一開始便採用動態生成字節碼的方式,主要是因爲生成過程需要耗費一定的時間。對於那些整個生命週期中僅執行數次的反射調用,動態生成字節碼將得不償失。

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

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

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


目前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 虛擬機》專欄。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章