JVM-內存分配與垃圾回收

內存分配

整個內存: 堆內存(年輕代大小 + 年老代大小)+ 非堆(持久代)。wKioL1h90sjQ5Cp_AABMZWOxHes891.png

1、堆參數:

-Xms:初始內存,默認是物理內存的1/64。

-Xmx:最大內存,默認是物理內存的1/4。默認空餘堆內存小於40%時,JVM就會增大堆直到-Xmx的最大限制;空餘堆內存大於70%時,JVM會減少堆直到 -Xms的最小限制。因此服務器一般設置-Xms、-Xmx相等以避免在每次GC 後調整堆的大小。 

-Xmn:指定年輕代: 包括Eden和兩個Survivor區。

-XX:NewRatio:年輕代(-Xmn)與年老代的比值(除去持久代),默認值4表示年輕代與年老代所佔比值爲1:4,年輕代佔整個堆棧的1/5,Xms=Xmx並且設置了Xmn的情況下,不需要設置-XX:NewRatio。

2、非堆就是JVM留給自己用的,所以方法區(虛擬機規範未規定此區域的具體數據結構,由虛擬機廠商自行實現)、JVM內部處理或優化所需的內存(如JIT編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法 的代碼都在非堆內存中,是各個線程共享的內存區域。

非堆(持久代)內存分配 

-XX:PermSize:初始值,默認是物理內存的1/64;

-XX:MaxPermSize:最大值,默認是物理內存的1/4。

回收算法:

1、標記清除:會產生大量不連續的內存碎片,分配較大對象時,無法找到足夠的連續內存,觸發一次垃圾收集。

2、複製:將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲了原來的一半。

新生代回收採用複製算法,將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor[1]。當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間,所以Survivor區總有一個是空的。如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,需要依賴其他內存(這裏指老年代)進行分配擔保。

涉及到的參數:-XX:SurvivorRatio默認爲8,則兩個Survivor區與一個Eden區的比值爲2:8,一個Survivor區佔整個年輕代的1/10。

3、標記-整理算法:老年代的特點,標記過程與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。

4、分代:在新生代中,選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。

對象所需內存的大小在類加載完成後便可完全確定,爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。

垃圾收集器:

對回收算法的實現,暫略過。

內存分配方式:指針碰撞和空閒列表

1、指針碰撞:如果Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離,這種分配方式稱爲“指針碰撞”(Bump the Pointer)。

2、空閒列表:如果Java堆中的內存並不是規整的,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分對象實例,並更新列表上的記錄,這種分配方式稱爲“空閒列表”(Free List)。

選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過程的收集器時,系統採用的分配算法是指針碰撞,而使用CMS這種基於Mark-Sweep算法的收集器時,通常採用空閒列表。

內存分配的併發控制:CAS和TLAB

對象創建在虛擬機中是非常頻繁的行爲,即使是僅僅修改一個指針所指向的位置,在併發情況下也並不是線程安全的,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。

主要通過兩種方式解決:1.CAS加上失敗重試分配內存地址。2. TLAB 爲每個線程分配一塊緩衝區域進行對象分配,new對象內存的分配均需要進行加鎖,這也是new開銷比較大的原因,所以Sun Hotspot JVM爲了提升對象內存分配的效率,對於所創建的線程都會分配一塊獨立的空間,這塊空間又稱爲TLAB,TLAB僅作用於新生代的Eden,因此在編寫Java程序時,通常多個小的對象比大的對象分配起來更加高效。使用-XX:+/-UseTLAB

內存分配完成後操作:

虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行。接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。根據虛擬機當前的運行狀態的不同,如是否啓用偏向鎖等,對象頭會有不同的設置方式。在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但從Java程序的視角來看,對象創建纔剛剛開始,<init>方法還沒有執行,所有的字段都還爲零。執行new指令之後會接着執行<init>方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完全產生出來。

對象頭:

1、官方稱它爲“Mark Word”,包括兩部分信息,第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。並不是所有的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息並不一定要經過對象本身。

2、這部分數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit。 

3、如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,因爲虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數據中卻無法確定數組的大小。

4、對象需要存儲的運行時數據很多,其實已經超出了32位、64位Bitmap結構所能記錄的限度,但是對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲儘量多的信息,它會根據對象的狀態複用自己的存儲空間。例如,在32位的HotSpot虛擬機中,如果對象處於未被鎖定的狀態下,那麼Mark Word的32bit空間中的25bit用於存儲對象哈希碼,4bit用於存儲對象分代年齡,2bit用於存儲鎖標誌位,1bit固定爲0,而在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)複用其它空間。

對象實例數據:

1、實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。

2、無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。

3、這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛擬機默認的分配策略爲longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的字段總是被分配到一起。在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果CompactFields參數值爲true(默認爲true),那麼子類之中較窄的變量也可能會插入到父類變量的空隙之中。

4、由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

Hotspot發起內存回收過程:

根節點GC Roots:

1、棧幀中的本地變量表中引用的對象。

2、方法區中類static屬性、final常量引用的對象。

3、本地方法棧中JNI(即一般說的Native方法)引用的對象。

安全點:

1、安全點的選定基本上是以程序長時間執行,最明顯特徵就是指令序列複用,例如方法調用、循環跳轉、異常跳轉等,所以具有這些功能的指令纔會產生Safepoint。

2、在安全點生成OopMap的數據結構:HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用,這樣,GC在掃描時就可以直接得知這些信息了。

3、GC發生時讓所有線程(這裏不包括執行JNI調用的線程)跑到最近的安全點,枚舉根節點,已經提前在安全點生成OopMap的數據結構(可以快速且準確地完成GCRoots枚舉),可達性分析,必須在一個能確保一致性的快照中進行,不可以出現分析過程中對象引用關係還在不斷變化的情況。

安全區域:

指在一段代碼之中,引用關係不會發生變化,在這個區域中的任意地方開始GC都是安全的。

在線程執行到Safe Region中的代碼時,首先標識自己已經進入了Safe Region,那樣,當在這段時間裏JVM要發起GC時,就不用管標識自己爲Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的信號爲止。

GC中斷:

1、搶先式中斷:不需要線程的執行代碼主動去配合,在GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。

2、主動式中斷:當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起。輪詢標誌的地方和安全點是重合的。


經過JIT編譯後被拆散爲標量類型並間接地棧上分配,啓動了本地線程分配緩衝,將按線程優先在TLAB上分配, 少數情況下也可能會直接分配在老年代中。

垃圾回收:

1、弱引用:當垃圾收集器工作時,被弱引用關聯的對象總是被無條件回收。

2、虛引用:無法通過虛引用來取得一個對象實例,爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

3、finalize:

執行finalize()方法,對象將會放置在一個叫做F-Queue的隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的情況),將很可能會導致F-Queue隊列中其他對象永久處於等待,甚至導致整個內存回收系統崩潰。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行

方法區回收:永久代)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類

無用的類包括:

1、該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。

2、加載該類的ClassLoader已經被回收。

3、該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

4、大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

新生代回收:

1、動態對象年齡判定:虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

2、在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許冒險,那這時也要改爲進行一次Full GC。如果出現了HandlePromotionFailure失敗,那就只好在失敗後重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁

3、JDK 6 Update 24之後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。不再關心HandlePromotionFailure的設置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章