天天看點

Java查漏系列(2)——java記憶體區域

前一節大緻的介紹了一下JVM的體系結構,如下圖:

Java查漏系列(2)——java記憶體區域

其中,Runtime DataArea(運作時資料區)是整個JVM的重點,平時,由于我們編寫java程式很少關心記憶體的釋放問題,這個都是JVM來自動管理的,不過,也正是因為Java程式員把記憶體控制的權力交給了JVM,一旦出現洩漏和溢出,如果不了解JVM是怎樣使用記憶體的,那排查錯誤将會是一件非常困難的事情。這裡就大緻的介紹一下JVM的這一區域。

JVM中,所有的資料和程式都存放在運作時資料區,如上圖,這個區域又包括幾個子區域,它們各自有各自的用途和生命周期, MethodArea和Heap是基于JVM執行個體的,即JVM的每個執行個體都有一個它自己的方法域和一個堆;PC Register和Stack是基于線程的,即每個線程建立的時候,都會擁有自己的程式計數器和棧;Native Method Stack是為虛拟機用到的Native方法服務。下面分别介紹這幾個區域:

1.Heap(堆)

一個JVM執行個體隻存在一個堆記憶體,對于絕大多數應用來說,Java堆是虛拟機管理最大的一塊記憶體。Java堆是被所有線程共享的,在虛拟機啟動時建立。類加載器讀取了類檔案後,需要把類、方法、常變量放到堆記憶體中,以友善執行器執行,堆記憶體分為三部分:

a)Permanent Space(永久存儲區)

永久存儲區是一個常駐記憶體區域,用于存放JDK自身所攜帶的Class,Interface的中繼資料,也就是說它存儲的是運作環境必須的類資訊,一般被裝載進此區域的資料是不會被垃圾回收器回收掉的,關閉JVM才會釋放此區域所占用的記憶體。

b)Young Generation Space(新生區)

新生區是類的誕生、成長、消亡的區域,一個類在這裡産生,應用,最後被垃圾回收器收集,結束生命。新生區又分為兩部分:伊甸區(Eden space)和幸存者區(Survivor pace),所有的類都是在伊甸區被new出來的。幸存區有兩個:0區(Survivor 0space)和1區(Survivor 1space)。當伊甸園的空間用完時,程式又需要建立對象,JVM的垃圾回收器将對伊甸園區進行垃圾回收,将伊甸園區中的不再被其他對象所引用的對象進行銷毀。然後将伊甸園中的剩餘對象移動到幸存0區。若幸存0區也滿了,再對該區進行垃圾回收,然後移動到1區。那如果1區也滿了呢?再移動到養老區。

c)Tenure Generation Space(養老區)

養老區用于儲存從新生區篩選出來的JAVA對象,一般池對象都在這個區域活躍。

三個區的示意圖如下:

Java查漏系列(2)——java記憶體區域

之是以将堆記憶體再進行分區,主要是基于這樣一個事實:不同對象的生命周期是不一樣的。在Java程式運作的過程中,會産生大量的對象,其中有些對象是與業務資訊相關,比如Http請求中的Session對象、線程、Socket連接配接,這類對象跟業務直接挂鈎,是以生命周期比較長。但是還有一些對象,主要是程式運作過程中生成的臨時變量,這些對象生命周期會比較短,比如:String對象,由于其不變類的特性,系統會産生大量的這些對象,有些對象甚至隻用一次即可回收。試想,在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因為每次回收都需要周遊所有存活對象,但實際上,對于生命周期長的對象而言,這種周遊是沒有效果的,因為可能進行了很多次周遊,但是他們依舊存在。是以,對堆進行分區管理是采用了分治的思想,把不同生命周期的對象放在不同區域,不同區域采用最适合它的垃圾回收方式進行回收。分區之後可以提高JVM垃圾收集的效率,進而優化記憶體管理。

無論對Java堆如何劃分,目的都是為了更好的回收記憶體,或者更快的配置設定記憶體。如果在堆中無法配置設定記憶體,并且堆也無法再擴充時,将會抛出OutOfMemoryError異常。

2.Method Area(方法域)

方法域實際上就是堆中的永久存儲區(Permanent Space),它還有個别名叫做Non-Heap(非堆),是以也可以将方法域看作堆的一個邏輯部分。方法域中存放了每個Class的結構資訊,包括常量池、字段描述、方法描述等等。這個區域除了和Java堆一樣不需要連續的記憶體,也可以選擇固定大小或者可擴充外,甚至可以選擇不實作垃圾收集。相對來說,垃圾收集行為在這個區域是相對比較少發生的,但并不是某些描述那樣永久存儲區不會發生GC(至少對目前主流的商業JVM實作來說是如此),這裡的GC主要是對常量池的回收和對類的解除安裝,雖然回收的“成績”一般也比較差強人意,尤其是類解除安裝,條件相當苛刻。對類的解除安裝需要滿足下面3個條件:

  1)該類所有的執行個體都已經被GC,也就是JVM中不存在該Class的任何執行個體;

  2)加載該類的ClassLoader已經被GC;

  3)該類對應的java.lang.Class 對象沒有在任何地方被引用,如不能在任何地方通過反射通路該類的方法。

3.Stack(棧)

棧的生命周期也是與線程相同。棧描述的是Java方法調用的記憶體模型:每個方法被執行的時候,都會同時建立一個棧幀(Frame)用于存儲本地變量表、操作棧、動态連結、方法出入口等資訊。每一個方法的調用至完成,就意味着一個棧幀在棧中的入棧至出棧的過程。棧幀是一個記憶體區塊,是一個資料集,是一個有關方法(Method)和運作期資料的資料集,當一個方法A被調用時就産生了一個棧幀F1,并被壓入到棧中,A方法又調用了B方法,于是産生棧幀F2也被壓入棧,執行完畢後,先彈出F2棧幀,再彈出F1棧幀,遵循“後進先出”原則。棧幀中主要儲存3類資料:本地變量(LocalVariables),包括輸入參數和輸出參數以及方法内的變量;棧操作(Operand Stack),記錄出棧、入棧的操作;棧幀資料(Frame Data),包括類檔案、方法等等。

棧中有兩種異常狀況:如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError異常;如果棧可以動态擴充,當擴充時無法申請到足夠記憶體則抛出OutOfMemoryError異常。

4.PC Register(程式計數器)

每一個Java線程都有一個程式計數器來用于儲存程式執行到目前方法的哪一個指令。對于非Native方法,這個區域記錄的是正在執行的VM原語的位址,該位址指向方法域中的方法位元組碼,由執行引擎讀取下一條指令。如果正在執行的是Natvie方法,這個區域則為空。

5.Native Method Stack(本地方法棧)

本地方法棧與VM棧所發揮作用是類似的,隻不過VM棧為虛拟機運作VM原語服務,而本地方法棧是為虛拟機使用到的Native方法服務。它的實作的語言、方式與結構并沒有強制規定,甚至有的虛拟機(譬如Sun Hotspot虛拟機)直接就把本地方法棧和VM棧合二為一。和VM棧一樣,這個區域也會抛出StackOverflowError和OutOfMemoryError異常。

這裡對運作時資料區的幾個邏輯組成部分做了個大緻的介紹,其中,同樣是存儲資料的棧和堆有什麼差別呢?這可能也是我們編碼時容易忽略的地方。下一節來分析一下這個。