1. 程式人生 > >不止面試02-JVM記憶體模型面試題詳解

不止面試02-JVM記憶體模型面試題詳解

第一部分:面試題

本篇文章我們將嘗試回答以下問題:

  1. 描述一下jvm的記憶體結構
  2. 描述一下jvm的記憶體模型
  3. 談一下你對常量池的理解
  4. 什麼情況下會發生棧記憶體溢位?和記憶體溢位有什麼不同?
  5. String str = new String(“abc”)建立了多少個例項?

第二部分:深入原理

ok,開始。怎們還是先講原理,再說答案。如果時間不足,也可以直接跳到最後看答案。

本次分享我們主要圍繞jvm記憶體結構展開,這也是java面試必考知識點之一。所以我們先來看看jvm記憶體結構到底是啥樣子。

1. jvm記憶體模型

我們首先看下面這張圖:

這張圖是虛擬機器的結構圖,當我們在討論jvm記憶體模型時,指的就是中間五彩的那條區域:執行時資料區(runtime data area)。

我們把這個區域單獨畫出來,如下圖所示:


現在我們來看看每個區域的用途

1.1 棧(Stack)

每當一個執行緒去執行方法時,就會同時在棧裡面建立一個棧幀(Stack Frame)用於儲存區域性變量表、運算元棧、動態連結、方法出口等。

每一個方法從被呼叫到執行完成的過程,都對應著一個棧幀從入棧到出棧的過程。

棧是執行緒私有的,每個執行緒在棧中保有自己的資料,別的執行緒無法訪問。

在棧中我們可能遇到兩種異常:StackOverflowError和OutOfMemoryError

StackOverflowError是指執行緒請求的棧深度大於虛擬機器所允許的深度

OutOfMemoryError是指棧擴充套件時無法申請到足夠的記憶體

這兩種異常我們會在後面的文章中詳細講到。

1.2 本地方法棧(Native Method Stack)

本地方法棧和棧差不多,區別只在於本地方法棧為Native方法服務。

Native 方法就是一個Java呼叫非Java程式碼的介面,比如JNI。

本地方法棧也是執行緒私有的。

1.3 程式計數器(PC Register)

要理解程式計數器,我們需要知道java程式碼最終都要編譯成一條條的位元組碼,然後由位元組碼直譯器一條條的執行的。而程式計數器可以看作是當前執行緒所執行的位元組碼的行號計數器。

如果正在執行的是一個java方法,那麼這個計數器記錄的是正在執行的位元組碼指令地址。如果正在執行的是Native方法,那麼計數器的值為Undefined。

程式計數器也是執行緒私有的,每條執行緒都有一個獨立的程式計數器,各執行緒的程式計數器互不影響。

程式計數器是唯一一個不會OOM的記憶體區域。

1.4堆(Heap)

堆應該是java記憶體中佔用空間最大的一個區域,大家喜聞樂見的垃圾回收就主要發生在這個區域。

堆的唯一作用就是存放物件,不過並非所有物件都在堆中。這個我們以後會講到。

堆如果空間不足,就會丟擲OOM異常。

堆是可以讓多個執行緒共享的。

1.5 方法區(Method Area)

方法區也是可以讓多個執行緒共享的。

方法區主要用來存放類的版本,欄位,方法,介面、常量池和執行時常量池。

常量池裡儲存著字面量和符號引用。

還記得我們在jvm類載入面試題詳解裡說到的“載入”過程嗎?其中說到“類載入器把類讀入記憶體”。這裡的所說的“記憶體”,指的就是方法區。

和方法區相關的知識點有永久代、元空間、常量池等。這幾個概念非常容易把人繞暈,所以接下來我會盡量畫圖說明,給大家講清楚。

1.5.1 永久代與元空間

作為和堆一樣可以讓執行緒共享的區域,堆之外的的空間被叫做非堆(Non-Heap)。可以粗略地理解為非堆裡包含了永久代,而永久代裡又包括了方法區。

我們常常把永久代和方法區等同起來,然而永久代其實是Hotspot虛擬機器把分代GC的範圍擴充套件到方法區的產物。(分代GC我們會在後面講到)。如下圖:


所以,永久代和方法區雖然基本上指的是同一片記憶體區域,但是實質上是有差別的。

而到了jdk1.8中,永久代被取消,取而代之的便是元空間。

元空間跟永久代最大的區別就在於,元空間直接使用機器記憶體,不在jvm虛擬機器內。所以理論上你的機器記憶體有多大,元空間就能有多大。

此外,之前永久代的符號引用(Symbols)轉移到了堆外記憶體(native heap);字面量(interned strings)和類的靜態變數(class statics)轉移到了堆內(heap)。

這樣做的好處在於可以減少OOM,同時方便HotSpot和JRockit合併。

1.5.2 虛擬機器的多實現

上一個小節我們提到了HotSpot和JRockit,這是個啥呢?

要知道,jdk的全稱是Java Development Kit ,Java開發工具包。可以簡單認為,jdk就是各種java開發工具+java虛擬機器。而HotSpot就是目前Oracle jdk預設使用的虛擬機器。

我們可以用java -version檢視目前使用的虛擬機器:

~ java -version
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)

看見最後一行了嗎?“Java HotSpot(TM) 64-Bit”說明我目前正在用的就是HotSpot。

除了HotSpot之外,還有很多其他的虛擬機器,比如JRockit就是由BEA公司開發的一款java虛擬機器,號稱世界最快。

