GC回收算法
什么是垃圾?
類比日常生活中,如果一個東西經常沒被使用,那么就可以說是垃圾。
同理,如果一個對象不可能再被引用,那么這個對象就是垃圾,應該被回收。
垃圾:不可能再被引用的對象。
finalize方法
- 在對象沒有被引用時調用
- 在Object類里定義
新生代與老年代
IBM公司的研究表明,在新生代中的對象 98% 是朝生夕死的。
在實際的 JVM 新生代划分中,不是采用等分為兩塊內存的形式。而是分為:Eden 區域、Survivorfrom 區域、Survivorto 區域 這三個區域。
所以在HotSpot虛擬機中,JVM 將內存划分為一塊較大的Eden空間和兩塊較小的Survivor空間,其大小占比是8:1:1。當回收時,將Eden和Survivofrom中還存活的對象一次性復制到Survivorto空間上,最后清理掉Survivorfrom和剛才用過的Eden空間。
新生代一般占據堆的1/3空間,老年代占據2/3。
判斷對象是否存活
引用計數法
在一個對象被引用時加一,被去除引用時減一,這樣我們就可以通過判斷引用計數是否為零來判斷一個對象是否為垃圾。這種方法我們一般稱之為「引用計數法」。主流的Java虛擬機里面都沒有選用引用計數算法來管理內存
-
什么是循環引用?(環)
A 引用了 B,B 引用了 C,C 引用了 A,它們各自的引用計數都為 1。但是它們三個對象卻從未被其他對象引用,(假設有1000個對象時,這三個就是垃圾;如果只有4個對象,那么另外一個就是垃圾)只有它們自身互相引用。從垃圾的判斷思想來看,它們三個確實是不被其他對象引用的,但是此時它們的引用計數卻不為零。
可達性分析算法
通過一系列名為”GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
GC Roots:
- 虛擬機棧中引用的對象
- 方法區靜態屬性引用的對象
- 方法區常量引用的對象
- JNI引用的對象(Native方法)
根搜索算法:一種通過遍歷的方式判斷對象是否可達的垃圾標記算法。
垃圾回收算法
垃圾回收——標記清除算法(適用老年代)
它將垃圾回收分為兩個階段:標記階段和清除階段。
在標記階段,標記所有從根節點出發的可達對象。因此,所有未被標記的對象就是未被引用的垃圾對象。
在清除階段,清除所有未被標記的對象。
問題:產生空間碎片。
垃圾回收——復制算法(適合年輕代)
將內存分為兩部分,每次只使用其中一部分。在垃圾回收時,將正在使用的內存中的存活對象復制到未使用的內存塊中,之后清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收。
問題:不會產生空間碎片,但內存折半
垃圾回收——標記整理算法(適合老年代。)
對比於標記清除算法,在清除階段,它會將所有的存活對象移動到內存的另一端。之后清理邊界之外的所有空間。
這種算法既避免了碎片的產生,又不需要兩塊相同的內存空間,因此性價比較高。
“因地制宜”——分代算法
分代算法,就是根據 JVM 內存的不同內存區域,采用不同的垃圾回收算法。
例如對於存活對象少的新生代區域,比較適合采用復制算法。這樣只需要復制少量對象,便可完成垃圾回收,並且還不會有內存碎片。
而對於老年代這種存活對象多的區域,比較適合采用標記壓縮算法或標記清除算法,這樣不需要移動太多的內存對象。
GC回收器
Serial 回收器
- 單線程串行回收
- 使用復制算法
- 會產生較長時間的停頓(Stop the world)
- 不會產生線程切換的開銷
通過JVM參數-XX:+UseSerialGC可以使用串行垃圾回收器。
ParNew回收器
- 多線程並行回收
- 新生代回收器,采用復制算法
參數控制:-XX:+UseParNewGC
Parallel Scavenge回收器
- 多線程並行回收
- 新生代回收器,采用復制算法
- 追求高吞吐量,充分利用CPU資源【吞吐量優先】
開啟參數:-XX:+UseParallelGC
GC自適應調節策略:Parallel Scavenge收集器可設置-XX:+UseAdptiveSizePolicy參數
當開關打開時不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的對象年齡(-XX:PretenureSizeThreshold)等,虛擬機會根據系統的運行狀況收集性能監控信息,動態設置這些參數以提供最優的停頓時間和最高的吞吐量,這種調節方式稱為GC的自適應調節策略。
Serial Old 回收器
- 老年代單線程回收
- 使用標記整理算法
Parallel Old回收器
- 老年代多線程回收
- 使用標記整理算法
串行與並行的效率分析:
以新生登記為例,假設新生人數較多,數量在5000,使用串行的方式,可以理解為一個人復制5000的登記工作;
效率可想而知。
使用並行的方式可以理解為有100個人負責登記,效率就會顯著提升。
但如果新生只有50個人,一個人登記就綽綽有余了。
新生的數量可以理解為GC回收對象的數量,而負責登記的人就是CPU的核心計算數量。
對於新生代,回收次數頻繁,使用並行方式高效。
對於老年代,回收次數少,使用串行方式節省資源。(CPU並行需要切換線程,串行可以省去切換線程的資源)
CMS回收器
- 並發低停頓收集器
- 使用標記清除算法
- 四個階段
- 初始標記 (標記GC Roots可以直接關聯的對象,速度很快)
- 並發標記 (進行GC Roots Tracing,判斷對象是否存活)
- 重新標記 (校准並發標記對象的存活狀態)
- 並發清除 (回收標記的對象)
- 初始標記和重新標記仍然需要Stop The World
- CMS缺點
- 由於並發帶來的CPU資源消耗
- 由於並發收集在回收過程中產生的浮動垃圾無法清除
- 使用標記清除算法帶來的空間碎片問題
G1回收器
- 使用於JDK1.7。
- 使用分代垃圾回收策略。
- 新特性:使用分區算法。使內存不再連續。
- 支持很大的堆,高吞吐量。
通過JVM參數 -XX:+UseG1GC 使用G1垃圾回收器
G1特點:
-
並行與並發:並行體現在G1可以利用CPU的多個核心,縮短stop the world時間;並發體現在某些收集器和Java線程可以同時執行。
-
分代收集
-
空間整合,G1收集器采用標記整理算法,不會產生內存空間碎片。分配大對象時不會因為無法找到連續空間而提前觸發下一次GC。
-
能建立可預測的時間停頓模型,可以指定在M時間段內,垃圾回收時間不能超過N
並行與並發:
並行:同時處理多個任務。
並發:串行處理多個任務,但任務之間的切換很快,感覺上是並行執行。
並行是建立在多核CPU上的,多核指的是在一塊CPU上集成多個計算引擎。引擎之間可同時進行運算。
舉例:
單核運算時代就好比400米短跑,每跑完一個400米執行完一個任務。
多核運算時代可以理解為4*100接力,雖然單個任務的執行和單核一樣,但對於多個任務來說,單核是需要完成400的全部距離才能進行第二個任務。但多核只要第一個人跑完了100米,就可以開始第二個任務了。
並行處理多個任務的能力可以理解為核心之間的接力賽。
並發是指通過CPU在多個任務之間快速切換來達到同時執行的效果。
- 分區算法(G1內存結構)
在G1回收器之前,垃圾回收器分配的內存都是連續的。
在G1回收器中,垃圾回收器將內存分為大量區塊。
humongous:存儲巨型對象,當對象超過普通區塊的一半時,分配一個巨型區塊。
- G1回收器工作步驟
- 新生代 GC
- 並發標記周期
- 混合收集
- 如果需要,可能進行 FullGC
新生代GC
Eden區被占滿,新生代GC啟動,回收Eden和Survivor。
注:survivor會被回收掉一部分,但回收后至少有一個survivor區存在。
為什么???
新生代GC采用復制算法,將Eden區中的存活對象復制到Survivor區中。
並發標記周期
- 初始標記:標記從根節點直接可達的對象。(會產生全局停頓)
- 根區域掃描:掃描Survivor區直接可達的老年代區域對象,並標記。(和應用程序並發,但不能和新生代GC同時執行【新生代GC有修改Survivor的操作】)
- 並發標記:掃描並查找整個堆內存存活對象,並標記。(可以被新生代GC打斷)
- 再次標記:由於應用程序持續進行,需要修正標記結果。(會產生全局停頓)
- 獨占清理:計算各個區域的存活對象和GC回收比例,並進行排序,識別可以混合回收的區域。為下階段做鋪墊。(有停頓)
- 並發清理階段:識別並清理完全空閑的區域。
混合回收
優先回收垃圾比例高的區域。(GC:Garbage First)
執行年輕代和老年代GC。
混合GC執行多次之后,會觸發新生代GC。然后循環:
GC的兩種觸發情況
-
Minor GC:新對象產生,申請Eden區失敗后會觸發Minor GC
-
Full GC:對整個堆的對象進行清理。
-
觸發條件
-
System.gc()方法的調用
-
老年代空間不足
-
方法區空間不足
-
-
了解GC日志
查看JDK8默認使用哪種回收器
java -XX:+PrintCommandLineFlags -version
-XX:+UseParallelGC
使用Parallel Scavenge新生代回收器和Parallel Old老年代回收器
[GC (Allocation Failure) [PSYoungGen: 5986K->696K(8704K)] 5986K->704K(9216K), 0.0018526 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 4792K->696K(8704K)] 4800K->704K(9216K), 0.0031653 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 4945K->680K(8704K)] 4953K->688K(9216K), 0.0022002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 4776K->712K(8704K)] 4784K->720K(9216K), 0.0007493 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 4808K->648K(8704K)] 4816K->656K(9216K), 0.0008800 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 4744K->664K(8704K)] 4752K->672K(9216K), 0.0008349 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
[GC (Allocation Failure) --[PSYoungGen: 4760K->4760K(8704K)] 4768K->5268K(9216K), 0.0022344 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 4760K->113K(8704K)] [ParOldGen: 508K->496K(512K)] 5268K->609K(9216K), [Metaspace: 3222K->3222K(1056768K)], 0.0069196 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 4209K->192K(8704K)] 4705K->688K(9216K), 0.0007751 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 4288K->160K(8704K)] 4784K->656K(9216K), 0.0018608 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Allocation Failure
表明本次引起GC的原因是因為在年輕代中沒有足夠的空間能夠存儲新的數據了。
PSYoungGen
新生代Eden和FromSpace,PS指Parallel Scavenge ,
PSOldGen
老年代
[PSYoungGen: 5986K->696K(8704K)] 5986K->704K(9216K)
中括號內:GC回收前年輕代堆大小,回收后大小,(年輕代堆總大小)
括號外:GC回收前年輕代和老年代大小,回收后大小,(年輕代和老年代總大小)
user代表用戶態回收耗時,sys內核態回收耗時,rea實際耗時。