【學習筆記】—JVM(二)垃圾收集器和內存分配策略

一、爲什麼要回收

  在上一部分在對每個數據區分析了,最後指出了他會拋出什麼異常,最多的就是OOM,內存溢出異常。在Java堆中一個程序要創建太多的實例對象,但有些數據只用了一次之後卻再沒有使用,如果不將它清除掉,對內存而言永遠是不夠用的。

二、如何判斷對象不再使用,需要清理

1.引用計數法算法

給對象添加一個引用計數器,每當有一個地方引用它時計數器加1,失效時減1;爲0時則不會再被使用。

Java虛擬機沒有選用這個方式,因爲它很難解決對象相互循環引用的問題。

Object a = new Object();
Object b = new Object();
a.name = b;
b.name = a;

這兩個之間都不可能在被訪問到,理應清楚,但是相互引用,都沒有失效,計數器不會爲0,所以不會被清楚。

2.可達性分析算法

通過一些被稱"GC Roots"的對象作爲起點,從這些節點開始向下搜索,搜索走過的路徑被稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots節點到該節點不可達),則證明該對象是不可用的。

可達性分析算法
Java中可作爲GC Root的對象包括下面幾種:
  - 虛擬機棧中的引用對象。
  - 方法區中靜態屬性引用的對象。
  - 方法區中常量引用的對象。
  - 本地方法棧中JNI(Native方法)引用的對象。

3.再給你一次機會

  被可達性分析算法標記爲不可達的對象中,有些並不是馬上就回收了,至少經歷兩次標記才真的被回收:
    第一次標記進行篩選,條件是該對象有沒有finalize()方法,沒有就是真的沒用的了。有的話將其放入一個叫Q-finalize的隊列。
    第二次標記對其調用方法(並不會承諾等待它運行結束,避免如果進入死循環等永久等待的情況),只要他的方法中與其他對象建立了關係,那他就不會被回收。每個對象的finalize()方法只會被調用一次,第二次之後會視爲他沒有finalize()方法。
  finalize()方法運行代價高昂,不確定性大,無法保證每個對象的調用順序。所以一般用try-finally或者其他方法代替。

三、垃圾收集算法

1.標記-清除算法

  Mark-Sweep,直接上圖
標記-清除算法
  不足
    - 效率低下
    - 產生空間碎片,由圖可知,雖然清理後有了很多空間,但這時有一個較大的對象需要分配時就要再回收一次垃圾。

2.複製算法

  Conping 將內存劃分爲兩部分,每次使用其中一部分。快要用完時將還活着的複製到另一塊上面去,並對已經使用過的空間一次清理掉。運行高效,實現簡單
複製算法
  不足:
    - 代價昂貴,所以一般不劃分一半,而是劃分爲一塊較大的Eden空間和兩個較小的Survivor空間默認比例時8:1。當Survivor空間不夠用時,需要其他內存(老年代)進行分配擔保。

3.標記-整理算法

  Mark-Compact
標記-整理算法
  一般根據每一代的特性來選擇不同的算法的進行分代收集,新生代用複製算法,老年代使用標記-整理或者標記-清除算法

新生代:主要是用來存放新生的對象。一般佔據堆的1/3空間。由於頻繁創建對象,所以新生代會頻繁觸發MinorGC進行垃圾回收。
老年代:主要存放應用程序中生命週期長的內存對象。老年代的對象比較穩定,所以MajorGC不會頻繁執行。在進行MajorGC前一般都先進行了一次MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。

四、算法的實現(HotSpot)

1.枚舉根節點

  上文中的可達性分析GC Root找引用鏈,這一瞬間必須在一個能確保一致性的快照之中進行。"一致性"指整個系統凍結在一瞬間,不可以出現分析過程中對象的引用關係還在不斷變化的情況,稱之爲"Stop The Word",對用戶來說顯然不能停頓太久,所以不可能一個不漏的去檢查完所有執行上下文和全局的引用變量而是使用一組叫做OopMap的數據結構來實現

