1. 程式人生 > >JVM之自動記憶體管理機制

JVM之自動記憶體管理機制

Java虛擬機器執行時資料區域:由所有執行緒共享的資料區有:方法區和堆,執行緒隔離的資料區有:虛擬機器棧、本地方法棧、程式計數器。

程式計數器:一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。為了執行緒切換後能恢復到正確的執行位置,每條執行緒需要有一個獨立的程式計數器,各個執行緒之前計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體,它是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

Java虛擬機器棧:也是執行緒私有的,它的生命週期與執行緒相同。描述的是Java方法執行的記憶體模型:每個方法在執行的時候都會建立一個幀棧,幀棧用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每個方法從呼叫直至執行完成的過程,就是對應著一個幀棧在虛擬機器棧中入棧出棧的過程。

區域性變量表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(reference型別)、returnAddress型別(指向了一條位元組碼指令的地址)。如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError,如果虛擬機器棧可以棟帶擴充套件,並且誒擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。

本地方法棧:與虛擬機器棧的作用相似,它們之間的區別不過是虛擬機器棧為虛擬機器執行Java方法服務,而本地方法棧則為虛擬機器使用到的Native方法服務。

Java堆:對大多數應用來說,Java堆是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立,此記憶體區域的唯一目的就是存放物件例項。Java堆是垃圾收集器管理主要區域,因此很多時候被稱作“GC堆”。

方法區:各個執行緒共享的記憶體區域,它用於儲存虛擬機器載入的類資訊、常量、靜態變數、既是編譯器編譯後的程式碼等資料。在HotSpot中,很多人稱之為“永久代”。除了和Java堆一樣不需要連續的記憶體和可以選擇固定大小或者課拓展外,還可以選擇不實現垃圾收集,如果收集,這部分割槽域的回收目標主要針對常量池的回收和對型別的解除安裝。

執行時常量池是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容在類載入後進入方法區的執行時常量池中存放。

直接記憶體:並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,在JDK1.4加入NIO後,它可以使用Native函式庫直接分配對外記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer物件作為這塊記憶體的應用進行操作。這樣能在一些場景顯著提高效能,因為避免了在Java堆和Native堆中來回複製資料。

HotSpot物件

物件的建立:

1.虛擬機器遇到一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否被載入、解析和初始化過。

2.在類載入檢查通過後,接下來虛擬機器將為新生物件分配記憶體。為物件分配空間的任務相當於把一塊確定大小的的記憶體從Java堆中劃分出來。分配方式由兩種:一是“指標碰撞”,當Java堆記憶體是絕對規整的,所有用過的記憶體都放到一邊,空閒的記憶體放到一邊,中間放一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閒空間那邊挪動一段與物件相等大小的距離。二是“空閒佇列”,當Java'堆不規整時,已使用的記憶體和空閒的記憶體相互交錯,虛擬機器必須維護一個列表,記錄上哪些記憶體塊是可用的。Java堆是否規整是由垃圾收集器是否帶有壓縮整理功能決定的。因此,在使用Serial、ParNew等帶有Compact過程的收集器,系統採用的是指標碰撞,當使用CMS這種基於Mark-Sweep演算法的收集器,通常使用空閒列表。物件建立不是執行緒安全的,解決方法由兩種:一是對分配記憶體空間的動作進行同步處理,實際上虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性,另一種是把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,成為本地執行緒緩衝區(TLAB)。哪個執行緒要分配記憶體,就在哪個執行緒上的TLAB上分配,只有TLAB用完並分配新的TLAB時,才需要同步鎖定.

3.記憶體分配完成後,虛擬機器需要將分配到的記憶體空間初始化為0;

4.虛擬機器要對物件進行必要的設定。

5.執行new指令後會接著執行<init>方法,把物件按照程式設計師的意思進行初始化。

物件的記憶體佈局:

分為三塊區域,物件頭、例項資料、對齊填充。

物件頭:第一部分用於儲存物件自身的執行時資料,例如雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。第二部分是型別指標,即物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

示例資料:物件陣陣儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容。

對齊填充:不是必須存在的,僅僅起著佔位符的作用。

物件的訪問定位:

Java程式需要通過棧上的reference資料來操作堆上的具體物件。目前訪問方式有使用控制代碼和直接指標:使用控制代碼的話,Java堆中將會劃分一塊記憶體出來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料和型別資料各自的具體地址資訊。使用直接指標訪問的話,reference中儲存的直接就是物件地址,使用控制代碼的好處就是reference中儲存的是穩定的控制代碼地址,在物件移動時只會改變控制代碼中的例項資料指標,而reference本身不需要改動,直接指標訪問方式的最大好處就是速度更快。

