Java記憶體區域與記憶體溢位
Java虛擬機中的記憶體分配圖:
各區域的特性總結如下表:
情線程要在堆上分配內存,那麼可能出現內存分配的同步問題,解決方案有兩個,一個就是同步內存分配動作;另一個就是採用TLAB,即在Java堆中針對每個線程先預先分配一小區塊執行緒私有的本地執行緒分配緩衝。這樣當執行緒需要分配記憶體時就在自己的TLAB上進行,從而避免同步的開銷。但是當TLAB分配滿重新分配TLAB時仍需要同步;
判斷一個類別是否屬於無用的類,「可以」回收的條件為:1 此類所有的實例都已經被回收;2 載入該類別的ClassLoader已經被回收; 3 該類別對於的java.lang.Class物件沒有在任何地方被引用。注意,只是可以,而不是要回收。
記憶體分配方式:虛擬機器使用哪一種方式由記憶體是否規整決定,而記憶體是否規整則由回收演算法決定。
指標碰撞:假設所有已經被分配的記憶體放在一邊,而空閒的放在另外一邊,二者中間則用一個指標來作為分界點,當需要分配記憶體時只需要移動該指標即可,這樣的方式稱為指針碰撞。使用此方法的有Serial、ParNew等帶有Compact的回收器
空閒列表:虛擬機器中已分配的內存和空閒的內存並不規整,處於相互交錯的情形,那麼就需要通過維護一個列表來表示哪些記憶體是可用,當需要分配記憶體時則需要透過查詢清單來尋找可用大小的記憶體。這樣的方式稱為空閒列表。使用此方法的有CMS等這種基於Mark-Sweep演算法的回收器
物件在HotSpot虛擬機中的記憶體佈局如下表所示:
在Java規格中,對於reference類型只規定了一個指向物件的引用,但沒有規定透過何種方式去存取引用的資料。因此對於不同的虛擬機器也有不同的存取方式,主要有兩種方式:
使用句柄:在Java堆中劃出一塊區域作為句柄池來儲存句柄,一個句柄包括物件實例資料的指標以及物件資料類型的指針,reference中儲存的就是物件的句柄的位址。 reference透過句柄來間接存取物件。其好處在於:當物件移動時,只需要改變矩形中的指標即可,而對應的reference則不需要做變動;
直接存取:reference儲存的是物件位址,透過reference就可以直接存取資料。 Java對中的資料物件中含有到物件資料類型的指針,例如上面提到的類型指針。此種方式的好處就是存取速度快,相比使用句柄的方式而言少了一次指標定位的開銷。 HotSpotVM使用的是這種方式。
兩種使用方式的圖示說明如下圖:圖片來源 http://www.th7.cn/Program/java/201604/846729.shtml
垃圾回收演算法不可能再被用的演算法有兩種:
引用計數演算法:對於一個對象,其被引用的次數增加一次,則計數加1,當引用失效的時候就減1.當其計數為0時,則認為該對象死去。此演算法的特點是效率高,但是很難解決物件之間的相互引用問題。使用此演算法的有MS的COM技術以及Python等
可達性分析演算法:此演算法的核心就是從GC Roots開始,偵測所引用的所有物件。若一個物件到GC Roots之間沒有任何引用鏈,那麼認為該物件已死。使用此演算法的有Java、C#等。
可以作為GC Roots的物件有:
虛擬機棧(堆疊幀中的本地變數表)中引用的物件
方法區中類別靜態屬性所引用的物件
方法區中常數所引用的物件
Native方法中引用的物件
在Java中有四種引用強度:
強引用:強引用永遠不會被垃圾回收器回收
軟引用:系統提供SoftReference類別來表示,表示還有但是非必須的對象。其回收時機在於把若引用物件回收完後記憶體還不夠,則回收此類引用,若還不夠,則OOM
弱引用:透過WeakReference類別來表示,此類引用只能生存到下一次垃圾回收之前。當垃圾收集器工作時,無論當前記憶體是否足夠都會回收只被弱引用關聯的物件。即只要發生了GC,弱引用必定被回收。其與已經沒有到GC Roots的引用鏈的區別在於:還是可以透過弱引用來存取這些物件的,但是沒有引用鏈的物件則永遠不可能再被存取到了。
虛引用:透過PhantomReference來實現,其對物件的生存時間沒有任何影響,其存在的意義在於能在被虛引用引用的物件被回收的時候收到一個系統通知。
系統的GC工作流程如下圖所示,總的來說,一個物件被回收可能會經過兩次標記過程,並且可能在finalize方法中拯救自己以避免被回收。
幾種典型的垃圾回收演算法:
HotSpot VM中的垃圾回收演算法的具體實作細節:為了結果的準確,GC在掃描時是需要凍結所有執行緒的。目前主流的Java虛擬採取的都是準確式GC,即係統知道每個記憶體位置的資料到底是一個什麼資料型別,以HotSpot為例,它採取的是一個叫做OopMap的資料結構來實現這樣的映射記錄。有了這樣的訊息,虛擬機直接知道哪些地方存放著物件的引用,從而避免了對記憶體挨個的檢查,加快了GC掃描的速度。程式的每條指令都可能導致引用關係或記憶體資料的變化,即會導致OopMap的變化,這樣的話,如果給每個指令都產生一個對應的OopMap資料那麼是相當佔用空間的,於是提出了安全點的概念(SafePoint),即只有當每個執行緒都運行到線程對應的安全點時才進行GC掃描,從而也只要給安全點上的指令生成OopMap即可,這樣就減少了OopMap的數量。而安全點的選取要考慮到GC頻率與系統效能的綜合影響,一般選取方法呼叫、循環跳轉、異常跳轉等「具有讓程式長時間運作的特性」的點。為了讓線程都跑到安全點停頓下來以進行GC掃描,有搶先式中斷和主動式中斷兩種方式。這裡又有另一個問題,如果遇到一個例如處於Sleep狀態的線程,那麼它是不會走動的,如果它恰好不是在一個安全點Sleep,那麼意味著它永遠不會走到安全點來,所以又提出了安全區域(SafeRegion)的概念。即在這個區域內的點都是安全點。線程進入安全點之後會標誌自己進入了安全區域,必須等GC執行完了才會離開安全區域。
各個垃圾回收器:
幾條最普遍的內存分配規則:
對象優先分配在Eden區:當Eden區內存不夠的時候,系統將發起一次速度較快的Minor GC
大物件直接進入老年代:對於較長的數組以及字串等大對象,直接把他們分配在老年代區。因此,對於那種生命週期較短的大對象,很容引起GC,應該盡量避免。
長期存活的對象將進入老年代:一個若在Survivor區經歷多次Minor GC還存活,默認15次,則將被移入老年代區。
動態物件年齡判定:此條是結合上一條的,要是Survivor中相同年齡的所有物件的大小和超過Survivor的一半,那麼年齡大於或等於該年齡的物件將移入老年代區,無需等到15次
空間分配擔保:針對新生代的複製收集演算法。若參數容許,當執行Minor GC之後,若存活的物件無法全部放在Survivor中,那麼將把多的物件直接放入老年代區。若老年代也沒足夠的空間,那麼將發生Full GC以獲得更多空間