1. 程式人生 > >jvm 深入理解自動內存分配與垃圾回收

jvm 深入理解自動內存分配與垃圾回收

效率 有一個 eth 介紹 無法 不一致 是否 bool mem

要想了解jvm自動內存分配,首先必須了解jvm的運行時數據區域,否則如何知道在哪裏進行自動內存分配,如何進行內存分配,回收哪裏的垃圾對象?

jvm運行時數據區:程序計數器,虛擬機棧,本地方法棧,方法區,堆

程序計數器:由於程序指令是一條一條順序執行,一條執行完之後必須知道下一條該執行那條指令,那麽程序計數器就是來記錄下一條指令的地址,如果調用本地方法,則程序計數器記錄空值,還有由於java線程由cpu調度並發執行,所以程序計數器也有助於線程狀態的恢復,程序計數器如果線程共享,在頻繁的線程調度下保持同步影響效率,所以程序計數器是線程私有的,且程序計數器是運行時內存區域中唯一一處不會產生OutOfMemoryError錯誤

虛擬機棧:線程私有的內存區域,存放棧幀信息,每一個方法調用返回就對應著一個棧幀進棧出棧的過程,棧頂的棧幀表示當前執行方法的信息,棧幀信息主要包括:局部變量表,操作數棧,動態鏈接,返回地址等信息,局部變量表用於存放方法中出現的變量,局部變量表第一個為this,表示當前引用對象,表大小在編譯時可知,每一個method_ref有一個max_locals屬性,表示最大的局部變量表大小,操作數棧用於存放方法中操作數和直接結果,操作數棧一次最多能操作棧頂前兩位操作數,動態鏈接用於指向該棧幀對應的方法區常量池中的引用,為了支持方法調用中的動態鏈接,返回地址信息用於存儲方法執行完時方法的返回地址,即調用者的地址,方法正常結束和異常結束都會返回至調用者地址,但是方法正常結束可能會返回返回值,但異常結束不會有返回值,虛擬機棧可能出現的錯誤有OutOfMemoryError和OutOfStackError

本地方法棧:線程私有的內存區域,與虛擬機棧類似,唯一不同的是存放本地方法對應的棧幀信息

方法區:線程共享的內存區域,存放類信息,靜態變量,常量,編譯後程序代碼等信息,類加載器在類加載階段就是將字節碼文件的靜態結構轉換至方法區的運行時結構,包括運行時常量池,存放字面量及符號引用,方法區也可能會產生OutOfMemoryError錯誤

:線程共享的內存區域,存放對象實例,垃圾回收的主要工作區域,為了方便內存自動分配和垃圾回收,將堆區分為新生代和老年代,新生代又可以詳細分為Eden區,From survivor,To survivor區。堆也可能會發生OutOfMemoryError錯誤

主要的內存異常:內存泄漏,內存溢出,內存泄漏是指無法回收無用的對象,而無用的對象始終占用著內存空間,使cpu無法分配此內存空間,造成內存泄漏。內存泄漏嚴重的話最終將會導致內存溢出,內存溢出是指無法開辟足夠的內存空間來滿足對象的創建

自動內存分配機制:一個對象在實例化之前,對象的大小就已經確定,正確來說,對象的大小編譯時就已經確定,最後會詳細介紹如何確定對象大小。首先會根據內存是否規整采取不同的內存分配算法,如果內存規整,則會根據指針碰撞法來為對象劃分內存空間,如果內存不規整,即內存由不連續的內存碎片構成,則需要維護一個內存的空閑列表,再根據空閑列表為對象分配內存空間。而內存是否規整則取決於垃圾回收算法是否包含整理操作,一般的,復制算法,標記-整理算法自帶整理操作,可采用指針碰撞法,而標記-清除算法不含有整理操作,只能用空閑列表法為對象分配內存。因為在多線程的環境下,內存空間又是共享資源,不采取措施的話可能會造成多個線程搶占同一塊內存區域,造成內存分配錯亂,一般來說有兩種解決方案,一種是在線程進行內存空間分配時進行cas失敗重試措施來解決多線程競爭問題,另一種是為每個線程分配一塊線程本地分配緩沖TLAB,每個線程在進行內存空間申請的時候在自己的分配緩沖區域進行申請,多個線程不會發生錯亂。內存分配策略:對象優先在新生代Eden區分配,大對象直接進入老年代,長期存活的對象進入老年代,在survivor區中某個同一年齡的對象大於等於整個survivor區域的一半,則survivor區中大於此年齡的對象進入老年代

