Java記憶體結構
Java堆(Java Heap)
java堆是java虛擬機器所管理的記憶體中最大的一塊,是被所有執行緒共享的一塊記憶體區域。
在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,這一點在Java虛擬機器規範中的描述是:所有的物件例項以及陣列都要在堆上分配。
java堆是垃圾收集器管理的主要區域,因此也被成為“GC堆”(Garbage Collected Heap)。從記憶體回收角度來看java堆可分為:新生代和老生代(當然還有更細緻的劃分,在下一章會講到)。
從記憶體分配的角度看,執行緒共享的Java堆中可能劃分出多個執行緒私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。無論怎麼劃分,都與存放內容無關,無論哪個區域,儲存的都是物件例項,進一步的劃分都是為了更好的回收記憶體,或者更快的分配記憶體。
根據Java虛擬機器規範的規定,java堆可以處於物理上不連續的記憶體空間中。當前主流的虛擬機器都是可擴充套件的(通過 -Xmx 和 -Xms 控制)。如果堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將會丟擲OutOfMemoryError異常。
Java虛擬機器棧(Java Virtual Machine Stacks)
java虛擬機器也是執行緒私有的,它的生命週期和執行緒相同。
虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀(Stack Frame)用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。
我們們常說的堆記憶體、棧記憶體中,棧記憶體指的就是虛擬機器棧。區域性變數表存放了編譯期可知的各種基本資料型別(8個基本資料型別)、物件引用(地址指標)、returnAddress型別。
區域性變數表所需的記憶體空間在編譯期間完成分配。在執行期間不會改變區域性變數表的大小。
這個區域規定了兩種異常狀態:如果執行緒請求的棧深度大於虛擬機器所允許的深度,則丟擲StackOverflowError異常;如果虛擬機器棧可以動態擴充套件,在擴充套件是無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。
本地方法棧(Native Method Stack)
本地方法棧與虛擬機器棧所發揮作用非常相似,它們之間的區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的native方法服務。本地方法棧也是丟擲兩個異常。
方法區(Method Area)
方法區與java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。它有個別命叫Non-Heap(非堆)。當方法區無法滿足記憶體分配需求時,丟擲OutOfMemoryError異常。
直接記憶體(Direct Memory)
直接記憶體不是虛擬機器執行時資料區的一部分,也不是java虛擬機器規範中定義的記憶體區域。但這部分割槽域也唄頻繁使用,而且也可能導致OutOfMemoryError異常
在JDK1.4中新加入的NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。
執行時常量池(Runtime Constant Pool)
執行時常量池是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在載入後進入方法區的執行時常量池中存放。
程式計數器(Program Counter Register)
程式計數器是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器。
由於Java虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,一個處理器都只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都有一個獨立的程式計數器,各個執行緒之間計數器互不影響,獨立儲存。稱之為“執行緒私有”的記憶體。程式計數器記憶體區域是虛擬機器中唯一沒有規定OutOfMemoryError情況的區域。
執行引擎
虛擬機器核心的元件就是執行引擎,它負責執行虛擬機器的位元組碼,一般戶先進行編譯成機器碼後執行。
垃圾收集系統
垃圾收集系統是Java的核心,也是不可少的,Java有一套自己進行垃圾清理的機制,開發人員無需手工清理
新生代與老年代
絕大多數情況下,物件首先分配在eden區,在新生代回收後,如果物件還存活,則進入s0或s1區,之後每經過一次
新生代回收,如果物件存活則它的年齡就加1,物件達到一定的年齡後,則進入老年代。
如何判斷物件是否存活
引用計數法
什麼是引用計數演算法:給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效時,計數器值減1.任何時刻計數器值為0的物件就是不可能再被使用的。那為什麼主流的Java虛擬機器裡面都沒有選用這種演算法呢?其中最主要的原因是它很難解決物件之間相互迴圈引用的問題。
根搜尋演算法
根搜尋演算法的基本思路就是通過一系列名為”GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。
那麼問題又來了,如何選取GCRoots物件呢?在Java語言中,可以作為GCRoots的物件包括下面幾種:
(1). 虛擬機器棧(棧幀中的區域性變數區,也叫做區域性變數表)中引用的物件。
(2). 方法區中的類靜態屬性引用的物件。
(3). 方法區中常量引用的物件。
(4). 本地方法棧中JNI(Native方法)引用的物件。
下面給出一個GCRoots的例子,如下圖,為GCRoots的引用鏈。
垃圾回收機制策略
複製演算法
概念
如果jvm使用了coping演算法,一開始就會將可用記憶體分為兩塊,from域和to域, 每次只是使用from域,to域則空閒著。當from域記憶體不夠了,開始執行GC操作,這個時候,會把from域存活的物件拷貝到to域,然後直接把from域進行記憶體清理。
應用場景
coping演算法一般是使用在新生代中,因為新生代中的物件一般都是朝生夕死的,存活物件的數量並不多,這樣使用coping演算法進行拷貝時效率比較高。jvm將Heap 記憶體劃分為新生代與老年代,又將新生代劃分為Eden(伊甸園) 與2塊Survivor Space(倖存者區) ,然後在Eden –>Survivor Space 以及From Survivor Space 與To Survivor Space 之間實行Copying 演算法。 不過jvm在應用coping演算法時,並不是把記憶體按照1:1來劃分的,這樣太浪費記憶體空間了。一般的jvm都是8:1。也即是說,Eden區:From區:To區域的比例是
始終有90%的空間是可以用來建立物件的,而剩下的10%用來存放回收後存活的物件。
1、當Eden區滿的時候,會觸發第一次young gc,把還活著的物件拷貝到Survivor From區;當Eden區再次觸發young gc的時候,會掃描Eden區和From區域,對兩個區域進行垃圾回收,經過這次回收後還存活的物件,則直接複製到To區域,並將Eden和From區域清空。
2、當後續Eden又發生young gc的時候,會對Eden和To區域進行垃圾回收,存活的物件複製到From區域,並將Eden和To區域清空。
3、可見部分物件會在From和To區域中複製來複制去,如此交換15次(由JVM引數MaxTenuringThreshold決定,這個引數預設是15),最終如果還是存活,就存入到老年代
注意: 萬一存活物件數量比較多,那麼To域的記憶體可能不夠存放,這個時候會藉助老年代的空間。
優缺點
優點:在存活物件不多的情況下,效能高,能解決記憶體碎片和java垃圾回收演算法之-標記清除 中導致的引用更新問題。
缺點: 會造成一部分的記憶體浪費。不過可以根據實際情況,將記憶體塊大小比例適當調整;如果存活物件的數量比較大,coping的效能會變得很差。
標記清除演算法
概念
該演算法有兩個階段。
1. 標記階段:找到所有可訪問的物件,做個標記
2. 清除階段:遍歷堆,把未被標記的物件回收
應用場景
該演算法一般應用於老年代,因為老年代的物件生命週期比較長。
優缺點
標記清除演算法的優點和缺點
1. 優點
- 是可以解決迴圈引用的問題
- 必要時才回收(記憶體不足時)
2. 缺點:
- 回收時,應用需要掛起,也就是stop the world。
- 標記和清除的效率不高,尤其是要掃描的物件比較多的時候
- 會造成記憶體碎片(會導致明明有記憶體空間,但是由於不連續,申請稍微大一些的物件無法做到),
標記壓縮演算法
標記清除演算法和標記壓縮演算法非常相同,但是標記壓縮演算法在標記清除演算法之上解決記憶體碎片化
概念
壓縮演算法簡單介紹
任意順序 : 即不考慮原先物件的排列順序,也不考慮物件之間的引用關係,隨意移動物件;
線性順序 : 考慮物件的引用關係,例如a物件引用了b物件,則儘可能將a和b移動到一塊;
滑動順序 : 按照物件原來在堆中的順序滑動到堆的一端。
優缺點
優點:解決記憶體碎片問題,缺點壓縮階段,由於移動了可用物件,需要去更新引用。
Minor GC和Full GC區別
概念:
新生代 GC(Minor GC):指發生在新生代的垃圾收集動作,因為 Java 物件大多都具
備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。
老年代 GC(Major GC / Full GC):指發生在老年代的 GC,出現了 Major GC,經常
會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略裡
就有直接進行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會比 Minor GC 慢 10
倍以上。
JVM的永久代中會發生垃圾回收麼?
垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細檢視垃圾收集器的輸出資訊,就會發現永久代也是被回收的。這就是為什麼正確的永久代大小對避免Full GC是非常重要的原因。請參考下Java8:從永久代到後設資料區
(注:Java8中已經移除了永久代,新加了一個叫做後設資料區的native記憶體區)