標籤:static jdk1.4 開始 局部變數 編譯器 tab double 直接記憶體 執行
1.java記憶體模型1.JVM記憶體模型
JVM記憶體模型如,需要聲明一點,這是《Java虛擬機器規範(Java SE 7版)》規定的內容,實際地區由各JVM自己實現,所以可能略有不同。以下對各地區進行簡短說明。
1.1程式計數器
程式計數器是眾多程式設計語言都共有的一部分,作用是標示下一條需要執行的指令的位置,分支、迴圈、跳轉、異常處理、線程恢複等基礎功能都是依賴程式計數器完成的。
對於Java的多線程程式而言,不同的線程都是通過輪流獲得cpu的時間片啟動並執行,這符合電腦群組成原理的基本概念,因此不同的線程之間需要不停的獲得運行,掛起等待運行,所以各線程之間的計數器互不影響,隔離儲存區 (Isolated Storage)。這些資料區屬於線程私人的記憶體。
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處於使用中狀態,又有年輕代的對象存留時間為往往很短,因此使用複製演算法進行記憶體回收。
- 年老代由於對象存活期比較長,並且沒有可擔保的資料區,所以往往使用標記-清除與標記-整理演算法進行記憶體回收。
Java記憶體模型以及gc演算法