垃圾收集與內存分配策略
1. 垃圾收集
(1)引用計數法:?
????????- 給對象維護一個引用計數器, 當對象被引用時, 計數器加一;??
????????- 當引用失效時, 計數器減一, 當計數器歸零時, 回收該對象;
????????- 該方法實現(xiàn)簡單, 判定效率也高, 但是主流虛擬機并沒有使用該方法, 因為它難以處理循環(huán)引用的問題;
(2)可達性分析法:

?
????????-?通過一系列的“GC Roots”對象作為起點進行搜索,如果在“GC Roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的,比如Object5, Object6, Object7處于不可達狀態(tài). ?
????????- 不過要注意的是被判定為不可達的對象不一定就會成為可回收對象。被判定為不可達的對象要成為可回收對象必須至少經歷兩次標記過程,如果在這兩次標記過程中仍然沒有逃脫成為可回收對象的可能性,則基本上就真的成為可回收對象了.
其中可作為GC Roots的對象:??
? ? - 虛擬機棧(棧幀中的本地變量表)中的引用的對象.
? ? - 方法區(qū)中類靜態(tài)屬性引用的對象 ? ?
? ? - 方法區(qū)中常量引用的對象
? ? - 本地方法棧中JNI(即native方法)引用的對象
兩次標記的過程:
可達性算法中不可達的對象, 并不一定非死不可, 他其實是被判緩刑. 可達性分析后發(fā)現(xiàn)一個對象沒有與GC Roots相連的引用鏈, 它就會被打上第一次標記, 同時對它進行一次篩選, 判斷是否有必要執(zhí)行finalize(),該方法從Object繼承, 如果該對象執(zhí)行過一次 finalize()或者沒有重寫過finalize(),那它將被第二次標記, 二次標記后將被回收.如果該對象被判斷為有必要執(zhí)行finalize(),它將被放入F-Queue隊列, 有一個低優(yōu)先級的線程去執(zhí)行這些對象的finalize(), 虛擬機承諾會觸發(fā)這些finalize方法, 但不保證會等到它結束(finalize可能執(zhí)行緩慢,甚至發(fā)生死循環(huán)),如果在finalize中該對象成功自救(把自己賦值給某個成員變量或靜態(tài)變量), 他將不會被二次標記, 不會出現(xiàn)在即將被回收的對象集合.?
**每一個對象最多只有一次finalize()自救機會, 盡量不要使用finalize(),它執(zhí)行時不確定性大, 對象的執(zhí)行順序未知, 由于低優(yōu)先級線程負責, 虛擬機也不會一直等它,它甚至都不一定能執(zhí)行結束**
1.1 強引用, 軟引用, 弱引用, 虛引用
引用: reference類型的數據中存儲的數值代表另一處內存的起始地址.
這種定義下, 一個對象只存在引用或沒有被引用兩種狀態(tài).
JDK1.2之后, 引用的概念增強了;
- 強引用:
? ? ? Object a=new Object();這樣的引用就是強引用, 強引用只要存在,對象就不會被回收
- 軟引用:
? ? ? 軟引用引用的對象只有在內存即將不足時才會被回收, 回收后仍然空間不足才會發(fā)生內存溢出.(可以用來實現(xiàn)對內存敏感的高速緩存)
- 弱引用:
? ? ? 弱引用的對象只能存活到下一次GC之前, 無論是否內存空間不足, 一旦發(fā)生GC, 它都將被消滅
- 虛引用:
? ? ? 最弱的引用關系, 弱到你甚至不能通過虛引用獲得對象實例, 虛引用的存在也不會影響對象的生存.它的存在是當這個對象被回收時收到一條通知(即如果你不想操作一個對象,但是關心它是否被回收的話, 可以使用).
1.2 方法區(qū)的回收
- 后續(xù)JDK版本的方法區(qū)移除(改為本地內存的元空間), 但是方法區(qū)本就是一種功能上的要求,可以使用不同手段實現(xiàn), 元空間只是換了一種實現(xiàn)方式.
- 虛擬機并沒有要求對方法區(qū)必須要有回收機制, 因為這部分回收效率較低, 不會有太多的可回收資源, 實現(xiàn)垃圾收集也會增加GC的復雜度.
- 該區(qū)域可回收的資源主要有, 廢棄常量和無用的類信息, 常量的回收較簡單, 當常量未被引用時回收(判斷是否被引用可以參照堆的引用判定方法). ?
但是類的回收就很困難, 原因:
1. 類的所有實例都被回收
2. 類的classLoader被回收
3. 類的class對象未被任何地方引用, 沒有被任何地方的反射使用.
由于第三方類的加載大部分通過系統(tǒng)類加載器AppClassLoader負責, 該加載器很顯然不能被回收, 所以類的卸載只能考慮自己的classLoader加載的.如果大量使用動態(tài)代理等頻繁自定義classLoader來加載大量的類時, 需要有類的卸載功能, 來確保不發(fā)生內存溢出.
1.3 垃圾收集算法
1.3.1 標記清除算法
標記需要回收的對象, 標記完成后再執(zhí)行統(tǒng)一回收.
該算法作為基礎回收算法, 有兩個問題需要處理:
1. 效率問題: 標記和清除兩個過程效率都不高
2. 空間問題: 清理完成后,會產生大量的空間碎片, 一旦之后的程序需要分配較大的空間,找不到合適大小的空間,就需要執(zhí)行一次新的垃圾收集.
1.3.2 復制算法
解決效率問題, 把內存空間分為兩塊, 只使用其中一塊, 當要進行回收時, 把這塊空間中不需要回收的對象復制到另一塊空間中, 然后格式化被使用的這一塊.
這種算法的問題:
1. 如果沒有很多對象回收, 復制的對象的過程會很緩慢, 因為存活的太多了.
2. 空間浪費很大, 只能使用空間的一半, 另一半準備用來進行復制存活者
上面的這兩個問題, 在新生代中其實都不是問題, 根據IBM的調查, 新生代對象98%都是“朝生暮死”, 需要復制的只有剩下的2%;
由于只有少量對象存活, 內存區(qū)域劃分時就沒必要對半分了, 劃分的使用區(qū)Eden, 復制存放區(qū)是兩個Survivor,?HotSpot整個新生代Eden和兩個survivor默認比例8:1, 每次使用Eden和一個survivor, 把存活者放入另一個survivor, 然后清空Eden和剛用過的survivor.
但是survivor由于空間有限, 一旦發(fā)生極端情況, 比如Eden中對象全部存活, 只能依靠分配擔保.
當survivor空間不足, 將把對象直接放入老年代, 老年代中的對象通常不是這么來的.
1.3.3 標記整理算法
老年代很顯然不能使用復制法(每次回收存活大量對象, 沒有其他空間可以用來擔保), 在該區(qū)域回收時將所有保留的對象移動到空間一端順序排列好, 然后清理端邊界之外的對象.
1.3.4 分代收集算法
堆劃分為新生代和老年代, 新生代對象存活時間短, 使用復制法, 付出少量的復制成本完成收集; 老年代對象存活數量多, 使用標記整理法, 不需要額外空間分配擔保, 不需要像復制法一樣復制所有存活對象;
1.4 垃圾收集算法需要解決的問題
1.4.1 枚舉根節(jié)點
可達性算法中的根結點GC Roots是一個集合, 并不是說內存中所有的對象都指向同一個根節(jié)點, 而是有很多根節(jié)點, 而生產環(huán)境中的應用可能僅僅方法區(qū)就幾百兆.
全局引用(例如常量和類靜態(tài)變量等)和執(zhí)行上下文(例如本地方法棧)都可以作為GC Root節(jié)點.
另外, 可達性分析的過程需要整個內存區(qū)域的引用之間的關系陷入停滯才能進行, 這需要所有的Java執(zhí)行線程停頓
(sun將這種過程稱之為stop the world)? ? ?來!大聲和我念!雜瓦魯多~~~
好在JVM使用準確式GC策略, 對于內存數據而言, 數據是否屬于引用類型是明確可知的, 我們不需要在系統(tǒng)停頓之后搜索整個執(zhí)行上下文和全局引用來確定對象是基本類型還是引用類型. JVM使用特定的數據結構記錄引用類型的分布情況(類加載時, Hotspot在OopMap中記錄這個類的對象的什么偏移量上是什么類型,類加載時對象并未創(chuàng)建,這些數據是計算得到的,JIT編譯時也會在特定位置記錄棧和寄存器中的引用的位置).
1.4.2 安全點
解決了如何分辨引用和尋找所有的GC Root, 但是OopMap中的引用關系由于代碼的運行隨時都可能變化, 我們需要實時的更新OopMap嗎?
實際上不需要隨時更新OopMap, 引發(fā)OopMap更新的代碼指令非常多, 實時更新代價很大, 而且OopMap是為垃圾回收服務的, 我們不會一直進行回收, 沒必要為每一條指令去維護OopMap. 只有在到達一些“安全點”時, 才記錄引用信息, 并且程序也是在此處才能停頓. GC并不是在安全點才開始執(zhí)行, 它是屬于一個獨立的線程, 只是執(zhí)行時會去關注其它線程是否停頓.
安全點的數量太少會導致GC長時間等待, 太多時程序運行負擔很大.
安全點的選定基本上是以程序“是否具有讓程序長時間執(zhí)行的特征”為標準進行選定的--因為每一條指令執(zhí)行的時間都非常短暫,程序不太可能因為指令流長度太長這個原因而過長時間運行, “長時間執(zhí)行”的最明顯的特征就是指令序列復用,例如方法調用、循環(huán)跳轉、異常跳轉等, 所以具有這些功能的指令才會產生安全點.
安全點的位置: 循環(huán)末尾,方法返回之前,調用方法之后,拋異常的位置.
要理解為啥選這些位置, 需要結合前文提到的class文件javap反編譯之后的指令.
我們的安全點選擇時要防止兩個安全點“離得太遠”, 比如選擇循環(huán)跳轉(這里說的是循環(huán)跳轉不是整個循環(huán), 而是單次循環(huán)的末尾位置),在循環(huán)的末尾是goto到循環(huán)開始的位置這樣一條指令, 就是為了防止我們用兩個安全點把“循環(huán)整個包住”,?否則一個循環(huán)10萬次, 那很遺憾, GC得一直等到循環(huán)結束才等到下一個安全點;
選擇方法返回之前設置安全點的原因也同理, 我們不能讓兩個安全點把“整個方法包住”, 因為方法執(zhí)行可能很久, 在方法返回之前, 就保證了一個方法至少含有一個安全點, 而不會發(fā)生一個安全點產生后, 進入一個方法, 要一直等待方法結束才會產生另一個安全點的情況.
方法調用之后可以產生, 原因也很簡單, 如果說是遞歸1000次, 我們如果只是在方法返回之前加了一個安全點的話, 那一定會產生如下情況, 設想遞歸方法開始運行, 第一個安全點是遞歸運行到最內層(第1000層)產生, 在那之前的每一層其實都不會結束, 都不會返回, 都不會產生安全點, 這也是一種安全點距離過遠的情況, 加上方法調用后產生就可以避免該問題了.
**如何到達安全點?**
1. 搶先式中斷: 也叫被動中斷, 這種方式不需要線程主動配合, 它們只需要聽命令就行, GC發(fā)生時, 中斷所有線程, 發(fā)現(xiàn)有不在安全點的, 恢復那個線程, 讓它跑到安全點.
2. 主動式中斷: 這里的主動指的是線程主動, 設置一個標識, 各個線程在執(zhí)行時輪詢該標識, 發(fā)現(xiàn)標識為真時, 主動將自己中斷掛起, 而輪詢標識的地方和安全點是重合的.
1.4.3 安全區(qū)
安全點只解決了程序運行時, 可以在GC時很快進入安全點這樣的就緒位置.
而對于sleep和blocked的線程, 無法執(zhí)行安全點機制的邏輯, JVM顯然也不會一直等到這些線程運行起來.
安全區(qū)將會解決這個問題. 在安全區(qū)中引用關系不會發(fā)生變化, 此處可以隨時進行GC. 線程進入安全區(qū)后, 會被打上標識, 此時如果發(fā)生GC, JVM可以不用管處于安全區(qū)的線程, 安全區(qū)中的線程如果要離開安全區(qū)需要先檢查根節(jié)點枚舉是否已經完成, 完成枚舉, 則可以離開, 線程繼續(xù)進行;否則必須等到可以離開的信號為止.
1.5 垃圾收集器

