C、C++等語言中,內存的分配和釋放由程序代碼來完成,容易出現因爲程序員漏寫內存釋放代碼引發的內存泄露,最終致使系統內存耗盡。
Java代碼運行在JVM中,由JVM來管理 堆Heap 內存的分配和回收(Garbage Collection),把程序員從繁瑣的內存管理工做中釋放出來,更專一於業務開發。Java內存回收工做由標記(識別可回收對象)和回收(釋放可回收對象)兩個步驟組成。
和程序代碼釋放內存相比,內存自動管理會佔用一部分CPU時間,Stop The World特色回暫停業務程序運行,很是影響執行效率。Java各版本中,一直致力於內存管理算法的優化,造成了一套針對各類內存分區(新生代、老年代)和運行場景(單核、多核、客戶端、服務端)的特色而針對性設計的內存回收算法。程序員
這裏說的內存回收機制,主要是指針對 堆Heap和 元空間Metaspace內存的回收,線程相關內存(棧、本地棧、程序計數器)內存隨線程建立和回收,直接內存的釋放由其在堆內存中引用釋放時觸發。
在內存被回收前,系統必須標記哪些內存已經沒有人使用能夠釋放,這個工做就由內存標記算法的來完成,在Java各版本中,使用過以下幾種標記算法。算法
這是早期的內存標記算法,每一個堆中分配的對象都有一個引用計數器,計數一個對象被引用的次數。當對象建立並賦值給變量時,計數爲1,當有其餘變量引用該對象時,引用計數+1;但引用此對象的變量超出存活範圍或釋放對對象引用(包括變量引用了其餘對象或變量被設置爲null等),引用計數-1。對象的引用計數爲0時,表示此對象可被垃圾收集器回收。
引用計數法的有點是簡單,執行速度快,只要變量一遍對象檢測引用計數是否爲0便可判斷是否可回收;肯定是沒法檢測出循環引用而致使內存沒法回收。多線程
採用跟搜索算法,搜索算法引入了圖論,把全部對象間的關係當作一張圖,內存標記從一組根節點(GC Root Set)開始,經過遞歸搜索,創建對象的引用關係圖,當搜索完畢後,圖外的對象就是可回收對象。這是目前Java中使用的內存標記算法。
併發
可做爲GC Root的對象包括:框架
採用跟蹤算法標記內存對象後,再掃描堆內存中未被標記的對象,進行回收。此算法不移動對象,僅對不存活對象進行回收,在存活對象佔比高的狀況下處理效率高,但不移動對象會引發內存碎片。性能
此方法和標記-清理算法使用相同標記算法,但在對不存活對象回收時,會把存活對象向內存前部空閒區域移動,同時更新對象的指針。此方法在清理的基礎上,會對對象進行移動,執行成本較高,但可解決內存碎片問題。基於此算法的內存回收實現,通常會增長句柄和句柄表。優化
該算法把內存分爲空閒區和對象區,新建對象存儲到對象區中。當對象區滿時,先採用跟蹤算法對對象進行標記,再把存活對象拷貝到空閒區,清空原對象區,空閒區和對象區互換角色。在拷貝過車中,程序須要暫停,此算法適用於存活對象叫少的狀況,能夠解決內存碎片問題。spa
JDK8中,堆中移除了永生代區域,堆內存主要由新生代和老年代兩部分組成。其中新生代由一個伊甸園(Eden)和兩個倖存者Survivor From和Survivor To 3部分組成,新建立對象首先保存在Eden中,當Eden中對象達到必定數量時,JVM觸發Minor GC,GC時,先把Eden和From中的存活對象拷貝到Survivor To區,再清除Eden和From兩個區域的數據,最後From和To互換身份,完成一次內存回收。新生代區域對象數量大,存活時間短,通常採用複製-清除算法,經過這種結構和回收方式來提升垃圾回收效率,減小內存碎片。
通過若干(默認15)次後還存活的對象,將進入老年代區,當老年代數據滿時,會觸發Major GC(又稱Full GC),此時新生代、老年代、元區域、直接內存區域都會執行GC操做。
老年代:新生代的內存大小默認比例爲2:1。Eden和兩個Survivor的比例爲8:1:1。線程
上述的內存標記算法、回收方式和分代策略是垃圾回收的方法,根據這些方法,針對不一樣的用戶場景(Server、Client)和系統配置(單線程、多線程),JVM實現了適用於各場景的垃圾回收器。設計
年輕代收集器
老年代收集器
混合收集器
Serial是單線程收集器,Serial收集器只能使用單個線程進行收集工做,在收集的時候必須得停掉其它線程,等待收集工做完成其它線程才能夠繼續工做。
Serial收集器是JVM中最先的垃圾收集器,也是JDK1.3前的惟一收集器,再也不適用於現代多核CPU和Server(服務端)場景,可是很是的適合單核CPU和Client場景。
ParNew是Serial的升級版,其工做的流程和Serial基本一致,主要的改進是支持多線程同時執行垃圾回收工做,即上圖中的GC Thread支持多線程,能夠充分利用多核CPU的性能。它是HotSpot上第一個真正意義實現併發的收集器。GC默認開啓線程數等於CPU數量,可經過 -XX:ParallelGCThreads
來控制垃圾收集線程的數量。
Parallel Scavenge是吞吐量優先的收集器,其工做方式和ParNew基本同樣,可是它以提升系統吞吐量(Throughput)爲設計目標,吞吐量=業務運行時間/系統總運行(業務+GC)時間。
ParNew等收集器的關注點是儘可能縮小垃圾回收的停頓時間,而縮短停頓時間必然須要提升垃圾回收的頻率,致使業務線程和GC線程間頻繁的切換,從而增長CPU在現場切換上的損耗。
而以吞吐量爲設計目標的Parallel Scavenge收集器,能夠經過擴大新生代內存容量,減小垃圾回收發生的次數,雖然提升了單次GC的時長,但減小了線程切換開銷,從總體上能夠提升系統的吞吐量。
Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是:
參數 | 做用 | 說明 |
---|---|---|
-XX:MaxGCPauseMillis | 控制最大垃圾收集停頓時間 | 單次GC的最大毫秒數 |
-XX:GCTimeRatio | 設置吞吐量大小 | 業務:GC時間比例,默認爲99,即GC時間佔比爲 1/(1+99)=1% |
單次GC時間參數並不是設置的越小越好,而是一把雙刃劍,若是減小單次GC時間,必然致使GC頻率的上升;而設置的增大,則必然須要更大的內存來支撐。
因爲Parallel Scavenge和其餘收集器(Serial、ParNew、CMS等)使用了不用的設計框架,致使其沒法和CMS協同工做。
工做模式基本和新生代的Serial同樣爲單線程,它採用標記-整理算法,這個模式主要是給Client模式下的JVM使用。若是是Server模式有兩大用途:
Parallel Scavenge的老年版本,JDK6開始出現,採用標記-整理算法。Parallel Old的出現結合Parallel Scavenge,真正的造成「吞吐量優先」的收集器組合。JDK7和8中,做爲老年代默認的收集器。
在JDK6之前,新生代的Parallel Scavenge只能和Serial Old配合使用,而Serial Old爲單線程,Server模式下沒法充分利用多核CPU,這種組合沒法讓應用的吞吐量最大化。
CMS收集器是以最短回收停頓時間爲目標的收集器。重視響應,以帶來好的用戶體驗,是併發低停頓收集器,經過-XX:+UseConcMarkSweepGC參數啓用CMS收集器。
CMS採用支撐多線程併發的標記-清除算法,它的運做分爲4個階段:
CMS在初始標記和從新標記階段須要暫停業務線程,在執行時間上,初始標記 < 從新標記 < 併發標記,因此時間最長的併發標記,業務線程和GC線程併發運行,因此用戶感覺上,GC暫停的時間很短。但其也存在幾個缺點,具體以下:
(CPU數+3)/4
,性能很容易受CPU核數影響
爲了解決CMS致使的內存碎片問題,CMS模式提供了
-XX:+UseCMSCompactAtFullCollection
選項,選項默認開啓,用於CMS要進行Full GC時進行內存碎片整理,因爲內存整理的過程沒法併發,須要中止業務進程,因此啓這個選項會影響性能。