OopMap:記錄了棧上本地變量到堆上對象的引用關係。

2. 安全點

  通過OopMap可以快速完成GC Roots的枚舉,但是爲每一條指令都生成對應的OopMap成本太高。其實也沒有每條都生成,OopMap只是在特定的位置生成記錄。而這些特定的位置就叫做"安全點"。即程序執行時並不是在所有地方都停頓下來開始GC,而是在到達安全點的時候才能暫停
  選定標準:是否具有讓程序長時間執行的特徵。
  如何讓所有線程都到安全點上再停下來:
    - 搶斷式中斷
      在GC發生時,把所有線程先中斷,如果不在安全點上則讓線程繼續執行,直到安全點爲止。(現在幾乎沒有用這種方式的)
    - 主動式中斷
      設置一個輪詢標誌,和安全點是重合的,每個線程執行時主動去輪詢這個標誌,當發現中斷標誌爲真的時候,就自己主動中斷掛起。這樣就實現了線程都在安全點停下來

3.安全區域

  線程運行時可以到達安全點自己中斷,但是不執行的線程呢,處於Sleep,Block的線程。設置一個安全區域,在這個區域中的任意地方都是安全的,GC時不會去管它。但是當一個線程要從安全區出來時,就要先檢查GC是否完成,沒有完成就需要等待完成才能出來。

五、垃圾收集器

1.Serial收集器

  新生代,複製算法,單線程
在這裏插入圖片描述
  缺點:在GC時必須停止其他所有的工作線程,單核嘛
  最主要的優點:簡單而高效
  適用於Client模式下的虛擬機

Client模式:啓動的JVM採用的是輕量級的虛擬機,更注重編譯的速度,啓動快。更適合在客戶端的版本下
Server模式:啓動的JVM採用的是重量級的虛擬機,對程序採用了更多的優化,啓動慢,但是啓動進入穩定期長期運行之後Server模式的程序運行速度比Client要快很多,更注重編譯的質量,多用於服務端

2.Serial Old收集器

  老年代,標記-整理算法,單線程。
  可以和Parallel Scavenge搭配使用。

3.ParNew收集器

  新生代,複製算法,多線程。
  簡單來說就是Serial的多線程版本
在這裏插入圖片描述
  除了Servial收集器外,目前只有他能和CMS搭配使用
  適用於Server模式的虛擬機

4.Parallel Scavenge收集器

  新生代,複製算法,多線程
  上面的收集器都是注重儘可能的縮短Stop The Word的時間,而Parallel Scavenge注重的是達到一個可控制的吞吐量

吞吐量:CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

在這裏插入圖片描述
  主要適用在後臺運算而不需要太多交互的任務

5.Parallel Old收集器

  老年代,標記-整理算法,多線程
  Parallel Scavenge收集器的老年代版本

6.CMS收集器

  老年代,標記-清除算法
CMS示意圖
  分爲一下4個步驟
    1.初始標記
    2.併發標記
    3.重新標記
    4.併發清除
  其中初始標記和重新標記需要Stop The Word,但是很快,主要時間是在併發標記上,但是併發標記又是與線程併發進行的。
  缺點:
    - 對CUP資源非常敏感,雖說是併發進行的不會StopTheWord,但是佔用了一部分的CPU資源,會導致應用程序變得緩慢,總吞吐量降低。
    - 整理-清除算法會產生碎片
    - 無法處理浮動垃圾,因爲並行運行,你媽打掃房間,你一邊嗑瓜子,她只有先打掃一邊,你新產生的垃圾只有等那邊打掃完了才能過來打掃。

7.G1收集器

  範圍是整個Java堆,將新生代,老年代劃分爲多個大小相等的Region,G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收空間大小以及所需時間),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region
在這裏插入圖片描述
  大致分爲以下幾個步驟
    1.初始標記
    2.併發標記
    3.最終標記
    4.篩選回收

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章