1.5.1 Serial
該收集器是一款單線程收集器, 如上圖用于處理新生代垃圾收集(使用的是復制算法).
在進行收集時會暫停所有工作線程, 所以在Server模式下, 不要使用這個收集器, 巨大的新生代空間會有很明顯的停頓, 但是Client模式下可以使用, 但運行線程都暫停時, 垃圾收集線程等于可以“集中火力”, 單個GC線程工作也不存在線程切換等額外消耗, 沒有任何線程與其競爭任何資源了, Client模式下新生代空間并不大, 停頓時間可以控制在一個不容易被感知的范圍.
1.5.2 ParNew
這也是一款新生代收集器, 是Serial的多線程版本(使用復制算法), 在工作時也必須暫停所有工作線程, 多線程處理垃圾收集工作, 在單核處理器上, 該收集器的性能不會比Serial強, 因為該做的工作一點沒少, CPU還額外需要進行線程切換. 但在核心數量多時, 可以充分發(fā)揮多核優(yōu)勢, 多個核心共同處理收集工作, 可以提高收集速度, 壓低停頓時間.
該收集器是CMS收集器(老年代收集器)默認使用的新生代收集器, Parallel Scavenge不能與CMS配合, 當老年代使用CMS, 新生代只能在Serial和Parnew中選一個. Parnew默認開啟的收集線程等于CPU核心數.
1.5.3 Parallel Scavenge
多線程收集器, 新生代收集器, 使用復制算法, 不能與老年代收集器CMS聯(lián)用, 其特殊之處是可以進行吞吐量的控制.
Parallel Scavenge不能與CMS配合也是因為這兩個指標互相沖突. 停頓時間的縮短是犧牲了吞吐量和新生代空間獲得的.(吞吐量等于業(yè)務代碼運行時間/總時間,總共運行100分鐘,其中1分鐘處理垃圾,吞吐量就是99%).
新生代空間越小, 停頓時間越短, 但是這里指的是單次停頓時間, 新生代空間縮小, 將導致GC更加頻繁, 總的GC時間會更大(吞吐量將降低),原來10秒一次GC, 每次100毫秒, 現(xiàn)在5秒一次, 而停頓時間一般都會大于50毫秒, 頻繁的GC必然會導致定位垃圾時檢索到很多不回收的空間, 這部分時間在每一次GC里都要重新檢查一遍,所以頻繁GC在總的GC運行時間上大概率是虧本買賣(這個問題就像是Mysql插入100萬條數據, 關閉自動提交后總時間會下降很多一樣的原理, 越頻繁的折騰, 每一次的時間縮短彌補不了次數的增加導致的總時間的膨脹).
而吞吐量越大, 運行GC的總時間越短, 單次停頓的時間會更長.
提高吞吐量和停頓時間短各有優(yōu)勢, 停頓時間短用戶交互更好, 吞吐量高能更有效的利用CPU時間.
控制吞吐量的參數:
? ? -XX:MaxGCPauseMillis 最大停頓時間(大于0的毫秒數), 虛擬機將盡可能保證停頓時間不超過這個數.
? ? -XX:GCTimeRatio GC時間占比(1/(1+X),X屬于開區(qū)間(0,100)), 例如設置為19, GC占用時間就是1/(1+19)=5%, 默認99, 1/(1+99)=1%, 即1%的GC時間
Parallel Scavenge的另一個特點是: GC自適應調節(jié).
使用參數: -XX:+UseAdaptiveSizePolicy 打開后,系統(tǒng)將自動配置新生代大小(-Xmn), Eden和Survivor區(qū)的比例(-XX:SurvivorRatio), 晉升老年代對象的年齡(-XX:PretenureSizeThreshold)等參數,以獲得最合適的吞吐量.開啟該功能在使用GCTimeRatio為虛擬機設立一個目標, 它將自動完成其他設置工作.
1.5.4 Serial Old
單線程的老年代收集器, 使用標記-整理算法, 適用于Client模式.

