本篇文章咱們將嘗試回答如下問題:html
ok,開始。怎們仍是先講原理,再說答案。若是時間不足,也能夠直接跳到最後看答案。java
本次分享咱們主要圍繞jvm內存結構展開,這也是java面試必考知識點之一。因此咱們先來看看jvm內存結構究竟是啥樣子。面試
咱們首先看下面這張圖:
shell
這張圖是虛擬機的結構圖,當咱們在討論jvm內存模型時,指的就是中間五彩的那條區域:運行時數據區(runtime data area)。segmentfault
咱們把這個區域單獨畫出來,以下圖所示:併發
如今咱們來看看每一個區域的用途jvm
每當一個線程去執行方法時,就會同時在棧裏面建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等。工具
每個方法從被調用到執行完成的過程,都對應着一個棧幀從入棧到出棧的過程。post
棧是線程私有的,每一個線程在棧中保有本身的數據,別的線程沒法訪問。開發工具
在棧中咱們可能遇到兩種異常:StackOverflowError和OutOfMemoryError
StackOverflowError是指線程請求的棧深度大於虛擬機所容許的深度
OutOfMemoryError是指棧擴展時沒法申請到足夠的內存
這兩種異常咱們會在後面的文章中詳細講到。
本地方法棧和棧差很少,區別只在於本地方法棧爲Native方法服務。
Native 方法就是一個Java調用非Java代碼的接口,好比JNI。
本地方法棧也是線程私有的。
要理解程序計數器,咱們須要知道java代碼最終都要編譯成一條條的字節碼,而後由字節碼解釋器一條條的執行的。而程序計數器能夠看做是當前線程所執行的字節碼的行號計數器。
若是正在執行的是一個java方法,那麼這個計數器記錄的是正在執行的字節碼指令地址。若是正在執行的是Native方法,那麼計數器的值爲Undefined。
程序計數器也是線程私有的,每條線程都有一個獨立的程序計數器,各線程的程序計數器互不影響。
程序計數器是惟一一個不會OOM的內存區域。
堆應該是java內存中佔用空間最大的一個區域,你們喜聞樂見的垃圾回收就主要發生在這個區域。
堆的惟一做用就是存放對象,不過並不是全部對象都在堆中。這個咱們之後會講到。
堆若是空間不足,就會拋出OOM異常。
堆是可讓多個線程共享的。
方法區也是可讓多個線程共享的。
方法區主要用來存放類的版本,字段,方法,接口、常量池和運行時常量池。
常量池裏存儲着字面量和符號引用。
還記得咱們在jvm類加載面試題詳解裏說到的「加載」過程嗎?其中說到「類加載器把類讀入內存」。這裏的所說的「內存」,指的就是方法區。
和方法區相關的知識點有永久代、元空間、常量池等。這幾個概念很是容易把人繞暈,因此接下來我會盡可能畫圖說明,給你們講清楚。
做爲和堆同樣可讓線程共享的區域,堆以外的的空間被叫作非堆(Non-Heap)。能夠粗略地理解爲非堆裏包含了永久代,而永久代裏又包括了方法區。
咱們經常把永久代和方法區等同起來,然而永久代實際上是Hotspot虛擬機把分代GC的範圍擴展到方法區的產物。(分代GC咱們會在後面講到)。以下圖:
因此,永久代和方法區雖然基本上指的是同一片內存區域,可是實質上是有差異的。
而到了jdk1.8中,永久代被取消,取而代之的即是元空間。
元空間跟永久代最大的區別就在於,元空間直接使用機器內存,不在jvm虛擬機內。因此理論上你的機器內存有多大,元空間就能有多大。
此外,以前永久代的符號引用(Symbols)轉移到了堆外內存(native heap);字面量(interned strings)和類的靜態變量(class statics)轉移到了堆內(heap)。
這樣作的好處在於能夠減小OOM,同時方便HotSpot和JRockit合併。
上一個小節咱們提到了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兩款虛擬機,這也是爲何要把它們合併的緣由之一。
上面咱們提到了常量池、運行時常量池和字符串常量池,如今咱們來看看這幾個池子究竟是個什麼關係。
如下爲jdk1.7的狀況。
首先仍是來張圖:
數據是怎麼在這幾個池子裏流轉的呢?
首先,當類被加載的時候,class文件就會被加載到方法區,裏面有塊區域就被稱爲常量池。常量池主要存儲兩個東西:字面量(Literal)和符號引用(Symbolic References)。
下面這個圖清楚的展現了常量池的內容:
當程序運行到該類的時候,常量池的大部份內容都會進入運行時常量池。可是String不一樣,String對象會存在堆裏,而後在字符串常量池保存一個引用。
當主線程開始實例化字符串的時候,先到字符串常量池找有沒有相等的字符串,有的話就直接把引用賦值給變量。
不過,須要注意的是,若是以建立對象的方式建立字符串,好比new String("abc"),那麼,會在內存中開闢一塊全新的內存地址,建立一個新對象,而後把引用賦值給變量,就沒有字符串常量池的事了。
看到這兒你可能要問了,what?直接內存是什麼鬼?
直接內存並非虛擬機運行時數據區的一部分,可是這部分區域卻能夠被虛擬機使用,且使用不當也會OOM,因此乾脆在這講一下。
在jdk1.4中加入的NIO類引入了一種基於通道和緩衝區的IO方式,能夠直接分配堆外內存,並操做。理論上來講,直接內存因爲不在虛擬機內,因此不受虛擬機內存大小控制(是否是有點像元空間?)。可是若是這塊空間太大,可能會擠佔虛擬機的內存,畢竟物理內存有限,從而使虛擬機在動態擴展的時候因爲空間不足而拋出OOM。
jvm內存結構到此結束,下面咱們來講說java內存模型。
看下面這張圖:
java8中,元空間(METASPACE)取代了永久代(PREM GEN),而且移到了堆外內存(Native Memory)中。
看到這你是否是在想:什麼鬼?難道剛剛咱們說的不是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()方法後,纔會到字符串常量池建立引用對象。固然有時候這個過程會自動完成,但狀況比較複雜,難以肯定。
有不少面試官其實本身也搞不清,因此不妨先告訴他建立了兩個對象,而後再分析一番,效果更好。
永久代(PermGen)和元空間的區別(Metaspace)
java用這樣的方式生成字符串:String str = "Hello",到底有沒有在堆中建立對象?
JVM內存溢出詳解
系列文章總目錄:https://mp.weixin.qq.com/s/56JgXLArTAEDj1f3y4arLA