Java虛擬機器必知的四大知識要點
作者 | 鄭雨迪
來源 | 極客時間《深入拆解 Java 虛擬機器》
作為一位 Java 程式設計師,在盡情享受 Java 虛擬機器帶來好處的同時,我們還應該去了解和思考“這些技術特性是如何實現的”,去了解最底層的原理。只有熟悉 JVM,你才能在遇到 OutOfMemory 等異常時,不會束手無策,不會一臉懵逼地上網找解決辦法,最後就算改了幾個啟動引數解決了問題,也還是雲裡霧裡。
這次,我會從我專欄裡提取了學習 Java 虛擬機器的 X 大知識要點,助力大家深入理解 JVM,知其然也知其所以然。 不過你在看知識點之前,最好能問問自己你會怎麼回答,再和我提供的內容做對比,這樣子提升會比較明顯。
第一大知識要點:Java 位元組碼是如何在虛擬機器裡執行的?
我將以 HotSpot 虛擬機器為例,從虛擬機器以及底層硬體兩個角度,來分享解析。
1、從虛擬機器視角來看
執行 Java 程式碼首先需要將它編譯而成的 class 檔案載入到 Java 虛擬機器中。載入後的 Java 類會被存放於方法區中。實際執行時,虛擬機器會執行方法區內的程式碼。
如果你熟悉 X86 的話,你會發現這和段式記憶體管理中的程式碼段類似。而且,Java 虛擬機器同樣也在記憶體中劃分出堆和棧來儲存執行時資料。不同的是,Java 虛擬機器會將棧細分為面向 Java 方法的 Java 方法棧,面向用 C++ 寫的 native 方法的本地方法棧,以及存放各個執行緒執行位置的 PC 暫存器。

在執行過程中,每當呼叫進入一個 Java 方法,Java 虛擬機器會在當前執行緒的 Java 方法棧中生成一個棧幀,用以存放區域性變數以及位元組碼的運算元。這個棧幀的大小是提前計算好的,而且 Java 虛擬機器不要求棧幀在記憶體空間裡連續分佈。
當退出當前執行的方法時,不管是正常返回還是異常返回,Java 虛擬機器均會彈出當前執行緒的當前棧幀,並將之捨棄。
2、從硬體視角來看
Java 位元組碼無法直接執行。因此,Java 虛擬機器需要將位元組碼翻譯成機器碼。
在 HotSpot 裡面,上述翻譯過程有兩種形式:第一種是解釋執行,相當於同聲傳譯,即每解析一條位元組碼,便翻譯成機器碼並執行;第二種是即時編譯(Just-In-Time compilation,JIT),則相當於線下翻譯,即將整個方法中所包含的位元組碼統一翻譯成機器碼後在執行。

前者的優勢在於無需等待編譯,而後者的優勢在於實際執行速度更快。HotSpot 預設採用混合模式,綜合瞭解釋執行和即時編譯兩者的優點。它會先解釋執行位元組碼,而後將其中反覆執行的熱點程式碼,以方法為單位進行即時編譯。
第二大知識要點:Java 虛擬機器是如何載入 Java 類的?
Java 虛擬機器載入 Java 類的過程可分為載入、連結以及初始化三大步驟。
載入是指查詢位元組流,並且據此建立類的過程。載入需要藉助類載入器,在 Java 虛擬機器中,類載入器使用了雙親委派模型,即接收到載入請求時,會先將請求轉發給父類載入器。