使用場景:
1. 與新生代收集器Parallel Scavenge配合
2. CMS收集器的備用收集器, 并發(fā)收集發(fā)生Concurrent Mode Failure時, 切換到Serail Old.
1.5.5 Parallel Old
多線程老年代收集器, 使用標記整理算法.
在1.6之后可用, 在此之前, 一旦新生代使用Parallel Scavenge, 那么老年但只能使用Serial Old, 而該收集器的單線程工作模式, 其性能上限很低, 最終導致即使使用Parallel Scavenge整體吞吐量也不高.
Parallel Scavenge + Parallel Old的組合在關注吞吐量以及CPU資源敏感時很強勢.

1.5.6?CMS
并發(fā)收集, 老年代收集器, 使用標記-清除算法, 致力于降低停頓時間.

CMS收集過程:
1. 初始標記(需要停頓),檢索GC Roots能直接關聯(lián)到的對象, 即找到引用鏈的根結點, 這個根結點是有很多的, 并不是所有對象都會鏈接到同一個根上, 這些GC Root并沒有一個容器來存儲它們, GC Root直接關聯(lián)的對象, 我的理解就是與根直接相連的對象, 如下圖:

2. 并發(fā)標記, 長耗時, 與用戶線程一起運行, 不需要停頓, 進行GC Roots tracing, 這是在按照引用鏈繼續(xù)向下查找, 顯然這需要遍歷眾多的引用樹.因為是并發(fā)運行的,在運行期間會發(fā)生新生代的對象晉升到老年代、或者是直接在老年代分配對象、或者更新老年代對象的引用關系等等,對于這些對象,都是需要進行重新標記的,否則有些對象就會被遺漏,發(fā)生漏標的情況。為了提高重新標記的效率,該階段會把上述對象所在的Card標識為Dirty,后續(xù)只需掃描這些Dirty Card的對象,避免掃描整個老年代,并發(fā)標記階段只負責將引用發(fā)生改變的Card標記為Dirty狀態(tài),不負責處理;
3. 重新標記(需要停頓), 在并發(fā)收集過程中發(fā)生變動的對象重新修正它們的標記記錄.
4. 并發(fā)清除, 長耗時, 清理時用戶線程可運行.
CMS使用標記清除算法, 而它與其他收集器在標記這一步區(qū)別很大, 其他收集器無論單線程還是多線程并發(fā), 它們在標記時都是全程停頓. 要壓低停頓時間就要把“標記”這個長過程, 進一步細分, 要想在媽媽打掃房間時可以繼續(xù)扔紙屑就要考慮在打掃完后把剛扔的紙屑再補掃(重新標記), 而補上的這次打掃是必須停頓的, 即在新的打掃時是不準再扔了的.
然而, CMS也存在它的問題:
1. 對CPU資源敏感, 任何多線程并發(fā)的程序都有這個問題, 在并發(fā)階段, 雖然用戶線程沒有停頓, 但是GC線程和用戶線程都在運行(CPU不再像平時一樣只處理業(yè)務代碼了), 吞吐量下降.虛擬機提供了一種應對方法, “增量式并發(fā)收集”這是一種如同單核CPU時代搶占式的模擬多任務的策略, 讓GC線程和用戶線程交替執(zhí)行, 互相競爭CPU資源, GC爭奪到的資源越少, 用戶線程越是可以正常運行, 但吞吐量會越低, 實際證明該策略并不好用, GC要做的工作一點也沒少, 而且在競爭過程中又引入了更多的線程切換.
2. 無法處理浮動垃圾, 在并發(fā)階段, 用戶線程也會繼續(xù)產生垃圾, 而此時標記過程不能把這部分垃圾統(tǒng)計進去, 只能等下次GC時處理, 這部分就是浮動垃圾. 由于并發(fā)過程用戶線程的運行, CMS必須更早的開始進行GC, 而不能像其他的收集器一樣在老年代快被填滿時進行. 如果并發(fā)運行時, 發(fā)生空間不足, 就會出現(xiàn)Concurrent Mode Fail, 此時會切換到Serial Old收集器.
3. 標記清除算法, 會產生空間碎片, 這些碎片化的空間很難使用, 特別是要分配大對象時. 空間不足時, 會提前進行一次Full GC.
? ? ? ? -XX:+UseCMSCompactAtFullCollection開關參數, 開啟時,CMS在即將進行Full GC時, 會執(zhí)行空間壓縮進行內存整理, 該過程不能并發(fā), 所以停頓時間會變長, 默認開啟
? ? ? ? -XX:CMSFullGCsBeforeCompaction設置多少次不帶整理的Full GC之后來一次帶整理的, 默認0, 每次Full GC后都整理
1.5.7 G1
并發(fā)收集, 同時適用于新生代和老年代, 使用標記整理算法, 可預測的停頓.
這種標記整理算法是從宏觀角度看的, G1把新生代和老年代進行了更加的細分, 劃為很多區(qū)域, GC發(fā)生在這些區(qū)域時, 其實是使用復制算法.
G1收集器對新生代和老年代進行細化, 每一個區(qū)域都是一個回收區(qū), 每一個回收區(qū)自帶一個集合Remembered Set, 這個集合用來保存非本區(qū)域的對象, 而且本區(qū)域的對象一定被Remembered Set中的對象引用, 即系統(tǒng)會在引用類型的數據發(fā)生寫操作時, 先中斷該操作, 檢查被引用的對象是否是跨區(qū)的對象, 如果是就通過CardTable把相關引用的信息寫到它引用的那個對象的區(qū)域的Remembered Set中, 這樣就可以在對一個區(qū)域進行GC時, 把Remembered Set加入GC Roots的枚舉中即可保證不發(fā)生遺漏.