垃圾收集器與記憶體分配策略

如何判斷物件已死亡?

方法一:引用計數法:給物件新增一個引用計數器,每當由一個地方引用它時,計數器值就加1,當引用失效時,計數器減1,當計數器為0時,就是物件死亡的時候。

侷限性:很難解決物件之間互相迴圈引用的問題。

方法二:可達性分析演算法:通過一系列的稱為“GC Roots”的物件作為起始點,從這些店開始向下搜尋,搜尋所走過的路徑成為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。

可作為GC Roots的物件包括:1.虛擬機器棧中引用的物件。2.方法區中類靜態屬性引用的物件。3.方法區中常量引用的物件。4.本地方法棧中JNI引用的物件。

無論是哪個方法,都跟引用有關。JDK1.2之後,Java對引用分為:

強引用:就是指程式程式碼中普遍存在的,類似“Object obj = new Object()”這類的引用。只要強引用還在,垃圾收集器就永遠不會回收掉被引用的物件。

軟引用:用來描述一些還有用但並非必須的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍內進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。,用SoftReference來實現軟引用。

弱引用:用來描述非必須物件,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。利用WeakReference類來實現弱引用。

虛引用:一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項,為一個物件設定虛引用的目的是能在這個物件被收集器回收時收到一個系統通知。用PhantomReference來實現。

即使可達性分析不可達的物件,也並非是“非死不可”,這時候他們處於“緩刑”,要真正宣告一個物件死亡,需要進行兩次標記過程:1.如果物件在進行可達性分析後發現沒有與GC Roots想連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。虛擬機器將當物件沒有覆蓋finalize方法或者fianlize方法已經被呼叫過都視為“沒有必要執行”。如果由必要執行finalize方法,那麼這個物件將會被放置在一個F-Queue的佇列之中,並在稍後由一個虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行。2.然後GC堆F-Queue進行第二次標記,如果物件要在finalize方法中拯救自己-只要重新與引用鏈的任何一個物件建立關聯即可,這樣在第二次標記時就會被移除“即將回收”的集合。如果沒有逃脫,就會被回收。

方法區的回收:

主要兩部分:廢棄常量和無用的類。

廢棄常量和回收Java堆中的物件很類似。沒有任何物件引用這個常量,也沒有其他地方引用這個常量,則當發生垃圾回收的時候就會被清理出常量池。

判斷是否為無用的類的三個條件:1.該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項。2.載入該類的ClassLoader已經被回收。3.該類物件的java.lang.Class物件沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。

垃圾回收演算法

1.標記-清除演算法:

由標記和清除階段構成,標記階段是把所有活動的物件都做上標記的階段。

清除階段就是把那些沒有標記的物件回收的階段。標記階段在遍歷物件的時候使用深度優先搜尋和廣度優先搜尋。在清除階段,從堆首地址開始,按順序一個個遍歷物件的標誌位,設定了標誌位的,也就是活動的物件,就取消它的標誌位,然後進行下一次GC,我們必須把非活動物件回收再利用,回收物件就是把物件作為分塊,連線到被稱為“空閒連結串列”的單向連結串列。當mutator申請分塊時,搜尋空閒連結串列並尋找大小合適的分塊,這就叫做分配。分配策略有三種:First-fit:最初發現大於等於size的分塊就會立即返回該分塊。Best-fit:返回大於等於size的最小分塊。Worst-fit:找出空閒連結串列中最大的分塊,將其分割成mutator申請的大小和分割後剩餘的大小。根據分配策略的不同我們會產生不同的小分塊,但是他們如果是連續的,我們就可以把這些小分塊連在一起形成大分塊,這種操作就是合併。

缺點:時間問題:標記和清除效率都不高。空間問題:會產生大量不連續記憶體碎片。與寫時複製技術不相容。

標記-清除演算法優化:

1.多個空閒連結串列:利用分快大小不同的空閒連結串列,即建立只連線大分塊的空閒連結串列和只連線小分塊的空閒連結串列,這樣按照申請的分塊大小選擇空閒連結串列就能在短時間內找到合適的分快了。