連結,是指將建立成的類合併至 Java 虛擬機器中,使之能夠執行的過程。連結還分驗證、準備和解析三個階段,分別完成“驗證被載入類是否滿足 Java 虛擬機器約束”,“為被載入類靜態欄位分配記憶體”,以及“將被載入類中的符號引用解析成為實際引用”的工作。其中,Java 虛擬機器規範並不要求解析階段一定要在連結步驟中完成。
初始化,則是為標記為常量值的欄位賦值,以及執行 <clinit> 方法的過程。類的初始化僅會被執行一次,這個特性被用來實現單例的延遲初始化。
第三大知識要點:Java 虛擬機器是如何進行垃圾回收的?
Java 虛擬機器中的垃圾回收器採用可達性分析來探索所有存活的物件。它從一系列 GC Roots 出發,邊標記邊探索所有被引用的物件。為了防止在標記過程中堆疊的狀態發生改變,Java 虛擬機器採取安全點機制來實現 Stop-The-World 操作,暫停其他非垃圾回收執行緒。
回收垃圾物件的記憶體共有三種基礎演算法,分別為:會造成記憶體碎片的清除演算法、效能開銷較大的壓縮演算法、以及堆使用效率較低的複製演算法。
通常來說,Java 虛擬機器會採用分代回收的思想,將堆劃分為新生代和老年代,並且通過在不同代中應用不同的垃圾回收演算法。
傳統的做法是將新生代再劃分為 Eden 區和兩個大小一致的 Survivor 區。在只針對新生代的 Minor GC 中,Eden 區和非空 Survivor 區的存活物件會被複制到空的 Survivor 區中,當 Survivor 區中的存活物件複製次數超過一定數值時,它將被晉升至老年代。
因為 Minor GC 只針對新生代進行垃圾回收,所以在列舉 GC Roots 的時候,它需要考慮從老年代到新生代的引用。為了避免掃描整個老年代,Java 虛擬機器引入了名為卡表的技術,大致地標出可能存在老年代到新生代的引用的記憶體區域。
G1 垃圾回收器將堆劃分為多個等大的區域,每個區域都可以充當 Eden 區,Survivor 區或者老年代區。G1 會優先收集垃圾最多的區域,從而最大化垃圾回收的效益。這也是 Garbage First 名字的由來。
Java 11 中引入的實驗性垃圾回收器 ZGC,僅在掃描 GC Roots 時請求 Stop-The-World,暫停應用執行緒。因此,它宣稱可將 GC 暫停時間控制在 10ms 以下。ZGC 暫時沒有應用分代回收的思路,將整個堆空間看成一塊,其代價是垃圾回收 CPU 消耗較高。
第四大知識要點:Java 記憶體模型是什麼?
在現代計算機系統中,程式碼通常不會按照書寫順序執行。造成這一情況的原因有三個,分別為編譯器的重排序,處理器的亂序執行,以及記憶體系統的重排序。
以記憶體系統重排序為例,在多處理器體系架構下,每個處理器都可能快取了一部分資料。由於時刻保持快取資料與記憶體資料同步的效能代價太大,因此部分體系架構可能允許快取資料與記憶體資料不同步。這對 Java 程式的影響便是,兩個不同的 Java 執行緒在同一時間內看到的同一塊記憶體地址中的值可能不同。
Java 記憶體模型是針對上述問題而提出的一套規範,用以允許 Java 程式設計師更為細緻地定義 Java 程式的記憶體行為。它通過定義了一系列的 happens-before 操作,讓應用程式開發者能夠輕易地表達不同執行緒的操作之間的記憶體可見性。
在遵守 Java 記憶體模型的前提下,即時編譯器以及底層體系架構能夠調整記憶體訪問操作,以達到效能優化的效果。如果開發者沒有正確地利用 happens-before 規則,那麼將可能導致資料競爭。
Java 記憶體模型是通過記憶體屏障來禁止重排序的。對於即時編譯器來說,記憶體屏障將限制它所能做的重排序優化。對於處理器來說,記憶體屏障會導致快取的重新整理操作。
擴充套件閱讀
我的專欄《深入拆解 Java 虛擬機器》已完結,非常感謝在我專欄完結之前的 16000 多名訂閱使用者,在未了解完整內容的情況下,毅然訂閱了我的專欄。為不辜負大家的信任,我幾乎每篇專欄都會大量閱讀 HotSpot 的原始碼,和同事討論實現背後的設計理念,在這個過程中,我也發現了一些 HotSpot 中的 Bug,或年久失修的程式碼,又或者是設計不合理的地方,這大概算作寫專欄和我本職工作重疊的地方吧。
專欄雖然到此已經結束了,但是並不代表你對 Java 虛擬機器學習的停止。我想,專欄的內容僅僅是為你打開了 JVM 學習的大門,裡面的風景,還是需要你自己來探索。在文章的後面,我列出了一系列的 Java 虛擬機器技術的相關部落格和閱讀資料,你仍然可以繼續加餐。
你可以關注國內幾位 Java 虛擬機器大咖的知乎或微信公眾號:
-
R 大,個人認為是中文圈子裡最瞭解 Java 虛擬機器設計實現的人,你可以關注他的知乎賬號:
ofollow,noindex">https://www.zhihu.com/people/rednaxelafx
-
你假笨,原阿里 Java 虛擬機器團隊成員,現 PerfMa CEO,公眾號:你假笨
-
江南白衣,唯品會資深架構師,公眾號:春天的旁邊;
-
佔小狼,美團基礎架構部技術專家,公眾號:佔小狼的部落格
-
楊曉峰,前甲骨文首席工程師,公眾號:小肥羊聊 Java。
如果英文閱讀沒問題的話,你可以關注:
-
Cliff Click
-
Aleksey Shipilëv
-
Nitsan Wakart
你也可以關注
-
Java Virtual Machine Language Submit
-
Oracle Code One
關於 Java 虛擬機器的演講,以便掌握 Java 的最新發展動向。
當然,如果對 GraalVM 感興趣的話,你可以訂閱我們團隊的部落格,之後我會考慮逐一進行翻譯
至於其他閱讀材料,你可以參考 R 大的這份書單
https://www.douban.com/doulist/2545443/
或者這個彙總貼
https://github.com/deephacks/awesome-jvm
如果本專欄已經激發了你對 Java 虛擬機器的學習熱情,那麼我建議你著手閱讀 HotSpot 原始碼,並且回饋 OpenJDK 開源社群。這種回饋並不一定是提交 patch,也可以是 bug report 或者改進建議等等。
道阻且長,努力加餐~!
可以說,Java 虛擬機器就是每一位 Java 工程師進階加薪的利器,你想往上升,你想深入技術,不想一直停留在簡單開發,或者你在做 Java 效能分析、調優工作時,那麼,Java 虛擬機器絕對是一把助力的利劍。