JVM學習2-垃圾回收機制
- 1 從JVM角度看對象在記憶體中的建立
-
- 1.1 對象建立的流程
- 1.2 給對象配置設定記憶體
- 1.3 配置設定記憶體的線程安全性問題
- 1.4 對象的結構
- 1.4 對象的通路定位
- 2 垃圾回收
- 2.1垃圾回收概述
- 2.2 引用計數法
- 2.3 可達性分析法
- 3 回收政策
-
- 3.1标記-清除算法
- 3.2 複制算法
- 3.3 标記-整理-清除算法
- 3.4 分代收集算法
- 4 垃圾收集器
-
- 4.1 Serial收集器
- 4.2 ParNew收集器
- 4.2 Parallel Scavenge收集器
- 4.3 CMS收集器
- 4.4 G1收集器
1 從JVM角度看對象在記憶體中的建立
1.1 對象建立的流程
通過New 建立一個對象會在堆記憶體中開辟空間

1.2 給對象配置設定記憶體
配置設定記憶體就是指針移動的過程
1、指針碰撞:堆記憶體是規整的,可用記憶體和不可用記憶體是分開的。
2、空閑清單:堆記憶體維護一張表來記錄哪些記憶體(記憶體編号)沒有使用。
使用哪一種配置設定方式,是由記憶體中的堆是否規整決定的,而是否規整是由垃圾回收政策決定的。如果垃圾回收器帶有壓縮功能就使用指針碰撞,否則就使用空閑清單。
1.3 配置設定記憶體的線程安全性問題
如果多個對象同時建立,記憶體配置設定也就是堆記憶體指針的指向就會有線程安全性問題。
1、同步,加鎖,安全但是執行效率太低。
2、本地線程配置設定緩沖。也就是在堆記憶體中為每一個線程配置設定這個線程獨有的區域。TLAB
1.4 對象的結構
Header (對象頭Mark Word)
1、自身運作時資料,如哈希值 (Obect類的hash是Native方法),GC分代年齡(為垃圾回收機制使用 ),鎖狀态标志,線程持有的鎖,偏向線程ID,偏向時間戳等。
2、類型指針:對象指向它的類的中繼資料的指針,虛拟機通過它來确定這個對象時哪個類的執行個體。并不是所有對象都保留對象指針。數組對象還會有記錄長度的資料。
3、InstanceData:資料的執行個體,真正存儲在對象的引用側率,HotSpot的配置設定側率是相同寬度的資料放在一起。是以有可能出現父類中定義的變量在子類之前。
4、Padding占位符:自動記憶體管理要求對象的啟事位址必須是8個位元組的整數倍,對象頭是8個位元組,如果對象的執行個體資料不是8個位元組的整數倍,需要padding補齊。
1.4 對象的通路定位
Java虛拟機隻規定了由棧記憶體中一個引用類型執行堆記憶體的一塊位址,但這塊位址并不一定是這個對象本身,可以是對象本身,也可是其他的一塊記憶體區域。對象的通路定位有兩種實作方式:使用句柄、和使用指針。
HotSpot使用的是指針。
2 垃圾回收
2.1垃圾回收概述
1、如何判定對象為垃圾對象
引用計數法
可達性分析法
2、如何回收
回收政策:标記清除、複制算法、标記整理、分代收集算法
垃圾回收器:serial、Parnew、Cms、G1
3、何時回收
2.2 引用計數法
顧名思義:在對象中添加一個引用計數器、當有地方引用這個對象的時候,引用計數器的值就+1,當引用失效的時候、計數器的值減1.(将引用置為null.),雖然效率高,但是不會使用,因為這樣計數會存在問題,如果棧中的引用置為null,但是堆中的記憶體又指向堆中别的對象,垃圾回收器隻将直接引用的對象清除,而間接不會被清除。
通過代碼來驗證JVM是否采用的這種方法判斷一個對象是否為垃圾對象。需要配置JVM參數
-verbose:gc 列印垃圾回收的日志資訊
-XX:+PrintGCDetails 詳細的GC資訊
public class Test01 {
private Object instacnce;
//讓對象建立時占用一定大小的空間,回收前後有明顯的變化
public Test01(){
byte[] b = new byte[20*1024*1024];
}
public static void main(String[] args) {
Test01 test01 = new Test01();
Test01 test02 = new Test01();
//互相引用
test01.instacnce = test02;
test02.instacnce = test01;
//引用置為null
test01 = null;
test02 = null;
//手動調用GC
System.gc();
}
}
可以看到記憶體有很大的變化,說明,JVM并不是使用的引用計數器算法
[GC (System.gc()) [PSYoungGen: 23808K->728K(38400K)] 44288K->21208K(125952K), 0.0080063 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 728K->0K(38400K)] [ParOldGen: 20480K->632K(87552K)] 21208K->632K(125952K), [Metaspace: 3276K->3276K(1056768K)], 0.0165887 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
Heap
PSYoungGen total 38400K, used 333K [0x00000000d5d00000, 0x00000000d8780000, 0x0000000100000000)
eden space 33280K, 1% used [0x00000000d5d00000,0x00000000d5d534a8,0x00000000d7d80000)
from space 5120K, 0% used [0x00000000d7d80000,0x00000000d7d80000,0x00000000d8280000)
to space 5120K, 0% used [0x00000000d8280000,0x00000000d8280000,0x00000000d8780000)
ParOldGen total 87552K, used 632K [0x0000000081600000, 0x0000000086b80000, 0x00000000d5d00000)
object space 87552K, 0% used [0x0000000081600000,0x000000008169e298,0x0000000086b80000)
Metaspace used 3282K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
2.3 可達性分析法
可以解決引用計數器法的問題。定義了一個GCRoot,如果順着GCRoot一直往下走,可以通過引用鍊找到這個對象,說明這個對象是可用的,否則回收掉這個對象。
可以作為GCRoots的對象有哪些?
1、虛拟機棧,也就是局部變量表
2、方法區的類屬性所引用的對象
3、方法區中常量所引用的對象
4、本地方法棧中引用的對象
3 回收政策
3.1标記-清除算法
對可以清除的,正在使用的,未被使用的進行标記,但是這樣會帶來一個問題,就是記憶體中出現越來越多的不連續的記憶體空間。導緻如果要存儲一個大對象時再出發一次垃圾回收算法,又帶來性能問題。
回收之前和回收之後的記憶體圖,紅色是使用的,黃色是标記為可以被清除的。白色表示未被使用的
3.2 複制算法
将堆記憶體分為下面的幾大塊,垃圾回收器主要關注新生代
1. 新生代
Eden 伊甸園
Survivor 存活區
Tenured Gen
2. 老年代
當對象第一次建立,放在堆中,第一次執行垃圾回收,将存活的對象複制到Survivor1中,将Eden中的空間全部恢複為未使用,第二次垃圾回收,會選擇的将Survivor1中仍然存活的對象放在Tenured Gen中,然後将Survivor1剩餘的對象放在另一個Survovord2空間中,然後将Survivor1記憶體清空。再将Eden中存活的對象放在Survivor1中。如此循環,提高記憶體的使用率、
如果Eden不夠用還會借用Survivor記憶體。Survivor不夠用,還會使用記憶體擔保借用TenuredGen。
3.3 标記-整理-清除算法
複制算法如果存活超過百分之10,就需要記憶體擔保,這樣對新生代 來說是可行的,但是對于老年代,每次存活很可能超過百分之90,這樣會讓效率變得慢。是以标記整理算法主要是針對老年代。
将需要回收的記憶體往右移動,右邊有不需要回收的往左邊移動,這樣右邊就全都是需要回收的記憶體。
3.4 分代收集算法
分代收集算法并不是一種新的算法,而是将複制算法和标記整理算法進行結合,記憶體分為新生代和老年代,根據兩者之間不同的特點,針對新生代和回收率較高的記憶體區域,選擇複制算法,對于老年代和回收率低的選擇标記整理清除算法。
4 垃圾收集器
4.1 Serial收集器
垃圾回收器在虛拟機的規範中并沒有作出要求,是以不同的公司可以實作不同的收集器。不同的收集器适用的環境不同,垃圾回收器也會影響性能。
JDK1.3之前主要就是使用的Serial收集器,也是最基本,發展最悠久的單線程的垃圾收集器。适用于 桌面應用程式,用于新生代記憶體複制算法。
4.2 ParNew收集器
多線程的收集器。用于用戶端性能不如Serial收集器,在JDK1.5,SUN公司開發了CMS收集器,這款收集器真正做到了扔垃圾和打掃衛生同時進行的功能。但是CMS是用于回收老年代記憶體,如果使用CMS回收老年代記憶體,新生代記憶體就必須使用Serial或ParNew。也就是CMS和Parial(後邊會提到)是不能一同工作的。至于其他的收集器和Serial使用了大量系相同的代碼,使用了複制算法
4.2 Parallel Scavenge收集器
使用的複制算法,針對的是新生代記憶體,新生代收集器,多線程收集器。似乎和ParNew差不多。不同點是最初設計的關注點上,ParNew關注的是縮短垃圾回收的時間。Parallel是達到一個可控制的吞吐量。
吞吐量是指CPU用于運作使用者代碼的時間與CPU消耗的總時間的比值。是以,計算吞吐量的公式:吞吐量 = (計算使用者代碼的時間) / (執行使用者代碼的時間 + 垃圾回收所占用的時間)
例如,虛拟機一共運作了100分鐘,那麼垃圾回收時間用了1分鐘,吞吐量是99%,
-XX:MaxGCPauseMillis 垃圾收集器最大停頓時間
設定100ms,10秒停頓一次, 1ms,1秒停頓一次。這是一個杠杆,需要一個合理的值
-XX:GCTimeRatio 吞吐量大小
(0,100)
設定99 就是垃圾回收時間為1%
4.3 CMS收集器
Concurrent Mark Sweep并發标記清除收集器,采用的垃圾回收算法是标記清除算法,第一步标記、第二步清除,标記清除算法會産生垃圾碎片、性能比較低。雖然它的效率比較低,但是可以通過一定的手段來提高效率,CMS收集器用于老念代的收集,和ParNew 一起工作。
并發和并行的了解。
并發是指垃圾打掃和産生垃圾同時進行
并行是指打掃垃圾是打掃垃圾,産生垃圾産生垃圾,不過都是有多個線程同時執行
下面的兩個圖分别表示并行與并發。綠色表示垃圾回收,紅色表示産生垃圾
工作過程:
1) 初始标記
2) 并發标記
3) 重新标記
4) 并發清理
CMS并不是完全并發的一種垃圾收集器,而是在标記和清除等兩個比較消耗時間的過程中使用并發執行。CPU占用率高,吞吐量提升不上去。
1、初始标記是标記GCRoot能直接關聯到的對象,速度很快
2、并發标記是接着往下找
3、重新标記是為了修正并發标記期間,因使用者程式運作而導緻産生變動的那一部分對象的标記記錄,是對并發标記的修正。
4、并發清理,把标記的對象進行清理
優點:
1)并發收集
2)低停頓
缺點:
1)占用大量的CPU資源,因為有線程的開銷
2)無法處理浮動垃圾
3)出現Concurrent Mode Faliure:在并發清理時候,使用者線程産生的對象,需要存放到一塊臨時的記憶體空間,如果這塊記憶體過大,會浪費空間,如果給的過小就會出現此錯誤,進而觸發Serial收集器來收集,導緻更費時間
4)産生空間碎片
4.4 G1收集器
Garbage First ,當今最為強大的收集器。Jdk9預設采用此收集器,最早在2004年,Sun公司實驗室,發表了G1的論文,但是不是主流的,主流的依然是spot,JDK6中內建到了JVM中,JDK7正式放入JDK中。
結合了前面幾種收集器的優勢:并發、分代收集、空間整合以及可預測的停頓。并行和并發:充分利用多核CPU,縮短停頓時間。前面已經說過:并行能減少停頓時間、并發能提高速度。G1的分代收集與之前的分代有差別:并不是嚴格的分為新生代老年代的實體隔離,而是分為一塊一塊的記憶體。G1還有一個特點是空間整合,類似于标記-整理-清除算法那樣。可預測的停頓。
步驟:
1)初始标記
2)并發标記
3)最終标記
4)篩選回收:通過Remembersert來記錄對象在不同記憶體區域的變化,在判斷一個對象是否為垃圾的時候可以避免全表掃描。