對象內存布局:已經為對象分配了一塊內存區域,接下來就是對此內存區域根據對象信息進行數據填充了。對象內存布局有對象頭,對象體,對齊填充,因為每個對象的內存空間要保證8字節的整數倍大小,當對象頭對象體總共空間大小不是8字節整數倍時,需要填充某些字節。至於為什麽對象大小要為8字節整數倍我也不知道?很多地方只是說hotspot虛擬機實現的規範要這樣,至於為什麽,怕是要問hotspot開發人員了,或許hotspot虛擬機是把8字節當做一個整體作為一個單位? 對象頭信息:包含兩至三部分數據,第一部分包括對象的哈希值,鎖標誌,是否采用偏向鎖,線程ID等,這部分數據成為 mark word,對象鎖一般四種狀態:無鎖,偏向鎖,輕量級鎖,重量級鎖,隨著多線程對對象的使用競爭,鎖強度逐漸增強。第二部分是對象的類型指針,指向方法區對應的Class對象,用於找到對象所屬類的元數據信息,對象類型指針在虛擬機規範中並不一定必須存在,因為對象頭可以不包含類指針信息,如果采用句柄的對象訪問方式,類指針信息可以存放在句柄中,但hotspot虛擬機中采用直接指針的對象訪問方式,所以對象頭一般包含類型指針,第三部分可有可無,如果對象是數組,第三部分表示數組長度,4字節大小。對象體信息:存放對象的實例數據,包含從父類繼承來的屬性值

對象內存分配一般過程:檢查類型是否加載,進行內存分配(如果設置TLAB則采用內存分配緩沖為對象開辟內存空間,若沒有則采用cas失敗重試機制),為對象實例屬性賦默認零值,int類型為0,boolean類型為false,引用類型為null,設置對象頭信息,完成過後在虛擬機看來對象已經創建完畢,而對於開發人員來說,創建對象才剛剛開始,因為還沒進行<init>方法,接下來執行<init>方法根據實例屬性賦值及代碼塊對對象賦值

自動垃圾回收機制:自動垃圾回收中幾個主要問題:哪些對象需要標誌為垃圾對象?何時對垃圾對象進行回收操作?采用何種算法進行垃圾回收操作?

判斷垃圾對象的算法:引用計數法,可達性分析法(根搜索算法),引用計數法比較好理解,每個對象都自帶一個計數器,每當有一個對象引用自己時,計數器加1,當對象放棄引用自己時,計數器減1,當計數器值為0時則代表此對象為垃圾對象,雖然此算法好理解,也比較容易實現,但是有一些問題,如果兩個對象沒有其他對象引用,卻雙方互相引用對象,即無用對象循環引用導致內存泄漏。大部分虛擬機采用可達性分析算法,先將一些對象作為roots,在roots引用鏈上對象表示有用,沒有在roots引用鏈上對象表示無用,可以判為垃圾對象,虛擬機規定將虛擬機棧變量引用的對象,本地方法棧變量引用的對象,方法區常量池引用的對象,方法區靜態變量引用的對象可以作為roots對象

何時進行垃圾回收:當內存空間無法滿足為新對象開辟新空間時進行垃圾回收操作,垃圾回收操作包括Minor GC,Full GC,Minor GC主要負責新生代垃圾對象的回收,Full GC主要負責整個共享內存區域的垃圾對象的回收,包括方法區,當新生代Eden區域內存空間無法滿足新對象創建時,且新對象不是大對象,不會直接進入老年代,這時會觸發Minor GC,由於Minor GC采用復制算法,Full GC采用標記-整理算法或標記-清除算法,老年代作為新生代的分配擔保,因為新生代to survivor區無法滿足存活對象,所以需要老年代分配擔保存放多余的存活對象,如果老年代沒有足夠的空間進行分配擔保,則會先觸發Full GC,用於Full GC是整個共享內存區域的垃圾回收,所以一般伴隨著一次Minor GC,不僅在分配擔保不成時會觸發Full GC,當新生代對象進入老年代而老年代沒有足夠空間時也會觸發Full GC,Full GC主要進行老年代對象的回收,其次還會進行方法區垃圾對象的回收,主要是回收廢棄常量和無用的類,判斷廢棄常量很簡單,只要這個系統沒有對象在引用此常量時,就會被判為廢棄常量,判斷無用的類比較苛刻,需要滿足以下三個條件:1.該類所有實例對象都已經被回收 2.沒有任何對象通過反射訪問該類 3.執行該類加載操作的類加載器也已經被回收

垃圾回收算法:復制算法,標記-清除算法,標記-整理算法

復制算法:將內存區域分為兩塊,一半為空,一半用於存儲對象,當進行垃圾回收時,只需要改變不是垃圾對象的地址指針為空區域,自帶整理過程,新內存地址一字排開。然後清空另一半區域,即全部回收。但存活對象已經改變地址不會被回收,如果空間為1:1的話,則也不需要分配擔保。缺點:內存空間利用率不高

標記-清除算法:垃圾回收過程分為標記和清除兩個階段,先根據可達性分析算法標記出所有垃圾對象,然後清除所有垃圾對象。缺點:標記和清除兩個過程的效率都不高,而且垃圾回收過後由於沒有整理操作,容易產生內存碎片,產生內存碎片之後,會更容易觸發垃圾回收操作,惡性循環

標記-整理操作:垃圾回收過程分為標記和整理兩個階段,先標記出所有垃圾對象,然後改變存活對象的內存指針,清空剩余垃圾對象

jvm根據新生代對象和老年代對象不同特性采用分代收集算法,由於新生代對象存活率低,大多用完就不用,判為垃圾對象,所以采用復制算法,但復制算法不是1:1,而是9:1,具體來說是Eden:From survivor:To survivor=8:1:1,因為新生代對象存活率極其低,不需要一半空間來存放存活對象,十分之一差不多就足夠,實在不夠的話還可以用老年代作為分配擔保,用於極少對象存活,所以復制算法中復制操作也少了,效率很高,由於老年代大多數都是大對象,存活率高的對象,采用復制算法將有大量復制操作,而且復制操作都將對大對象進行復制,而且沒有內存為老年代作分配擔保,若硬要采用復制算法,那不僅老年代內存空間利用率低,而且回收效率也會變低,一般老年代采用標記-整理算法或標記-清除算法,快速清除大對象

對象大小計算:對象大小根據根據jvm位數不同而不同,而且在64位虛擬機中,為了提高空間利用率,可能會采取壓縮處理,這項處理也會對對象大小造成影響。在32位虛擬機中引用類型用4字節來表示,且對象頭 mark word部分空間大小為4字節,所以對象頭總共大小為8字節。在64位虛擬機中,未采用壓縮處理的話,引用類型為8字節,且對象頭 mark word部分空間大小為8字節,所以總共大小為16字節,註意:只有在64位虛擬機情況下,才會去考慮壓縮處理,在壓縮處理情況下,主要講引用類型8字節壓縮為4字節,所以壓縮情況下對象頭為12字節,上述如果對象表示數組的話,再加4字節數組長度

對象的字段布局:按照對象屬性聲明順序且結合long/double 8字節,int/float 4字節,short/char 2字節,byte/boolean 1 字節,reference順序進行字段布局,且對下一個字段長度進行對齊(這裏跟網上其他說法不一致,測試得到),如果64位采用壓縮處理的話 reference

4字節,且由於對象頭為12字節,如果存在long/double類型屬性,則優先將一個int/float填充4字節為long/double8字節整數倍,若沒有則填充short/char/boolean/byte填充4字節為long/double類型的整數倍,若仍沒有填滿,則空字節對齊填充,註意首4字節填充過程中也要保證對下字段對齊

例子:采用壓縮的64位虛擬機 class Person{byte a;short b;long c;Person d} 則首先12字節對象頭,然後兩字節short類型填充4字節,沒有填滿,則繼續byte類型填充,仍然沒有填充滿,則空字節繼續填充至16字節,然後8字節long類型,最好4字節Person引用類型,即0-11對象頭 12-13 short類型 14 byte類型 15空字節填充 16-23 long類型 24-27 Person引用類型

jvm 深入理解自動內存分配與垃圾回收