垃圾收集器和内存分配策略
說明:本篇文章是在閱讀《深入理解Java虛擬機》過程中的一些筆記和分析,由于本人能力有限,如果有書寫錯誤的地方,歡迎各位大佬批評指正!我們互相交流,學習,共同進步!
該項目的地址:https://github.com/xiaoheng1/jvm-read
GC 需要考慮三件事:
(1)什么樣的對象需要回收?
(2)什么時候回收?
(3)怎么回收?
我們知道:程序計數器、棧、本地方法棧這三個區域的生命周期和線程是一樣的,所以這塊的內存的回收可以不用考慮. 關于棧的內存分配,棧中局部
變量表的大小,一般來說在編譯器就可知,所以這塊的內存分配我們也可以不用考慮. 我們需要專注的是是對堆內存和方法區內存的分配和回收.
我們現在來看第一個問題,什么樣的對象需要被回收?
我們的答案肯定是沒有用的對象. 那如何確定這個對象是否有用了?引用計數法.
引用計數法的思路是:給對象中增加一個引用計數器,每當有一個地方引用它時,引用計數器 +1,當引用失效時,引用計數器 -1. 任務時候,引用
計數器為 0,則說明對象不可能在被使用.
引用計數器實現雖然簡單,但是無法解決相互引用的問題,例如:
A a = new A();
B b = new B();
a.instanceB = b;
b.instanceA = a;
a = null;
b = null;
那我們說下第二種思路:可達性分析. 可達性分析是說通過一系列稱為 “GC Roots” 的節點出發,開始向下搜索,搜索走過的路徑稱為引用鏈,任意
一個對象到引用鏈不可達,則說明該對象不可用.
那 Java 中可作為 GC Roots 的節點有哪些了?
(1)虛擬機棧中引用的對象
(2)方法區中類靜態屬性所引用的對象
(3)方法區中常量所引用的對象
(4)本地方法棧中引用的對象
無論是通過引用計數器,還是可達性分析來判斷對象是存活,都和引用有關. Java 中的引用可以分為四種:強引用、軟引用、弱引用、虛引用.
(1)強引用,類似這樣的 A a = new A(); a 持有的就是強引用. 強引用只要存在,GC 就不會回收被引用的對象.
(2)軟引用,用來描述一些有用,但是非必須的對象. 在系統將要發生內存溢出異常之前,會把這些對象進行第二次回收,如果還是沒有足夠內存,才會
拋出內存溢出異常.
(3)弱引用,用來描述一些非必須的對象,只能存活到下一次發生 GC 之前. 也就是說,當發生 GC 時,無論系統中內存是否足夠,這類對象都將被
回收.
(4)虛引用,它是最弱的一個引用,它只是用在對象被回收的時候,收到一個系統通知.
那我們在來說下,當發現一個對象到引用鏈沒有路徑時,這個對象是否必須被回收?其實不是的. 確定一個對象確實死亡有至少有兩次確認.
(1)該對象不可達
(2)在該對象不可達的基礎上做一次篩選,看該對象的 finalize() 方法是否被調用,如果該對象沒有重寫 finalize() 方法或者 finalize()
方法被調用,則宣告該對象死亡,可以進行回收.
如果該對象被判定需要執行 finalize() 方法,那么該對象會被加入到 F-Queue 的隊列中,稍后由虛擬機建立的一個低優先級的線程進行執行.
finalize() 方法是對象逃脫死亡的最后一次機會. 如果對象在 finalize() 方法中完成自我救贖,那么它將被移除 “即將回收的集合”,否則,它
將等待被回收.
值得注意的是:finalize 方法只會被調用一次,同時該方法的執行代價大,不確定性高,所以大家最好是不要使用該方法.
回收方法區
在 JDK1.8 以前,方法區在 HotSpot 虛擬機中被稱為永久代. 但是這并不意味著方法區不會發生垃圾回收,只是說回收的比率比新生代低.
永久代的垃圾回收主要回收兩部分:
(1)廢棄常量
(2)無用的類
回收廢棄常量:加入字面量 “hello world” 進入到了常量池,但是系統中沒有任何一個 String 對象叫做 “hello world”, 換句話說,沒有任何
一個字符串對象引用常量池中的 “hello world” 常量,也沒有其他地方引用這個字面量,那么在發生 GC 時,且有必要時,這個 “hello world”
將會被清理出常量池.
看到這塊是否很疑惑?什么叫做沒有其他地方引用這個字面量了?比如說:System.out.println(“hello world”) 這個算其他地方引用了這個
字面量嗎?還有就是什么叫有必要了?難道說發生 GC 后,還要滿足一定的條件才能回收這個廢棄常量嗎?
我覺得吧,對于 System.out.println(“hello world”) 這個應該算在其他地方使用到了這個 “hello world” 常量.
new String(“hello world”) 會先檢查字符串常量池中是否存在 “hello world”,如果不存在的話,先在字符串常量池中創建 “hello world”,
然后在堆中創建一個 “hello world” 的字符串對象.
判定一個類是否是"無用類"的條件相對比較苛刻. 需要滿足如下條件:
(1)該類的所有實例都已經被回收
(2)加載該類的 ClassLoader 已經被回收
(3)該類對應的 java.lang.Class 對象沒有在任何地方被引用.
滿足上面三個條件,只是說這個類可以被回收,它不像對象那樣,沒有就被回收. 是否對類進行回收,HotSpot 虛擬機提供了 -Xnoclassgc 參數進行
控制,還可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看類加載和卸載信息.
垃圾收集算法
(1)標記-清除算法:它分為兩個階段,第一階段標記,首先標記所有需要被回收的對象,第二階段回收被標記的對象. 雖然這個算法簡單,但是它的
標記和清除效率都不高,且會產生碎片.
(2)復制算法:將內存空間分為兩塊,一塊用于存放新生對象,另一塊存放存活對象.當一塊內存滿了后,就將存活的對象拷貝到另一塊上去,然后清空
以前那塊的內存. 這個方法的好處不會產生內存碎片,缺點是內存利用率不高,每次只能使用一半的內存.
新生代中,98% 的對象都是朝生夕死,所以復制算法劃分空間的時候,并不需要按照 1:1進行劃分.
Eden : From Survivor : To Survivor = 8 : 1 : 1.
每次使用的時候,使用 Eden + 一個 Survivor 的空間,換句話說,新生代的可利用空間在 90%,只有 10% 的空間會被浪費. 但是是不是有
一個疑惑?10% 的空間夠存放活著的對象嗎?所以這有一個擔保(老年代進行擔保). 如果活著的對象過多,一個 Survivor 空間不夠,那么這些對象
將直接通過分配擔保機制進入到老年代.
現在來說下為啥不使用 0 個 Survivor ?如果使用 0 個 Survivor 的話,那么新生代滿了后,觸發GC,活著的對象進入到老年代(可能這些對象
在下一次GC的時候就會被回收),很快,老年代也滿了,Full GC 發生的頻率大大加大.
為啥不是一個 Survivor 了?如果是一個 Survivor 的話?那 Eden 和 Survivor 和比例如何劃分了?假設設置為 8:1,那么 Survivor 的
空間很容易被填滿,觸發 Minor GC. 這樣總體上沒有降低 Minor GC 的頻率,而且 GC 的時間間隔也不平均,如果將 Eden : Survivor 設置
為 1 : 1 的話,內存利用率不高.
使用兩個 Survivor 的話,為啥就可以了?使用兩個的話,在觸發 GC 時,會回收 Eden 和 Survivor 區域內的對象,這樣活著的對象更加少,
所以效率更高.
(3)標記-整理算法:第一階段也是標記,第二階段是讓活著的對象都向一段移動,然后直接清理掉端邊界以外的內存.
(4)分代收集算法:根據對象的特性,劃分為不同的代,比如新生代和老年代. 針對不同代對象的特性,采用不同的回收算法. 例如:對新生代采用
復制算法,對老年代采用標記-清除或標記-整理算法.
Hotspot 的算法實現
上面我們只是說了可達性分析的思路,在 HopSpot 中是如何做的了?
枚舉根節點.
現在很多應用的方法區中就有好幾百兆,如果逐個檢查這里面的引用,那么將會是非常耗時的. 在進行可達性分析的時候,系統必然在某個時間點上
進行凍結(不能我在執行可達性分析的時候,引用還是在變化,那么將無法獲得準確的分析).所以這點會導致 GC 進行時必須停止所有Java執行程序
(Stop the World).
保守式 GC:如果 JVM 不記錄任何這種類型的數據,那么它就無法區分內存中某個位置上的數據到底應當解讀為引用類型還是整型還是其他?這種
情況下實現出來的 GC 就是保守 GC. 所以在進行 GC 的時候,就需要遍歷整個內存空間,看這是不是一個指向堆中的指針,雖然這種實現方式很
簡單,但是效率太低.
目前主流的 Java 虛擬機使用的都是準確式 GC,也就是說虛擬機應當有辦法知道哪些地方存放著對象的引用. 在 HotSpot 的實現中,是使用一組
稱為 OopMap 的數據結構來達到這個目的的,在類加載完成的時候,HotSpot 就把對象內什么偏移量上是什么類型的數據計算出來,在 JIT 編譯
過程中,也會在特定位置記錄下棧和寄存器中那些位置是引用. 這樣,GC 在掃描時就可以直接得知這些信息了.
在 OopMap 的協助下,HotSpot 可以快速且準確地完成 GC Roots 枚舉,但是如果為每條指令都生成對應的 OopMap,那將會需要大量的額外空間,
這樣 GC 的空間成本將會變得很高.
下面說下 oopmap. oopmap 有一個 off 字段,我的理解是從指令開始,到 off 為止,這個 oopmap 記錄的就是這個范圍內的 oop(普通指針對象).
實際上,HotSpot 也確實沒有這么干,它只是在特定的位置記錄了這些信息,即程序執行時并非所有地方都能停頓下來執行 GC,只有達到安全點時才能
暫停. Safepoint 的選定即不能太少以至于讓 GC 等待時間太長,也不能過于頻繁以至于過分增大運行時的負荷.
所以安全點的選定基本上是以程序 是否具有讓程序長時間執行的特征 為標準進行選定的. 因為每條指令執行的時間都非常短,程序不太可能因為指令
流長度太長這個原因而過長時間運行,‘長時間執行’ 的最顯著特征是指令序列復用,例如方法調用、循環跳轉、異常跳轉等,所以這些功能的指令才
會產生 Safepoint.
對于 Safepoint 另一個需要考慮的問題是,如何在發生 GC 時讓所有程序都到最近的安全點上在停頓下來. 這里有兩種方案可供選擇:搶先式中斷和
主動式中斷,其中搶先試中斷不需要線程的執行代碼主動去配合,在 GC 發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,
就恢復線程,讓它跑到安全點上. 現在幾乎沒有虛擬機采用搶先式中斷來暫停線程從而響應 GC 事件.
主動式中斷的思想是當 GC 需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執行時主動的去輪詢這個標志,發現中斷
標志為真時就自己中斷掛起. 輪詢標志的地方和安全點是重合的,另外在加上創建對象需要分配內存的地方.
安全區域
上面其實有一個問題,針對執行程序這個可以完美實現,但是如何針對不執行的程序了?(例如:等待獲取鎖),對于這種情況,就需要安全區了.
安全區域是指在一段代碼片段之中,引用關系不會發生變化. 在這個區域中的任意地方開始 GC 都是安全的. 我們也可以把 Safe Region 看成是
Safepoint 的擴展.
當線程執行到 Safe Region 中的代碼時,首先標識自己已經進入了 Safe Region,那樣,當在這段時間里 JVM 要發起 GC 時,就不用管標識自己為
Safe Region 狀態的線程了. 在線程要離開 Safe Region 時,它要檢查系統是否以及完成了根節點的枚舉(或者整個 GC 過程),如果完成了,則
繼續執行,否則等待收到可以安全離開 Safe Region 的信號位置.
垃圾收集器
Serial + CMS
Serial + Serial Old
ParNew + CMS
ParNew + Serial Old
Parallel Scavenge + Serial Old
Parallel Scavenge + Parallel Old
G1
Serial 收集器是最基本、發展歷史最悠久的收集器,這是一個單線程收集器,它在進行垃圾收集的時候,必須暫停其他所有的工作線程,直到它
收集結束. 它是新生代收集器,采用復制算法. 它是虛擬機 Client 模式下的首選.
關于虛擬機 client 模式和 server 模式.
如果主機至少含有 2 cpu 和至少 2 GB 內存的話,會以 server 模式啟動,否則以 client 模式啟動.
server 模式和 client 模式的區別在哪里了?
1.server 模式和 C2 編譯器共同運行,更注重編譯的質量,啟動速度慢,但運行效率高,使用與服務器環境.
2.client 模式和 C1 編譯器共同運行,更注重編譯速度,啟動速度快,更適合在客戶端的版本下,針對 GUI 進行優化.
Serial Old 是老年代收集器,采用標記-整理算法. 主要意義在于給 Client 模式下的虛擬機使用. 如果在 Server 模式下,一種用途是在
JDK1.5 以及之前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的后備預案,在并發收集發生
Concurrent Mode Failure 時使用.
ParNew 其實是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其余行為和 Serial 一樣. 它是虛擬機 Server 端首選的新生
代收集器.默認情況下,它開啟的收集線程數與 CPU 的數量相同,可以使用 -XX:ParallelGCThreads 參數來限制垃圾收集的線程數.
并行:指多條垃圾收集線程并行工作,但是此時用戶線程任然處于等待狀態
并發:指用戶線程和垃圾收集線程同時執行(但不一定是并行,可能交替執行)
Parallel Scavenge 是新生代收集器,它與其他收集器的不同點在于,其他收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓狀態,而
Parallel Scavenge 的關注點是達到一個可控制的吞吐量. 所謂吞吐量就是 CPU 用于運行用戶代碼的時間與 CPU 總消耗時間的比值,即:
吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多線程和’標記-整理’算法.
CMS 是一種以獲得最短回收停頓時間為目標的收集器. 它是基于標記-清除算法實現的. 它的運作過程相對于前面幾種收集器更加復雜.
(1)初始標記
(2)并發標記
(3)重新標記
(4)并發清除
初始標記和重新標記這兩步仍然需要 STD, 初始標記只是標記 GC Root 能夠直接關聯到的對象,速度很快,并發標記階段就進行 GC Roots Tracing
的過程,而重寫標記階段則是為了修正并發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄.
CMS 收集器對 CPU 資源很敏感,在并發階段,它雖然不會導致用戶線程停頓,但是會因為占用了一部分 CPU 資源而導致應用程序變慢,總吞吐量會降低.
CMS 默認的啟動的回收線程數是(CPU 數量+3)/4,也就是當 CPU 在 4 個以上時,并發回收時垃圾收集線程不少于 25%的CPU資源. 但是當 CPU 資源
不足 4 個時,CMS 對用戶程序的影響很大,為了應對這種情況,虛擬機提供了一種 i-CMS,就是在并發標記和清理階段,讓 GC 線程和用戶線程交替
運行.
CMS 收集器無法處理浮動垃圾,可能出現 Concurrent Mode Failure 失敗而導致另一次 Full GC 的產生. 由于 CMS 并發清除階段程序運行
自然還會產生新的垃圾,這部分垃圾出現在標記過程后,CMS 無法在本次收集中處理掉它們,只好留待下一次處理. 這部分垃圾就被稱為"浮動垃圾".
也是由于垃圾收集階段用戶線程還需要運行,那也就還需要預留有足夠的內存空間給用戶線程使用,因此 CMS 收集器不能像其他收集器那樣等到老年代
幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發收集時的程序運行使用. CMS 收集器當老年代使用了 68% 的空間后就會被激活. 也可以
通過設置 -XX:CMSInitiatingOccupancyFraction 的值來提高觸發百分比.如果 CMS 運行期間預留的內存無法滿足程序需要,就會出現一次
“Concurrent Mode Failure” 失敗,這時虛擬機將啟動后背預案:臨時啟用 Serial Old 收集器來重新進行老年代的垃圾收集.
CMS 是基于標記-清除算法的收集器,收集結束后會產生大量空間碎片,為了解決這個問題,CMS 收集器提供了 -XX:+UseCompactArFullCollection
開關參數 用于在 CMS 收集器頂不住要進行 FullGC 時開啟內存碎片合并整理過程.
現在雖然解決了碎片化的問題,但是停頓時間不能太長,虛擬機提供了另一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用于設置執行多少
次不帶壓縮的 Full GC 后,跟著來一次帶壓縮的(默認值為 0,表示酶促進入 Full GC 時都要進行碎片整理)
GC 收集器
G1 是一款面向服務端應用的垃圾收集器,G1 收集器的特點
1.并行與并發:G1能重復利用多 CPU、多核環境下的硬件優勢,使用多個 CPU 來縮短 STW 停頓時間,部分其他收集器原本需要停頓 Java 線程來執行
GC 動作,G1 收集器任然可以通過并發的方式讓 Java 程序繼續執行.
2.分代收集:雖然 G1 可以不需要其他收集器配合就能管理整個 GC 堆,但它能夠采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過
多次 GC 的舊對象以獲取更好的收集效果.
3.空間整合: 與 CMS 標記-清除 算法不同,G1 整體來看是基于 標記-整理 算法實現的. 從局部(兩個 Region 之間)上來看,是基于賦值算法實現
的,但無論如何,這兩種算法都意味著 G1 在運行期間不會產生內存碎片.
4.可預測的停頓:這是 G1 相對于 CMS 的另一大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求停頓外,還能建立可預測的停頓
時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片內,消耗在垃圾收集上的時間不得超過 N 毫秒,這幾乎已經是實時 Java 的垃圾收集器
的特征了.
G1 之前的其他收集器進行收集的范圍都是整個新生代或老年代,而 G1 不是這樣,使用 G1 收集器時,Java 堆的內存布局就與其他收集器有很大差別,
它將整個Java 堆劃分為多個大小相等的獨立區域,雖然還保留有新生代和到年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分
Region 的集合.
G1 收集器之所以能簡歷可預測的停頓時間模型,是因為它可以有計劃地避免在整個 Java 堆中進行全區域的垃圾收集. G1 跟蹤各個 Region 里面的
垃圾堆積的價值大小(回收所得空間以及回收所需要時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region
(這也就是 Garbage-First 名稱的來由). 這種使用 Region 劃分內存空間以及有優先級的區域回收方式,保證了 G1 收集器在有限的時間內可以
獲取盡可能高的收集效率.
在 G1 收集器中,Region 之間的對象引用以及其他收集器中的新生代和老年代之間的對象引用,虛擬機都是使用 Remembered Set 來避免全堆掃描
的,G1 中每個 Region 都有一個與之對應的 Remembered Set,虛擬機發現程序在堆 Reference 類型的數據進行寫操作時,會產生一個 Write Barrier
暫時中斷寫操作,檢查 Reference 引用的對象是否處于不同的 Region 之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),
如果是,便通過 CardTable 把相關引用信息記錄到被引用對象所屬的 Region 的 Remembered Set 中. 當進行內存回收時,在 GC 根節點的枚舉
范圍中加入 Remembered Set 即可保證不對全堆掃描也不會有遺漏.
初始標記
并發標記
最終標記
篩選回收
初始階段僅僅只是標記一下 GC Roots 能直接關聯到的對象,并修改 TAMS(Next Top Ar Mark Start) 的值,讓下一階段用戶程序并發運行時,
能在正確可用的 Region 中創建新對象,這一階段需要停頓線程,但耗時很短. 并發標記階段是從 GC Root 開始對堆中對象進行可達性分析,找出
存活的對象,這階段耗時較長,但可與用戶程序并發執行. 最終標記階段則是為了修正在并發標記期間因用戶程序繼續運作而導致標記產生變動的那一
部分記錄,虛擬機將這段時間內對象變化記錄在線程 Remembered Set Logs 里,最終標記階段需要把 Remembered Set Logs 的數據合并到
Remembered Set 中,這階段需要停頓線程,但是可并發執行. 最后在篩選回收階段首先對各個 Region 的回收價值和成本進行排序,根據用戶所
期望的 GC 停頓時間來指定回收計劃.
理解 GC 日志
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
33.125 和 100.667 代表了 GC 發生的時間,這個數字的含義是從 Java 虛擬機啟動以來經過的毫秒數
[GC 和 [Full GC 說明了這次垃圾收集的停頓類型,而不是用來區分新生代 GC 還是老年代 GC 的. 如果有 Full,則說明此次 GC 是發生了 STW.
[DefNew、[Tenured、[Prem 表示 GC 發生的區域.
在 Serial 收集器中,新生代的名稱為 Default New Generation
如果是 ParNew 收集器,新生代的名稱為 ParNew -> Parallel New Generation
如果是 Parallel Scavenge,則新生代名稱為 PSYoungGen
3324K->152K(3712K) 含義是 GC 前該內存區域已使用容量 -> GC 后該內存區域已使用容量(該內存區域總容量)
3324K->152K(11904K) 表示 GC 前 Java 堆已使用容量 -> GC 后 Java 堆已使用容量(Java 堆總容量).
0.0031680 表示該內存區域 GC 所占用的時間,單位是秒.
user, sys, real 分別代表用戶態消耗的 CPU 時間、內核態消耗的 CPU 時間和操作空開始到結束所經過的墻鐘時間.
墻鐘時間包括各種非運算的等待耗時,例如等待磁盤 I/O、等待線程阻塞,而 CPU 時間不包括這些耗時,但是當系統有多核 CPU或者多核的話,多線程
會疊加這些時間,所以讀到的 user 或 sys 時間超過 real 是正常的.
內存分配與回收策略
Java 技術體系鎖提倡的自動內存管理最終解決兩個問題:給對象分配內存以及回收分配給對象的內存.
對象主要分配在新生代的 Eden 區上,如果啟動了本地線程分配緩沖,將按線程優先在 TLAB 上分配. 少數情況下也可能會直接分配在老年代中.
分配規則并不是百分百固定的,其細節取決于當前使用的哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置.
1.對象優先在 Eden 區上分配.
在大多數據情況下,對象在新生代 Eden 區中分配,當 Eden 區沒有足夠的空間進行分配時,虛擬機將發起一次 Minor GC.
虛擬機提供了 -XX:PrintGcDetails 這個收集器日志參數,告訴虛擬機在發生垃圾收集行為時打印內存回收日志,并且在退出的時候輸出當前的內存
各個區域分配情況.
新生代 GC(Minor GC) 發生在新生代的垃圾收集動作,因為 Java 對象大多是朝生夕死,所以 Minor GC 非常頻繁,一般回收速度較快.
老年代 GC(Major GC / Full GC) 發生在老年代的 GC,出現了 Major GC, 經常會伴隨至少一次 Minor GC(但非絕對,在 Parallel Scavenge
收集器的收集策略里就有直接進行 Major GC 的策略選擇過程). Major GC 事務速度一般比 Minor GC 慢 10 倍以上.
2.大對象直接進入老年代
所謂大對象,指的是需喲啊大量連續內存空間的 Java 對象,最典型的大對象就是那種很長的字符串以及數組, 經常出現大對象容易導致內存還有不少
空間時就提前觸發垃圾收集以獲取足夠連續的空間來 “安置” 他們.
虛擬機提供了一個 -XX:PretenureSizeThreshold 參數,令大于這個設置值的對象直接在老年代分配. 這樣做的目的是避免在 Eden 區以及在兩個
Survivor 區之間發生大量的內存復制.
PretenureSizeThreshold 參數只對 Serial 和 ParNew 兩款收集器有效,Parallel Scavenge 收集器不認識這個參數.
3.長期存活的對象將進入老年代
虛擬機給每個對象定義了一個對象年齡(Age) 計數器, 如果對象在 Eden 出生并經過第一次 Minor GC 后任然存活,并且能被 Survivor 容納的話,
將被移動到 Survivor 空間中,并且對象年齡設為 1. 對象在 Survivor 區中每熬過一次 Minor GC,年齡就增加 1 歲,當它的年齡增加到一定
程度(默認為 15 歲),就將會被晉升到老年代中. 對象晉升老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 設置.
4.動態對象年齡判定
如果在 Survivor 空間中相同年齡所有對象的總和大于 Survivor 空間的一般,年齡大于或等于該年齡的對象就可以直接進入老年代,無需等到
MaxTenuringThreshold 中要求的年齡.
5.空間分配擔保
在發送 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那么 Minor GC 是安全的,
如果不成立,則虛擬機會查看 HandlePromotionFailure 設置值是否允許擔保失敗,如果允許,那么會繼續檢查老年代最大可用連續空間是否大于
歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次 Minor GC,盡管這次 Minor GC 是有風險的;如果小于,或者
HandlePromotionFailure 設置不允許冒險,那么這時也好改為進行一次 Full GC.
新生代采用了復制收集算法,但是為了內存利用率,只使用其中一個 Survivor 空間來作為輪換備份,因此當出現大量對象在 Minor GC 后任然存活
的情況(最極端的情況在 Minor GC 后新生代所有對象都存活),就需要老年代進行分配擔保,把 Survivor 無法容納的對象直接進入老年代.
在 JDK 6 Update 24 之后,HandlePromotionFailure 這個參數不會再影響到虛擬機的空間分配擔保策略,代碼中已不再使用該參數.
總結
以上是生活随笔為你收集整理的垃圾收集器和内存分配策略的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 读白帽子讲WEB安全,摘要
- 下一篇: 水电水利建设项目水环境与水生生态保护技术