Java記憶體模型以及gc演算法

來源:互聯網
上載者:User

標籤: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. 總結
  1. Java虛擬機器規範中規定了對記憶體的分配,其中程式計數器、本地方法棧、虛擬機器棧屬於線程私人資料區,Java堆與方法區屬於線程共用資料。
  2. Jdk從1.7開始將字串常量區由方法區(永久代)移動到了Java堆中。
  3. Java從NIO開始允許直接操縱系統的直接記憶體,在部分情境中效率很高,因為避免了在Java堆與Native堆中來回複製資料。
  4. Java堆分為年輕代有年老代,其中年輕代分為1個Eden與2個Survior,同時只有1個Eden與1個Survior處於使用中狀態,又有年輕代的對象存留時間為往往很短,因此使用複製演算法進行記憶體回收。
  5. 年老代由於對象存活期比較長,並且沒有可擔保的資料區,所以往往使用標記-清除與標記-整理演算法進行記憶體回收。
 

Java記憶體模型以及gc演算法

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.