目錄
三、JVM垃圾回收的時候如何確定垃圾?什麼是GC Roots?
1、java.lang.StackOverflowError——棧溢出
2、java.lang.OutOfMemoryError: Java heap space——堆內存不夠用
3、java.lang.OutOfMemoryError: GC overhead limit exceeded——GC回收時間過長,頻繁GC又沒效果
4、java.lang.OutOfMemoryError: Direct buffer memory——直接內存掛了
5、java.lang.OutOfMemoryError: unable to create new native thread——創建線程數量達到上限,不能再創建更多本地線程了
6、java.lang.OutOfMemoryError:Metaspace——元空間滿了
1、Serial/Serial Coping收集器(用於新生代)
3、Parallel / Parallel Scavenge收集器(“吞吐量優先”收集器)(新生代)
一、GVM內存結構
1、JVM體系概覽
GC作用域:方法區和堆區
2、Java內存結構
(1)PC寄存器/程序計數器(Program Counter Register)
嚴格來說是一個數據結構,用於保存當前正在執行的程序的內存地址,由於Java是支持多線程執行的,所以程序執行的軌跡不可能一直都是線性執行。當有多個線程交叉執行時,被中斷的線程的程序當前執行到哪條內存地址必然要保存下來,以便用於被中斷的線程恢復執行時再按照被中斷時的指令地址繼續執行下去。爲了線程切換後能恢復到正確的執行位置,每個線程都需要有一個獨立的程序計數器,各個線程之間計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存,這在某種程度上有點類似於“ThreadLocal”,是線程安全的。
(2)Java棧(Java Stack)
Java棧總是與線程關聯在一起的,每當創建一個線程,JVM就會爲該線程創建對應的Java棧,在這個Java棧中又會包含多個棧幀(Stack Frame),這些棧幀是與每個方法關聯起來的,每運行一個方法就創建一個棧幀,每個棧幀會含有一些局部變量、操作棧和方法返回值等信息。每當一個方法執行完成時,該棧幀就會彈出棧幀的元素作爲這個方法的返回值,並且清除這個棧幀,Java棧的棧頂的棧幀就是當前正在執行的活動棧,也就是當前正在執行的方法,PC寄存器也會指向該地址。只有這個活動的棧幀的本地變量可以被操作棧使用,當在這個棧幀中調用另外一個方法時,與之對應的一個新的棧幀被創建,這個新創建的棧幀被放到Java棧的棧頂,變爲當前的活動棧。同樣現在只有這個棧的本地變量才能被使用,當這個棧幀中所有指令都完成時,這個棧幀被移除Java棧,剛纔的那個棧幀變爲活動棧幀,前面棧幀的返回值變爲這個棧幀的操作棧的一個操作數。
由於Java棧是與線程對應起來的,Java棧數據不是線程共有的,所以不需要關心其數據一致性,也不會存在同步鎖的問題。
在Java虛擬機規範中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。在Hot Spot虛擬機中,可以使用-Xss參數來設置棧的大小。棧的大小直接決定了函數調用的可達深度。
(3)本地方法棧(Native Method Stack)
本地方法棧和Java棧所發揮的作用非常相似,區別不過是Java棧爲JVM執行Java方法服務,而本地方法棧爲JVM執行Native方法服務。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。
(4)堆 (Heap)
堆是JVM所管理的內存中最大的一塊,是被所有Java線程鎖共享的,不是線程安全的,在JVM啓動時創建。堆是存儲Java對象的地方,這一點Java虛擬機規範中描述是:所有的對象實例以及數組都要在堆上分配。Java堆是GC管理的主要區域,從內存回收的角度來看,由於現在GC基本都採用分代收集算法,所以Java堆還可以細分爲:新生代和老年代;新生代再細緻一點有Eden空間、From Survivor空間、To Survivor空間等。
(5)方法區(Method Area)
方法區存放了要加載的類的信息(名稱、修飾符等)、類中的靜態常量、類中定義爲final類型的常量、類中的Field信息、類中的方法信息,當在程序中通過Class對象的getName.isInterface等方法來獲取信息時,這些數據都來源於方法區。方法區是被Java線程鎖共享的,不像Java堆中其他部分一樣會頻繁被GC回收,它存儲的信息相對比較穩定,在一定條件下會被GC,當方法區要使用的內存超過其允許的大小時,會拋出OutOfMemory的錯誤信息。方法區也是堆中的一部分,就是我們通常所說的Java堆中的永久區 Permanet Generation,大小可以通過參數來設置,可以通過-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。
(6)常量池(Constant Pool)
常量池本身是方法區中的一個數據結構。常量池中存儲瞭如字符串、final變量值、類名和方法名常量。常量池在編譯期間就被確定,並保存在已編譯的.class文件中。一般分爲兩類:字面量和應用量。字面量就是字符串、final變量等。類名和方法名屬於引用量。引用量最常見的是在調用方法的時候,根據方法名找到方法的引用,並以此定爲到函數體進行函數代碼的執行。引用量包含:類和接口的權限定名、字段的名稱和描述符,方法的名稱和描述符。
3、java8以後的jvm
二、常見的垃圾回收算法
1、引用計數算法
在堆中對每個對象都有一個引用計數器,當對象被引用時,引用計數器加1;當引用被置爲空或者離開這個作用域時,引用計數減1,這種做法的缺點是每次對對象賦值均要維護引用計數器,並且無法解決相互引用的問題,因此JVM沒有采用這個算法。
2、標記清除算法(追蹤回收算法)
標記-清除算法將垃圾回收分爲兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段,利用JVM維護的對象引用圖,從根節點開始遍歷對象的引用圖,同時標記遍歷到的對象,即爲可達對象。未被標記的對象就是未被引用的垃圾對象。然後,在清除階段,清除所有未被標記的對象。
它主要由兩個缺點:一個是效率問題,標記和清除過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致當程序在以後的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
3、複製回收算法——針對新生代
爲了解決標記清除算法的效率問題,出現了複製算法,它將可用內存按容量劃分爲大小相等的兩塊,每次使用其中的一塊。當這塊的內存用完了,就將還存活着的對象複製到另一塊上面,然後再把已使用過的內存空間一次清理掉。
優點是每次都是對其中的一塊進行內存回收,內存分配時就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。缺點是將內存縮小爲原來的一半,代價太高了一點。
現在的商業虛擬機都採用複製收集算法來回收新生代,有研究表明,新生代中的對象98%是朝生夕死的,所以並不需要按照1:1的比例來劃分內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地拷貝到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10%的內存是會被“浪費”的。當然,並不能保證每次回收都只有10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這裏指老年代)進行分配擔保(Handle Promotion)。即如果另外一塊Survivor空間沒有足夠的空間存放上一次新生代收集下來的存活對象,這些對象將直接通過分配擔保機制進入老年代。
4、標記整理算法(壓縮回收算法)——針對老年代
複製收集算法在對象存活率較高時就需要執行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用複製收集算法。
根據老年代的特點提出了“標記-整理”算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是把堆中活動的對象移動到堆中一端,這樣就會在堆中另外一端留出很大一片空閒區域,相當於對堆中的碎片進行處理
5、分代回收算法
當前商業虛擬機的垃圾收集都採用“分代收集”算法,這種算法並無新的方法,只是根據對象的存活週期的不同將內存劃分爲幾塊,一般是把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收
三、JVM垃圾回收的時候如何確定垃圾?什麼是GC Roots?
1、什麼是垃圾?
簡單說就是內存中已經不再被使用的空間就是垃圾。比如說沒有引用指向這個對象了
2、要進行垃圾回收,如何判斷一個對象是否可以被回收?
(1)引用計數法
Java中,引用和對象是有關聯的。如果要操作對象則必須用引用進行。可通過引用計數來判斷一個對象是否可以回收。簡單說,給對象中添加一個引用計數器,每當有一個地方引用它,計數器值加1每當有一個引用失效時,計數器值減1。任何時刻計數器值爲零的對象就是不可能再被使用的,那麼這個對象就是可回收對象
那爲什麼主流的Java虛擬機裏面都沒有選用這種算法呢?其中最主要的原因是它很難解決對象之間相互循環引用的問題
(2)枚舉根節點做可達性分析(根搜索路徑)
Java可以做GCRoots的對象:
- 虛擬機棧(棧幀中的局部變量區,也叫做局部變量表)中引用的對象
- 方法區中的類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(Native方法)引用的對象
例如:
四、JVM調優和參數配置,如何盤點查看JVM系統默認值?
1、JVM的參數類型
(1)標配參數
- java -version
- java -help
(2)X參數
- java -version:混合模式
- java -Xint -version:解釋執行
- java -Xcomp -version:第一次使用就編譯成本地代碼
- java -Xmixed -version:混合模式,先編譯再執行
(3)XX參數
說明:
jps -l 【查看java進程,定位進程編號】
jinfo -flag 某屬性 進程id 【查看jvm參數】
jstack 進程id 【查看進程運行情況,可用於排錯】
- A、Boolean類型:【-XX:+或者-某個屬性值】 +表示開啓,-表示關閉
舉例:
是否打印GC收集細節:-XX:+PrintGCDetails 或 -XX:-PrintGCDetails
是否使用串行垃圾收集器:-XX:+UseSerialGC或 -XX:-UseSerialGC
- B、KV設值類型:【-XX:屬性key=屬性值value】
設置Metaspace大小:-XX:MetaspaceSize = 128m
年齡閾值,默認15(對象被複制的次數):-XX:MaxTenuringThreshold = 15
- C、jinfo舉例,查看當前運行程序配置:【jinfo -flag 配置項 進程編號】
舉例:jps -l 【獲得進程編號】
I、jinfo -flag InitialHeapSize 進程編號 【初始堆內存】
II、jinfo -flags 進程編號
III、jinfo -flag MaxHeapSize 進程編號 【最大堆內存】
- D、-Xms 和 -Xmx
-Xms等價於-XX:InitialHeapSize
-Xmx等價於-XX:MaxHeapSize
2、查看JVM默認值
(1)-XX:+PrintFlagsInitial :查看默認值
java -XX:+PrintFlagsInitial
java -XX:+PrintFlagsInitial -version
(2)-XX:+PrintFlagsFinal :主要查看修改更新
java -XX:+PrintFlagsFinal
java -XX:+PrintFlagsFinal -version
說明:【“=”是默認值,“:=是修改過後的值”】
(3)PrintFlagsFinal 舉例,運行java命令的同時打印出參數
(4)-XX:+PrintCommandLineFlags -version
五、平時工作用過的JVM常用基本配置參數有哪些?
1、java查看虛擬機內存容量和最大內存量
2、JVM常用基本配置參數
(1)-Xms:初始大小內存,默認爲物理內存1/64,等價於-XX:InitialHeapSize
(2)-Xmx:最大分配內存,默認爲物理內存1/4,等價於-XX:MaxHeapSize
(3)-Xss:設置單個線程棧的大小,一般默認爲512K~1024K,等價於-XX:ThreadStackSize
(4)-Xmn:設置年輕代大小
(5)-XX:MetaspaceSize:設置元空間大小
典型設置案例:
(6)-XX:+PrintGCDetails:收集詳細GC日誌收集信息
(7)-XX:SurvivoRatio:設置新生代中eden和s0/s1空間的比例
默認-XX:SurvivoRatio=8,Eden:s0:s1=8:1:1,SurvivoRatio的值就是這隻eden區的比例,s0/s1相同
(8)-XX:NewRatio:配置新生代和老年代在堆結構的佔比
默認-XX:NewRatio=2,新生代佔1,老年代佔2,新生代佔整個堆的1/3,NewRatio值就是這事老年代的佔比
(9)-XX:MaxTenuringThreshold :設置垃圾最大年齡,就是從young到old要經過多少次的垃圾回收
3、實操設置JVM參數及收集詳細GC日誌收集信息
(1)-Xss查看和修改單個線程棧的大小
奇怪:這裏ThreadStackSize爲0
設置JVM參數
再次查看
如果是0表示用的是系統出廠默認值,其他值則表示是修改過的
(2)收集詳細GC日誌收集信息
假設我們現在把初始大小內存和最大內存容量設置爲10M,並打印GC日誌信息
配置jvm參數:
正常情況下:
現在定義了一個超過10M的數據,會進行GC和Full GC,出現OOM異常
(2)GC和Full GC垃圾回收參數解讀
- GC
- Full GC
六、強引用/軟引用/弱引用/虛引用分別是什麼
引用類型 | 說明 | 適用場景 |
強引用(默認支持模式) | 對於強引用的對象,就算是出現了OOM也不會對該對象進行回收,死都不收。 | |
軟引用 | 當系統內存充足時不會被回收,當系統內存不足時會被回收 | 高速緩存 |
弱引用 | 只要垃圾回收機制一運行,不管JVM的內存空間是否足夠,都會回收該對象佔用的內存 | |
虛引用 | 在任何時候都可能被垃圾回收器回收 | 當關聯的引用隊列中有數據的時候,意味着引用指向的堆內存中的對象被回收。通過這種方式,JVM運行我們在對象被銷燬後,做一些我們自己想做的事情 |
1、整體架構
2、強引用(默認支持模式)
當內存不足,JVM開始垃圾回收,對於強引用的對象,就算是出現了OOM也不會對該對象進行回收,死都不收。
強引用是我們最常見的普通對象引用,只要還有強引用指向一個對象,就能表明對象還“活着”,垃圾收集器不會碰這種對象。在java中最常見的就是強引用,把一個對象賦給一個引用變量,這個引用變量就是一個強引用。當一個對象被強引用變量引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即使該對象以後永遠都不會被用到,JVM也不會回收。因此強引用是造成Java內存泄漏的主要原因之一。
對於一個普通的對象,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值爲null,一般認爲就是可以被垃垃圾收集的了(當然具體回收時機還是要看垃圾收集策略)
舉例:只要還有強引用指向一個對象,就能表明對象還“活着”,垃圾收集器不會碰這種對象。
現象:obj1你回收你的,obj2是強引用,不會被回收
3、軟引用
軟引用是一種相對強引用弱化了一些的引用,需要用java.lang.ref.SoftReference類來實現,可以讓對象豁免一些垃圾收集。
對於只有軟引用的對象來說,當系統內存充足時不會被回收,當系統內存不足時會被回收
軟引用通常用在對內存敏感的程序中,比如高速緩存就有用到軟引用,內存夠用的時候就保留,不夠用就回收!
舉例:內存足夠的情況下軟引用不會被回收
現象:由於內存足夠,此時垃圾回收器不回收軟引用
舉例:內存不夠用的情況下軟引用會被回收
配置參數:-Xms10m -Xmx10m。JVM配置,故意產生大對象並配置大內存,讓它內存不夠了就導致OOM,看軟引用的回收情況
現象:由於內存不夠了,此時垃圾回收器回收軟引用
4、弱引用
弱引用需要用java.lang.WeakReference類來實現,它比軟引用的生命期更短。對於只有弱引用的對象來說,只要垃圾回收機制一運行,不管JVM的內存空間是否足夠,都會回收該對象佔用的內存
舉例:只要有gc弱引用就會被回收
EG:WeakHashMap
5、虛引用
虛引用需要java.lang.ref.PhantomReference類來實現。如果一個對象僅持有虛引用,那麼他就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收,它不能單獨使用也不能通過它訪問對象,虛引用必須和引用隊列聯合使用。
虛引用的主要作用是跟蹤對象被垃圾回收的狀態。僅僅是提供了一種確保對象被finalize以後,做某些事情的機制。PhantomReference的get方法總是返回null,因此無法訪問對應的引用對象。其意義在於說明一個對象已經進入finalization階段,可以被gc回收,用來實現比finalization機制更靈活的回收操作。
換句話說,設置虛引用關聯的唯一目的,就是在這個對象被收集器回收的時候收到一個系統通知或者後續添加進一步的處理。java技術允許使用finalize方法在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。
舉例:虛引用的get方總是返回null;虛引用被回收前需要被引用隊列ReferenceQueue保存下
6、軟引用和弱引用的適用場景
7、ReferenceQueue
java提供了4中引用類型,在垃圾回收時,都有各自的特點。
ReferenceQueue是用來配合引用工作的,沒有ReferenceQueue一樣可以運行。
創建引用的時候可以指定關聯的隊列,當GC釋放對象內存的時候,會將引用加入到引用隊列,如果程序發現某個虛引用已經被加入到引用隊列當中,那麼就可以在所引用的對象被內存回首之前採取必要的行動,這相當於是一種通知機制
當關聯的引用隊列中有數據的時候,意味着引用指向的堆內存中的對象被回收。通過這種方式,JVM運行我們在對象被銷燬後,做一些我們自己想做的事情
8、GCRoots和四大引用的小總結
七、談談你對OOM的認識
OOM類型 | 是什麼 | 產生原因 |
java.lang.StackOverflowError | 棧溢出 | 如不合理的遞歸調用 |
java.lang.OutOfMemoryError: Java heap space | 堆內存不夠用 | 如死循環、創建大的對象但是堆內存根本就不夠分配 |
java.lang.OutOfMemoryError: GC overhead limit exceeded | GC回收時間過長,頻繁GC又沒效果 | 當GC爲釋放很小空間佔用大量時間時拋出。一般是因爲堆太小,沒有足夠的內存。 |
java.lang.OutOfMemoryError: Direct buffer memory | 直接內存掛了 | 元空間並不在虛擬機中,而是使用本地內存。不歸GC管,所以GC不會回收,如果本地內存用完了就會拋出此異常。 |
java.lang.OutOfMemoryError: unable to create new native thread | 創建線程數量達到上限,不能再創建更多本地線程了 | 應用創建太多線程,超過系統承載能力 |
java.lang.OutOfMemoryError:Metaspace | 元空間滿了 |
虛擬機加載的類信息、常量池、靜態變量、即時編譯後的代碼不斷往元數據空間灌,佔據的總空間超過了Metaspace指定的元空間大小 |
1、java.lang.StackOverflowError——棧溢出
產生的原因:不合理的遞歸調用【過深的遞歸調用】
2、java.lang.OutOfMemoryError: Java heap space——堆內存不夠用
產生的原因:死循環
產生原因:超過堆內存大小限制
配置參數:-Xms10m -Xmx10m
3、java.lang.OutOfMemoryError: GC overhead limit exceeded——GC回收時間過長,頻繁GC又沒效果
程序在垃圾回收上花費了98%的時間,卻收集不回2%的空間,通常這樣的異常伴隨着CPU的衝高
GC回收時間過長時會拋出OutOfMemoryError。過長的定義是,超過98%的時間用來做GC並且回收了不到2%的堆內存。連續多次GC都只回收了不到2%的極端情況下才會拋出。
假如不拋出 GC overhead limit 錯誤會發生什麼情況:那就是GC清理的這麼點內存很快就會再次填滿,迫使GC再次執行。這樣就造成惡性循環,CPU使用率一直是100%,而GC卻沒有任何成果。
產生的原因:當GC爲釋放很小空間佔用大量時間時拋出。一般是因爲堆太小,沒有足夠的內存。雖然在做GC,但基本上沒回收
配置參數:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
解決方法:
(1)首先檢查程序有沒有死循環或者其他一些導致內存被大量佔用的程序,如果確定程序沒有問題,只是程序本身需要大內存時,通過設置增加內存。
(2)添加jvm啓動參數限制使用內存:-XX:UseGCOverheadLimit
4、java.lang.OutOfMemoryError: Direct buffer memory——直接內存掛了
產生的原因:元空間並不在虛擬機中,而是使用本地內存。不歸GC管,所以GC不會回收,如果本地內存用完了就會拋出此異常。【對象沒有new在堆內存,而是在本地內存】
配置參數:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
5、java.lang.OutOfMemoryError: unable to create new native thread——創建線程數量達到上限,不能再創建更多本地線程了
高併發請求服務器時,經常出現如下異常:java.lang.OutOfMemoryError: unable to create new native thread,準確地講該native thread異常與應用的平臺有關
運行結果:
非ROOT用戶登錄Linux系統測試->服務器級別參數調優:
6、java.lang.OutOfMemoryError:Metaspace——元空間滿了
java8之後使用Metaspace來代替永久代。Metaspace是在方法區HotSpot中實現的,它與持久代最大的區別在於:Metaspace並不在虛擬機內存中而是使用本地內存中。它用來存放虛擬機加載的類信息、常量池、靜態變量、即時編譯後的代碼。MaxMetaspaceSize初始化大小是20M左右。
產生原因:不斷生成類往元數據空間灌,類佔據的總空間超過了Metaspace指定的元空間大小
配置參數:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
八、七大垃圾收集器及其在生產上的配置
GC算法(引用計數/複製/標記/標整)是內存回收的方法論,垃圾收集器就是算法落地實現。
因爲目前爲止還沒有完美的收集器出現,更加沒有萬能的收集器,只是針對具體應用最適合的收集器,進行分代收集
七種主要的垃圾收集器:
垃圾收集器類型 | 串行/並行/併發 | 新生代/老年代 | 詳細說明 | 算法 | 目標 | 適用場景 |
串行垃圾回收器(Serial) | 串行 | 新生代 | 爲單線程環境設計並且只適用一個線程進行垃圾回收,會暫停所有的用戶線程 | 複製算法 | 響應速度優先 | 適用於單CPU環境,不適合服務器環境 |
Serial Old | 串行 | 老年代 | 現在已經沒有了 | 標記-整理算法 | 響應速度優先 | 單CPU環境下的Client模式、CMS的後備預案 |
並行垃圾回收器(Parallel) | 並行 | 新生代 | 多個垃圾回收器並行工作,此時用戶線程是暫停的 | 複製算法 | 響應速度優先 | 多CPU環境時在Server模式下與CMS配合 |
Parallel Scavenge | 並行 | 新生代 | 同上 | 複製算法 | 吞吐量優先 | 在後臺運算而不需要太多交互的任務,如科學計算/大數據處理 |
Parallel Old | 並行 | 老年代 | 同上 | 標記-整理算法 | 吞吐量優先 | 在後臺運算而不需要太多交互的任務,如科學計算/大數據處理 |
併發垃圾回收器(CMS) | 併發 | 老年代 | 用戶線程和垃圾收集器同時執行(不一定是並行,可能交替執行),不需要停頓用戶線程 | 標記-清除算法 | 響應速度優先 | 適用於對響應時間有要求的場景,集中在互聯網站或B/S系統服務端上的Java應用 |
G1垃圾回收器 | 併發 | both | 將堆內存分割成不同的區域然後併發地對其進行垃圾回收 | 標記-整理+複製算法 | 響應速度優先 | 面向服務端應用,將來替換CMS |
1、Serial/Serial Coping收集器(用於新生代)
單線程,在進行垃圾收集時必須暫停其他所有的工作線程,使用複製算法。虛擬機運行在Client模式下的默認新生代收集器。簡單而高效(與其他收集器的單線程比),對於限定單個CPU的環境來說,由於沒有線程交互的開銷可以獲得最高的單線程垃圾收集效率,因此Serial垃圾收集器依然是java虛擬機在client模式下默認的新生代垃圾收集器。
對應的JVM參數是:-XX:+UseSerialGC
開啓後會使用:Serial(Young區用)+Seiral Old(Old區用)的收集器組合
表示:新生代、老年代都會使用串行回收收集器,新生代使用複製算法,老年代使用標記-整理算法
JVM參數配置:
-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC
2、ParNew收集器(新生代)
ParNew收集器其實是Serial收集器的多線程版本,採用複製算法,它是許多運行在Server模式下的虛擬機中首選的新生代收集器,因爲除了Serial收集器外,目前只有它能與CMS收集器配合工作。
對應的JVM參數是:-XX:+UseParNewGC
啓用ParNew收集器,隻影響新生代的收集,不影響老年代,開啓上述參數後,會使用:ParNew(Young區用)+Seiral Old(Old區用)的收集器組合,新生代使用複製算法,老年代採用標記-整理算法。但是這樣的組合java8已不再推薦使用
JVM參數配置:
-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseParNewGC
3、Parallel / Parallel Scavenge收集器(“吞吐量優先”收集器)(新生代)
使用複製算法,並行多線程,這些特點與ParNew一樣
- Parallel Scavenge收集器的目的是達到一個可控制的吞吐量(Throughput),即CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,吞吐量=運行用戶代碼時間 /(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,吞吐量就是99%。
- 自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量。
停頓時間越短對於需要與用戶交互的程序來說越好,良好的響應速度能提升用戶的體驗;高吞吐量可以最高效率地利用CPU時間,儘快地完成程序的運算任務,主要適合在後臺運算而不太需要太多交互的任務。
對應的JVM參數是:-XX:+UseParallelGC 或 -XX:+UseParallelOldGC(可互相激活)
啓用Parallel Scavenge收集器,開啓上述參數後,會使用:Parallel Scavenge(Young區用)+ Parallel Old(Old區用)的收集器組合,新生代使用複製算法,老年代採用標記-整理算法。
JVM參數配置:
-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseParallelGC
-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseParallelOldGC
其他參數設置:
-XX:MaxGCPauseMillis 控制最大垃圾收集停頓時間。(大於0的毫秒數)停頓時間縮短是以犧牲吞吐量和新生代空間換取的。(新生代調的小,吞吐量跟着小,垃圾收集時間就短,停頓就小)。
-XX:GCTimeRatio 直接設置吞吐量大小,0<x<100 的整數,允許的最大GC時間=1/(1+x)。
-XX:+UseAdaptiveSizePolicy 一個開關參數,開啓GC自適應調節策略(GC Ergonomics),將內存管理的調優任務(新生代大小-Xmn、Eden與Survivor區的比例-XX:SurvivorRatio、晉升老年代對象年齡-XX: PretenureSizeThreshold 、等細節參數)交給虛擬機完成。這是Parallel Scavenge收集器與ParNew收集器的一個重要區別,另一個是吞吐量。
4、Serial Old收集器(老年代)
它是Serial收集器的老年代版本,單線程,使用“標記-整理”算法。主要意義是被Client模式下的虛擬機使用。
如果在Server模式下,它還有兩大用途:
- 在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用;
- 作爲CMS 收集器的後備預案,在併發收集發生Concurrent Mode Failure的時候使用。運行過程同Serial收集器。
5、Parallel Old收集器(老年代)
它是Parallel Scavenge收集器的老年代版本,多線程,使用“標記-整理”算法。在注重吞吐量及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old收集器。
6、CMS收集器(老年代)
它是一種以獲取最短回收停頓時間爲目標的收集器,儘可能縮短垃圾收集時用戶線程的停頓時間。優點:併發收集,低停頓。基於“標記-清除”算法。
目前很大一部分Java應用都集中在互聯網站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗,CMS收集器就非常適用於堆內存大、CPU核數多的服務端應用。運作過程分爲4個步驟:
- 初始標記(CMS initial mark):需要“Stop The World”,標記GC Roots能直接關聯到的對象,速度快。
- 併發標記(CMS concurrent mark):進行GC Roots Tracing 過程,和用戶線程一起工作。主要標記過程,標記全部對象
- 重新標記(CMS remark):需要“Stop The World”,修正併發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄。停頓時間:初始標記<重新標記<<併發標記
- 併發清除(CMS concurrent sweep):清除GC Roots不可達對象,和用戶線程一起工作。對於標記結果,直接清理對象,時間較長。
對應的JVM參數是:-XX:+UseConcMarkSweeoGC
啓用UseConcMarkSweeoGC 收集器,開啓上述參數後,會自動將-XX:+UseParNewGC打開,使用:ParNew(Young區用)+ CMS(Old區用)+ Serial Old的收集器組合,Serial Old將作爲CMS出錯的後備收集器,新生代使用複製算法,老年代採用標記-清除算法。
JVM參數配置:
-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseConcMarkSweeoGC
優點:併發收集低停頓
缺點:
- 併發執行,對CPU資源壓力大:由於併發進行,CMS在收集與應用程序會同時增加對堆內存的佔用,也就是說,CMS必須在老年代堆內存用盡之前完成垃圾回收,否則CMS回收失敗,將觸發擔保機制,串行老年代收集器將會以SWT的方式進行一次GC,從而才造成較大停頓時間。CMS默認的回收線程數: (CPU數量+3)/4
- 無法處理浮動垃圾:可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。併發清理階段用戶程序運行產生的垃圾過了標記階段所以無法在本次收集中清理掉,稱爲浮動垃圾。CMS收集器默認在老年代使用了68%的空間後被激活。若老年代增長的不是很快,可以適當調高參數-XX:CMSInitiatingOccupancyFraction 提高觸發百分比,但調得太高會容易導致“Concurrent Mode Failure”失敗。
- 基於“標記-清除”算法會產生大量空間碎片:老年代空間會隨着應用時長被逐步耗盡,最後將不得不通過擔保機制對堆內存進行壓縮。提供開關參數-XX:+UseCMSCompactAtFullCollection 用於在“ 享受”完Full GC服務之後進行碎片整理過程,內存整理的過程是無法併發的,停頓時間會變長。-XX:CMSFullGCsBeforeCompation 設置在執行多少次不壓縮的Full GC後,進行一次帶壓縮的。
7、G1收集器(Garbage First)
(1)以前收集器特點
- 年輕代和老年代都是各自獨立且連續的內存塊
- 年輕代收集使用單eden+S0+S進行復制算法
- 老年代收集必須掃描整個老年代區域
- 都是以儘可能少而快地執行GC爲設計原則
(2)G1是什麼及其特點
G1可解決內存碎片問題。它是一款面向服務端應用的收集器,主要應用在多CPU和大內存服務器環境下,極大減少垃圾收集的停頓時間。主要改變是Eden,Survivor和Tenured等內存區域不再是連續的了,而是變成一個個大小一樣的region,每個region從1M到32M不等。一個region可能屬於Eden,Survivor或Tenured的內存區域
特點:
(3)底層原理
G1區域化垃圾回收器:
做大的好處是化整爲零,避免全內存掃描,只需要按照區域來進行掃描即可
回收步驟:
G1收集器下的Young GC
4步過程:
常用參數配置:
-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseG1GC
與CMS相比有兩個顯著改進:
- 消除內存碎片
- 可以精確地控制停頓
8、怎麼查看默認的垃圾回收器是哪個?
JVM參數:java -XX:+PrintCommandLineFlags -version
9、小結:如何選擇垃圾回收器