G1回收過程:
1. 初始標記:除了檢索GC Roots直接關聯(lián)的對象之外, 還需要修改TAMS(Next Top at Mark Start)的值, 在下一個階段并發(fā)標記時, 用戶線程能夠使用正確的Region來創(chuàng)建新對象, 該過程需要停頓.
2. 并發(fā)標記:可達性分析階段, 不需要停頓, 與用戶線程并發(fā)執(zhí)行.
3. 最終標記:與CMS一樣, 并發(fā)標記階段會產生引用關系變動, 引用關系的變動將導致標記變動, 這部分的標記記錄將被記錄到Remembered Set Logs中, 最終標記階段將把Remembered Set Logs中的數據合并到Remembered Set中, 該部分一定需要停頓, 可以并發(fā)執(zhí)行, 我在這里的理解是Remembered Set已經進行過標記處理了, 其中的元素在初始標記進行GC Roots關聯(lián)對象的遍歷時就連帶著這部分一起檢查過了, 甚至這部分的元素也會被作為GC Roots(我猜的),并發(fā)過程中的變動會不會也包括這部分的變動呢, 變動發(fā)生, 最終標記階段對變動部分進行重標.
4. 篩選回收:在這個階段將各個Region按照回收價值和成本進行排序, 我們前面說過G1可以進行可預測的停頓, 系統(tǒng)會按照設置的停頓時間組織相應的區(qū)域優(yōu)先處理甚至只處理一部分區(qū)域, 力圖達到該目標. 該階段可以與用戶線程一起執(zhí)行, 但是由于只回收一部分區(qū)域, 只進行GC可以提高回收效率.
G1的問題:
1. 浮動垃圾的問題依然存在.
2. 該收集器也是以壓制停頓時間為目標, 如果關注吞吐量這一指標, 它并不比Parallel強.
1.6 三色標記法
CMS的可達性分析實現(xiàn)就是把對象標為三種顏色(黑,灰,白):
黑色:不回收, 本身和其下所有引用(這里說的是直接引用)都已被掃描.
灰色:本身被掃描但至少還有一個直接引用未被掃描
白色:沒有被掃描, 或者不可達
過程:
1. 所有對象一開始都是白的
2. GC Roots其直接引用的對象設置為灰色
3. 遍歷灰色的所有引用, 遍歷完成后設置為黑色, 其引用設置為灰色
4. 重復3, 直至沒有灰色
5. 回收所有白色
在STW時, 該過程可以保證沒有問題.
但如果是有用戶線程在運行, 則可能發(fā)生漏標和錯標:
漏標: 本來該標識為黑色, 沒有標為黑色, (在并發(fā)時, 對象重新被某個線程使用到),將導致錯刪被使用的對象

錯標: 本來不該是黑色, 結果被涂成黑色, (并發(fā)時, 對象的引用徹底消失了),會少刪了該對象.

1.7 GC日志
? ? -XX:PrintGCDetails 打印GC日志
日志格式如下:

不同的收集器, 年輕代老年代的名字會有區(qū)別, 上面的PSYoungGen是因為使用的Parallel Scavenge.
GC: 就是新生代或老年代發(fā)生一次GC
Full GC: 全內存區(qū)都發(fā)生, 可以看到后面有各個代的收集數據, 如果是調用System.gc()會顯示[Full GC(System)]
后面是各個區(qū)域的使用空間的大小變化, A->B(C), 從使用A那么多變成使用B那么多, 總空間還有C那么多. 通常后面還會有一個匯總, 總的內存使用, 如上面, 第三行6491K->6235K(163328K)就是匯總了前面的年輕代和老年代, 由于使用jdk1.8, 元空間屬于直接內存的一部分, 獨立出jvm運行時數據區(qū)了.
[Times:]: 表示使用的時間, 依次是: 用戶態(tài)消耗的CPU時間, 內核態(tài)消耗的CPU時間, 墻鐘時間(實際用時).
墻鐘時間除了包括CPU消耗的, 還有磁盤IO、線程阻塞等等耗時, 當CPU是多核時, CPU耗時是每個核心的相加, 所以它超過墻鐘時間是完全正常的.
1.8 垃圾收集常用參數

2. 內存分配與回收策略
2.1 對象優(yōu)先在Eden分配
? ? -Xms20M 初始大小20M
? ? -Xmx20M 最大20M
? ? -Xmn10M 年輕代10M (Eden+survivor)
? ? -XX:+PrintGCDetails 打印GC日志
? ? -XX:SurvivorRatio=8 (Eden:一個survivor=8:1)
? ? -XX:+UseSerialGC 使用Serial+Serial Old
? ? -Xloggc:gc.log GC日志文件輸出路徑

設置堆空間初始大小20M, 最大20M, 不能擴展, 年輕代10M, Eden:8M, 每個Survivor:1M, 分配對象大小2M,2M,2M,4M.

運行代碼, 日志顯示一次GC:Allocation Failure, 這是Minor GC, 只回收了DefNew即新生代, 因為新生代空間10M, 其中可以使用的是8+1, 一個survivor要作為GC時的復制區(qū), 分配第四個對象時, 新生代空間不足, 做了一次Minor GC后, 分配到老年代.
2.2 大對象直接在老年代
還有什么比一個大對象更讓人崩潰的嗎? 一群“朝生夕滅”的“短命大對象”.
? ? -XX:PretenureSizeThreshold=10240 超過該值直接老年代, 單位不能寫(單位B), 只支持Serial 和 Parnew
2.3 長期存活的對象進入老年代
分代收集, 需要分辨哪些對象放在老年代, 哪些放在新生代, 每個對象維護一個年齡計數器 ( 對象的對象頭的mark word中 ), 當經過一次MinorGC后, 對象還存活, 并且survivor還有空間容納, 將該對象移動到survivor, 對象年齡加一, 當年齡增加到一定閾值(默認15), 將晉升到老年代.
? ? -XX:MaxTenuringThreshold 設置年齡閾值
2.4 動態(tài)年齡判定
為適應不同程序的內存狀況, 虛擬機并不要求嚴格遵守年齡閾值才能晉升, 如果survivor中某個年齡的對象大小之和超過survivor的一半, 那么年齡大于等于該年齡的對象就可以直接晉升.
2.5 空間分配擔保
老年代的連續(xù)空間大于新生代對象的總大小或者歷次晉升的平均水平就進行Minor GC, 否則進行Full GC.