Java內存模型

面試中問到「內存模型」,一般是考察Java內存結構和GC而不是Happens-Before等更深刻、細緻的內容。內存模型是考察coder對一門語言的理解能力,從而進一步延伸到對JVM優化,和平時學習的深度上,是Java面試中最重要的一部分。這裏整理了內存結構和GC的知識點,Happens-Before模型預計在之後學習過JVM過再來整理。html

若是把內存模型看作一個數據結構,那麼面試中考察的重點分爲內存結構和GC,不過有時候會單獨問到GC,另外大問題分解爲小問題也方便理解。c++

內存結構

內存結構簡介

JVM的內存結構大概分爲:git

  1. 堆(heap):線程共享,全部的對象實例以及數組都要在堆上分配。回收器主要管理的對象。
  2. 方法區(MEATHOD AREA):線程共享,存儲類信息、常量、靜態變量、即時編譯器編譯後的代碼。
  3. 方法棧(JVM Stack):線程私有、存儲局部變量表、操做棧、動態連接、方法出口,對象指針。
  4. 本地方法棧(NATIVE METHOD STACK):線程私有。爲虛擬機使用到的Native 方法服務。如Java使用c或者c++編寫的接口服務時,代碼在此區運行。
  5. PC寄存器(PC Register):線程私有。指向下一條要執行的指令。

image.png
image.png

各區域詳細說明

在Java的內存結構中,咱們重點關注的是堆和方法區。github

image.png
image.png

堆的做用是存放對象實例和數組。從結構上來分,能夠分爲新生代和老生代。而新生代又能夠分爲Eden 空間、From Survivor 空間(s0)、To Survivor 空間(s1)。 全部新生成的對象首先都是放在年輕代的。須要注意,Survivor的兩個區是對稱的,沒前後關係,因此同一個區中可能同時存在從Eden複製過來 對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。並且,Survivor區總有一個是空的。面試

控制參數

-Xms設置堆的最小空間大小。-Xmx設置堆的最大空間大小。-XX:NewSize設置新生代最小空間大小。-XX:MaxNewSize設置新生代最小空間大小。 objective-c

垃圾回收

此區域是垃圾回收的主要操做區域。算法

異常狀況

若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError 異常。 數組

方法區

方法區(Method Area)與Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作Non-Heap(非堆),目的應該是與Java 堆區分開來。bash

不少人願意把方法區稱爲「永久代」(Permanent Generation),本質上二者並不等價,僅僅是由於HotSpot虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對於其餘虛擬機(如BEA JRockit、IBM J9 等)來講是不存在永久代的概念的。在Java8中永生代完全消失了。 數據結構

控制參數

-XX:PermSize 設置最小空間 -XX:MaxPermSize 設置最大空間。

垃圾回收

對此區域會涉及可是不多進行垃圾回收。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載,通常來講這個區域的回收「成績」比較難以使人滿意。

異常狀況

根據Java 虛擬機規範的規定, 當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError。

方法棧

每一個線程會有一個私有的棧。每一個線程中方法的調用又會在本棧中建立一個棧幀。在方法棧中會存放編譯期可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不等同於對象自己。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。

控制參數

-Xss控制每一個線程棧的大小。

異常狀況

在Java 虛擬機規範中,對這個區域規定了兩種異常情況:

  1. StackOverflowError: 異常線程請求的棧深度大於虛擬機所容許的深度時拋出;
  2. OutOfMemoryError 異常: 虛擬機棧能夠動態擴展,當擴展時沒法申請到足夠的內存時會拋出。

本地方法棧

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,其
區別不過是虛擬機棧爲虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則
是爲虛擬機使用到的Native 方法服務。

控制參數

在Sun JDK中本地方法棧和方法棧是同一個,所以也能夠用-Xss控制每一個線程的大小。

異常狀況

與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError 和OutOfMemoryError
異常。

PC計數器

它的做用能夠看作是當前線程所執行的字節碼的行號指示器。

異常狀況

此內存區域是惟一一個在Java 虛擬機規範中沒有規定任何OutOfMemoryError 狀況的區域。

參考:
Java虛擬機詳解02----JVM內存結構
JVM內存結構

GC

簡言之,Java程序內存主要(這裏強調主要二字)分兩部分,堆和非堆。你們通常new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。

複習堆內存模型

既然重點是堆內存,咱們就再看看堆的內存模型。

堆內存由垃圾回收器的自動內存管理系統回收。
堆內存分爲兩大部分:新生代和老年代。比例爲1:2。
老年代主要存放應用程序中生命週期長的存活對象。
新生代又分爲三個部分:一個Eden區和兩個Survivor區,比例爲8:1:1。
Eden區存放新生的對象。
Survivor存放每次垃圾回收後存活的對象。

image.png
image.png

關注這幾個問題:

  1. 爲何要分新生代和老年代?
  2. 新生代爲何分一個Eden區和兩個Survivor區?
  3. 一個Eden區和兩個Survivor區的比例爲何是8:1:1?

這幾個問題都是垃圾回收機制所採用的算法決定的。因此問題轉化爲,是何種算法?爲何要採用此種算法?

斷定可回收對象

在進行垃圾回收以前,咱們須要清除一個問題——什麼樣的對象是垃圾(無用對象),須要被回收?

目前最多見的有兩種算法用來斷定一個對象是否爲垃圾。

引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。

image.png
image.png

優勢是簡單,高效,如今的objective-c用的就是這種算法。

缺點是很難處理循環引用,好比圖中相互引用的兩個對象則沒法釋放。

這個缺點很致命,有人可能會問,那objective-c不是用的好好的嗎?我我的並無以爲objective-c好好的處理了這個循環引用問題,它實際上是把這個問題拋給了開發者。

可達性分析算法(根搜索算法)

爲了解決上面的循環引用問題,Java採用了一種新的算法:可達性分析算法。

從GC Roots(每種具體實現對GC Roots有不一樣的定義)做爲起點,向下搜索它們引用的對象,能夠生成一棵引用樹,樹的節點視爲可達對象,反之視爲不可達。

image.png
image.png

OK,即便循環引用了,只要沒有被GC Roots引用了依然會被回收,完美!

可是,這個GC Roots的定義就要考究了,Java語言定義了以下GC Roots對象:

  1. 虛擬機棧(幀棧中的本地變量表)中引用的對象。
  2. 方法區中靜態屬性引用的對象。
  3. 方法區中常量引用的對象。
  4. 本地方法棧中JNI引用的對象。

Stop The World

有了上面的垃圾對象的斷定,咱們還要考慮一個問題,請你們作好內心準備,那就是Stop The World。

由於垃圾回收的時候,須要整個的引用狀態保持不變,不然斷定是斷定垃圾,等我稍後回收的時候它又被引用了,這就全亂套了。因此,GC的時候,其餘全部的程序執行處於暫停狀態,卡住了。

幸運的是,這個卡頓是很是短(尤爲是新生代),對程序的影響微乎其微 (關於其餘GC好比並發GC之類的,在此不討論)。

因此GC的卡頓問題由此而來,也是情有可原,暫時無可避免。

垃圾回收

已經知道哪些是垃圾對象了,怎麼回收呢?

目前主流有如下幾種算法,目前JVM採用的是分代回收算法,而分代回收算法正是從這幾種算法發展而來。

標記清除算法 (Mark-Sweep)

標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出全部須要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。

image.png
image.png

優勢:簡單實現。

缺點:容易產生內存碎片(碎片太多可能會致使後續過程當中須要爲大對象分配空間時沒法找到足夠的空間而提早觸發新的一次垃圾收集動做)。

複製算法 (Copying)

複製算法將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。

image.png
image.png

優勢:實現簡單,運行高效且不容易產生內存碎片。

缺點:對內存空間的使用作出了高昂的代價,由於可以使用的內存縮減到原來的一半。

從算法原理咱們能夠看出,複製算法算法的效率跟存活對象的數目多少有很大的關係,若是存活對象不少,那麼複製算法算法的效率將會大大下降

標記整理算法 (Mark-Compact)

該算法標記階段和Mark-Sweep同樣,可是在完成標記以後,它不是直接清理可回收對象,而是將存活對象都向一端移動,而後清理掉端邊界之外的內存。

image.png
image.png

優勢:實現簡單,不容易產生內存碎片,內存使用高效。

缺點:效率很是低。

因此,特別適用於存活對象多,回收對象少的狀況下

分代回收算法

以上幾種算法都有各自的優勢和缺點,適用於不一樣的內存情景。而分代回收算法根據Java的語言特性,將複製算法和標記整理算法的的特色相結合,針對不一樣的內存情景使用不一樣的回收算法。

這裏重複一下兩種老算法的適用場景:

複製算法:適用於存活對象不多。回收對象多
標記整理算法: 適用用於存活對象多,回收對象少

兩種算法恰好互補,不一樣類型的對象生命週期決定了更適合採用哪一種算法。

因而,咱們根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。通常狀況下將堆區劃分爲老年代(Old Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集時只有少許對象須要被回收,使用標記整理算法,而新生代的特色是每次垃圾回收時都有大量的對象須要被回收,複製算法,那麼就能夠根據不一樣代的特色採起最適合的收集算法。

如今回頭去看堆內存爲何要劃分新生代和老年代,是否是以爲如此的清晰和天然了?

具體來看:

  1. 對於新生代,雖然採起的是複製算法,可是,實際中並非按照上面算法中說的1:1的比例來劃分新生代的空間,而是將新生代劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,比例爲8:1:1。爲何?下一節深刻分析。
  2. 老年代的特色是每次回收都只回收少許對象,這符合一個穩定系統的主要特徵——超過一半的對象會長期駐留在內存中。因此老年代的比例要大於新生代,默認的新生代:老年代的比例爲1:2。

這就是分代回收算法。

深刻理解分代回收算法

對於這個算法,我相信不少人仍是有疑問的,咱們來各個擊破,說清楚了就很簡單。

爲何不是一塊Survivor空間而是兩塊?

這裏涉及到一個新生代和老年代的存活週期的問題,好比一個對象在新生代經歷15次(僅供參考)GC,就能夠移到老年代了。問題來了,當咱們第一次GC的時候,咱們能夠把Eden區的存活對象放到Survivor A空間,可是第二次GC的時候,Survivor A空間的存活對象也須要再次用Copying算法,放到Survivor B空間上,而把剛剛的Survivor A空間和Eden空間清除。第三次GC時,又把Survivor B空間的存活對象複製到Survivor A空間,如此反覆。

因此,這裏就須要兩塊Survivor空間來回倒騰。

爲何Eden空間這麼大而Survivor空間要分的少一點?

新建立的對象都是放在Eden空間,這是很頻繁的,尤爲是大量的局部變量產生的臨時對象,這些對象絕大部分都應該立刻被回收,能存活下來被轉移到survivor空間的每每很少。因此,設置較大的Eden空間和較小的Survivor空間是合理的,大大提升了內存的使用率,緩解了Copying算法的缺點。

我看8:1:1就挺好的,固然這個比例是能夠調整的,包括上面的新生代和老年代的1:2的比例也是能夠調整的。

新的問題又來了,從Eden空間往Survivor空間轉移的時候Survivor空間不夠了怎麼辦?直接放到老年代去。

Eden空間和兩塊Survivor空間的工做流程

這裏原本簡單的Copying算法被劃分爲三部分後不少朋友一時理解不了,也確實很差描述,下面我來演示一下Eden空間和兩塊Survivor空間的工做流程。

如今假定有新生代Eden,Survivor A, Survivor B三塊空間和老生代Old一塊空間。

// 分配了一個又一個對象
放到Eden區
// 很差,Eden區滿了,只能GC(新生代GC:Minor GC)了
把Eden區的存活對象copy到Survivor A區,而後清空Eden區(原本Survivor B區也須要清空的,不過原本就是空的)
// 又分配了一個又一個對象
放到Eden區
// 很差,Eden區又滿了,只能GC(新生代GC:Minor GC)了
把Eden區和Survivor A區的存活對象copy到Survivor B區,而後清空Eden區和Survivor A區
// 又分配了一個又一個對象
放到Eden區
// 很差,Eden區又滿了,只能GC(新生代GC:Minor GC)了
把Eden區和Survivor B區的存活對象copy到Survivor A區,而後清空Eden區和Survivor B區
// ...
// 有的對象來回在Survivor A區或者B區呆了好比15次,就被分配到老年代Old區
// 有的對象太大,超過了Eden區,直接被分配在Old區
// 有的存活對象,放不下Survivor區,也被分配到Old區
// ...
// 在某次Minor GC的過程當中忽然發現:
// 很差,老年代Old區也滿了,這是一次大GC(老年代GC:Major GC)
Old區慢慢的整理一番,空間又夠了
// 繼續Minor GC
// ...
// ...複製代碼

觸發GC的類型

瞭解這些是爲了解決實際問題,Java虛擬機會把每次觸發GC的信息打印出來來幫助咱們分析問題,因此掌握觸發GC的類型是分析日誌的基礎。

  • GC_FOR_MALLOC: 表示是在堆上分配對象時內存不足觸發的GC。
  • GC_CONCURRENT: 當咱們應用程序的堆內存達到必定量,或者能夠理解爲快要滿的時候,系統會自動觸發GC操做來釋放內存。
  • GC_EXPLICIT: 表示是應用程序調用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號時觸發的GC。
  • GC_BEFORE_OOM: 表示是在準備拋OOM異常以前進行的最後努力而觸發的GC。

參考:


本文連接:Java內存模型
做者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。

相關文章
相關標籤/搜索