JVM面試2大難題,標準答案給你做出來了!

作者簡介:李國,前京東高級架構師。 本文選自:拉勾教育專欄《深入淺出Java虛擬機》

你好,我是李國,今天我們主要講解 JVM 的內存劃分以及棧上的執行過程。這塊內容在面試中主要涉及以下這 2 個面試題:

  • JVM 是如何進行內存區域劃分的?
  • JVM 如何高效進行內存管理?

帶著這 3 個問題,我們開始今天的學習,關於內存劃分的知識我希望在本課時你能夠理解就可以,不需要死記硬背,因為在後面的課時我們會經常使用到本課時學習的內容,也會結合工作中的場景具體問題具體分析,這樣你可以對 JVM 的內存獲得更深刻的認識。

首先,第一個問題:JVM的內存區域是怎麼高效劃分的?這也是一個高頻的面試題。很多同學可能通過死記硬背的方式來應對這個問題,這樣不僅對知識沒有融會貫通在面試中還很容易忘記答案。

為什麼要問到 JVM 的內存區域劃分呢?因為 Java 引以為豪的就是它的自動內存管理機制。相比於 C++的手動內存管理、複雜難以理解的指針等,Java 程序寫起來就方便的多。

然而這種呼之即來揮之即去的內存申請和釋放方式,自然也有它的代價。為了管理這些快速的內存申請釋放操作,就必須引入一個池子來延遲這些內存區域的回收操作。

我們常說的內存回收,就是針對這個池子的操作。我們把上面說的這個池子,叫作堆,可以暫時把它看成一個整體。

本文選自:拉勾教育專欄《深入淺出Java虛擬機》見文末了解更多

JVM 內存佈局

程序想要運行,就需要數據。有了數據,就需要在內存上存儲。那你可以回想一下,我們的 C++ 程序是怎麼運行的?是不是也是這樣?

JVM面試2大難題,標準答案給你做出來了!

Java 程序的數據結構是非常豐富的。其中的內容,舉一些例子:

  • 靜態成員變量
  • 動態成員變量
  • 區域變量
  • 短小緊湊的對象聲明
  • 龐大複雜的內存申請

這麼多不同的數據結構,到底是在什麼地方存儲的,它們之間又是怎麼進行交互的呢?是不是經常在面試的時候被問到這些問題?

我們先看一下 JVM 的內存佈局。隨著 Java 的發展,內存佈局一直在調整之中。比如,Java 8 及之後的版本,徹底移除了持久代,而使用 Metaspace 來進行替代。這也表示著 -XX:PermSize 和 -XX:MaxPermSize 等參數調優,已經沒有了意義。但大體上,比較重要的內存區域是固定的。

JVM面試2大難題,標準答案給你做出來了!

JVM 內存區域劃分如圖所示,從圖中我們可以看出:

  • JVM 堆中的數據是共享的,是佔用內存最大的一塊區域。
  • 可以執行字節碼的模塊叫作執行引擎。
  • 執行引擎在線程切換時怎麼恢復?依靠的就是程序計數器。
  • JVM 的內存劃分與多線程是息息相關的。像我們程序中運行時用到的棧,以及本地方法棧,它們的維度都是線程。
  • 本地內存包含元數據區和一些直接內存。

一般情況下,只要你能答出上面這些主要的區域,面試官都會滿意的點頭。但如果深挖下去,可能就有同學就比較頭疼了。下面我們就詳細看下這個過程。

虛擬機棧

JVM面試2大難題,標準答案給你做出來了!

棧是什麼樣的數據結構?你可以想象一下子彈上膛的這個過程,後進的子彈最先射出,最上面的子彈就相當於棧頂。

我們在上面提到,Java 虛擬機棧是基於線程的。哪怕你只有一個 main() 方法,也是以線程的方式運行的。在線程的生命週期中,參與計算的數據會頻繁地入棧和出棧,棧的生命週期是和線程一樣的。

棧裡的每條數據,就是棧幀。在每個 Java 方法被調用的時候,都會創建一個棧幀,併入棧。一旦完成相應的調用,則出棧。所有的棧幀都出棧後,線程也就結束了。每個棧幀,都包含四個區域:

  • 局部變量表
  • 操作數棧
  • 動態連接
  • 返回地址

我們的應用程序,就是在不斷操作這些內存空間中完成的。

JVM面試2大難題,標準答案給你做出來了!

本地方法棧是和虛擬機棧非常相似的一個區域,它服務的對象是 native 方法。你甚至可以認為虛擬機棧和本地方法棧是同一個區域,這並不影響我們對 JVM 的瞭解。

