Java垃圾回收(GC)、找垃圾的方式、GC Root、GC停顿、引用、垃圾收集算法、收集器、GC日志、安全点、安全区域
?
1.垃圾回收
1.1概念
在Java語言中,垃圾回收(Garbage Collection,GC)是一個非常重要的概念。
它的主要作用是回收程序中不再被使用的內存,Java提供的GC功能可以自動監測對象是否已經超過作用域從而達到自動回收內存的目的。即垃圾回收的是無任何引用的對象占據的內存空間而不是對象本身。
1.2任務
Java語言提供了垃圾回收器來自動檢測對象的作用域,可自動地把不再被使用的存儲空間釋放掉。
具體而言,垃圾回收器主要負責三個事情
- 分配內存;
- 保證不再被使用的內存被釋放掉;
- 保證被引用的對象不會被回收。
1.3方式
垃圾回收器通常是作為一個單獨的低優先級的線程運行,不可預知的情況下對內存堆中已經死亡或者長時間沒有使用的對象進行清除和回收,程序員不能實時的調用垃圾回收器對某個對象或所有對象進行垃圾回收。
2.垃圾回收
要回收的垃圾:不可能在被任何途徑使用的對象
找到這些對象的方法:
2.1引用計數法
給對象中添加一個引用計數器。
- 每當一個地方一用這個對象時,計數器值+1;
- 當引用失效時,計數器值-1。
任何計數值為0的對象就是不可能再被使用的對象。
Java中沒有使用這種算法,因為這種算法很難解決對象之間相互引用的情況。
舉例:
/*** 虛擬機參數:-verbose:gc*/ public class ReferenceCountingGC {private Object instance = null;private static final int _1MB = 1024 * 1024;/** 這個成員屬性唯一的作用就是占用一點內存 */private byte[] bigSize = new byte[2 * _1MB];public static void main(String[] args){ReferenceCountingGC objectA = new ReferenceCountingGC();ReferenceCountingGC objectB = new ReferenceCountingGC();objectA.instance = objectB;objectB.instance = objectA;objectA = null;objectB = null;System.gc();} }運行結果:
GC 4417K->288K(61440K), 0.0013498 secs] [Full GC 288K->194K(61440K), 0.0094790 secs]兩個對象相互引用著,但是虛擬機還是把兩個對象回收掉了。
這說明虛擬機并不是通過引用計數法來判定對象是否存活的。
2.2可達性分析
這個算法的思想是:用過一系列成為“GC Root”的對象作為起始點,從這些節點向下搜索,搜索所走過的路徑成為引用鏈。
當一個對象到GC Root沒有任何引用鏈(即GC Roots到對象不可達)時,則證明此對象是不可用的。
Java中的GC Roots對象包括:
- 虛擬機棧(棧幀中的局部變量區,也叫作局部變量表)中引用的對象;
- 方法去中的類靜態屬性引用的對象;
- 方法區中常量引用的對象;
- 本地方法棧中JNI(Native方法)引用的對象。
GC Roots舉例:
下圖為GC Roots的引用鏈
由圖可知,obj8、obj9、obj10都沒有到GC Roots對象的引用鏈。
即便obj9和obj10之間有引用鏈,它們還是會被當成垃圾處理,可以進行回收。
2.2.1 GC停頓(的原因)
判斷對象是否存活的可達性分析對時間的敏感還體現在GC停頓上。
因為可達性分析工作必須在一個能確保一致性的快照中進行,
- “一致性”的意思是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中對象引用關系還在不斷變化的情況。
- 不然的話可達性分析的結果就無法得到保證,這是導致GC進行時必須停頓所有Java執行線程的一個重要原因。
3.四種引用狀態
JDK1.2之前,Java中引用的定義很傳統:
? ? ? ? 如果引用類型的數據中存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表著一個引用。
(定義很純粹,但過于狹隘——一個對象只有被引用或者沒被引用兩種狀態)
我們希望描述這樣一類對象:
當內存的空間還足夠時,則能保留在內存中;
如果內存空間再進行垃圾收集后還是非常緊張,則可以拋棄這些對蒼。
JDK1.2之后,Java對引用的概念進行了擴展,將引用分為:強引用、軟引用、弱引用、虛引用四種,這四種引用強度一次減弱。
1.強引用
Object obj=new Object();這類引用;
如果一個對象具有強引用,那垃圾回收器當內存不夠時,寧愿拋出outofmemory錯誤也不會程序終止。
只要強引用還存在,垃圾回收器永遠不會回收掉被引用的對象。
2.軟引用
有些還有用但并非必需的對象。
如果一個對象具有軟引用,如內存空間足夠,垃圾回收器就不會回收它,如果內存不足,就會回收這些對象的內存(可以實現內存敏感的高速緩存,軟引用可以和一個引用隊列(ReferenceQueue)聯合使用)
如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
Java中SoftReference表示軟引用。
主要特點:
3.弱引用
非必需對象。
被弱引用關聯的對象智能生存到下一次垃圾回收之前。
垃圾回收器工作之后,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
Java中的類WeakReference表示弱引用。
4.虛引用
這個引用在內存中存在的唯一目的就是:在這個對象被收集器回收時收到一個系統通知。跟蹤垃圾回收過程,清理被銷毀對象的相關資源。
被虛引用關聯的對象,和其生存時間完全沒關系,無法通過虛引用來取得一個對象實例。
Java中的類PhantomReference表示虛引用。
對于可達性分析算法而言,未到達的對象并非是“非死不可”的,若要宣判一個對象死亡,至少需要經歷兩次標記階段:
運行結果如下:
leesfnullfinalize method executed!leesf yes, i am still alive :) no, i am dead : (由結果可知,該對象拯救了自己一次,第二次沒有拯救成功。
因為對象的finalize方法至多只被系統調用一次。
此外,從結果我們可以得知:一個堆對象的this(放在局部變量表中的第一項)引用會永遠存在,在方法體內可以將this吟詠風賦值給其他變量,這樣讀一中的對象就可以被其他變量所引用,即不會被回收。
4.方法區的垃圾回收
方法區的垃圾回收主要回收兩部分內容:
4.1判斷廢棄常量
沒有其他地方引用的常量。
以字面量回收為例,如果一個字符串“abc”已經進入了常量池,但是當前系統并沒有一個String對象引用了叫做“abc”的字面量,那么,如果發生垃圾回收并且有必要時,“abc”就會被系統移出常量池。
常量池中的其他類(接口)、方法、字段的符號引用也與此類似。
4.2判斷無用的類
需要滿足以下三個條件:
滿足以上三個條件的類可以進行垃圾回收。但是并不是無用就被回收,虛擬機提供了一些參數供我們配置。
5.垃圾收集算法
5.1標記—清除算法
最基礎的算法。
分為“標記”和“清除”兩個階段:
不足:
- 效率:標記和清除兩個階段的效率都不高;
- 空間:標記清除后會產生大量不連續的內存碎片。內存碎片太多可能導致以后程序運行過程中需要分配大對象時,無法找到足夠的連續內存而不得不提前出發一次垃圾收集動作。
執行過程如圖:
5.2復制(Copying)算法
為了解決效率問題。
將可用的內存分為兩塊,每次只用其中一塊,當這一塊內存用完了,就醬還存活著的對象復制到另一塊上面,然后再把已經是用過的內存空間一次性清理掉。
這樣每次就只需要對整個半區進行內存回收。
內存分配時也不需要考慮內存碎片等復雜情況。只需要移動指針,按照順序分配即可。
缺點:
內存縮小為了原來的一半,代價較高。
現在的商用虛擬機都采用這種算法來回收新生代。
不過研究表明1:1的比例非常不科學。因此新生代的內存被劃分為一塊較大的Eden空間和兩塊較小的Survivor空間。每次使用Eden和其中的一塊Survivor。
每次回收時,將Eden和Survivor中還存活的對象一次性復制到另外一塊Survivor空間上。最后清理掉Eden和剛才用過Survivor空間。
HotSpot虛擬機默認Eden區和Survivor區的比例為8:1,意思是,每次新生代中可用內存空間為整個新生代容量的90%.
當然,我們沒法保證每次回收的都只有不讀偶10%的對象存活,當Survivor空間不夠用時,需要依賴老年代進行分配擔保(Handle Promotion)。
5.3標記—整理(Mark-Compact)算法
復制算法在對象詢貨率較高的場景下需要進行大量的復制操作,小效率較低。
萬一對象存活率為100%,那么需要由額外的空間進行分配擔保。
老年代都是不易被回收的對象,對象存活率較高,因此一般不能直接選用復制算法。
根據老年代的特點,有人提出了另外一種標記—整理算法。
? ? ? ? 過程與標記—清除算法一樣,不過不是直接對可回收的對象進行整理,而是讓所有存活對象都向一段移動,然后直接清理掉邊界以外的內存。
工作過程如圖:
5.4分代收集算法
根據上面的內容,用一張圖概括一下堆內存的布局:
現代商用虛擬機基本都采用分代收集算法來進行垃圾回收。
根據對象的生命周期的不同將內存劃分為幾塊,然后根據各塊的特點采用最適當的收集算法。
大批對象死去,商量對象存活的(新生代),使用復制算法,復制成本低;對象存活率高,沒有額外的空間進行分配擔保的(老年代),采用標記—清除算法或者標記—整理算法。
6.垃圾收集器
垃圾收集器就是上面講的理論知識的具體實現了。
不同虛擬機提供的垃圾收集器可能會有很大的差別。
我們使用的是HotSpot,HotSpot所包含的收集器如圖:
上圖展示了7種作用域不同分代的收集器。
如果兩個收集器之間有連線,說明它們可以搭配使用。
虛擬機所處的區域說明它是屬于新生代收集器還是老年代收集器。
必須明確一個觀點:沒有最好的收集器,更加沒有萬能的收集器,只能選擇對具體應用最合適的收集器。
6.1Serial收集器
最基本、發展歷史最悠久的收集器。
采用復制算法的單線程的收集器。
- 單線程意味著它只會使用一個CPU或一條線程去完成垃圾收集工作;
- 另一方面也意味著它進行垃圾收集時必須暫停其它線程的所有工作,直到它收集結束為止。(在用戶不可見的情況下要把用戶正常工作的線程全部停掉)——這對很多應用是難以接受的。
目前為止,Serial收集器依然是虛擬機運行在Client模式下默認的新生代收集器,因為它簡單而高效。
用戶桌面應用場景中,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆甚至一兩百兆的新生代停頓時間在幾十毫秒到一百毫秒,只要不是頻繁地發生,這點停頓是完全可以接受的。
Serial收集器運行過程如下圖所示:
說明:
6.2ParNew收集器
其實是Serial的多線程版本。輸了使用多條線程進行垃圾收集之外,其余行為和Serial收集器完全一樣,包括使用的也是復制算法。
它是Server模式下的虛擬機首選的新生代收集器,其中一個很重要的和性能無關的原因是,處理Serial收集器外,目前只有它能與CMS收集器配合工作。
CMS收集器是一塊幾乎可以認為有劃時代意義的垃圾收集器。因為它第一次實現了讓垃圾收集線程與用戶線程基本上同時工作。
ParNew收集器在但CPU環境中絕對不會有比Serial收集器更好的效果,甚至由于線程交互的開銷,該收集器在兩個CPU的環境中都不能百分之百保證可以超越Serial收集器。
當然,隨著可用CPU數量的增加,它對于GC時系統資源的有效利用還是很有好處的。
它默認開啟的收集線程數與CPU相同,在CPU數量非常多的情況下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
6.3 Parallel Scavenge收集器
也是一個新生代收集器,也是使用復制算法,也是并行的多線程收集器。
特點:關注的點和其他收集器不同。
- CMS等收集器的關注點是盡可能縮短垃圾收集時用戶線程的停頓時間;
- Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。
吞吐量:CPU用于運行用戶代碼時間與CPU總消耗時間的比值。
即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
此外,Parallel Scavenge收集器是虛擬機運行在Server模式下的默認垃圾收集器。
?
- 停頓時間短適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗;
- 高吞吐量則可以高效率利用CPU時間,盡快完成運算任務,主要是和在后臺運算而不需要太多交互的任務。
虛擬機提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio兩個參數來精確控制最大垃圾收集停頓時間和吞吐量大小。
- GC停頓時間的縮短是以犧牲吞吐量和新生代空間換取的,所以第一個參數也不是越小越好;
由于與吞吐量密切相關,Parallel Scavenge收集器也稱作“吞吐量有限收集器”。
Parallel Scavenge收集器有一個-XX:UseAdaptiveSizePolicy參數,這是一個開關參數:
- 這個參數打開后,就不需要手動指定新生代的大小、Eden區和Survivor區參數等細節參數了;
- 虛擬機會根據這些參數以提供最合適的停頓時間或者最大的吞吐量。
- 適用于對收集器運作原理不是很了解,以至于在優化比較困難時。使用此收集器配合自適應調節策略,把內存管理的調優任務交給虛擬機來完成。
6.4Serial Old收集器
Serial 收集的老年版本,單線程,使用“標記—整理”算法,主要意義是給Client模式下的虛擬機使用。
6.5Parallel Old收集器
Parallel Scavenge收集器的老年版本,使用多線程和“標記—整理”算法。
此收集器在JDK1.6之后出現,“吞吐量優先收集器”終于有了比較名副其實的應用組合。
在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge收集器+Parallel Old收集器的組合。
運行過程如圖:
6.6CMS收集器
Concurrent Mark Sweep收集器以互毆最短回收停頓時間為目標,使用“標記—清除”算法,收集過程分為以下四步:
其中,2和4耗時最長,但是可以與用戶線程兵法執行。
運行過程如下:
說明:
6.7 G1收集器
G1是目前技術發展的最前沿成果之一。
HotSpot開發團隊賦予它的使命是未來可以替換掉JDK1.5中發布的CMS收集器。
與其他GC收集器相比,G1收集器有以下特點:
在G1之前的收集器,收集范圍都是整個新生代或者老年代。
使用G1收集器時,Java堆的內存布局與其他的收集器有很大的額差別,他將整個Java堆劃分為多個大小相等的獨立區域(Region)。雖然還保留由新生代和老年代的概念,但是新生代和老年代不再是物理隔離的了,它們都是一部分(可以不連續)Region的結合。
6.8常用的收集器組合
7.理解GC日志
每種收集器的日志形式都是由它們自身的實現決定的。即每種收集器的日志格式都可不一樣。
不過虛擬機為了方便用戶閱讀,將各個收集器的日志都維持了一定的共享。
看下面一段GC日志:
[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] [GC [DefNew: 2242K->0K(2368K), 0.0018814 secs] 2242K->2241K(7680K), 0.0019172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K), [Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heapdef new generation total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000, 0x0000000006ea0000)eden space 2176K, 2% used [0x00000000052a0000, 0x00000000052aaeb8, 0x00000000054c0000)from space 256K, 0% used [0x00000000054c0000, 0x00000000054c0000, 0x0000000005500000)to space 256K, 0% used [0x0000000005500000, 0x0000000005500000, 0x0000000005540000)tenured generation total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000, 0x000000000a6a0000)the space 5312K, 3% used [0x0000000006ea0000, 0x0000000006ed0730, 0x0000000006ed0800, 0x00000000073d0000)compacting perm gen total 21248K, used 2982K [0x000000000a6a0000, 0x000000000bb60000, 0x000000000faa0000)the space 21248K, 14% used [0x000000000a6a0000, 0x000000000a989980, 0x000000000a989a00, 0x000000000bb60000) No shared spaces configured.7.1?通過日志看到,系統的請求數目很少,每個請求資源也不多,但是系統堆內存占用非常高,可能出現了什么問題?怎樣定位問題在哪里
可能是發生了堆溢出或內存泄露,要解決堆內存異常的情況一般的手段是通過內存映像分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是有必要的,也就是要先分清楚是內存泄露還是內存溢出。
- 如果是內存泄露,可進一步通過工具查看泄露對象到GC Roots的引用鏈。于是就能找到泄露的對象是怎么與GC Roots相連接導致進行垃圾回收時沒能回收泄露對象所占的內存,有了泄露對象的信息和GC Roots引用鏈的信息,就可以準確地定位出泄露代碼的位置。
- 如果沒有發出內存泄露,也是說,內存中的對象確實還活著,那就應該去檢查虛擬機的堆參數(-Xmx與-Xms),與物理機器對比看是否還可以進一步擴大,從代碼上檢查是否存在某些對象聲明周期過長、持有狀態時間過長等,嘗試減少程序運行內存消耗。
8.垃圾回收利弊
8.1優點
垃圾回收器的存在一方面把開發人員從釋放內存的復雜工作中解脫出來,提高了開發人員的生產效率;
另一方面,對開發人員屏蔽了釋放內存的方法,可以避免因開發人員錯誤地操作內存而導致應用程序的崩潰,保證了程序的穩定性。
8.2缺點
但是垃圾回收也帶來了問題,為了實現垃圾回收,垃圾回收器必須跟蹤內存的使用情況,釋放沒用的對象,在完成內存的釋放后還需要處理堆中的碎片,這些操作必定會增加JVM的負擔,從而降低程序的執行效率。
9.垃圾回收存在的原因
在程序開發過程中如果忘記或者錯誤的釋放內存,往往會導致程序運行不正常甚至是導致程序崩潰。
為了減輕程序開發人員的工作和保證程序的安全與穩定,Java語言提供了垃圾回收器來自動檢測對象的作用域,可自動地回收不可能再被引用的對象所占用的內存。
要請求垃圾收集,可以調用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉顯式的垃圾回收調用。
10.垃圾回收的時機
垃圾回收可能發生的時間是堆可用空間不足或CPU空閑。
對于HotSpot虛擬機,用OopMap數據結構存儲 “程序中哪些地方存放著對象引用” 的信息。在類加載完成的時候,HotSpot就把對象內什么偏移量上是什么類型的數據計算出來,在JIT編譯過程中,也會在特定的位置上記錄下棧和寄存器中哪些位置是引用。這樣GC在掃描時就可以直接得到這些信息了。
10.1安全點
程序執行時并不是在所有地方都能停頓下來開始GC,只有到達安全點時才能暫停。其中安全點指的是記錄OopMap數據結構的位置。
- 安全點的選擇基本上是以程序“是否能讓程序長時間執行的特征”為標準進行選定的。
- “長時間執行”的最明顯的特征就是指令序列復用,例如方法調用、循環跳轉、異常跳轉等,所有具有這些功能的指令才會產生safepoint。
- 對于safepoint,另一個需要考慮的問題就是如何在GC發生時讓所有線程都“跑”到最近的安全點上再停頓下來。
- 有兩種方案可以選擇:搶先式中斷和主動式中斷。
10.1.1搶先式中斷
在GC發生時,首先把所有線程全部中斷,如果發現有些線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。
現在幾乎沒有虛擬機采用這種中斷方式應GC事件。
10.1.2主動式中斷
當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執行時主動輪詢這個標志,發現中斷標志為真時,就自己中斷掛起。輪詢標志的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。
10.2安全區域
安全點機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的安全點。但當線程處于sleep狀態或者blocked狀態時,這時候線程可能無法響應JVM的中斷請求,“走到”安全的地方中斷掛起。
對于這種情況,就需要安全區域來解決。
安全區域是指在一段代碼片段之中,引用關系不會發生變化,在這個區域的任何地方開始GC都是安全的。因此也可以把安全區域看做被擴展了的安全點。
部分整理自微信公眾號“Java編程精選”與《深入理解Java虛擬機——JVM高級特性與最佳實踐》
在線程執行到Safe Region中的代碼時,首先標識自己已經進入到了Safe Region,那樣,當在這段時間里JVM要發起GC時,就不用管標識自己為Safe Region狀態的線程了。在線程要離開Safe Region時,要檢查系統是否已經完成了根結點的枚舉(或者整個GC過程),如果完成了,那線程就繼續執行,否則它就繼續等待直到收到可以安全離開Safe Region的信號為止。
總之,程序并非是時時刻刻都可以去執行GC操作,只有程序運行到安全區域的時候才可以發起GC的操作。
?
總結
以上是生活随笔為你收集整理的Java垃圾回收(GC)、找垃圾的方式、GC Root、GC停顿、引用、垃圾收集算法、收集器、GC日志、安全点、安全区域的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 判断给定的整数数组是不是某二叉搜索树的后
- 下一篇: 面向对象1(super、this)