2.BiBOP方法:將大小相近的物件整理成固定大小的快進行管理的做法。把堆分割成固定大小的塊,讓每個塊只能配置同樣大小的物件。比如分成用於3個字的塊和分成用於2個字的塊,這樣就不會出現大小分配不均的分塊,不過有時候會利用率不高,比如用於2個字的塊中只有1-2個活動物件,那就沒有有效的利用堆了。

3.點陣圖標記法:單純的Mark-Sweep演算法中用於標記的位是被分配到各個物件的頭中的,而這個方法是隻收集各個物件的標誌位並且表格化,不跟物件一起管理。在標記的時候,在這個表格中的特定場所置位,集合了用於標記的位的表格成為位圖表格,利用這個表格進行標記稱為“點陣圖標記法”。

2.複製演算法:

將可用記憶體按容量劃分為大小相等的兩塊,酶促只使用其中的一塊。當這一塊用完時,就將還存活著的物件複製到另外一塊上面,然後再把已經使用過的記憶體一次性清理掉,這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時就不用考慮記憶體碎片等複雜問題,只要移動堆頂指標,按順序分配記憶體即可。再進行復制時,會類似於深度優先遍歷,從根出發進行遍歷活動的物件,並且複製到另一個記憶體區域,當遍歷完成後,清空原先的記憶體區域。

優點:1、優秀的吞吐量,因為它只需要搜尋並複製活動物件。2、可實現高速分配,因為複製演算法的分塊是一個連續的記憶體空間,只要分塊大小不小於申請的大小,就只要移動$free指標就可以進行分配了。3.不會發生碎片化,活動物件集中被安排在From空間的開頭,像這樣把物件重新集中,放在堆的一端的行為就叫做壓縮。4、與快取相容。

缺點:1.堆使用效率低下,畢竟是把記憶體空間分成了一半來使用。2.不相容保守式GC演算法。3.遞迴呼叫函式:複製某個物件時都要遞迴複製它的子物件,因此每次複製的時候都要呼叫函式,由此帶來額外負擔。

Cheney的GC複製演算法:用來消除第三個缺點。Cheney的GC複製演算法不是遞迴的,是迭代進行復制,採用的是類似於廣度優先遍歷的方法來進行遍歷活動物件並且複製。廣度優先遍歷搜尋需要佇列,即把搜尋物件保持在佇列中,一邊取出一邊搜尋,而scan和$free之間的堆就好比是這個佇列,$free每次向右移動,佇列就會追加物件,scan每次向右移動,就會由物件被取出和搜尋。

多空間複製演算法:GC複製演算法的最大缺點就是隻能利用半個堆,那我們把堆分成10份,其中無論如何都要空出一塊空間來當To空間,那我們就把這個額外的負擔降到1/10就行了。也就是把對其中的兩塊空間執行GC複製演算法,對剩下的(N-2)塊空間執行Mark-Sweep演算法。

3.標記-壓縮演算法

Lisp2演算法:

這裡的標記演算法跟Mark-Sweep演算法的標記演算法一致,壓縮階段讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。壓縮階段會有三個步驟:1.設定forwarding指標。程式首先搜尋整個堆,給活動物件設定forwarding指標2.更新指標。3.移動物件將活動物件移動到forwarding指標的引用目標處。

優點:堆利用率高。

缺點:需要對堆進行三次搜尋,吞吐量遠遠不夠好。

Two-Finger演算法:

需要可兩次搜尋堆,在Lisp2演算法中,通過執行壓縮操作使活動物件往左邊滑動,而在Two-Finger演算法中,則是通過執行壓縮操作來讓活動物件填補空閒空間,此時為了讓物件能恰好填補空閒空間,必須讓所有物件大小一致。

優點:不需要額外的記憶體空間以用於forwarding指標,記憶體使用效率高。壓縮只要搜尋兩次,吞吐量高。

表格演算法:

兩個步驟:1.移動物件以及構築間隙表格。2.更新指標。

優點:不需要多於空間,在壓縮前後保留物件的順序,所以可以通過快取來提高物件的訪問速度。

缺點:維持間隙表格需要付出很高的代價。

ImmixGC演算法:

是將GC標記-清除演算法和壓縮組合在一起。

4.分代垃圾回收演算法:

物件的年齡:經過一次GC後活下來的物件年齡為1歲。

新生代物件:剛生成的物件。

老年代物件:到達一定年齡的物件。

新生代GC:堆新生代物件執行的GC。新生代GC的前提是大部分新生代物件都沒有存活下來,GC在短時間內結束。

老年代GC:面向老年代物件的GC。

晉升:新生代物件上升為老年代物件。

