JVM層的GC調優是生產環境上必不可少的一個環節,由於咱們須要肯定這個進程能夠佔用多少內存,以及設定一些參數的閥值。以此來優化項目的性能和提升可用性,並且這也是在面試中常常會被問到的問題。html
想要進行GC調優,咱們首先須要簡單瞭解下JVM的內存結構,Java虛擬機的規範文檔以下:java
https://docs.oracle.com/javase/specs/jvms/se8/html/index.htmllinux
在介紹JVM內存結構以前,咱們須要先知道運行時數據區這樣的一個東西,它與JVM的內存結構有着必定的關聯。不過它屬因而一個規範,因此與JVM內存結構是有着物理上的區別的。運行時數據區以下:
web
1.程序計數器(Program Count Register,簡稱PC Register):面試
2.虛擬機棧(JVM Stacks):算法
3.堆Heap:服務器
4.方法區(Method Area):多線程
5.運行時常量池(Run-Time Constant Pool):併發
6.本地方法棧(Native Method Stacks):oracle
瞭解了運行時方法區規範後,咱們接下來看看JVM的內存結構圖:
如上圖,能夠看到JVM內存被分爲了兩大區,非堆區用於存儲對象之外的數據:
而堆區則用於存儲對象相關數據:
在圖中也能夠看到,堆區還被分爲了年輕代(young)和老年代(old)。那麼爲何會有年輕代:
咱們先來捋一捋,爲何須要把堆區分代?不分代不能完成它所作的事情麼?其實不分代也徹底能夠,分代的惟一理由就是優化GC性能。你先想一想,若是沒有分代,那咱們全部的對象都會存在同一個空間裏。當進行GC的時候,咱們就要找到哪些對象是沒有用的,這樣一來就須要對整個堆區進行掃描。而咱們的不少對象都是隻存活一瞬間的,因此GC就會比較頻繁,而每次GC都得掃描整個堆區,就會致使性能低下。不進行GC的話,又會致使內存空間很快被佔滿。
由於GC性能的緣由,因此咱們才須要對堆區進行分代。若是進行分代的話,咱們就能夠把新建立的對象專門存放到一個單獨的區域中,當進行GC的時候就優先把這塊存放「短命」對象的區域進行回收,這樣就會騰出很大的空間出來,而且因爲不用去掃描整個堆區,也能極大提升GC的性能。
年輕代中的GC:
從上圖中也能夠看到年輕代被分爲了三部分:1個Eden區和2個Survivor區,通常咱們都會簡稱爲S0、S1(同時它們還分爲from和to兩種角色),默認比例爲8:1。通常狀況下,最新建立的對象都會被分配到Eden區(一些大對象會特殊處理),這些對象通過第一次Minor GC後,若是仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增長1歲,當它的年齡增長到必定程度時,就會被移動到年老代中。
由於年輕代中的對象基本都是"短命"的(80%以上),因此在年輕代的垃圾回收算法使用的是複製算法,複製算法的基本思想就是將內存分爲兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象複製到另一塊上面。因此纔會有S0和S1區,複製算法的優勢就是吞吐量高、可實現高速分配而且不會產生內存碎片,因此才適用於做爲年輕代的GC算法。
在GC開始的時候,對象只會存在於Eden區和名爲「From」的Survivor區,Survivor區「To」是空的。緊接着進行GC,Eden區中全部存活的對象都會被複制到「To」,而在「From」區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到必定值(年齡閾值,能夠經過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到「To」區域。通過此次GC後,Eden區和From區已經被清空。這個時候,「From」和「To」會交換他們的角色,也就是新的「To」就是上次GC前的「From」,新的「From」就是上次GC前的「To」。無論怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到「To」區被填滿,「To」區被填滿以後,會將全部對象移動到年老代中。
JVM中的對象分配:
咱們瞭解完JVM內存結構後,再來看看一些經常使用的JVM參數:
1.設置年輕代的大小,和年輕代的最大值,具體的值須要根據實際業務場景進行判斷。若是存在大量臨時對象就能夠設置大一些,不然小一些,通常爲整個堆大小的1/3或者1/4。爲了防止年輕代的堆收縮,兩個參數的值需設爲同樣大:
2.設置Metaspace的大小,和Metaspace的最大值,一樣需設爲同樣大:
3.設置Eden和其中一個Survivor的比例,這個值也比較重要:
4.設置young和old區的比例:
5.這個參數用於顯示每次Minor GC時Survivor區中各個年齡段的對象的大小:
6.用於設置晉升到老年代的對象年齡的最小值和最大值,每一個對象在堅持過一次Minor GC以後,年齡就加1:
7.使用短直針,也就是啓用壓縮類空間(CCS):
8.設置CCS空間的大小,默認是一個G:
9.設置CodeCache的一個初始大小:
10.設置CodeCache的最大值:
11.設置多大的對象會被直接放進老年代:
12.長期存活的對象會被放入Old區,使用如下參數設置就能夠設置對象的最大存活年齡:
注:若是設置爲0的話,則年輕代對象不通過Survivor區,直接進入年老代。對於年老代比較多的應用,能夠提升效率。若是將此值設置爲一個較大值,則年輕代對象會在Survivor區進行屢次複製,這樣能夠增長對象再年輕代的存活時間,增長在年輕代即被回收的概論,linux64的java6默認值是15:
13.設置Young區每發生GC的時候,就打印有效的對象的歲數狀況:
14.設置Survivor區發生GC後對象所存活的比例值:
本小節咱們來簡單介紹一些常見的垃圾回收算法,衆所周知Java區別與C/C++的一點就是,Java是能夠自動進行垃圾回收的。因此在Java中的內存泄露概念和C/C++中的內存泄露概念不同。在Java中,一個對象的指針一直被應用程序所持有得不到釋放就屬因而內存泄露。而C/C++則是把對象指針給弄丟了,該對象就永遠沒法獲得釋放,這就是C/C++裏的內存泄露。
在進行垃圾回收的是時候,要如何確認一個對象是不是垃圾呢?在好久之前有一種方式就是使用引用計數,當一個對象指針被其餘對象所引用時就會進行一個計數。在進行垃圾回收時,只要這個計數存在,那麼就會判斷該對象就是存活的。而沒有引用計數的對象,就會被判斷爲垃圾,能夠進行回收。可是這種方法缺陷很明顯,計數會佔用資源不說,若是當一個A對象和一個B對象互相持有對方引用時,那麼這兩個對象的引用計數都不會爲0,就永遠不會被回收掉,這樣就會致使內存泄露的問題。
在Java中,則是採用枚舉根節點的方式:
如上圖,JVM會從根節點開始遍歷引用,只要順着引用路線所遍歷到的對象都會判斷爲存活對象,便是具備可達性的,這些對象就不會被回收。而沒有被遍歷到的對象,也就是圖中的E和F對象,即使它們倆互相都還存在引用,也會被回收掉,由於它們不存在根節點的引用路線中,便是不具備可達性的。
既然瞭解了JVM如何判斷一個對象是否爲垃圾後,咱們就能夠來看看一些垃圾回收算法了:
1.標記-清除:
2.複製算法:
3.標記-整理:
4.分代垃圾回收:
在上一小節瞭解了一些常見的垃圾回收算法後,咱們再來看看JVM中常見的垃圾收集器:
注:串行收集器幾乎不會在web應用中使用,因此主要介紹並行和併發收集器
串行 VS 並行 VS 併發:
停頓時間 VS 吞吐量:
開啓串行收集器:
開啓並行收集器:
併發收集器在JDK1.8裏有兩個,一個是CMS,CMS由於具備響應時間優先的特色,因此是低延遲、低停頓的,CMS是老年代收集器。開啓該收集器的參數以下:
另外一個是G1,開啓該收集器的參數以下:
垃圾收集器搭配圖:
注:實線表明可搭配使用的,虛線表示當內存分配失敗的時候CMS會退化成SerialOld。JDK1.8中建議使用的是G1收集器
有這麼多的垃圾收集器,那麼咱們要如何去選擇合適的垃圾收集器呢?這個是沒有具體答案的,都得按照實際的場景進行選擇,但通常都會按照如下原則來進行選擇:
其中並行收集器是支持自適應的,經過設置如下幾個參數,並行收集器會以停頓時間優先去動態調整參數:
當內存不夠的時候並行收集器能夠動態調整內存,雖然實際生產環境中用的比較少,至於每次動態調整多少內存,則使用如下參數進行設置:
瞭解了並行收集器後,咱們來簡單看看CMS收集器其餘的一些特性以及相關調優參數。
CMS垃圾收集過程:
CMS的缺點:
CMS的相關調優參數:
設置併發的GC線程數:
開啓如下參數能夠在Full GC以後對內存進行一個壓縮,以此減小空間碎片:
這個參數則是設置多少次Full GC以後才進行壓縮:
設置Old區存滿多少對象的時候觸發Full GC,默認值爲92%:
啓用該參數表示不可動態調整以上參數的值:
啓用該參數表示在Full GC以前先作Young GC:
在jdk1.7以前可使用如下參數,啓用回收Perm區:
在jdk1.8後,推薦使用的垃圾收集器是G1。G1收集器在jdk1.7中第一次出現,因此到了jdk1.8裏就很是成熟了。
G1收集器官網介紹以下:
The Garbage-First (G1) garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with high probability, while achieving high throughput. Whole-heap operations, such as global marking, are performed concurrently with the application threads. This prevents interruptions proportional to heap or live-data size.
The first focus of G1 is to provide a solution for users running applications that require large heaps with limited GC latency. This means heap sizes of around 6GB or larger, and stable and predictable pause time below 0.5 seconds.
官方文檔地址:
http://www.oracle.com/technetwork/java/javase/tech/g1-intro-jsp-135488.html
原理概述:
G1 也是屬於分代收集器的,可是G1的分代是邏輯上的,而不是物理上的
G1 將整個對區域劃分爲若干個Region,每一個Region的大小是2的倍數(1M,2M,4M,8M,16M,32M,經過設置堆的大小和Region數量計算得出。
Region區域劃分與其餘收集相似,不一樣的是單獨將大對象分配到了單獨的region中,會分配一組連續的Region區域(Humongous start 和 humonous Contoinue 組成),因此一共有四類Region(Eden,Survior,Humongous和Old),G1 做用於整個堆內存區域,設計的目的就是減小Full GC的產生。在Full GC過程當中因爲G1 是單線程進行,會產生較長時間的停頓。
G1的OldGc標記過程能夠和yongGc並行執行,可是OldGc必定在YongGc以後執行,即MixedGc在yongGC以後執行。
結構圖:
G1垃圾收集算法主要應用在多CPU大內存的服務中,在知足高吞吐量的同時,儘量的知足垃圾回收時的暫停時間,該設計主要針對以下應用場景:
G1的幾個概念:
G1中的Young GC過程,和以往的是同樣的:
可是G1中沒有Full GC,取而代之的是Mixed GC:
G1裏還有一個概念叫全局併發標記(global concurrent marking),和CMS的併發標記是相似的:
G1相關調優參數:
設置堆佔有率達到這個參數值則觸發global concurrent marking,默認值爲45%:
設置在global concurrent marking結束以後,能夠知道Region裏有多少空間要被回收,在每次YGC以後和再次發生Mixed GC以前,會檢查垃圾佔比是否達到此參數的值,只有達到了,下次纔會發生Mixed GC:
設置Old區的Region被回收時的存活對象佔比:
設置一次global concurrent marking以後,最多執行Mixed GC的次數:
設置一次Mixed GC中能被選入CSet的最多Old區的Region數量:
其餘參數:
注意事項:
至因而否須要切換到G1收集器,能夠根據如下原則進行選擇:
關於在Web應用中,如何判斷一個垃圾收集器的好壞,主要是看如下兩點,如下兩點都需爲優纔是好的垃圾收集器:
下一篇: