本文已經收錄到個人Github我的博客,歡迎大佬們光臨寒舍:java
個人GIthub博客git
GC
&內存分配?時代發展到如今,現在的內存動態分配與內存回收技術已經至關成熟,一切看似進入了「自動化」時代,難免發出疑問:"爲啥咱們還要了解垃圾收集和內存分配?"github
答案很簡單,當須要排查各類內存溢出/泄漏問題的時候,當垃圾收集成爲系統達到更高併發量的瓶頸的時候,咱們必須對"自動化"技術進行必要的監控和調節。面試
因此,咱們要了解下GC
&內存分配,爲工做中或者是面試中實際的須要打好基礎。算法
在瞭解對象存活的斷定以前,咱們先來了解下四種引用類型數組
StrongReference
- 具備強引用的對象不會被
GC
- 即使內存空間不足,
JVM
寧願拋出OutOfMemoryError
使程序異常終止,也不會隨意回收具備強引用的對象
SoftReference
- 只具備軟引用的對象,會在內存空間不足的時候被
GC
,若是回收以後內存仍不足,纔會拋出OOM
異常- 軟引用經常使用於描述有用但並不是必需的對象,好比實現內存敏感的高速緩存
WeakReference
- 只被弱引用關聯的對象,不管當前內存是否足夠都會被
GC
- 強度比軟引用更弱,經常使用於描述非必需對象
PhantomReference
僅持有虛引用的對象,在任什麼時候候均可能被
GC
(和弱引用同樣)緩存主要做用是爲了垃圾收集器回收時收到一個系統通知(
PhantomRefernece
類實現虛引用)安全與弱引用的區別:不一樣之處在於弱引用的
get
方法,虛引用的get
方法始終返回null
,弱引用能夠使用ReferenceQueue
,虛引用必須配合ReferenceQueue
使用數據結構必須和引用隊列 (
ReferenceQueue
)聯合使用,當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中多線程(想要了解虛引用詳細用法的讀者,能夠看下這篇文章:強軟弱虛引用,只有體會過了,才能記住)
定義:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的
然而在主流的Java虛擬機裏未選用引用計數算法來管理內存,主要緣由是它難以解決對象之間相互循環引用的問題,因此出現了另外一種對象存活斷定算法
//相互循環引用的DEMO
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/** * 這個成員屬性的意義是佔點內存,以便在GC日誌中看清楚是否有回收過 */
private byte[] bigSize =new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
複製代碼
定義:經過一系列被稱爲『GC Roots
』的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots
沒有任何引用鏈相連時,則證實此對象是不可用的
可做爲GC Roots的對象:
- 虛擬機棧中引用的對象,主要是指棧幀中的本地變量表
- 本地方法棧中
Native
方法引用的對象- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
JVM
內部的引用(基本數據類型對應的Class
對象)- 全部被同步鎖(
synchronized
關鍵字)持有的對象- 反映
JVM
內部狀況的JMXBean
、JVMTI
中的註冊的回調、本地代碼緩存等
Q:可達性分析算法中被斷定不可達的對象真的被判『死刑』了嗎?
A:在可達性分析算法中被斷定不可達的對象還未真的判『死刑』,一共至少要經歷兩次標記過程:
GC Roots
相鏈接的引用鏈,被第一次標記判斷對象是否有必要執行finalize()
方法;若被斷定爲有必要執行finalize()
方法,以後還會對對象再進行一次篩選,若是對象能在finalize()
中從新與引用鏈上的任何一個對象創建關聯,將被移除出「即將回收」的集合。
引伸:有關方法區的
GC
,可分紅兩部分
廢棄常量與回收
Java
堆中的對象的GC
很相似,即在任何地方都未被引用的常量會被GC
。無用的類
需知足如下三個條件纔會被
GC
:A.該類全部的實例都已被回收,即Java堆中不存在該類的任何實例;
B.加載該類的
ClassLoader
已經被回收;C.該類對應的
java.lang.Class
對象沒在任何地方被引用,即沒法在任何地方經過反射訪問該類的方法。
前文講了
JVM
會回收哪些對象,下文筆者將探究JVM
如何回收這些對象
Q1:三個假說是什麼?
在新生代上創建一個全局的數據結構(記憶集),將老年代劃分紅若干小塊,標識出老年代哪一塊內存存在跨代引用,
Minor GC
時,在跨代引用的內存裏的對象纔會加入到GC Roots
進行掃描
Q2:垃圾收集器一致的設計原則
Java
堆劃分出不一樣的區域,而後將回收對象依據其年齡(年齡是對象熬過垃圾收集過程的次數)分配到不一樣的區域之中儲存Q3:如何根據各個年代的特色選擇算法呢?
這三種算法,筆者將在下文爲您詳細解析
Appel
式回收分爲一塊較大的Eden
空間和兩塊較小的Survivor
空間,在HotSpot
虛擬機中默認比例爲8:1:1。每次使用Eden
和一塊Survivor
,回收時將這兩塊中存活着的對象一次性地複製到另一塊Survivor
上,再作清理。可見只有10%
的內存會被「浪費」,假若Survivor
空間不足還須要依賴其餘內存(老年代)進行分配擔保
- 『標記』和『清除』過程的效率不高
- 空間碎片太多。『標記』『清除』以後會產生大量不連續的內存碎片,可能會致使後續須要分配較大對象時,因沒法找到足夠的連續內存而提早觸發另外一次
GC
,影響系統性能
Stop The World
)解決方法:大部分時間使用標記-清除算法,當內存空間的碎片程度影響到內存分配,再使用標記-整理算法進行收集
HotSpot
算法實現&垃圾回收器接下來介紹如何在
HotSpot
虛擬機上實現對象存活斷定算法和垃圾收集算法,並保證虛擬機高效運行
主流JVM
使用的都是準確式GC
,在執行系統停頓以後無需檢查全部執行上下文和全局的引用位置,而是經過一些辦法直接獲取到存放對象引用的地方,在HotSpot
中是經過一組稱爲OopMap
的數據結構來實現的,完成類加載後會計算出對象某偏移量上某類型數據、JIT
編譯時會在特定的位置記錄棧和寄存器中是引用的位置。這樣GC
在掃描時就可直接得知這些信息,並快速準確地完成GC Roots
的枚舉
上述「特定的位置」被稱爲安全點,即程序執行時並不是在全部地方都停頓執行GC
,只在到達安全點時才暫停,下降GC
的空間成本
安全點的選定標準:可以讓程序長時間執行的地方,如方法調用、循環跳轉、異常跳轉等具備指令序列複用的特徵
使全部線程在最近的安全點上再停頓的方案:
- 搶先式中斷:無需代碼主動配合,在
GC
發生時把全部線程所有中斷,若線程中斷處不在安全點上就恢復線程,讓它「跑」到安全點上。如今幾乎沒有虛擬機實現採用搶先式中斷來暫停線程從而響應GC
事件- 主動式中斷:在
GC
要中斷線程時不直接對線程操做,而是設置一箇中斷標誌,讓各個線程在執行時主動輪詢它,當中斷標誌爲真時就本身中斷掛起
安全點機制只能保證程序執行時,在不太長的時間內遇到可進入
GC
的安全點,但在程序不執行時(如線程處於Sleep
或Blocked
狀態)線程沒法響應JVM
的中斷請求,此時就須要安全區域來解決
安全區域:引用關係不會發生變化的一段代碼片斷,在安全區域中的任意地方開始GC
都是安全的(由於引用關係不變),可看作是擴展的安全點
執行過程:
當線程執行到安全區域中的代碼時就標識一下,若是這時JVM
要發起GC
就不用管被標識的線程;
在線程要離開安全區域時檢查系統是否已經完成了根節點枚舉,若完成則線程能夠繼續執行,不然等待直到收到能夠安全離開安全區域的信號爲止
JVM
中七種回收器序號 | 收集器 | 收集範圍 | 算法 | 執行類型 |
---|---|---|---|---|
1 | Serial |
新生代 | 複製 | 單線程 |
2 | ParNew |
新生代 | 複製 | 多線程並行 |
3 | Parallel |
新生代 | 複製 | 多線程並行 |
4 | Serial Old |
老年代 | 標記整理 | 單線程 |
5 | CMS |
老年代 | 標記清除 | 多線程併發 |
6 | Parallel Old |
老年代 | 標記整理 | 多線程 |
7 | G1 |
所有 | 複製算法,標記-整理 | 多線程 |
注意併發和並行的概念:
在
GC
中:
- 並行:多條垃圾收集線程並行工做,而用戶線程仍處於等待狀態
- 併發:垃圾收集線程與用戶線程一段時間內同時工做(交替執行)
在普通情景中:
- 並行:**多個程序在多個
CPU
**上同時運行,任意一個時刻能夠有不少個程序同時運行,互不干擾- 併發:**多個程序在一個
CPU
**上運行,CPU
在多個程序之間快速切換,微觀上不是同時運行,任意一個時刻只有一個程序在運行,但宏觀上看起來就像多個程序同時運行同樣,由於CPU
切換速度很是快,時間片是64ms
(每64ms
切換一次,不一樣的操做系統有不一樣的時間),人類的反應速度是100ms
,你還沒反應過來,CPU
已經切換了好幾個程序了
對象的內存分配廣義上是指在堆上分配,主要是在新生代的
Eden
區上,若是啓動了TLAB
,將按線程優先在TLAB
上分配,少數狀況下也可能會分配在老年代中。分配細節仍是取決於所使用的GC
收集器組合以及虛擬機中與內存相關的參數的設置。如下介紹幾條廣泛的內存分配規則
Eden
分配:大多數狀況下對象在新生代Eden
區中分配,當Eden
區沒有足夠空間進行分配時虛擬機將發起一次Minor GC
- 新生代
GC
:發生在新生代的垃圾收集動做。較頻繁、回收速度也較快
老年代GC
(Major GC/Full GC
):發生在老年代的垃圾收集動做。出現Major GC
常常會伴隨至少一次的Minor GC
。速度通常比Minor GC
慢10倍以上
大對象直接進入老年代:對於須要大量連續內存空間的Java
對象(如很長的字符串以及數組),若是大於虛擬機設定的-XX:PretenureSizeThreshold
參數值將直接在老年代分配。這樣作的目的是避免在Eden
區及兩個Survivor
區之間發生大量的內存複製
長期存活的對象將進入老年代:虛擬機會給每一個對象定義一個年齡計數器,當對象在Eden
出生並通過第一次Minor
GC
後仍存活且能被Survivor
容納的話,將被移動到Survivor
空間中並將對象年齡設爲1;當對象在Survivor
區中每「熬過」一次Minor GC
年齡就+1,直至增長到必定程度(默認爲15歲
,可經過-XX: MaxTenuringThreshold
設置)就會被晉升到老年代中
動態對象年齡斷定:爲了能更好地適應不一樣程序的內存情況,虛擬機並不要求必定要達到-XX: MaxTenuringThreshold
設置值才能晉升到老年代,當Survivor
空間中相同年齡全部對象大小的總和大於Survivor
空間的一半,那麼年齡大於或等於該年齡的對象能夠直接進入老年代
空間分配擔保:在發生Minor GC
以前虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,如果,說明可確保Minor GC
是安全的,反之虛擬機會查看-XX:HandlePromotionFailure
設置值是否容許擔保失敗;若容許,會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小;若大於,將嘗試進行一次Minor GC
,若小於或者不容許擔保失敗,將改成進行一次Full GC
解釋:當大量對象在
MinorGC
後仍然存活的狀況時,須要藉助老年代進行分配擔保,把Survivor
沒法容納的對象直接進入老年代,但前提是老年代自己還有容納這些對象的剩餘空間,因爲在完成內存回收以前沒法預知實際存活對象,只好取以前每次回收晉升到老年代對象容量的平均大小值做爲經驗值,與老年代的剩餘空間進行比較,從而決定是否進行Full GC
來讓老年代騰出更多空間
恭喜你!已經看完了前面的文章,相信你對
JVM GC
&內存分配已經有必定深度的瞭解,下面,進行一下課堂小測試,驗證一下本身的學習成果吧!
Q1:垃圾回收算法你瞭解幾種?請你簡要分析一下,並說明其優缺點?
Q2:Java
的引用機制有幾種?請簡要分析下,並說明其在Android
中的應用場景有哪些?
Q3:安全點你瞭解過嗎?安全區呢?請你介紹下安全區相對安全點的優點在哪裏?
Q4:怎麼判斷對象是否存活呢?有幾種方法?
上面問題的答案,在前文都提到過,若是還不能回答出來的話,建議回顧下前文
若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力
本文參考連接: