1.java內存模型
1.JVM內存模型
JVM內存模型如上圖,需要聲明一點,這是《Java虛擬機規范(Java SE 7版)》規定的內容,實際區域由各JVM自己實現,所以可能略有不同。以下對各區域進行簡短說明。
1.1程序計數器
程序計數器是眾多編程語言都共有的一部分,作用是標示下一條需要執行的指令的位置,分支、循環、跳轉、異常處理、線程恢復等基礎功能都是依賴程序計數器完成的。
對於Java的多線程程序而言,不同的線程都是通過輪流獲得cpu的時間片運行的,這符合計算機組成原理的基本概念,因此不同的線程之間需要不停的獲得運行,掛起等待運行,所以各線程之間的計數器互不影響,獨立存儲。這些數據區屬於線程私有的內存。
1.2 Java虛擬機棧
VM虛擬機棧也是線程私有的,生命周期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法調用直至執行完的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
有人將java內存區域划分為棧與堆兩部分,在這種粗略的划分下,棧標示的就是當前講的虛擬機棧,或者是虛擬機棧對應的局部變量表。之所以說這種划分比較粗略是角度不同,這種划分方法關心的是新申請內存的存在空間,而我們目前談論的是JVM整體的內存划分,由於角度不同,所以划分的方法不同,沒有對與錯。
局部變量表存放了編譯期可知的各種基本類型,對象引用,和returnAddress。其中64位長的long和double占用了2個局部變量空間(slot),其他類型都占用1個。這也從存儲的角度上說明了long與double本質上的非原子性。局部變量表所需的內存在編譯期間完成分配,當進入一個方法時,這個方法在棧幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表大小。
由於棧幀的進出棧,顯而易見的帶來了空間分配上的問題。如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverFlowError異常;如果虛擬機棧可以擴展,擴展時無法申請到足夠的內存,將會拋出OutOfMemoryError。顯然,這種情況大多數是由於循環調用與遞歸帶來的。
1.3 本地方法棧
本地方法棧與虛擬機棧的作用十分類似,不過本地方法是為native方法服務的。部分虛擬機(比如 Sun HotSpot虛擬機)直接將本地方法棧與虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧也會拋出StactOverFlowError與OutOfMemoryError異常。
至此,線程私有數據區域結束,下面開始線程共享數據區。
1.4 Java堆
Java堆是虛擬機所管理的內存中最大的一塊,在虛擬機啟動時創建,此塊內存的唯一目的就是存放對象實例,幾乎所有的對象實例都在對上分配內存。JVM規范中的描述是:所有的對象實例以及數據都要在堆上分配。但是隨着JIT編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配(對象只存在於某方法中,不會逃逸出去,因此方法出棧后就會銷毀,此時對象可以在棧上分配,方便銷毀),標量替換(新對象擁有的屬性可以由現有對象替換拼湊而成,就沒必要真正生成這個對象)等優化技術帶來了一些變化,目前並非所有的對象都在堆上分配了。
當java堆上沒有內存完成實例分配,並且堆大小也無法擴展是,將會拋出OutOfMemoryError異常。Java堆是垃圾收集器管理的主要區域。
1.5 方法區
方法區與java堆一樣,是線程共享的數據區,用於存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯的代碼。JVM規范將方法與堆區分開,但是HotSpot將方法區作為永久代(Permanent Generation)實現。這樣方便將GC分代手機方法擴展至方法區,HotSpot的垃圾收集器可以像管理Java堆一樣管理方法區。但是這種方向已經逐步在被HotSpot替換中,在JDK1.7的版本中,已經把原本存放在方法區的字符串常量區移出。
至此,JVM規范所聲明的內存模型已經分析完畢,下面將分析一些經常提到的與內存相關的區域。
1.6 運行時常量池
運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等信息外,還有一項信息是常量池(Constant Poll Table)用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池存放。
其中字符串常量池屬於運行時常量池的一部分,不過在HotSpot虛擬機中,JDK1.7將字符串常量池移到了java堆中,通過下面的實驗可以很容易看到。
import java.util.ArrayList; import java.util.List; public class RunTimeContantPoolOOM { public static void main(String[] args) { List list = new ArrayList(); int i = 0; while(true){ list.add(String.valueOf(i++).intern()); } } }
在jdk1.6中,字符串常量區是在Perm Space中的,所以可以將Perm Spacce設置的小一些,XX:MaxPermSize=10M可以很快拋出異常:java.lang.OutOfMemoryError:Perm Space。
在jdk1.7以上,字符串常量區已經移到了Java堆中,設置-Xms:64m -Xmx:64m,很快就可以拋出異常java.lang.OutOfMemoryError:java.heap.space。
1.7 直接內存
直接內存不是JVM運行時的數據區的一部分,也不是Java虛擬機規范中定義的內存區域。在JDK1.4中引入了NIO(New Input/Output)類,引入了一種基於通道(Chanel)與緩沖區(Buffer)的I/O方式,他可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java中的DirectByteBuffer對象作為對這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java對和Native對中來回復制數據。
2.GC算法
2.1 標記-清除算法
最基礎的垃圾收集算法是“標記-清除”(Mark Sweep)算法,正如名字一樣,算法分為2個階段:1.標記處需要回收的對象,2.回收被標記的對象。標記算法分為兩種:1.引用計數算法(Reference Counting) 2.可達性分析算法(Reachability Analysis)。由於引用技術算法無法解決循環引用的問題,所以這里使用的標記算法均為可達性分析算法。
如圖所示,當進行過標記清除算法之后,出現了大量的非連續內存。當java堆需要分配一段連續的內存給一個新對象時,發現雖然內存清理出了很多的空閑,但是仍然需要繼續清理以滿足“連續空間”的要求。所以說,這種方法比較基礎,效率也比較低下。
2.2 復制算法
為了解決效率與內存碎片問題,復制(Copying)算法出現了,它將內存划分為兩塊相等的大小,每次使用一塊,當這一塊用完了,就講還存活的對象復制到另外一塊內存區域中,然后將當前內存空間一次性清理掉。這樣的對整個半區進行回收,分配時按照順序從內存頂端依次分配,這種實現簡單,運行高效。不過這種算法將原有的內存空間減少為實際的一半,代價比較高。
從圖中可以看出,整理后的內存十分規整,但是白白浪費一般的內存成本太高。然而這其實是很重要的一個收集算法,因為現在的商業虛擬機都采用這種算法來回收新生代。IBM公司的專門研究表明,新生代中的對象98%都是“朝生夕死”的,所以不需要按照1:1的比例來划分內存。HotSpot虛擬機將Java堆划分為年輕代(Young Generation)、老年代(Tenured Generation),其中年輕代又分為一塊Eden和兩塊Survivor。
所有的新建對象都放在年輕代中,年輕代使用的GC算法就是復制算法。其中Eden與Survivor的內存大小比例為8:2,其中Eden由1大塊組成,Survivor由2小塊組成。每次使用內存為1Eden+1Survivor,即90%的內存。由於年輕代中的對象生命周期往往很短,所以當需要進行GC的時候就將當前90%中存活的對象復制到另外一塊Survivor中,原來的Eden與Survivor將被清空。但是這就有一個問題,我們無法保證每次年輕代GC后存活的對象都不高於10%。所以在當活下來的對象高於10%的時候,這部分對象將由Tenured進行擔保,即無法復制到Survivor中的對象將移動到老年代。
2.3 標記-整理算法
復制算法在極端情況下(存活對象較多)效率變得很低,並且需要有額外的空間進行分配擔保。所以在老年代中這種情況一般是不適合的。
所以就出現了標記-整理(Mark-Compact)算法。與標記清除算法一樣,首先是標記對象,然而第二步是將存貨的對象向內存一段移動,整理出一塊較大的連續內存空間。
3. 總結
- Java虛擬機規范中規定了對內存的分配,其中程序計數器、本地方法棧、虛擬機棧屬於線程私有數據區,Java堆與方法區屬於線程共享數據。
- Jdk從1.7開始將字符串常量區由方法區(永久代)移動到了Java堆中。
- Java從NIO開始允許直接操縱系統的直接內存,在部分場景中效率很高,因為避免了在Java堆與Native堆中來回復制數據。
- Java堆分為年輕代有年老代,其中年輕代分為1個Eden與2個Survior,同時只有1個Eden與1個Survior處於使用中狀態,又有年輕代的對象生存時間為往往很短,因此使用復制算法進行垃圾回收。
- 年老代由於對象存活期比較長,並且沒有可擔保的數據區,所以往往使用標記-清除與標記-整理算法進行垃圾回收。