不止面試02-JVM內存模型面試題詳解


第一部分:面試題

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

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

第二部分:深入原理

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

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

1. jvm內存模型

我們首先看下面這張圖:
file

這張圖是虛擬機的結構圖,當我們在討論jvm內存模型時,指的就是中間五彩的那條區域:運行時數據區(runtime data area)。

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

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

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我們會在后面講到)。如下圖:

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

而到了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的情況。

首先還是來張圖:

file

數據是怎么在這幾個池子里流轉的呢?

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

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

file

當程序運行到該類的時候,常量池的大部分內容都會進入運行時常量池。但是String不同,String對象會存在堆里,然后在字符串常量池保存一個引用。

當主線程開始實例化字符串的時候,先到字符串常量池找有沒有相等的字符串,有的話就直接把引用賦值給變量。
不過,需要注意的是,如果以創建對象的方式創建字符串,比如new String("abc"),那么,會在內存中開辟一塊全新的內存地址,創建一個新對象,然后把引用賦值給變量,就沒有字符串常量池的事了。

1.6 直接內存(Direct Memory)

看到這兒你可能要問了,what?直接內存是什么鬼?

直接內存並不是虛擬機運行時數據區的一部分,但是這部分區域卻可以被虛擬機使用,且使用不當也會OOM,所以干脆在這講一下。

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

jvm內存結構到此結束,下面我們來說說java內存模型。

1.7 java7和java8的jvm內存模型區別

看下面這張圖:

file

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

2. Java內存模型

看到這你是不是在想:什么鬼?難道剛剛我們說的不是java內存模型嗎?

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

java內存模型是指Java Memory Model,簡稱JMM。用於屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平台下都能達到一致的並發效果,JMM規范了Java虛擬機與計算機內存是如何協同工作的:規定了一個線程如何和何時可以看到由其他線程修改過后的共享變量的值,以及在必須時如何同步的訪問共享變量。

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

file

第三部分:面試題答案

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/56JgXLArTAEDj1f3y4arLA


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM