Java查漏系列(2)——java內存區域

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


其中,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程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如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異常。

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章