繼上一篇 JVM學習之路2-對象內存佈局及逃逸分析 介紹完jvm相關對象在內存中如何佈局、如何進行訪問以及jvm進行逃逸分析並作優化以後,本篇準備聊一下jvm最關鍵的一個點(也是現實中遇到問題最多的點)垃圾回收,本篇相對比較長,你們能夠只看本身須要看的部分。
知識點
一、垃圾回收機制
二、垃圾收集算法及常見的垃圾收集器html
顧名思義,垃圾回收就是對垃圾進行回收,可是jvm中哪些屬於"垃圾"呢?無用的對象。大概整理了下目前主流的gc流程:
jvm判斷對象是否能夠被回收主要有兩種算法,引用計數法和可達性分析。下面分別來介紹一下。java
在對象中添加一個計數器,用來統計對象被引用的次數,每次對象被引用一次就加1,引用減小一次就減1,當引用次數爲0的時候就說明對象沒有被使用,那就能夠銷燬了。算法
可達性分析也比較簡單,就是有一系列做爲"gc root"的對象做爲起始點,由該節點開始向下查找,當一個對象到gc root沒有任何引用鏈連接時就認爲該對象能夠被回收了,目前主流的虛擬機都是採用這種方式進行回收斷定。如下對象能夠做爲gc root:
一、虛擬機棧中引用的對象;
二、方法區中類靜態屬性引用的對象;
三、方法區中常量引用的對象;
四、本地方法棧中引用的對象;
要注意一點,正常來講對象不可達都會被回收,可是對象也並不是"非死不可",咱們能夠經過重載finalize()對對象再引用一次,便可阻止對象被回收(不建議這麼作)。segmentfault
在談到對象是否被引用的時候,咱們要講一下java中的4種引用類型:強引用、弱引用、軟引用、虛引用;多線程
只要對象還被引用,那就不會被gc所回收。這個是咱們大多數時候在用的,舉個例子:併發
A a = new A();
這個a就是一個強引用。jvm
若是一個對象只有弱引用在引用它,那麼下次gc的時候就會被回收。這個平時咱們代碼中用得很少,可是在看源碼的時候會發現用得仍是比較多的(ThreadLocal就是用得弱引用key的管理),這也是一種內存對象的管理方式。舉個例子:函數
ReferenceQueue<Employee> referenceQueue = new ReferenceQueue<>(); WeakReference<A> weakA = new WeakReference<>(new A()); WeakReference<A> weakA = new WeakReference<>(new A(), referenceQueue);
上述代碼中的weakA就是一個弱引用,在下次gc的時候就會被回收。佈局
若是一個對象只有軟引用,在內存即將不夠的時候,會將這些對象進行回收。舉個例子:性能
ReferenceQueue<Employee> referenceQueue = new ReferenceQueue<>(); SoftReference<A> softEmployee = new SoftReference<>(new A()); SoftReference<A> softEmployee = new SoftReference<>(new A(),referenceQueue);
該引用能夠認爲沒有被引用,也沒法經過該引用獲取到對象實例,總得來講該引用只有一種做用,就是在對象被回收的時候收到系統通知。舉個例子:
PhantomReference<Employee> phantomEmployee = new PhantomReference<>(new Employee(), referenceQueue);
能夠看出虛引用只有一個構造函數,而且須要傳遞引用隊列,引用隊列的做用就是在對象被回收以後能在引用隊列裏找到該引用(不是該對象)。固然弱引用、軟引用均可以用到引用隊列。
在介紹內存模型中堆的概念的時候,說到堆上的空間分爲新生代、老年代(默認1:2),新生代又分爲eden區、to區、from區(默認8:1:1),有一張圖已經比較清晰的展示了不一樣代的狀況,這裏把相關的概念介紹下,後面分析gc日誌的時候仍是比較有用的。
新生代收集(Minor GC/Young GC):對於新生代空間上的垃圾進行回收。
老年代收集(Major GC/Old GC):對於老年代空間上的垃圾進行回收。Major GC有些資料也指整堆收集。
整堆收集(Full GC):收集全部堆和方法區。
對於大對象,咱們知道是直接進入老年代的,因此代碼中儘可能避免大對象:
針對不一樣的分代,有不一樣的收集算法適配,目前常見的垃圾收集算法有三種:標記-清除、標記-複製、標記-整理。
最先最基礎的算法,很是簡單,對於知足回收條件的對象進行標記,而後在回收的時候對帶標記的對象進行回收。
缺點:
一、效率低,存在大量對象時就會有大量的標記和清除動做;
二、內存碎片,會產生大量不連續的內存碎片,致使後面分配內存可能由於連續空間不夠致使須要full gc;
該算法也比較簡單,就是將可用內存分爲相等的兩塊,一塊爲在用的內存,一塊爲空閒內存,當在用內存空間不夠分配的時候,就會將該內存中存活的對象複製一份到空閒內存中,而後對在用內存進行清理。
缺點:
一、內存浪費,因爲存在空閒內存,最多可能會浪費50%內存空間;
二、對象存活率較高時會有大量複製,存在效率問題;
目前主流的虛擬機對於新生代的回收都是採用該算法。
和標記-清除很像,只不過是在清除完後會對空間進行整理,以保證內存空間的連續,減小內存碎片。
缺點:
一、存活對象較多時候,內存空間整理會比較耗性能,形成卡頓;
垃圾收集器就是垃圾收集算法的實踐者。直接引用書裏的一張圖,大概瞭解一下對於不一樣區目前有哪些收集器以及如何搭配使用。
存在連線的說明是能夠搭配使用的收集器。下面咱們逐一介紹這些收集器。
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、Parallel Old、CMS
整堆收集器:G1
顧名思義,單線程收集器,會有一個額外線程對垃圾進行收集,在收集的過程當中會暫停其餘全部工做線程。
缺點:體驗差,會形成程序卡頓;
優勢:簡單高效、內存消耗少;
算法:標記-複製;
使用場景:對於新生代只須要幾百兆內存的應用,卡頓時間差很少在幾十上百毫秒,非頻繁收集下是能夠接受的。應用於客戶端應用。
設置參數:-XX:+UseSerialGC
其實就是Serial的多線程版本,在單核系統下相比Serial沒有優點,多核下就有優點,會有多個線程同時進行垃圾回收,優缺點和Serial同樣。
使用場景:配合CMS進行使用,應用於服務端應用。
設置參數:
"-XX:+UseParNewGC":強制指定使用ParNew;
"-XX:+UseConcMarkSweepGC":指定使用CMS後,會默認使用ParNew做爲新生代收集器;
"-XX:ParallelGCThreads":指定垃圾收集的線程數量,ParNew默認開啓的收集線程與CPU的數量相同;
與ParNew相似,可是關注點在吞吐量而不在卡頓,該收集器與其老年代版本爲jdk8默認收集器。
吞吐量=處理器用於運行用戶代碼的時間/處理器總消耗時間的比值,也就是儘可能讓處理器運行用戶代碼。該收集器支持經過參數設置自適應策略,虛擬機會自動進行調優。
算法:標記-複製。
使用場景:後臺計算類應用。
設置參數:
"-XX:MaxGCPauseMillis":控制最大垃圾收集停頓時間,大於0的毫秒數,MaxGCPauseMillis設置得稍小,停頓時間可能會縮短,但也可能會使得吞吐量降低,由於可能致使垃圾收集發生得更頻繁;
"-XX:GCTimeRatio":設置垃圾收集時間佔總時間的比率,0<n<100的整數,GCTimeRatio至關於設置吞吐量大小,計算方法是1 / (1 + n)
,默認值99;
"-XX:+UseAdptiveSizePolicy":開啓Jvm自適應策略,JVM會根據當前系統運行狀況收集性能監控信息,動態調整這些參數,以提供最合適的停頓時間或最大的吞吐量;
Serial收集器的老年代版本,沒什麼好多說的。
算法:標記-整理。
Parallel Scavenge收集器的老年代版本,傳說中的吞吐量組合,沒什麼好多說的。
算法:標記-整理。
全稱Concurrent Mark Sweep,以低停頓做爲目標的收集器,基於標記-清除算法,總體分爲4個過程:初始標記、併發標記、從新標記、併發清除。其中初始標記和從新標記是須要中止全部用戶線程的,可是耗時較短。默認收集線程數爲(cpu核數+3)/4個。
優勢:收集效率高,低停頓。
缺點:內存碎片,cpu數量少於4個狀況下對應用影響較大,沒法處理浮動垃圾(併發標記和清除過程當中用戶線程在運行又產生的垃圾)。
應用場景:對於響應性能要求高的系統。
設置參數:
"-XX:+UseConcMarkSweepGC":指定使用CMS收集器;
全稱Garbage-First,該收集器的使命是比較重的,一個里程碑的收集器,它開創了收集器面向局部收集的設計思路和基於Region的內存佈局形式,再也不區分新生代和老年代。什麼是Region?能夠理解爲一塊連續的內存區域,一個堆分爲大小相等的N個Region。G1就是基於Region進行垃圾回收,Region既會扮演新生代eden、survivor區,又會扮演老年代區。用Humongous區存放大對象(對象大小超過Region一半,一個不夠則用多個Region放)。
大體堆分佈變成這樣(引用自參考資料第四個博客):
基本上和上面提到的垃圾收集器實現徹底不同了,不須要回收新生代整個區或者老年代整個區,G1內部會維護一個回收的優先級列表,在目標時間週期內只要把列表中"價值"(可回收空間以及所需時間)最大的Region回收過來就行。
回收步驟:
一、初始標記,須要停頓,耗時短。
二、併發標記,無需停頓。
三、最終標記,須要停頓,耗時短。
四、篩選回收,對各個Region的回收價值和成本進行排序,制定具體的回收計劃,無需停頓。
G1雖然好,可是咱們也不是必定要用G1,仍是得結合具體場景和實際狀況來選擇;
優勢:延遲可控,高吞吐,沒有內存碎片,不須要和其餘收集器搭配使用。
缺點:爲了解決對象跨Region引用問題,須要記憶集進行記錄引用關係,須要額外內存開銷。
算法:總體看集於標記-整理算法進行收集,局部看基於標記-複製算法進行收集。
應用場景:面向服務端應用,針對具備大內存、多處理器的機器。
設置參數:
"-XX:+UseG1GC":指定使用G1收集器。
"-XX:InitiatingHeapOccupancyPercent":當整個Java堆的佔用率達到參數值時,開始併發標記階段;默認爲45。
"-XX:MaxGCPauseMillis":爲G1設置暫停時間目標,默認值爲200毫秒。
"-XX:G1HeapRegionSize":設置每一個Region大小,範圍1MB到32MB,要爲2的N次冪;目標是在最小Java堆時能夠擁有約2048個Region。
"-XX:ConcGCThreads":併發GC使用的線程數。
本篇內容比較多又比較長,花了很多時間來寫,基本上說清了jvm的垃圾回收機制,包括對4種引用類型也作了相關使用介紹,對於垃圾收集的算法以及主流的7種收集器也作了較爲詳細的說明,固然如今還有ZGC、Shenandoah這些更牛逼的收集器,可是用的很少,就不花時間整理介紹了,有須要你們能夠再本身找一下資料學習。後面有新的收穫會再進行補充,下一篇會介紹Jvm的類加載機制,在系列文最後會寫調優案例,到時候會結合相關案例來實踐前面的理論。
參考資料:
周志明《深刻理解Java虛擬機》
https://www.cnblogs.com/jswan...
https://www.cnblogs.com/cxxjo...
https://blog.csdn.net/pedro7k...