後來 JRockit被Oracle收購,於是Oracle就同時擁有了HotSpot和JRockit兩款虛擬機器,這也是為什麼要把它們合併的原因之一。

1.5.3 各種常量池

上面我們提到了常量池、執行時常量池和字串常量池,現在我們來看看這幾個池子到底是個什麼關係。

以下為jdk1.7的情況。

首先還是來張圖:

資料是怎麼在這幾個池子裡流轉的呢?

首先,當類被載入的時候,class檔案就會被載入到方法區,裡面有塊區域就被稱為常量池。常量池主要儲存兩個東西:字面量(Literal)和符號引用(Symbolic References)。

下面這個圖清楚的展示了常量池的內容:

當程式執行到該類的時候,常量池的大部分內容都會進入執行時常量池。但是String不同,String物件會存在堆裡,然後在字串常量池儲存一個引用。

當主執行緒開始例項化字串的時候,先到字串常量池找有沒有相等的字串,有的話就直接把引用賦值給變數。
不過,需要注意的是,如果以建立物件的方式建立字串,比如new String("abc"),那麼,會在記憶體中開闢一塊全新的記憶體地址,建立一個新物件,然後把引用賦值給變數,就沒有字串常量池的事了。

1.6 直接記憶體(Direct Memory)

看到這兒你可能要問了,what?直接記憶體是什麼鬼?

直接記憶體並不是虛擬機器執行時資料區的一部分,但是這部分割槽域卻可以被虛擬機器使用,且使用不當也會OOM,所以乾脆在這講一下。

在jdk1.4中加入的NIO類引入了一種基於通道和緩衝區的IO方式,可以直接分配堆外記憶體,並操作。理論上來說,直接記憶體由於不在虛擬機器內,所以不受虛擬機器記憶體大小控制(是不是有點像元空間?)。但是如果這塊空間太大,可能會擠佔虛擬機器的記憶體,畢竟實體記憶體有限,從而使虛擬機器在動態擴充套件的時候由於空間不足而丟擲OOM。

jvm記憶體結構到此結束,下面我們來說說java記憶體模型。

1.7 java7和java8的jvm記憶體模型區別

看下面這張圖:

java8中,元空間(METASPACE)取代了永久代(PREM GEN),並且移到了堆外記憶體(Native Memory)中。

2. Java記憶體模型

看到這你是不是在想:什麼鬼?難道剛剛我們說的不是java記憶體模型嗎?

實際上,jvm記憶體模型和java記憶體模型的確是兩個比較容易混淆的概念。

java記憶體模型是指Java Memory Model,簡稱JMM。用於遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的併發效果,JMM規範了Java虛擬機器與計算機記憶體是如何協同工作的:規定了一個執行緒如何和何時可以看到由其他執行緒修改過後的共享變數的值,以及在必須時如何同步的訪問共享變數。

好吧,上面這段話著實不大好理解。我們化繁為簡,主要記住JMM規範了程式中變數的訪問規則,保證了操作的原子性、可見性、有序性。

第三部分:面試題答案

1.描述一下jvm記憶體模型

jvm記憶體模型分為5個區域,其中執行緒獨佔的區域是棧、本地方法棧、程式計數器,執行緒共享的區域是堆、方法區。

2.描述一下java記憶體模型

回答這個問題一定要問清楚是不是要問java記憶體模型,確認是的話,可以回答:java記憶體模型規定了變數的訪問規則,保證操作的原子性、可見行、有序性。

3.談一下你對常量池的理解

常量池是類在編譯後儲存常量的一塊區域,當程式執行到該類,常量池的大部分資料會進入執行時常量池,字串會進入字串常量池。

4.什麼情況下會發生棧記憶體溢位?和記憶體溢位有什麼不同?

棧記憶體溢位指程式呼叫的棧深度多大或棧空間不足時丟擲的StackOverflowError。

一般所謂記憶體溢位指的是堆記憶體溢位,丟擲的是OutOfMemoryError:java heap space。

在jdk1.7中,還可能出現永久代記憶體溢位,丟擲的是OutOfMemoryError: PermGen space

在jdk1.8中,則會出現元空間溢位,丟擲的是OutOfMemoryError:MetaSpace

5.String str = new String(“abc”)建立了多少個例項?

雖然很多部落格都告訴我們建立了兩個物件:一個字串abc物件和一個字串常量池裡指向abc的引用物件。

但實際情況要更復雜一點。

實際上在執行完String str = new String(“abc”)之後,其實只建立了一個物件:堆裡的字串物件。而str直接指向該物件。在執行intern()方法後,才會到字串常量池建立引用物件。當然有時候這個過程會自動完成,但情況比較複雜,難以確定。

有很多面試官其實自己也搞不清,所以不妨先告訴他建立了兩個物件,然後再分析一番,效果更好。

引用文獻

JVM虛擬機種類

什麼是HotSpot VM

永久代(PermGen)和元空間的區別(Metaspace)

JVM的Heap Memory和Native Memory

JAVA8裡的native heap究竟是個什麼概念?

方法區和常量池

java用這樣的方式生成字串:String str = "Hello",到底有沒有在堆中建立物件?

Java記憶體模型(JMM)總結

JVM記憶體溢位詳解
系列文章總目錄:https://mp.weixin.qq.com/s/56JgXLArTAEDj1f3y4a