這裡有一個比較特殊的數據類型叫作 returnAdress。因為這種類型只存在於字節碼層面,所以我們平常打交道的比較少。對於 JVM 來說,程序就是存儲在方法區的字節碼指令,而 returnAddress 類型的值就是指向特定指令內存地址的指針。

JVM面試2大難題,標準答案給你做出來了!

這部分有兩個比較有意思的內容,面試中說出來會讓面試官眼前一亮。

  1. 這裡有一個兩層的棧。第一層是棧幀,對應著方法;第二層是方法的執行,對應著操作數。注意千萬不要搞混了。
  2. 你可以看到,所有的字節碼指令,其實都會抽象成對棧的入棧出棧操作。執行引擎只需要傻瓜式的按順序執行,就可以保證它的正確性。

這一點很神奇,也是基礎。我們接下來從線程角度看一下里面的內容。

本文選自:拉勾教育專欄《深入淺出Java虛擬機》見文末了解更多

程序計數器

那麼你設想一下,如果我們的程序在線程之間進行切換,憑什麼能夠知道這個線程已經執行到什麼地方呢?

既然是線程,就代表它在獲取 CPU 時間片上,是不可預知的,需要有一個地方,對線程正在運行的點位進行緩衝記錄,以便在獲取 CPU 時間片時能夠快速恢復。

就好比你停下手中的工作,倒了杯茶,然後如何繼續之前的工作?

程序計數器是一塊較小的內存空間,它的作用可以看作是當前線程所執行的字節碼的行號指示器。這裡面存的,就是當前線程執行的進度。下面這張圖,能夠加深大家對這個過程的理解。

JVM面試2大難題,標準答案給你做出來了!

可以看到,程序計數器也是因為線程而產生的,與虛擬機棧配合完成計算操作。程序計數器還存儲了當前正在運行的流程,包括正在執行的指令、跳轉、分支、循環、異常處理等。

我們可以看一下程序計數器裡面的具體內容。下面這張圖,就是使用 javap 命令輸出的字節碼。大家可以看到在每個 opcode 前面,都有一個序號。就是圖中紅框中的偏移地址,你可以認為它們是程序計數器的內容。

JVM面試2大難題,標準答案給你做出來了!

JVM面試2大難題,標準答案給你做出來了!


堆是 JVM 上最大的內存區域,我們申請的幾乎所有的對象,都是在這裡存儲的。我們常說的垃圾回收,操作的對象就是堆。

堆空間一般是程序啟動時,就申請了,但是並不一定會全部使用。

隨著對象的頻繁創建,堆空間佔用的越來越多,就需要不定期的對不再使用的對象進行回收。這個在 Java 中,就叫作 GC(Garbage Collection)。

由於對象的大小不一,在長時間運行後,堆空間會被許多細小的碎片佔滿,造成空間浪費。所以,僅僅銷燬對象是不夠的,還需要堆空間整理。這個過程非常的複雜,我們會在後面有專門的課時進行介紹。

那一個對象創建的時候,到底是在堆上分配,還是在棧上分配呢?這和兩個方面有關:對象的類型和在 Java 類中存在的位置。

Java 的對象可以分為基本數據類型和普通對象。

對於普通對象來說,JVM 會首先在堆上創建對象,然後在其他地方使用的其實是它的引用。比如,把這個引用保存在虛擬機棧的局部變量表中。

對於基本數據類型來說(byte、short、int、long、float、double、char),有兩種情況。

我們上面提到,每個線程擁有一個虛擬機棧。當你在方法體內聲明瞭基本數據類型的對象,它就會在棧上直接分配。其他情況,都是在堆上分配。

注意,像 int[] 數組這樣的內容,是在堆上分配的。數組並不是基本數據類型。

JVM面試2大難題,標準答案給你做出來了!

這就是 JVM 的基本的內存分配策略。而堆是所有線程共享的,如果是多個線程訪問,會涉及數據同步問題。這同樣是個大話題,我們在這裡先留下一個懸念。

好了, 本次分享就講到這了,下次我將從覆蓋 JDK 的類開始講解類的加載機制,一定要持續關注【拉勾教育】哦~

查看後續內容:拉勾教育專欄《深入淺出Java虛擬機》見文末了解更多

版權聲明:本文版權歸屬拉勾教育及該專欄作者,任何媒體、網站或個人未經本網協議授權不得轉載、鏈接、轉貼或以其他方式複製發佈/發表,違者必究。


分享到:


相關文章: