2020年學習Java,不要忘記Java虛擬機必學的四大知識要點


2020年學習Java,不要忘記Java虛擬機必學的四大知識要點


作為一位 Java 程序員,在盡情享受 Java 虛擬機帶來好處的同時,我們還應該去了解和思考“這些技術特性是如何實現的”,去了解最底層的原理。只有熟悉 JVM,你才能在遇到 OutOfMemory 等異常時,不會束手無策,不會一臉懵逼地上網找解決辦法,最後就算改了幾個啟動參數解決了問題,也還是雲裡霧裡。

這次,我會從我專欄裡提取了學習 Java 虛擬機的 X 大知識要點,助力大家深入理解 JVM,知其然也知其所以然。 不過你在看知識點之前,最好能問問自己你會怎麼回答,再和我提供的內容做對比,這樣子提升會比較明顯。

第一大知識要點:Java 字節碼是如何在虛擬機裡運行的?

我將以 HotSpot 虛擬機為例,從虛擬機以及底層硬件兩個角度,來分享解析。

1、從虛擬機視角來看

執行 Java 代碼首先需要將它編譯而成的 class 文件加載到 Java 虛擬機中。加載後的 Java 類會被存放於方法區中。實際運行時,虛擬機會執行方法區內的代碼。

如果你熟悉 X86 的話,你會發現這和段式內存管理中的代碼段類似。而且,Java 虛擬機同樣也在內存中劃分出堆和棧來存儲運行時數據。不同的是,Java 虛擬機會將棧細分為面向 Java 方法的 Java 方法棧,面向用 C++ 寫的 native 方法的本地方法棧,以及存放各個線程執行位置的 PC 寄存器。

2020年學習Java,不要忘記Java虛擬機必學的四大知識要點

在運行過程中,每當調用進入一個 Java 方法,Java 虛擬機會在當前線程的 Java 方法棧中生成一個棧幀,用以存放局部變量以及字節碼的操作數。這個棧幀的大小是提前計算好的,而且 Java 虛擬機不要求棧幀在內存空間裡連續分佈。

當退出當前執行的方法時,不管是正常返回還是異常返回,Java 虛擬機均會彈出當前線程的當前棧幀,並將之捨棄。

2、從硬件視角來看

Java 字節碼無法直接執行。因此,Java 虛擬機需要將字節碼翻譯成機器碼。

在 HotSpot 裡面,上述翻譯過程有兩種形式:第一種是解釋執行,相當於同聲傳譯,即每解析一條字節碼,便翻譯成機器碼並執行;第二種是即時編譯(Just-In-Time compilation,JIT),則相當於線下翻譯,即將整個方法中所包含的字節碼統一翻譯成機器碼後在執行。

2020年學習Java,不要忘記Java虛擬機必學的四大知識要點

前者的優勢在於無需等待編譯,而後者的優勢在於實際運行速度更快。HotSpot 默認採用混合模式,綜合瞭解釋執行和即時編譯兩者的優點。它會先解釋執行字節碼,而後將其中反覆執行的熱點代碼,以方法為單位進行即時編譯。

第二大知識要點:Java 虛擬機是如何加載 Java 類的?

Java 虛擬機加載 Java 類的過程可分為加載、鏈接以及初始化三大步驟。

加載是指查找字節流,並且據此創建類的過程。加載需要藉助類加載器,在 Java 虛擬機中,類加載器使用了雙親委派模型,即接收到加載請求時,會先將請求轉發給父類加載器。

2020年學習Java,不要忘記Java虛擬機必學的四大知識要點

鏈接,是指將創建成的類合併至 Java 虛擬機中,使之能夠執行的過程。鏈接還分驗證、準備和解析三個階段,分別完成“驗證被加載類是否滿足 Java 虛擬機約束”,“為被加載類靜態字段分配內存”,以及“將被加載類中的符號引用解析成為實際引用”的工作。其中,Java 虛擬機規範並不要求解析階段一定要在鏈接步驟中完成。

初始化,則是為標記為常量值的字段賦值,以及執行 <clinit> 方法的過程。類的初始化僅會被執行一次,這個特性被用來實現單例的延遲初始化。/<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 內存模型是通過內存屏障來禁止重排序的。對於即時編譯器來說,內存屏障將限制它所能做的重排序優化。對於處理器來說,內存屏障會導致緩存的刷新操作。


2020年學習Java,不要忘記Java虛擬機必學的四大知識要點


分享到:


相關文章: