前言:在慕課網上學習劍指Java面試-Offer直通車時所做的筆記
目錄
第一章 對象被判定爲垃圾的標準
當沒有被其它對象引用時,它就是垃圾,其佔據的內存就要被釋放,同時此對象也要被銷燬。
判斷對象不被引用的算法有:引用計數算法,可達性分析算法。
1.1 引用計數法
用過判斷對象的引用數量來決定對象是否可以被回收。
堆中的每個對象實例都有一個引用計數器,當一個對象被創建時,若該對象實例分配給一個引用變量,該對象實例的引用計數會被設置爲1,若該對象又被另外一個對象所引用,則該對象的引用計數器繼續加1,變爲2.當該對象的實例的某個引用超過了生命週期或者被設置爲一個新值時,該對象實例的引用計數便會減1。
任何引用計數爲0的對象實例可以被當作垃圾收集。
優點:執行效率高,程序執行受影響較小。
缺點:無法檢測出循環引用的情況,導致內存泄漏。
如子對象有對父對象的引用,父對象反過來引用子對象。這樣引用計數永遠不會爲0.
主流的垃圾收集器並未用此種算法。
1.2 可達性分析算法
通過判斷對象的引用鏈是否可達來決定對象是否可以被回收。
此算法來自離散數學中的圖論,程序把所有的引用關係看作一張圖,通過一系列名爲GC Root的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑就被稱爲引用鏈即reference chain,當一個對象從GC Root沒有任何引用鏈相連,從圖論上來說這個對象到GC Root不可達,那在這時則證明了這個對象是不可用的,它也就被標記爲垃圾。
垃圾回收器會對內存中的整個對象圖進行遍歷,它從GC Root開始,然後是跟對象引用的其它對象,比如實例對象,回收器將訪問到的所有對象標記爲存活,當標記結束後不可達的對象會被回收器清除。
可以作爲GC Root的對象
虛擬機棧中引用的對象(棧幀中的本地變量表)。比如我們在java方法李new了一個Object,並賦值給了一個局部變量,那麼在局部變量沒有被銷燬之前,new出的Object就會成爲GC Root。
方法區中的常量引用的對象,比如類裏某個常量存儲的是某個對象的地址。那麼被保存的對象也成了GC的跟對象。當別的對象引用到它就會變成圖上的關係。
方法區中的類靜態屬性引用的對象。
本地方法棧中JNI(Native方法)的引用對象
活躍線程的引用對象
第二章 垃圾回收算法
2.1 標記-清除算法(Mark and Sweep)
標記:從根集合進行掃描,對存活的對象進行標記。
清除:對堆內存從頭到尾進行線性遍歷,回收不可達對象內存。
經過掃描,發現A,C,D等不可達,是垃圾對象,所以進行掃描清除。
由於標記清除不需要進行對象的移動,並且進隊不存活的對象進行處理,因此標記清楚後會產生大量不連續的內存碎片,隨便較多可能導致後續無法找到足夠的連續內存而不得不觸發另一次垃圾回收動作,如果向上圖所以情況,有一個需要佔三個內存塊的對象要存儲,會導致vuter(音譯,不知道是啥)一直是暫停狀態,collector一直在嘗試進行垃圾收集,直到outOfMemory。
2.2 複製算法
將可用的內存按容量,按一定比例劃分爲兩塊或者多個塊,其中一塊或者兩塊作爲對象面,其他的作爲空閒面。對象主要在對象面上創建,當被定義爲對象面的內存用完之後,就將存活的對象從對象面複製到空閒面,然後將對象面所有對象內存清除。
這種算法適用於對象存活率低的場景,比如年輕代,這樣每次都會整個半區進行回收也就不用考慮碎片等情況了。推導重建只需要移動堆頂指針然後推導重建即可。
總的來說:
解決碎片化問題,順序分配內存,簡單高效,適用於對象存活率低的場景。
市面上商用的虛擬機都採用此方法回收年輕代。
複製算法在應對存錯率較高的情況就有點力不從心了,要進行較多的複製操作,更關鍵的是,如果不想浪費百分之50的空間,就需要有額外的空間進行擔保,以應對所有對象都百分之百存活的極端情況。
2.3 標記-整理算法(Compacting)
標記:從根集合進行掃描,將存活的對象進行標記。
清除:移動所有存活的對象,且按照內存地址次序依次排列,然後將末端內存地址以後的內存全部回收。
通過可達性算法標記可回收的對象,清除時會將存活對象壓縮到內存一端,之後把存活對象以外的內存空間清除掉。
好處:避免了內存的不連續性,不用設置兩塊內存互換,適合於存活率高的場景,如老年代的回收。
2.4 分代收集算法
將堆內存進一步劃分,將生命週期不同的對象劃分到不同的內存區域,讓不同的區域以採用不同的垃圾回收算法。
jd6,jdk7時候:
堆內存可以分爲年輕代,老年代,永久代這三個區域。
jdk8以後的版本:
永久代被去掉了,只剩下了年輕帶和老年代。
年輕帶存活率低會採用複製算法,老年代存錯率高會採用標記-清除或標記-整理算法.
分代收集算法的GC分爲兩種
2.4.1 Minor GC
Minor GC:發生在年輕代中的垃圾收集動作,所採用的是複製算法。
年輕代幾乎是所有java對象出生的地方,即java申請的內存以及存放都是在這個地方,java中大部分對象不需要長久存活,有朝生夕滅的性質,新生代是垃圾收集的頻繁區域。
年輕代:儘可能快速地收集掉那些生命週期短的對象。
年輕代主要分爲Eden區和兩個Survivor區,對象剛被創建出來的時候,內存空間首先分配在Eden區的,如果Eden區放不下,對象也可能被直接放在Survivor區,甚至是老年代中,兩個Survivor區被定義爲from區與to區,哪個是from哪個是to不是固定的,會隨着垃圾回收的進行相互轉換。
每次使用Eden和其中的一塊Survivor,當進行垃圾回收時將Eden和Survivor中存活的對象,一次性複製到另一塊Survivor空間上,最後清理掉Eden區和剛剛的Survivor空間。當Survivor空間不夠用的時候則需要老年代進行分配的擔保。
具體步驟:
首先對象在Eden出生被擠滿
觸發依次Minor GC,清理Eden區域並將其中的存儲哦對象轉到S0(Survivor)中,並將其年齡加1。
假設Eden區再次被填滿,則將Eden和S0中的存活對象都移動到S1中,並代數加1。
Eden又滿了,則將Eden和S1中的存活對象都移動到S0中,並代數加1。
如此週而復始,達到某個值時,默認是15,這些對象會成爲老年代,當然這也不是固定的,有的較大的對象,Eden或者Survivor裝不下的就會進入老年代。
常用的調優參數:
2.4.2 Full GC
老年代:存放生命週期較長的對象
主要採用標記清理或者標記整理方法進行回收。
當觸發老年代的垃圾回收時通常新生代也會進行垃圾回收,即對整個堆進行垃圾回收,這便是所謂的Full GC,MajorGC通常與Full GC是等價的。
Full GC比MinorGC慢,但執行頻率低。
觸發FullGC的條件:
創建一個大對象,Eden區域不足,選擇保存在老年代中,老年代空間也不足,觸發FullGC。
永久代空間不足(jdk以前)
CMS GC時出現promotion failed,concurrent mode failure。promotion failed在進行Minor GC時Survivor放不下,老年代也放不下,所以觸發。concurrent mode failure是在執行CMS GC時同時有對象要放入老年代中,但是老年代空間不足,便會觸發。
Minor GC晉升到老年代的平均大小大於老年代的剩餘空間。
調用System.gc() (只是提醒,具體是否回收看虛擬機)
使用RMI來進行RPC或管理的JDK應用,每小時執行以此Full GC。
第三章 新生代垃圾收集器
學習垃圾收集器之前要先學習幾個名詞:
Stop-the-world:意味着JVM由於要執行GC而停止了應用程序的執行,這種情況會在任意一種算法中發生。GC優化很多時候就是指減少Stop-the-world時間,使系統具有高吞吐,低停頓的特點。
Safepoint:分析過程中對象引用關係不會發生變化的點。GC相當於是保潔阿姨打掃衛生,如果阿姨一邊打掃衛生,一邊有人扔垃圾該怎麼辦呢?保潔阿姨可以在開始打掃前,就不讓其他人扔垃圾,這樣就解決了,在可達性分析中,要分析哪個對象沒有引用了的時候,必須在一個快照的狀態點進行,在這個點線程都被凍結了,不可以出現對象引用關係還再不斷變化的情況,因此分析結果需要在某個點具有確定性,節點叫做安全點,一般在方法調用,循環跳轉,異常跳轉等纔會產生安全點,一旦GC發生讓所有的線程都跑到安全點在停頓下來,如果發現線程不在安全點,就恢復線程,等線程跑到安全點再說,安全點不能太少也不能太多,太少會讓GC等待太長的時間,太多增加程序的負荷。
JVM運行模式: JVM有兩種運行模式, Client模式啓動較快,Server模式啓動較慢, 但是啓動進入穩定期運行之後,Server模式的運行速度會比Client要快,因Server模式啓動的JVM需要重量級虛擬機,對程序採用了更多的優化,而Client採用的JVM是輕量級的虛擬機.
即使經過長時間的發展,java的垃圾收集器仍在不斷地演進中,不同大小的設備,不同特徵的運用場景都需要有不同的垃圾收集器來滿足特定的要求,因此垃圾收集器不存在哪個好,哪個壞這一說.不同的廠商,不同的JVM,提供的選擇也不同.
一些常見的垃圾收集器以及它們之間的關係還有適用範圍:
新生代的垃圾收集器在上面.老年代的在下面,它們之間有連線的話表明可以搭配使用.
3.1 Serial收集器
可以通過在程序啓動的時候設置+UseSerialGC來啓動這個收集器.
Serial收集器是java虛擬機中最基本也是歷史最悠久的收集器.
單線程收集,進行垃圾收集時,必須暫停所有工作線程.
優點是:簡單高效,Client模式下默認的年輕代收集器.
一次收集不超過100ms,是可以接受的.
3.2 ParNew收集器
多線程收集,其餘的行爲,特點和Serial收集器一樣.默認開啓的收集線程數與CPU相同,在CPU數量非常多的情況下,可以使用某參數來限制線程個數.
單核執行效率不如Serial,在多核下執行纔有優勢.
它能與CMS收集器配合工作,是現在主流的年輕代收集器,使用的是複製算法.
3.3 Parallel Scavenge
系統吞吐量:
使用複製算法,多線程.
之前的是關注停頓時間,現在的是垃圾收集器更關注吞吐量.
在多核下執行纔有優勢,Server模式下默認的年輕代收集器.
如果對垃圾收集器不瞭解,我們可以使用此收集器配合UseAdaptiveSizePolicy參數,它會把內存的調優交給虛擬機完成.
第四章 老年代垃圾收集器
4.1 Serial Old收集器
它是Serial的老年代版本.
單線程收集,進行垃圾收集時,必須暫停所有工作線程.
簡單高效,Client模式下默認的老年代收集器.
4.2 Parallel Old收集器
Jdk6之後開始提供,在此之前Parallel Scavenge一直處於比較尷尬的狀態,如果新生代選擇Parallel Scavenge,那麼老年代除Serial Old別無選擇,由於Serial Old在服務端上的拖累,使用了Parallel Scavenge也未必能達到吞吐量最大的效果.
直到Parallel Old出現,Parallel Scavenge終於有了最終的應用場合,
它是多線程,以吞吐量優先.
4.3 CMS收集器
它的整個垃圾回收過程可以分爲以下六步:
1.初始標記 : stop-the-world
在這個過程中需要虛擬機停下正在執行的任務,這個過程從垃圾回收的根對象開始,只掃描到和跟對象直接關聯到的對象,並做標記,
2.併發標記:併發追溯標記,程序不會停頓.
在初始標記的基礎上,繼續向下標記,和用戶的程序併發執行.
3.併發預清理: 查找執行併發標記階段從年輕代晉升到老年代的對象.
通過重新掃描,減少下一階段重新標記的工作.
4.重新標記:暫停虛擬機,掃描CMS堆中的剩餘對象.
掃描從跟對象開始,並向下關聯.
5.併發清理:清理垃圾對象,程序不會停頓.
6.併發重置:重置CMS收集器的數據結構.等待下一次垃圾回收.
它採用的是標記清除算法,這樣會帶來內存空間碎片化的問題,如果碰到需要較大的連續內存空間,則只能觸發一次GC.
4.4 Garbage First收集器
Garbage First收集器既用於年輕代,也用於老年代的收集器,hotspot開發團隊賦予Garbage First的使命是未來可以替換掉JDK5中發佈的CMS收集器,與其它收集器相比,Garbage First有如下的特點.
1.併發和併發,使用多個CPU來縮短stop-the-world的時間,與用戶線程併發執行.
2.分代收集.獨立管理整個堆,但是能夠採用不用的方式處理新創建對象,和熬過多次GC的舊對象.
3.空間整合:基於標記整理算法
4可預測的停頓:可指定在長度n毫秒的時間內,消耗在垃圾收集上的時間不得超過m毫秒.
會將整個java堆內存劃分成多個大小相等的獨立Region,年輕代和老年代不再物理隔離,新生代和老年代可以是不連續的集合.在JVM啓動時不需要再決定哪個是年輕代,哪個是老年代,隨着年輕代被收集後,它會變爲可用狀態,也可以分配成老年代.
G1的年輕代收集和其它的收集器一樣,都是回收整個年輕代,但是它的老年代不需要整個老年代進行回收,只有一部分region被調用,Garbage First年輕代由eden region和survivor region組成,當一個JVM分配eden region失敗後就會觸發一個年輕代回收,G1年輕代收集器會移動所有存儲對象,從edne region到survivor region這就是copy出survivor的過程.