Ungar的分代垃圾回收:

堆的結構總共劃分成四個空間,分別為1個生成空間、2個大小相等的倖存空間以及一個老年代空間。分別用$new_start、$survivor1_start、$survivor2_start、$old_start這四個變數引用它們的開頭。我們將生成空間和倖存空間合稱為新生代空間,新生代物件會分配到新生代空間,老年代物件會分配到老年代空間。

還有一個和堆不一樣的陣列,稱為記錄集,設為$rs.

生成空間就是生成物件的空間,也就是進行分配的空間。當生成空間滿了的時候,新生代GC就會啟動,將生成空間中的所有活動物件複製,就跟GC複製演算法一樣,目標是倖存空間。

2個倖存空間和GC複製演算法中的From空間和To空間很像,在每次執行新生代GC時候,活動物件就會被複制到另一個倖存空間,在此我們將正在使用的倖存空間作為From倖存空間,將沒有使用的倖存空間作為To倖存空間裡,也就是說生成空間和From倖存空間裡面的活動物件都會複製到To倖存空間裡去,這就是新生代GC。

注意一點,從老年代到新生代空間也會有引用,因此除了一般GC裡的根,我們還需要把從老年代空間的引用當作根來處理,但是如果考慮到老年代物件的引用,那麼又要搜尋堆中的所有物件,這樣就削減了分代垃圾回收的優勢,所以我們加入了記錄集。記錄集記錄了從老年代物件到新生代物件的引用。

記錄集中記錄的是發出引用的物件,而不是記錄引用的物件,這樣做是為了避免當新生代晉升時無法更新引用。

為了將老年代物件記錄到記錄集中,我們用了寫入屏障。

物件的結構:物件的頭部除了包含物件的種類和大小之外,還有以下這三條資訊:1.物件的年齡,表示的是物件從新生代GC中存活下來的次數。2.已經複製完畢的標誌:用來防止重複複製相同物件的標誌。3.已經向記錄集記錄完畢的標誌:用來防止向記錄集中重複記錄的標誌,這個標誌只用於老年代物件。1跟2只用於新生代物件。

老年代空間佔滿後,就執行老年代GC,老年代GC就用到了標記-清除演算法。

優點:吞吐量得到改善

缺點:新生代GC花費的時間增多、老年代GC頻繁執行。

記錄各代之間的引用方法:

使用記錄集會使對應每個發出引用的物件花費了一個字,這樣記憶體空間的使用效率就不高了,再加上如果各代之間的引用很多,還會出現記錄集溢位問題。所以提供了兩個方法替代記錄集:

1.卡片標記:首先把老年代空間分割成大小相等的卡片,為每個卡片準備一個與之對應的標誌位,將標誌位進行標記表格管理,參照點陣圖標記,當因為改寫指標而產生從老年代物件到新生代物件的引用時要實現對被改寫的域所屬的域所屬的卡片設定標誌位,這項操作可以通過寫入屏障實現。即使物件是跨兩張卡片也沒有影響。GC時會尋找位圖表格,當找到設定了標誌位的卡片時就會從卡片頭開始尋找指向新生代的引用。

優點:有效的利用記憶體空間。

缺點:如果在標記表格裡設定了很多位,那麼可能就會在搜尋卡片上花費大量時間。

2.頁面標記法:許多的OS是以頁面為單位來管理記憶體空間的,因此如果在卡片標記中將卡牌和頁面設定成同樣大小,就能得到OS的幫助了。一旦mutator對堆內的某一個頁面進行寫入操作時,OS會設定跟這個頁面對應的位,這叫做頁面重寫標誌位,卡片標記中是搜尋標記表格,而頁面標記是搜尋這個頁面重寫標誌位。然而不是所有OS都具備這種結構,我們為不能利用頁面重寫標誌位的OS準備了一種叫記憶體保護功能。具體就是說mutator執行過程中保護老年代空間不被寫入。

多代垃圾回收:分為多代,除了最老的一代以外,每代都有一個記錄集,X代的記錄集只記錄來自比X老的其他代的引用,分代數量越多,物件變成垃圾的機會就會越大,但是也不能過度增加分代數量,因為分代數量增加,沒代的空間就相應減少了,這樣一來各代的引用就變多了,各代中垃圾回收的時間也越來越長。

