Java相較於其餘編程語言更加容易學習,這其中很大一部分緣由要歸功於JVM的自動內存管理機制。 對於從事C語言的開發者來講,他們擁有每個對象的「全部權」,更大的權力也意味着更多的職責,C開發者須要維護每個對象「從生到死」的過程,當對象廢棄不用時必須手動釋放其內存,不然就會發生內存泄漏。而對於Java開發者來講,JVM的自動內存管理機制解決了這個讓人頭疼的問題,不容易出現內存泄漏和內存溢出的問題了,GC讓開發者更加專一於程序自己,而不用去關心內存什麼時候分配、什麼時候回收、以及如何回收。前端
在聊GC前,有必要先了解一下JVM的內存模型,知道JVM是如何規劃內存的,以及GC的主要做用區域。 如圖所示,JVM運行時會將內存劃分爲五大塊區域,其中「方法區」和「堆」隨着JVM的啓動而建立,是全部線程共享的內存區域。虛擬機棧、本地方法棧、程序計數器則是隨着線程的建立被建立,線程運行結束後也就被銷燬了。java
程序計數器(Program Counter Register)是一塊很是小的內存空間,幾乎能夠忽略不計。 它能夠看做是線程所執行字節碼的行號指數器,指向當前線程下一條應該執行的指令。對於:條件分支、循環、跳轉、異常等基礎功能都依賴於程序計數器。算法
對於CPU的一個核心來講,任意時刻只能跑一個線程。若是線程的CPU時間片用完就會被掛起,等待OS從新分配時間片再繼續執行,那線程如何知道上次執行到哪裏了呢?就是經過程序計數器來實現的,每一個線程都須要維護一個私有的程序計數器。數據庫
若是線程在執行Java方法,計數器記錄的是JVM字節碼指令地址。若是執行的是Native方法,計數器值則爲Undefined
。編程
程序計數器是惟一一個沒有規定任何OutOfMemoryError狀況的內存區域,意味着在該區域不可能發生OOM異常,GC不會對該區域進行回收!數組
虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,生命週期和線程相同。緩存
虛擬機棧描述的是Java方法執行的內存模型,JVM要執行一個方法時,首先會建立一個棧幀(Stack Frame)用於存放:局部變量表、操做數棧、動態連接、方法出口等信息。棧幀建立完畢後開始入棧執行,方法執行結束後即出棧。服務器
方法執行的過程就是一個個棧幀從入棧到出棧的過程。數據結構
局部變量表主要用來存放編譯器可知的各類基本數據類型、對象引用、returnAddress類型。局部變量表所需的內存空間在編譯時就已經確認,運行期間不會修改局部變量表的大小。多線程
在JVM規範中,虛擬機棧規定了兩種異常:
本地方法棧(Native Method Stack)也是線程私有的,與虛擬機棧的做用很是相似。 區別是虛擬機棧是爲執行Java方法服務的,而本地方法棧是爲執行Native方法服務的。
與虛擬機棧同樣,JVM規範中對本地方法棧也規定了StackOverflowError和OutOfMemoryError兩種異常。
Java堆(Java Heap)是線程共享的,通常來講也是JVM管理最大的一塊內存區域,同時也是垃圾收集器GC的主要管理區域。
Java堆在JVM啓動時建立,做用是:存放對象實例。 幾乎全部的對象都在堆中建立,可是隨着JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術使得「全部對象都分配在堆上」不那麼絕對了。
因爲是GC主要管理的區域,因此也被稱爲:GC堆。 爲了GC的高效回收,Java堆內部又作了以下劃分:
JVM規範中,堆在物理上能夠是不連續的,只要邏輯上連續便可。經過-Xms -Xmx
參數能夠設置最小、最大堆內存。
方法區(Method Area)與Java堆同樣,也是線程共享的一塊內存區域。 它主要用來存儲:被JVM加載的類信息,常量,靜態變量,即時編譯器產生的代碼等數據。 也被稱爲:非堆(Non-Heap),目的是與Java堆區分開來。
JVM規範對方法區的限制比較寬鬆,JVM甚至能夠不對方法區進行垃圾回收。這就致使在老版本的JDK中,方法區也別稱爲:永久代(PermGen)。
使用永久代來實現方法區不是個好主意,容易致使內存溢出,因而從JDK7開始有了「去永久代」行動,將本來放在永久代中的字符串常量池移出。到JDK8中,正式去除永久代,迎來元空間。
垃圾收集(Garbage Collection)簡稱爲「GC」,它的歷史遠比Java語言自己久遠,在1960年誕生於麻省理工學院的Lisp是第一門開始使用內存動態分配和垃圾收集技術的語言。
要想實現自動垃圾回收,首先須要思考三件事情: 前面介紹了JVM的五大內存區域,程序計數器佔用內存極少,幾乎能夠忽略不計,並且永遠不會內存溢出,GC不須要對其進行回收。虛擬機棧、本地方法棧隨線程「同生共死」,棧中的棧幀隨着方法的運行有條不紊的入棧、出棧,每一個棧幀分配多少內存在編譯期就已經基本肯定,所以這兩塊區域內存的分配和回收都具有肯定性,不太須要考慮如何回收的問題。
方法區就不同了,一個接口到底有多少個實現類?每一個類佔用的內存是多少?你甚至能夠在運行時動態的建立類,所以GC須要針對方法區進行回收。
Java堆也是如此,堆中存放着幾乎全部的Java對象實例,一個類到底會建立多少個對象實例,只有在程序運行時才知道,這部份內存的分配和回收是動態的,GC須要重點關注。
實現自動垃圾回收的第一步,就是判斷到底哪些對象是能夠被回收的。通常來講有兩種方式:引用計數算法和可達性分析算法,商用JVM幾乎採用的都是後者。
在對象中添加一個引用計數器,每引用一次計數器就加1,每取消一次引用計數器就減1,當計數器爲0時表示對象再也不被引用,此時就能夠將對象回收了。
引用計數算法(Reference Counting)雖然佔用了一些額外的內存空間,可是它原理簡單,也很高效,在大多數狀況下是一個不錯的實現方案,可是它存在一個嚴重的弊端:沒法解決循環引用。
例如一個鏈表,按理只要沒有引用指向鏈表,鏈表就應該被回收,可是很遺憾,因爲鏈表中全部的元素引用計數器都不爲0,所以沒法被回收,形成內存泄漏。
目前主流的商用JVM都是經過可達性分析來判斷對象是否能夠被回收的。 這個算法的基本思路是:
經過一系列被稱爲「GC Roots」的根對象做爲起始節點集,從這些節點開始,經過引用關係向下搜尋,搜尋走過的路徑稱爲「引用鏈」,若是某個對象到GC Roots沒有任何引用鏈相連,就說明該對象不可達,便可以被回收。
對象可達指的就是:雙方存在直接或間接的引用關係。 根可達或GC Roots可達就是指:對象到GC Roots存在直接或間接的引用關係。
能夠做爲GC Roots的對象有如下幾類: 可達性分析就是JVM首先枚舉根節點,找到一些爲了保證程序能正常運行所必需要存活的對象,而後以這些對象爲根,根據引用關係開始向下搜尋,存在直接或間接引用鏈的對象就存活,不存在引用鏈的對象就回收。
關於可達性分析的詳細描述,能夠看筆者的文章:《大白話理解可達性分析算法》。
JVM將內存劃分爲五大塊區域,不一樣的GC會針對不一樣的區域進行垃圾回收,GC類型通常有如下幾大類:
何時觸發GC,以及觸發什麼類型的GC呢?不一樣的垃圾收集器實現不同,你還能夠經過設置參數來影響JVM的決策。
通常來講,新生代會在Eden
區用盡後纔會觸發GC,而Old
區卻不能這樣,由於有的併發收集器在清理過程當中,用戶線程能夠繼續運行,這意味着程序仍然在建立對象、分配內存,這就須要老年代進行「空間分配擔保」,新生代放不下的對象會被放入老年代,若是老年代的回收速度比對象的建立速度慢,就會致使「分配擔保失敗」,這時JVM不得不觸發Full GC,以此來獲取更多的可用內存。
定位到須要回收的對象之後,就要開始進行回收了。如何回收對象又成了一個問題。 什麼樣的回收方式會更加的高效呢?回收後是否須要對內存進行壓縮整理,避免碎片化呢?針對這些問題,GC的回收算法大體分爲如下三類:
具體算法的回收細節,下面會介紹到。
JVM將堆劃分紅不一樣的代,不一樣的代中存放的對象特色不同,針對不一樣的代使用不一樣的GC回收算法進行回收能夠提高GC的效率。
目前大多數JVM的垃圾收集器都遵循「分代收集」理論,分代收集理論創建在三個假說之上。
絕大多數對象都是朝生夕死的。
想一想看咱們寫的程序是否是這樣,絕大多數時候,咱們建立一個對象,只是爲了進行一些業務計算,獲得計算結果後這個對象也就沒什麼用了,便可以被回收了。 再例如:客戶端要求返回一個列表數據,服務端從數據庫查詢後轉換成JSON響應給前端後,這個列表的數據就能夠被回收了。 諸如此類,均可以被稱爲「朝生夕死」的對象。
熬過越屢次GC的對象就越難以回收。
這個假說徹底是基於機率學統計來的,經歷過屢次GC都沒法被回收的對象,能夠假定它下次GC時仍然沒法被回收,所以就不必高頻率的對其進行回收,將其挪到老年代,減小回收的頻率,讓GC去回收效益更高的新生代。
跨代引用相對於同代引用是極少的。
這是根據前兩條假說邏輯推理得出的隱含推論:存在互相引用關係的兩個對象,應該傾向於同時生存或者同時消亡的。 舉個例子,若是某個新生代對象存在跨代引用,因爲老年代對象難以消亡,該引用會使得新生代對象在收集時一樣得以存活,進而在年齡增加以後晉升到老年代中,這時跨代引用也隨即被消除了。
跨代引用雖然極少,可是它仍是可能存在的。若是爲了極少的跨代引用而去掃描整個老年代,那每次GC的開銷就太大了,GC的暫停時間會變得難以接受。若是忽略跨代引用,會致使新生代的對象被錯誤的回收,致使程序錯誤。
JVM是經過記憶集(Remembered Set)來解決的,經過在新生代創建記憶集的數據結構,來避免回收新生代時把整個老年代也加進GC Roots的掃描範圍,減小GC的開銷。
記憶集是一種由「非收集區域」指向「收集區域」的指針集合的抽象數據結構,說白了就是把「年輕代中被老年代引用的對象」給標記起來。記憶集能夠有如下三種記錄精度:
字長精度和對象精度太精細化了,須要花費大量的內存來維護記憶集,所以許多JVM都是採用的「卡精度」,也被稱做:「卡表」(Card Table)。卡表是記憶集的一種實現,也是目前最經常使用的一種形式,它定義了記憶集的記錄精度、與對內存的映射關係等。
HotSpot使用一個字節數組來實現卡表,它將堆空間劃分紅一系列2次冪大小的內存區域,這個內存區域就被稱做「卡頁」(Card Page),卡頁的大小通常都是2的冪次方數,HotSpot採用2的9次冪,即512字節。字節數組的每個元素都對應着一個卡頁,若是某個卡頁內的對象存在跨代引用,JVM就會將這個卡頁標記爲「Dirty」髒的,GC時只須要掃描髒頁對應的內存區域便可,避免掃描整個堆。
卡表的結構以下圖所示:
卡表只是用來標記哪一塊內存區域存在跨代引用的數據結構,JVM如何來維護卡表呢?何時將卡頁變髒呢?
HotSpot是經過「寫屏障」(Write Barrier)來維護卡表的,JVM攔截了「對象屬性賦值」這個動做,相似於AOP的切面編程,JVM能夠在對象屬性賦值先後介入處理,賦值前的處理叫做「寫前屏障」,賦值後的處理叫做「寫後屏障」,僞代碼以下:
void setField(Object o){ before();//寫前屏障 this.field = o; after();//寫後屏障 }
開啓寫屏障後,JVM會爲全部的賦值操做生成相應的指令,一旦出現老年代對象的引用指向了年輕代的對象,HotSpot就會將對應的卡表元素置爲髒的。
請將這裏的「寫屏障」和併發編程中內存指令重排序的「寫屏障」區分開,避免混淆。
除了寫屏障自己的開銷外,卡表在高併發場景下還面臨着「僞共享」的問題,現代CPU的緩存系統是以「緩存行」(Cache Line)爲單位存儲的,Intel的CPU緩存行的大小通常是64字節,多線程修改互相獨立的變量時,若是這些變量在同一個緩存行中,就會致使彼此的緩存行無端失效,線程不得不頻繁發起load指令從新加載數據,而致使性能下降。
一個Cache Line是64字節,每一個卡頁是512字節,64✖️512字節就是32KB,若是不一樣的線程更新的對象處在這32KB以內,就會致使更新卡表時正好寫入同一個緩存行而影響性能。爲了不這個問題,HotSpot支持只有當元素未被標記時,纔將其置爲髒的,這樣會增長一次判斷,可是能夠避免僞共享的問題,設置-XX:+UseCondCardMark
來開啓這個判斷。
標記清除算法分爲兩個過程:標記、清除。
收集器首先標記須要被回收的對象,標記完成後統一清除。也能夠標記存活對象,而後統一清除沒有被標記的對象,這取決於內存中存活對象和死亡對象的佔比。
缺點:
爲了解決標記清除算法產生的內存碎片問題,標記複製算法進行了改進。
標記複製算法會將內存劃分爲兩塊區域,每次只使用其中一塊,垃圾回收時首先進行標記,標記完成後將存活的對象複製到另外一塊區域,而後將當前區域所有清理。
缺點是:若是大量對象沒法被回收,會產生大量的內存複製開銷。可用內存縮小爲一半,內存浪費也比較大。 因爲絕大多數對象都會在第一次GC時被回收,須要被複制的每每是極少數對象,那麼就徹底不必按照1:1去劃分空間。 HotSpot虛擬機默認Eden區和Survivor區的大小比例是8:1,即Eden區80%,From Survivor區10%,To Survivor區10%,整個新生代可用內存爲Eden區+一個Survivor區即90%,另外一個Survivor區10%用於分區複製。
若是Minor GC後仍存活大量對象,超出了一個Survivor區的範圍,那麼就會進行分配擔保(Handle Promotion),將對象直接分配進老年代。
標記複製算法除了在對象大量存活時須要進行較多的複製操做外,還須要額外的內存空間老年代來進行分配擔保,因此在老年代中通常不採用這種回收算法。
可以在老年代中存活的對象,通常都是歷經屢次GC後仍沒法被回收的對象,基於「強分代假說」,老年代中的對象通常很難被回收。針對老年代對象的生存特徵,引入了標記整理算法。
標記整理算法的標記過程與標記清除算法一致,可是標記整理算法不會像標記清除算法同樣直接清理標記的對象,而是將存活的對象都向內存區域的一端移動,而後直接清理掉邊界外的內存空間。 標記整理算法相較於標記清除算法,最大的區別是:須要移動存活的對象。 GC時移動存活的對象既有優勢,也有缺點。
缺點 基於「強分代假說」,大部分狀況下老年代GC後會存活大量對象,移動這些對象須要更新全部reference引用地址,這是一項開銷極大的操做,並且該操做須要暫停全部用戶線程,即程序此時會阻塞停頓,JVM稱這種停頓爲:Stop The World(STW)。
優勢 移動對象對內存空間進行整理後,不會產生大量不連續的內存碎片,利於後續爲對象分配內存。
因而可知,無論是否移動對象都有利弊。移動則內存回收時負責、內存分配時簡單,不移動則內存回收時簡單、內存分配時複雜。從整個程序的吞吐量來考慮,移動對象顯然更划算一些,由於內存分配的頻率比內存回收的頻率要高的多的多。
還有一種解決方式是:平時不移動對象,採用標記清除算法,當內存碎片影響到大對象分配時,才啓用標記整理算法。
按照《Java虛擬機規範》實現的JVM就不勝枚舉,且每一個JVM平臺都有N個垃圾收集器供用戶選擇,這些不是一篇文章能夠說的清楚的。固然,開發者也不必瞭解全部的垃圾收集器,以Hotspot JVM爲例,主流的垃圾收集器主要有如下幾大類: 串行:單線程收集,用戶線程暫停。 並行:多線程收集,用戶線程暫停。 併發:用戶線程和GC線程同時運行。
前面已經說過,大多數JVM的垃圾收集器都遵循「分代收集」理論,不一樣的垃圾收集器回收的內存區域會有所不一樣,大多數狀況下,JVM須要兩個垃圾收集器配合使用,下圖有虛線鏈接的表明兩個收集器能夠配合使用。
最基礎,最先的垃圾收集器,採用標記複製算法,僅開啓一個線程完成垃圾回收,回收時會暫停全部用戶線程(STW)。 使用
-XX:+UseSerialGC
參數開啓Serial收集器,因爲是單線程回收,所以Serial的應用範圍很受限制:
使用標記複製算法,多線程的新生代收集器。 使用參數
-XX:+UseParallelGC
開啓,ParallelGC的特色是很是關注系統的吞吐量,它提供了兩個參數來由用戶控制系統的吞吐量: -XX:MaxGCPauseMillis:設置垃圾回收最大的停頓時間,它必須是一個大於0的整數,ParallelGC會朝着這個目標去努力,若是這個值設置的太小,ParallelGC就不必定能保證了。若是用戶但願GC停頓的時間很短,ParallelGC就會嘗試減少堆空間,由於回收一個較小的堆確定比回收一個較大的堆耗時短嘛,可是這樣會更頻繁的觸發GC,從而下降系統的吞吐量。
-XX:GCTimeRatio:設置吞吐量的大小,它的值是一個0~100的整數。假設GCTimeRatio爲n,那麼ParallelGC將花費不超過1/(1+n)
的時間進行垃圾回收,默認值爲19,意味着ParallelGC用於垃圾回收的時間不會超過5%。
ParallelGC是JDK8的默認垃圾收集器,它是一款吞吐量優先的垃圾收集器,用戶能夠經過-XX:MaxGCPauseMillis
和-XX:GCTimeRatio
來設置GC最大的停頓時間和吞吐量。但這兩個參數是互相矛盾的,更小的停頓時間就意味着GC須要更頻繁進行回收,從而增長GC回收的總體時間,致使吞吐量降低。
ParNew也是一個使用標記複製算法,多線程的新生代垃圾收集器。它的回收策略、算法、及參數都和Serial同樣,只是簡單的將單線程改成多線程而已,它的誕生只是爲了配合CMS
收集器使用而存在的。CMS
是老年代的收集器,可是Parallel Scavenge
不能配合CMS
一塊兒工做,Serial是串行回收的,效率又過低了,所以ParNew就誕生了。
使用參數-XX:+UseParNewGC
開啓,不過這個參數已經在JDK9以後的版本中刪除了,由於JDK9默認G1收集器,CMS已經被取代,而ParNew就是爲了配合CMS而誕生的,CMS廢棄了,ParNew也就沒有存在價值了。
使用標記整理算法,和Serial同樣,單線程獨佔式的針對老年代的垃圾收集器。老年代的空間一般比新生代要大,並且標記整理算法在回收過程當中須要移動對象來避免內存碎片化,所以老年代的回收要比新生代更耗時一些。
Serial Old做爲最先的老年代垃圾收集器,還有一個優點,就是它能夠和絕大多數新生代垃圾收集器配合使用,同時它還能夠做爲CMS併發失敗的備用收集器。
使用參數-XX:+UseSerialGC
開啓,新生代老年代都將使用串行收集器。和Serial同樣,除非你的應用很是輕量,或者CPU的資源十分緊張,不然都不建議使用該收集器。
ParallelOldGC是一款針對老年代,多線程並行的獨佔式垃圾收集器,和Parallel Scavenge同樣,屬於吞吐量優先的收集器,Parallel Old的誕生就是爲了配合Parallel Scavenge使用的。
ParallelOldGC使用的是標記整理算法,使用參數-XX:+UseParallelOldGC
開啓,參數-XX:ParallelGCThreads=n
能夠設置垃圾收集時開啓的線程數量,同時它也是JDK8默認的老年代收集器。
CMS(Concurrent Mark Sweep)是一款里程碑式的垃圾收集器,爲何這麼說呢?由於在它以前,GC線程和用戶線程是沒法同時工做的,即便是Parallel Scavenge,也不過是GC時開啓多個線程並行回收而已,GC的整個過程依然要暫停用戶線程,即Stop The World。這帶來的後果就是Java程序運行一段時間就會卡頓一會,下降應用的響應速度,這對於運行在服務端的程序是不能被接收的。
GC時爲何要暫停用戶線程? 首先,若是不暫停用戶線程,就意味着期間會不斷有垃圾產生,永遠也清理不乾淨。 其次,用戶線程的運行必然會致使對象的引用關係發生改變,這就會致使兩種狀況:漏標和錯標。
爲了實現併發收集,CMS的實現比前面介紹的幾種垃圾收集器都要複雜的多,整個GC過程能夠大概分爲如下四個階段: 一、初始標記 初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快。初始標記的過程是須要觸發STW的,不過這個過程很是快,並且初試標記的耗時不會由於堆空間的變大而變慢,是可控的,所以能夠忽略這個過程致使的短暫停頓。
二、併發標記 併發標記就是將初始標記的對象進行深度遍歷,以這些對象爲根,遍歷整個對象圖,這個過程耗時較長,並且標記的時間會隨着堆空間的變大而變長。不過好在這個過程是不會觸發STW的,用戶線程仍然能夠工做,程序依然能夠響應,只是程序的性能會受到一點影響。由於GC線程會佔用必定的CPU和系統資源,對處理器比較敏感。CMS默認開啓的GC線程數是:(CPU核心數+3)/4,當CPU核心數超過4個時,GC線程會佔用不到25%的CPU資源,若是CPU數不足4個,GC線程對程序的影響就會很是大,致使程序的性能大幅下降。
三、從新標記 因爲併發標記時,用戶線程仍在運行,這意味着併發標記期間,用戶線程有可能改變了對象間的引用關係,可能會發生兩種狀況:一種是本來不能被回收的對象,如今能夠被回收了,另外一種是本來能夠被回收的對象,如今不能被回收了。針對這兩種狀況,CMS須要暫停用戶線程,進行一次從新標記。
四、併發清理 從新標記完成後,就能夠併發清理了。這個過程耗時也比較長,且清理的開銷會隨着堆空間的變大而變大。不過好在這個過程也是不須要STW的,用戶線程依然能夠正常運行,程序不會卡頓,不過和併發標記同樣,清理時GC線程依然要佔用必定的CPU和系統資源,會致使程序的性能下降。
CMS開闢了併發收集的先河,讓用戶線程和GC線程同時工做成爲了可能,可是缺點也很明顯: 一、對處理器敏感 併發標記、併發清理階段,雖然CMS不會觸發STW,可是標記和清理須要GC線程介入處理,GC線程會佔用必定的CPU資源,進而致使程序的性能降低,程序響應速度變慢。CPU核心數多的話還稍微好一點,CPU資源緊張的狀況下,GC線程對程序的性能影響很是大。
二、浮動垃圾 併發清理階段,因爲用戶線程仍在運行,在此期間用戶線程製造的垃圾就被稱爲「浮動垃圾」,浮動垃圾本次GC沒法清理,只能留到下次GC時再清理。
三、併發失敗 因爲浮動垃圾的存在,所以CMS必須預留一部分空間來裝載這些新產生的垃圾。CMS不能像Serial Old收集器那樣,等到Old區填滿了再來清理。在JDK5時,CMS會在老年代使用了68%的空間時激活,預留了32%的空間來裝載浮動垃圾,這是一個比較偏保守的配置。若是實際引用中,老年代增加的不是太快,能夠經過-XX:CMSInitiatingOccupancyFraction
參數適當調高這個值。到了JDK6,觸發的閾值就被提高至92%,只預留了8%的空間來裝載浮動垃圾。 若是CMS預留的內存沒法容納浮動垃圾,那麼就會致使「併發失敗」,這時JVM不得不觸發預備方案,啓用Serial Old收集器來回收Old區,這時停頓時間就變得更長了。
四、內存碎片 因爲CMS採用的是「標記清除」算法,這就意味這清理完成後會在堆中產生大量的內存碎片。內存碎片過多會帶來不少麻煩,其一就是很難爲大對象分配內存。致使的後果就是:堆空間明明還有不少,但就是找不到一塊連續的內存區域爲大對象分配內存,而不得不觸發一次Full GC,這樣GC的停頓時間又會變得更長。 針對這種狀況,CMS提供了一種備選方案,經過-XX:CMSFullGCsBeforeCompaction
參數設置,當CMS因爲內存碎片致使觸發了N次Full GC後,下次進入Full GC前先整理內存碎片,不過這個參數在JDK9被棄用了。
介紹完CMS垃圾收集器後,咱們有必要了解一下,爲何CMS的GC線程能夠和用戶線程一塊兒工做。
JVM判斷對象是否能夠被回收,絕大多數採用的都是「可達性分析」算法,關於這個算法,能夠查看筆者之前的文章:大白話理解可達性分析算法。
從GC Roots開始遍歷,可達的就是存活,不可達的就回收。
CMS將對象標記爲三種顏色: 標記的過程大體以下:
這個過程正確執行的前提是沒有其餘線程改變對象間的引用關係,然而,併發標記的過程當中,用戶線程仍在運行,所以就會產生漏標和錯標的狀況。
漏標 假設GC已經在遍歷對象B了,而此時用戶線程執行了A.B=null
的操做,切斷了A到B的引用。 原本執行了
A.B=null
以後,B、D、E均可以被回收了,可是因爲B已經變爲灰色,它仍會被當作存活對象,繼續遍歷下去。 最終的結果就是本輪GC不會回收B、D、E,留到下次GC時回收,也算是浮動垃圾的一部分。
實際上,這個問題依然能夠經過「寫屏障」來解決,只要在A寫B的時候加入寫屏障,記錄下B被切斷的記錄,從新標記時能夠再把他們標爲白色便可。
錯標 假設GC線程已經遍歷到B了,此時用戶線程執行了如下操做:
B.D=null;//B到D的引用被切斷 A.xx=D;//A到D的引用被創建
B到D的引用被切斷,且A到D的引用被創建。 此時GC線程繼續工做,因爲B再也不引用D了,儘管A又引用了D,可是由於A已經標記爲黑色,GC不會再遍歷A了,因此D會被標記爲白色,最後被當作垃圾回收。 能夠看到錯標的結果比漏表嚴重的多,浮動垃圾能夠下次GC清理,而把不應回收的對象回收掉,將會形成程序運行錯誤。
錯標只有在知足下面兩種狀況下才會發生:
只要打破任一條件,就能夠解決錯標的問題。
原始快照和增量更新 原始快照打破的是第一個條件:當灰色對象指向白色對象的引用被斷開時,就將這條引用關係記錄下來。當掃描結束後,再以這些灰色對象爲根,從新掃描一次。至關於不管引用關係是否刪除,都會按照剛開始掃描時那一瞬間的對象圖快照來掃描。
增量更新打破的是第二個條件:當黑色指向白色的引用被創建時,就將這個新的引用關係記錄下來,等掃描結束後,再以這些記錄中的黑色對象爲根,從新掃描一次。至關於黑色對象一旦創建了指向白色對象的引用,就會變爲灰色對象。
CMS採用的方案就是:寫屏障+增量更新來實現的,打破的是第二個條件。
當黑色指向白色的引用被創建時,經過寫屏障來記錄引用關係,等掃描結束後,再以引用關係裏的黑色對象爲根從新掃描一次便可。
僞代碼大體以下:
class A{ private D d; public void setD(D d) { writeBarrier(d);// 插入一條寫屏障 this.d = d; } private void writeBarrier(D d){ // 將A -> D的引用關係記錄下來,後續從新掃描 } }
G1的全稱是「Garbage First」垃圾優先的收集器,JDK7正式使用,JDK9默認使用,它的出現是爲了替代CMS收集器。
既然要替代CMS,那麼毫無疑問,G1也是併發並行的垃圾收集器,用戶線程和GC線程能夠同時工做,關注的也是應用的響應時間。
G1最大的一個變化就是,它只是邏輯分代,物理結構上已經不分代了。它將整個Java堆劃分紅多個大小不等的Region,每一個Region能夠根據須要扮演Eden區、Survivor區、或者是老年代空間,G1能夠對扮演不一樣角色的Region採用不一樣的策略去處理。
G1以前的全部垃圾收集器,回收的範圍要麼是整個新生代(Minor GC)、要麼是整個老年代(Major GC)、再就是整個Java堆(Full GC)。而G1跳出了這個樊籠,它能夠面向堆內任何部分來組成回收集(Collection Set,簡稱CSet
)進行回收,衡量標準再也不是它屬於哪一個分代,而是判斷哪一個Region垃圾最多,選擇回收價值最高的Region回收,這也是「Garbage First」名稱的由來。
雖然G1仍然保留了分代的概念,可是新生代和老年代再也不是固定不變的兩塊連續的內存區域了,它們都是由一系列Region組成的,並且每次GC時,新生代和老年代的空間大小會動態調整。G1之因此能控制GC的停頓時間,創建可預測的停頓時間模型,就是由於它將Region做爲單次回收的最小單元,每次回收的內存空間都是Region大小的整數倍,這樣就能夠避免在整個Java堆內進行全區域的垃圾收集。
G1會跟蹤每一個Region的垃圾數量,計算每一個Region的回收價值,在後臺維護一個優先級列表,而後根據用戶設置的容許GC停頓的時間來優先回收「垃圾最多」的Region,這樣就保證了G1可以在有限的時間內回收儘量多的可用內存。
G1的整個回收週期大概能夠分爲如下幾個階段:
和CMS同樣,由於併發回收時用戶線程仍然在運行,即分配內存,所以若是回收速度跟不上內存分配的速度,G1也會在必要的時候觸發一個Full GC來獲取更多的可用內存。
使用參數-XX:+UseG1GC
來開啓G1收集器,-XX:MaxGCPauseMillis
來設置目標最大停頓時間,G1會朝着這個目標去努力,若是GC停頓時間超過了目標時間,G1就會嘗試調整新生代和老年代的比例、堆大小、晉升年齡等一系列參數來企圖達到預設目標。 -XX:ParallelGCThreads
用來設置並行回收時GC的線程數量,-XX:InitiatingHeapOccupancyPercent
用來指定整個Java堆的使用率達到多少時觸發併發標記週期的執行,默認值是45。
ZGC是在JDK11才加入的具備實現性質的低延遲垃圾收集器,它的目標是但願在儘量對吞吐量影響不大的前提下,實如今任意堆內存大小下均可以把GC的停頓時間控制在十毫秒之內。
ZGC面向的是超大堆,最大支持4TB
的堆空間,它和G1同樣,也是採用Region的內存佈局形式。
ZGC最大的一個特色就是它採用着色指針Colored Pointer
技術來標記對象。以往,若是JVM須要在對象上存儲一些額外的、只供GC或JVM自己使用的數據時(如GC年齡、偏向線程ID、哈希碼),一般會在對象的對象頭上增長額外的字段來記錄。ZGC就厲害了,直接把標記信息記錄在對象的引用指針上。
Colored Pointer
是什麼?爲何對象引用的指針自己也能夠存儲數據呢? 在64位系統中,理論上能夠訪問的內存大小爲2的64次冪字節,即16EB。可是實際上,目前遠遠用不到這麼大的內存,所以基於性能和成本的考慮,CPU和操做系統都會施加本身的約束。例如AMD64架構只支持54位(4PB)的地址總線,Linux只支持46位(64TB)的物理地址總線,Windows只支持44位(16TB)的物理地址總線。
在Linux系統下,高18位不能用來尋址,剩餘的46位能支持最大64TB的內存大小。事實上,64TB的內存大小在目前來講也遠遠超出了服務器的須要。因而ZGC就盯上了這剩下的46位指針寬度,將其高4位提取出來存儲四個標誌信息。經過這些標誌位,JVM能夠直接從指針中看到其引用對象的三色標記狀態、是否進入了重分配集(即被移動過)、是否只能經過finalize()方法才能被訪問到。這就致使JVM能利用的物理地址總線只剩下42位了,即ZGC能管理的最大內存空間爲2的42次冪字節,即4TB。 目前ZGC還處於實驗階段,能查到的資料也很少,筆者之後再整理更新吧。
待寫......
6. GC的調優待寫......