列車垃圾回收:列車垃圾回收中將老年代空間按照一定大小進行劃分,每個劃分出來的空間稱為車廂,由一個以上的車廂連線成的東西叫做列車,一次老年代GC是以一個車廂作為GC物件的。當新生代GC將新生代物件晉升時,我們要準備已有的車廂或者有空的車廂,將物件安排進分塊裡,各車廂裡的分塊是單獨作為一個連續的記憶體空間存在的。執行老年代GC時,開頭列車的開頭車廂是GC的物件。

垃圾回收以車廂為單位,比如說由三個列車ABC,A列車裡面有A1,A2,A3車廂,B列車由B1,B2車廂,C列車由C1,C2,C3車廂,按照A1-A2-A3-B1-B2-C1-C2-C3的順序掃描,當掃到A3結束後,如果A1,A2,A3都沒有被B列車以及C列車的車廂中的物件引用,而只是A列車內部互相引用,則整列A車都清除。如果A列車中由物件被其他的列車的物件引用,比如說A1被B1引用,則把A1中引用的物件移到B1車廂內,並且掃描A1是否對其他由引用,比如A2被A1引用,則把A2中的物件也加入到B1,若B1滿了,則要在B列車後面新加一輛車廂,來存放被轉移過來的物件。掃描完一個列車後,就清理。

優點:Ungar的分代垃圾回收為代表的方法都是因為老年代GC而增加了mutator的最大暫停時間。而在列車垃圾回收中,一次老年代GC只將堆中非常小的一部分當成GC物件,因此縮減了最大暫停時間。另外這個方法還可以回收迴圈的大型垃圾,因為會把相互引用的物件都安排到同一個列車裡去,然後整個回收列車,就可以回收大型垃圾。

缺點:寫入屏障所產生的額外負擔大,因此吞吐量會稍微差點。

5.增量式垃圾回收:

增量式垃圾回收是將GC和mutator一點點交替執行的手法,為了防止停止型GC的傷害。

三種演算法:

1.三色標記演算法:GC物件分成三種情況:白色代表還未搜尋過的物件。灰色代表正在搜尋的物件。黑色代表已經搜尋完成的物件。

GC開始執行前所有物件都是白色的,GC一開始執行,所有從根能達到的物件都會被標記,然後被堆到棧裡。GC只是發現了這樣的物件,沒有搜尋完它們,所以這些物件是灰色物件。灰色物件會依次從棧中被取出,其子物件也會變為灰色,當所有子物件都變為灰色時,它就變成黑色。活動物件全部變為黑色,垃圾變為白色。

優點:縮短了最大暫停時間,降低了吞吐量。

2.steele的演算法

3.湯淺的演算法

這三種的寫入屏障、分配等大有不同。

6.RC IMMix演算法

此演算法改善了引用計數法的“合併型引用計數法”和Immix演算法結合了起來。

首先介紹合併型引用計數法:把注意力放在了某一時期最初和最後的狀態,在這個期間不進行計數器的增減。即使指標發生改動,計數器也不會增減,指標改動時的資訊被註冊到更改緩衝區,但是在這段時間,計數器會保持一個錯誤的值。等到緩衝區慢了,我們就執行GC,來查詢更改緩衝區,並正確的設定計數器的值。這樣做就可以減少計數器增減次數,處理變得更加簡單。優點是增加了吞吐量,缺點是增加了mutator的暫停時間,因為在查詢更改緩衝區的過程需要讓mutator暫停。

接下來就是兩個演算法融合:不僅物件有計數器,線也有計數器,這樣可以獲悉線內是否存在活動物件。不過線的計數器表示線裡存在活動物件的數量。如果這個數變為0,則將整個線回收。當物件的計數器為0時,對線的計數器進行減量操作,當線的計數器為0時,回收整個線。

RC IMMix演算法通過限定物件來實現壓縮,這個物件就是“新物件”。沒有經歷過GC的物件就是新物件。所有指向新物件的指標都是上一次GC之後生成的,也就是說,所有引用新物件的物件都被註冊到更改緩衝區,因此,可以通過查詢更改緩衝區來只對新物件進行復制操作。這種以新物件為物件進行壓縮,稱為被動的碎片整理。但是這個方法不能堆舊物件進行壓縮,也無法回收有迴圈引用的垃圾。所以又出現了積極的碎片整理:首先決定複製到哪個塊,然後把能夠通過指標從根查詢到的物件全部複製過去,這裡用到的就是標記-壓縮演算法。

優點:吞吐量改善。

缺點:增加暫停時間,當線中還有一個活動物件,線就必須存在,就會白白消耗大部分線。