前言
這段時間懈怠了,罪過!
最近看到有同事也開始用上了微信公眾號寫博客了,挺好的~給他們點贊,這博客我也不推廣,默默的靜靜的,主要是擔心自己堅持不了。以前寫過時間事件日志現在也不寫了;寫過博客也不寫了;月記也不寫了。
堅持平凡事就是偉大,本來計劃一周一篇的,這次沒有嚴格執行。懈怠了
這個GC跟JVM內容太多了,理論性東西多些,少年時還能記個八九成,好久沒弄,都忘記了。這次權當整理溫習,再看看《深入理解JVM虛擬機》,找些過去寫的博客挖點東西過來!
GC
Java GC(Garbage Collection,垃圾收集,垃圾回收)機制,是Java與C++/C的主要區別之一,作為Java開發者,一般不需要專門編寫內存回收和垃圾清理代碼,對內存泄露和溢出的問題,也不需要像C程序員那樣戰戰兢兢。這是因為在Java虛擬機中,存在自動內存管理和垃圾清掃機制。概括地說,該機制對虛擬機中的內存進行標記,并確定哪些內存需要回收,根據一定的回收策略,自動的回收內存,永不停息(Nerver Stop)的保證虛擬機中的內存空間,防止出現內存泄露和溢出問題。
主要從這幾個問題入手,就差不多了
- Java內存區域
- 哪些內存需要回收?
- 什么時候回收
- 如何回收
- 監控和優化GC
Java內存區域
- 程序計數器(Program Counter Register)
程序計數器是一個比較小的內存區域,用于指示當前線程所執行的字節碼執行到了第幾行,可以理解為是當前線程的行號指示器。字節碼解釋器在工作時,會通過改變這個計數器的值來取下一條語句指令。每個程序計數器只用來記錄一個線程的行號,所以它是線程私有(一個線程就有一個程序計數器)的。
如果程序執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機字節碼指令地址;如果正在執行的是一個本地(native,由C語言編寫完成)方法,則計數器的值為Undefined,由于程序計數器只是記錄當前指令地址,所以不存在內存溢出的情況,因此,程序計數器也是所有JVM內存區 域中唯一一個沒有定義OutOfMemoryError的區域。
- 虛擬機棧(JVM Stack)
一個線程的每個方法在執行的同時,都會創建一個棧幀(Statck Frame),棧幀中存儲的有局部變量表、操作站、動態鏈接、方法出口等,當方法被調用時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。局部變量表中存儲著方法的相關局部變量,包括各種基本數據類型,對象的引用,返回地址等。在局部變量表中,只有long和double類型會占用2個局部變量空間(Slot,對于32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,局部變量表是在編譯時就已經確定 好的,方法運行所需要分配的空間在棧幀中是完全確定的,在方法的生命周期內都不會改變。虛擬機棧中定義了兩種異常,如果線程調用的棧深度大于虛擬機允許的最大深度,則拋出StatckOverFlowError(棧溢出);不過多 數Java虛擬機都允許動態擴展虛擬機棧的大小(有少部分是固定長度的),所以線程可以一直申請棧,知道內存不足,此時,會拋出 OutOfMemoryError(內存溢出)。每個線程對應著一個虛擬機棧,因此虛擬機棧也是線程私有的。
- 本地方法棧(Native Method Statck):
本地方法棧在作用,運行機制,異常類型等方面都與虛擬機棧相同,唯一的區別是:虛擬機棧是執行Java方法的,而本地方法棧是用來執行native方法的,在很多虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將本地方法棧與虛擬機棧放在一起使用。本地方法棧也是線程私有的。
- 堆區(Heap)
堆區是理解Java GC機制最重要的區域,沒有之一。在JVM所管理的內存中,堆區是最大的一塊,堆區也是Java GC機制所管理的主要內存區域,堆區由所有線程共享,在虛擬機啟動時創建。堆區的存在是為了存儲對象實例,原則上講,所有的對象都在堆區上分配內存(不過現代技術里,也不是這么絕對的,也有棧上直接分配的)。一般的,根據Java虛擬機規范規定,堆內存需要在邏輯上是連續的(在物理上不需要),在實現時,可以是固定大小的,也可以是可擴展的,目前主流的虛擬機都是可擴展的。如果在執行垃圾回收之后,仍沒有足夠的內存分配,也不能再擴展,將會拋出OutOfMemoryError:Java heap space異常。
-Xms 參數設置最小值
-Xmx 參數設置最大值
例:VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
若-Xms=-Xmx,則可避免堆自動擴展。
-XX:+HeapDumpOnOutOfMemoryError 可以讓虛擬機在出現內存溢出是dump出當前的內存堆轉儲快照。
- 方法區(Method Area)
在Java虛擬機規范中,將方法區作為堆的一個邏輯部分來對待,但事實上,方法區并不是堆(Non-Heap);另外,不少人的博客中,將Java GC的分代收集機制分為3個代:青年代,老年代,永久代,這些作者將方法區定義為“永久代”,這是因為,對于之前的HotSpot Java虛擬機的實現方式中,將分代收集的思想擴展到了方法區,并將方法區設計成了永久代。不過,除HotSpot之外的多數虛擬機,并不將方法區當做永久代,HotSpot本身,也計劃取消永久代。
方法區是各個線程共享的區域,用于存儲已經被虛擬機加載的類信息(即加載類時需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態變量、編譯器即時編譯的代碼等。方法區在物理上也不需要是連續的,可以選擇固定大小或可擴展大小,并且方法區比堆還多了一個限制:可以選擇是否執行垃圾收集。一般的,方法區上 執行的垃圾收集是很少的,這也是方法區被稱為永久代的原因之一(HotSpot),但這也不代表著在方法區上完全沒有垃圾收集,其上的垃圾收集主要是針對常量池的內存回收和對已加載類的卸載。在方法區上進行垃圾收集,條件苛刻而且相當困難,效果也不令人滿意,所以一般不做太多考慮,可以留作以后進一步深入研究時使用。在方法區上定義了OutOfMemoryError:PermGen space異常,
在內存不足時拋出。
運行時常量池(Runtime Constant Pool)是方法區的一部分,用于存儲編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字符串表示某個變量、接口的位置,直接引用就是根據符號引用翻譯出來的地址,將在類鏈接階段完成翻譯);運行時常量池除了存儲編譯期常量外,也可以存儲在運行時間產生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果調用的字符“abc”已經在常量池中,則返回池中的字符串地址,否則,新建一個常量加入池中,并返回地址)。
-XX:MaxPermSize 設置上限
-XX:PermSize 設置最小值
例:VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
- 直接內存(Direct Memory)
直接內存(Direct Memory)并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域,但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現,所以我們放到這里一起講解。
Direct Memory滿了之后,系統不會自動回收這段內存; 而是要等Tenured Generation滿觸發GC時,Direct Memory才會被跟著回收。
在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆里面的DirectByteBuffer對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復制數據。
顯然,本機直接內存的分配不會受到Java堆大小的限制,但是,既然是內存,則肯定還是會受到本機總內存(包括RAM及SWAP區或者分頁文件)的大小及處理器尋址空間的限制。服務器管理員配置虛擬機參數時,一般會根據實際內存設置-Xmx等參數信息,但經常會忽略掉直接內存,使得各個內存區域的總和大于物理內存限制(包括物理上的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。
-XX:MaxDirectMemorySize 設置最大值,默認與java堆最大值一樣。
例 :-XX:MaxDirectMemorySize=10M -Xmx20M
哪些內存被回收
根據運行時數據區域的各個部分,程序計數器、虛擬機棧、本地方法棧三個區域隨著線程而生,隨線程滅而滅。棧中的棧幀隨著方法的進入和退出而進棧出棧。每個棧幀分配多少內存在類結構確定下來的時候就基本已經確定。所以這個三個區域內存回收時方法或者線程結束而回收的,不需要太多關注;而java堆和方法區則不一樣,一個接口不同實現類,一個方法中不同的分支,在具體運行的時候才能確定創建那些對象,所以這部分內存是動態的,也是需要垃圾回收機制來回收處理的。
- 堆內存
判斷堆內的對象是否可以回收,要判斷這個對象實例是否確實沒用,判斷算法有兩種:引用計數法和根搜索算法。
引用計數法:就是給每個對象加一個計數器,如果有一個地方引用就加1,當引用失效就減1;當計數器為0,則認為對象是無用的。這種算法最大的問題在于不能解決相互引用的對象,如:A.b=B;B.a=A,在沒有其他引用的情況下,應該回收;但按照引用計數法來計算,他們的引用都不為0,顯然不能回收。
根搜索算法:這個算法的思路是通過一系列名為“GC Roots”的對象作為起點,
從這個節點向下搜索,搜索所經過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(圖論的不可達)時,則證明該對象不可用。
java等一大部分商用語言是用根搜索算法來管理內存的,java中可以做為GC Roots的對象有如下幾種:
虛擬機棧(棧幀中的本地變量表)中的引用的對象;
方法區中的類靜態屬性引用的對象;
方法區中常量引用的對象;
本地方法棧JNI(Native)的引用對象;
無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鏈是否可達,判定對象是否存活都與“引用”有關。在JDK1.2以前,Java中的引用的定義很傳統如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表著一個引用。這種定義很純粹,但是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對于如何描述一些“食之無味,棄之可惜”的對象就顯得無能為力。我們希望能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;如果內存空間在進行垃圾收集后還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。
在JDK 1.2之后,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。
- 強引用
只要強引用還存在,垃圾收集器永遠不會收掉被引用的對象
- 軟引用
在系統將要發生內存異常之前,將會把這些對象列進回收范圍之中進行第二次回收。
- 弱引用
被弱引用關聯的對象只能生存道下一次垃圾收集發生之前。
- 虛引用
一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象的實例。
finalize()方法
在Object類中
protected void finalize() throws Throwable { }
注意下這個訪問控制符是protected
finalize()在什么時候被調用?
有三種情況
- 所有對象被Garbage Collection時自動調用,比如運行System.gc()的時候.
- 程序退出時為每個對象調用一次finalize方法。
- 顯式的調用finalize方法
當一個對象不可到達時,并不是馬上就被回收的。
當對象沒有覆蓋finalize()方法,或者finalized()已經被JVM調用過,那就是沒有必要執行finalzied()
;Finalizer線程執行它,但并不保證等待它執行結束,這主要是防止finalize()出現問題,導致Finalizer線程無限等待,整個內存回收系統崩潰
具體的finalize流程:
對象可由兩種狀態,涉及到兩類狀態空間,一是終結狀態空間 F = {unfinalized, finalizable, finalized};二是可達狀態空間 R = {reachable, finalizer-reachable, unreachable}。各狀態含義如下:
unfinalized: 新建對象會先進入此狀態,GC并未準備執行其finalize方法,因為該對象是可達的
finalizable: 表示GC可對該對象執行finalize方法,GC已檢測到該對象不可達。正如前面所述,GC通過F-Queue隊列和一專用線程完成finalize的執行
finalized: 表示GC已經對該對象執行過finalize方法
reachable: 表示GC Roots引用可達
finalizer-reachable(f-reachable):表示不是reachable,但可通過某個finalizable對象可達
unreachable:對象不可通過上面兩種途徑可達
- 新建對象首先處于[reachable, unfinalized]狀態(A)
- 隨著程序的運行,一些引用關系會消失,導致狀態變遷,從reachable狀態變遷到f-reachable(B, C, D)或unreachable(E, F)狀態
- 若JVM檢測到處于unfinalized狀態的對象變成f-reachable或unreachable,JVM會將其標記為finalizable狀態(G,H)。若對象原處于[unreachable, unfinalized]狀態,則同時將其標記為f-reachable(H)。
- 在某個時刻,JVM取出某個finalizable對象,將其標記為finalized并在某個線程中執行其finalize方法。由于是在活動線程中引用了該對象,該對象將變遷到(reachable, finalized)狀態(K或J)。該動作將影響某些其他對象從f-reachable狀態重新回到reachable狀態(L, M, N), 這就是對象重生
- 處于finalizable狀態的對象不能同時是unreahable的,由第4點可知,將對象finalizable對象標記為finalized時會由某個線程執行該對象的finalize方法,致使其變成reachable。這也是圖中只有八個狀態點的原因
- 程序員手動調用finalize方法并不會影響到上述內部標記的變化,因此JVM只會至多調用finalize一次,即使該對象“復活”也是如此。程序員手動調用多少次不影響JVM的行為
- 若JVM檢測到finalized狀態的對象變成unreachable,回收其內存(I)
- 若對象并未覆蓋finalize方法,JVM會進行優化,直接回收對象(O)
注:System.runFinalizersOnExit()等方法可以使對象即使處于reachable狀態,JVM仍對其執行finalize方法
對finalize()的一句話概括:
JVM能夠保證一個對象在回收以前一定會調用一次它的finalize()方法。這句話中兩個陷阱:回收以前一定和一次
但有很多地方是講,JVM不承諾這一定調用finalize(),這就是上面的陷阱造成的
你永遠不知道它什么時候被調用甚至會不會調用(因為有些對象是永遠不會被回收的,或者被回收以前程序就結束了),但如果他是有必要執行finalize()的,那在GC前一定調用一次且僅一次,如果在第一次GC時沒有被回收,那以后再GC時,就不再調用finalize()
- 方法區
很多人認為方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規范中確實說過可以不要求虛擬機在方法區實現垃圾收集,而且在方法區進行垃圾收集的“性價比”一般比較低:在堆中,尤其是在新生代中,常規應用進行一次垃圾收集*++一般可以回收70%~95%的空間++,而永久代的垃圾收集效率遠低于此。
方法區回收主要有兩部分:廢棄的常量和無用的類。廢棄的常量判斷方法和堆中的對象類似,只要判斷沒有地方引用就可以回收。相比之下,判斷一個類是否無用,條件就比較苛刻,需要同時滿足下面3個條件才能算是“無用的類”:
- 該類的所有實例都已經被回收,也就是java堆中不存在該類的任何實例;
- 加載該類的ClassLoader已經被回收;
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足上述3個條件的無用類進行回收,這里說的僅僅是“可以”,而不是和對象一樣,不使用了就必然會回收。是否對類進行回收,
HotSpot虛擬機提供了
-Xnoclassgc參數進行控制,
還可以使用
-verbose:class
-XX:+TraceClassLoading
-XX:+TraceClassUnLoading查看類的加載和卸載信息。
在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出
如何回收
選擇合適的GC collector是JVM調優最重要的一項,前提是先了解回收算法
“標記-清除”(Mark-Sweep)
算法分為“標記”和“清除”兩個階段:
首先標記出所有需要回收的對象,在標記完成后統一回收掉所有被標記的對象
主要缺點有兩個
- 一個是效率問題,標記和清除過程的效率都不高
- 一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致,當程序在以后的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作
“復制”(Copying)
它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
只是這種算法的代價是將內存縮小為原來的一半,未免太高了一點
現在的商業虛擬機都采用這種收集算法來回收新生代,IBM的專門研究表明,新生代中的對象98%是朝生夕死的,所以并不需要按照1∶1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當回收時,將Eden和Survivor中還存活著的對象一次性地拷貝到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor的空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的內存是會被“浪費”的。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保(Handle Promotion)。
在對象存活率較高時就要執行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
-XX:SurvivorRatio=4
設置年輕代中Eden區與Survivor區的大小比值。
設置為4,則Eden區與兩個Survivor區的比值為4:1:1,一個Survivor區占整個年輕代的1/6
為什么新生代有兩個survivor?
StackOverflow上面給出的解釋是:
The reason for the HotSpot JVM's two survivor spaces is to reduce the need to deal with fragmentation. New objects are allocated in eden space. All well and good. When that's full, you need a GC, so kill stale objects and move live ones to a survivor space, where they can mature for a while before being promoted to the old generation. Still good so far. The next time we run out of eden space, though, we have a conundrum. The next GC comes along and clears out some space in both eden and our survivor space, but the spaces aren't contiguous. So is it better to
- Try to fit the survivors from eden into the holes in the survivor space that were cleared by the GC?
- Shift all the objects in the survivor space down to eliminate the fragmentation, and then move the survivors into it?
- Just say "screw it, we're moving everything around anyway," and copy all of the survivors from both spaces into a completely separate space--the second survivor space--thus leaving you with a clean eden and survivor space where you can repeat the sequence on the next GC?
Sun's answer to the question is obvious.
“標記-整理”(Mark-Compact)
此算法結合了“標記-清除”和“復制”兩個算法的優點。也是分兩階段,
- 第一階段從根節點開始標記所有被引用對象,
- 第二階段遍歷整個堆,把清除未標記對象并且把存活對象“壓縮”到堆的其中一塊,按順序排放。此算法避免了“標記-清除”的碎片問題,同時也避免了“復制”算法的空間問題。
“分代收集”(Generational Collection)
當前商業虛擬機的垃圾收集都采用“分代收集”(Generational Collection)算法,
這種算法并沒有什么新的思想,只是根據對象的存活周期的不同將內存劃分為幾塊。
一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收。
新生代 GC(Minor GC):指發生在新生代的垃圾收集動作,因為 Java 對象大多都具
備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。老年代 GC(Major GC):指發生在老年代的 GC,出現了 Major GC,經常
會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略里
就有直接進行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會比 Minor GC 慢 10
倍以上。
虛擬機給每個對象定義了一個對象年齡(Age)計數器。如果對象在 Eden 出生并經過第一次 Minor GC 后仍然存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間中,并將對象年齡設為 1。對象在 Survivor 區中每熬過一次 Minor GC,年齡就增加 1 歲,當它的年齡增加到一定程度(默認為 15 歲)時,就會被晉升到老年代中。
對象晉升老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。
垃圾收集器
按系統線程分
注意并發(Concurrent)和并行(Parallel)的區別:
- 并發是指用戶線程與GC線程同時執行(不一定是并行,可能交替,但總體上是在同時執行的),不需要停頓用戶線程(其實在CMS中用戶線程還是需要停頓的,只是非常短,GC線程在另一個CPU上執行);
- 并行收集是指多個GC線程并行工作,但此時用戶線程是暫停的;
這個跟傳統的并發并行概念不同
并行是物理的,并發是邏輯的。
并行是和串行對立。
Serial收集器
Serial是最基本、歷史最悠久的垃圾收集器,使用復制算法,曾經是JDK1.3.1之前新生代唯一的垃圾收集器。目前也是ClientVM下 ServerVM 4核4GB以下機器的默認垃圾回收器。
串行收集器并不是只能使用一個CPU進行收集,而是當JVM需要進行垃圾回收的時候,需要中斷所有的用戶線程,知道它回收結束為止,因此又號稱“Stop The World” 的垃圾回收器。注意,JVM中文名稱為java虛擬機,因此它就像一臺虛擬的電腦一樣在工作,而其中的每一個線程就被認為是JVM的一個處理器,因此大家看到圖中的CPU0、CPU1實際為用戶的線程,而不是真正機器的CPU,大家不要誤解哦。
串行回收方式適合低端機器,是Client模式下的默認收集器,對CPU和內存的消耗不高,適合用戶交互比較少,后臺任務較多的系統。
Serial收集器默認新舊生代的回收器搭配為Serial+ SerialOld
新生代、老年代使用串行回收;新生代復制算法、老年代標記-壓縮
在J2SE5.0上,在非server模式下,JVM自動選擇串行收集器。
也可以顯示進行選擇,在Java啟動參數中增加:
-XX:+UseSerialGC
Serial Old收集器
SerialOld是舊生代Client模式下的默認收集器,單線程執行,使用“標記-整理”算法
在Server模式下,主要有兩個用途:
- 在JDK1.5之前版本中與新生代的Parallel Scavenge收集器搭配使用。
- 作為年老代中使用CMS收集器的后備垃圾收集方案。
ParNew收集器
ParNew收集器其實就是多線程版本的Serial收集器,
Stop The World
他是多CPU模式下的首選回收器(該回收器在單CPU的環境下回收效率遠遠低于Serial收集器,所以一定要注意場景哦)
Server模式下的默認收集器。
新生代并行,老年代串行;新生代復制算法、老年代標記-壓縮
-XX:+UseParNewGC ParNew收集器
ParNew收集器默認開啟和CPU數目相同的線程數
-XX:ParallelGCThreads 限制線程數量
Parallel Scavenge收集器
Parallel Scavenge收集器也是一個新生代垃圾收集器,同樣使用復制算法,也是一個多線程的垃圾收集器,也稱吞吐量優先的收集器
所提到的吞吐量=程序運行時間/(JVM執行回收的時間+程序運行時間),假設程序運行了100分鐘,JVM的垃圾回收占用1分鐘,那么吞吐量就是99%。在當今網絡告訴發達的今天,良好的響應速度是提升用戶體驗的一個重要指標,多核并行云計算的發展要求程序盡可能的使用CPU和內存資源,盡快的計算出最終結果,因此在交互不多的云端,比較適合使用該回收器。
可以通過參數來打開自適應調節策略,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量;也可以通過參數控制GC的時間不大于多少毫秒或者比例
新生代復制算法、老年代標記-壓縮
-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行
Parallel Scavenge收集器提供了兩個參數用于精準控制吞吐量:
a.-XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間,是一個大于0的毫秒數。
b.-XX:GCTimeRation:直接設置吞吐量大小,是一個大于0小于100的整數,
也就是程序運行時間占總時間的比率,默認值是99,即垃圾收集運行最大1%(1/(1+99))的垃圾收集時間
-XX:+UseAdaptiveSizePolicy,這是個開關參數,
打開之后就不需要手動指定新生代大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、
新生代晉升年老代對象年齡(-XX:PretenureSizeThreshold)等細節參數
Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器是在JDK 1.6中才開始提供
在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old正是為了在年老代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略
參數控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行
新生代Parallel Scavenge和年老代Parallel Old收集器搭配運行過程圖:
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用都集中在互聯網站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。
由于整個過程中耗時最長的并發標記和并發清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內存回收過程是與用戶線程一起并發地執行。老年代收集器(新生代使用ParNew)
優點:并發收集、低停頓
缺點:產生大量空間碎片、并發階段會降低吞吐量
從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“標記-清除”算法實現的,它的運作過程相對于前面幾種收集器來說要更復雜一些,整個過程分為4個步驟,包括:
初始標記(CMS initial mark)
并發標記(CMS concurrent mark)
重新標記(CMS remark)
并發清除(CMS concurrent sweep)
a.初始標記:只是標記一下GC Roots能直接關聯的對象,速度很快,仍然需要暫停所有的工作線程。
b.并發標記:進行GC Roots跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。
c.重新標記:為了修正在并發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,仍然需要暫停所有的工作線程。
d.并發清除:清除GC Roots不可達對象,和用戶線程一起工作,不需要暫停工作線程。
由于耗時最長的并發標記和并發清除過程中,垃圾收集線程可以和用戶現在一起并發工作,所以總體上來看CMS收集器的內存回收和用戶線程是一起并發地執行。
CMS收集器工作過程:
其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。
CMS收集器有以下三個不足:
CMS收集器對CPU資源非常敏感,其默認啟動的收集線程數=(CPU數量+3)/4,在用戶程序本來CPU負荷已經比較高的情況下,如果還要分出CPU資源用來運行垃圾收集器線程,會使得CPU負載加重。
CMS無法處理浮動垃圾(Floating Garbage),可能會導致Concurrent ModeFailure失敗而導致另一次Full GC。由于CMS收集器和用戶線程并發運行,因此在收集過程中不斷有新的垃圾產生,這些垃圾出現在標記過程之后,CMS無法在本次收集中處理掉它們,只好等待下一次GC時再將其清理掉,這些垃圾就稱為浮動垃圾。
CMS垃圾收集器不能像其他垃圾收集器那樣等待年老代機會完全被填滿之后再進行收集,需要預留一部分空間供并發收集時的使用,可以通過參數-XX:CMSInitiatingOccupancyFraction來設置年老代空間達到多少的百分比時觸發CMS進行垃圾收集,默認是68%。
如果在CMS運行期間,預留的內存無法滿足程序需要,就會出現一次ConcurrentMode Failure失敗,此時虛擬機將啟動預備方案,使用Serial Old收集器重新進行年老代垃圾回收。CMS收集器是基于標記-清除算法,因此不可避免會產生大量不連續的內存碎片,如果無法找到一塊足夠大的連續內存存放對象時,將會觸發因此Full GC。CMS提供一個開關參數-XX:+UseCMSCompactAtFullCollection,用于指定在Full GC之后進行內存整理,內存整理會使得垃圾收集停頓時間變長,CMS提供了另外一個參數-XX:CMSFullGCsBeforeCompaction,用于設置在執行多少次不壓縮的Full GC之后,跟著再來一次內存整理
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC后,進行一次碎片整理;整理過程是獨占的,會引起停頓時間變長
-XX:+CMSFullGCsBeforeCompaction 設置進行幾次Full GC后,進行一次碎片整理
-XX:ParallelCMSThreads 設定CMS的線程數量(一般情況約等于可用CPU數量)
G1收集器
G1可謂博采眾家之長,力求到達一種完美。他吸取了增量收集優點,把整個堆劃分為一個一個等大小的區域(region)。內存的回收和劃分都以region為單位;同時,他也吸取了CMS的特點,把這個垃圾回收過程分為幾個階段,分散一個垃圾回收過程;而且,G1也認同分代垃圾回收的思想,認為不同對象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。為了達到對回收時間的可預計性,G1在掃描了region以后,對其中的活躍對象的大小進行排序,首先會收集那些活躍對象小的region,以便快速回收空間(要復制的活躍對象少了),因為活躍對象小,里面可以認為多數都是垃圾,所以這種方式被稱為Garbage First(G1)的垃圾回收算法,即:垃圾優先的回收。
與CMS收集器相比G1收集器有以下特點:
空間整合,G1收集器采用標記整理算法,不會產生內存空間碎片。分配大對象時不會因為無法找到連續空間而提前觸發下一次GC。
可預測停頓,這是G1的另一大優勢,降低停頓時間是G1和CMS的共同關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為N毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特征了。
收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存布局與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,它們都是一部分(可以不連續)Region的集合。
G1的新生代收集跟ParNew類似,當新生代占用達到一定比例的時候,開始出發收集。
和CMS類似,G1收集器收集老年代對象會有短暫停頓。
標記階段,首先初始標記(Initial-Mark),這個階段是停頓的(Stop the World Event),并且會觸發一次普通Mintor GC。對應GC log:GC pause (young) (inital-mark)
Root Region Scanning,程序運行過程中會回收survivor區(存活到老年代),這一過程必須在young GC之前完成。
-
Concurrent Marking,在整個堆中進行并發標記(和應用程序并發執行),此過程可能被young GC中斷。在并發標記階段,若發現區域對象中的所有對象都是垃圾,那個這個區域會被立即回收(圖中打X)。同時,并發標記過程中,會計算每個區域的對象活性(區域中存活對象的比例)。
image Remark, 再標記,會有短暫停頓(STW)。再標記階段是用來收集 并發標記階段 產生新的垃圾(并發階段和應用程序一同運行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。
-
Copy/Clean up,多線程清除失活對象,會有STW。G1將回收區域的存活對象拷貝到新區域,清除Remember Sets,并發清空回收區域并把它返回到空閑區域鏈表中。
image -
復制/清除過程后。回收區域的活性對象已經被集中回收到深藍色和深綠色區域。
image
-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #開啟
-XX:MaxGCPauseMillis =50 #暫停時間目標
-XX:GCPauseIntervalMillis =200 #暫停間隔目標
-XX:+G1YoungGenSize=512m #年輕代大小
-XX:SurvivorRatio=6 #幸存區比例
什么時候回收
Minor GC觸發
- Eden區域滿了,或者新創建的對象大小 > Eden所剩空間
- CMS設置了CMSScavengeBeforeRemark參數,這樣在CMS的Remark之前會先做一次Minor GC來清理新生代,加速之后的Remark的速度。這樣整體的stop-the world時間反而短
- Full GC的時候會先觸發Minor GC
啥時候會觸發CMS GC?
CMS不等于Full GC,很多人會認為CMS肯定會引發Minor GC。CMS是針對老年代的GC策略,原則上它不會去清理新生代,只有設置CMSScavengeBeforeRemark優化時,或者是concurrent mode failure的時候才會去做Minor GC
1、舊生代或者持久代已經使用的空間達到設定的百分比時(CMSInitiatingOccupancyFraction這個設置old區,perm區也可以設置);
2、JVM自動觸發(JVM的動態策略,也就是悲觀策略)(基于之前GC的頻率以及舊生代的增長趨勢來評估決定什么時候開始執行),如果不希望JVM自行決定,可以通過-XX:UseCMSInitiatingOccupancyOnly=true來制定;
3、設置了 -XX:CMSClassUnloadingEnabled 這個則考慮Perm區;
啥時候會觸發Full GC?
一、舊生代空間不足:java.lang.outOfMemoryError:java heap space;
二、Perm空間滿:java.lang.outOfMemoryError:PermGen space;
三、CMS GC時出現promotion failed 和concurrent mode failure(Concurrent mode failure發生的原因一般是CMS正在進行,但是由于old區內存不足,需要盡快回收old區里面的死的java對象,這個時候foreground gc需要被觸發,停止所有的java線程,同時終止CMS,直接進行MSC。);
四、統計得到的minor GC晉升到舊生代的平均大小大于舊生代的剩余空間;
五、主動觸發Full GC(執行jmap -histo:live [pid])來避免碎片問題;
六、調用System.gc時,系統建議執行Full GC,但是不必然執行,-XX:+DisableExplicitGC 禁用System.gc()調用
GC策略選擇總結
jvm有client和server兩種模式,這兩種模式的gc默認方式是不同的:
client模式下,新生代選擇的是串行gc,舊生代選擇的是串行gc
server模式下,新生代選擇的是并行回收gc,舊生代選擇的是并行gc
一般來說我們系統應用選擇有兩種方式:吞吐量優先和暫停時間優先,對于吞吐量優先的采用server默認的并行gc方式,對于暫停時間優先的選用并發gc(CMS)方式。
監控與調優
GC日志
-XX:+PrintGC 輸出GC日志
-XX:+PrintGCDetails 輸出GC的詳細日志
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的輸出路徑
-verbose.gc開關可顯示GC的操作內容。
打開它,可以顯示最忙和最空閑收集行為發生的時間、收集前后的內存大小、收集需要的時間等
-XX:+PrintGCTimeStamps和-XX:+PrintGCDateStamps
使用-XX:+PrintGCTimeStamps可以將時間和日期也加到GC日志中。表示自JVM啟動至今的時間戳會被添加到每一行中。例子如下:
1 0.185: [GC 66048K->53077K(251392K), 0.0977580 secs]
2 0.323: [GC 119125K->114661K(317440K), 0.1448850 secs]
3 0.603: [GC 246757K->243133K(375296K), 0.2860800 secs]
如果指定了-XX:+PrintGCDateStamps,每一行就添加上了絕對的日期和時間。
1 2014-01-03T12:08:38.102-0100: [GC 66048K->53077K(251392K), 0.0959470 secs]
2 2014-01-03T12:08:38.239-0100: [GC 119125K->114661K(317440K), 0.1421720 secs]
3 2014-01-03T12:08:38.513-0100: [GC 246757K->243133K(375296K), 0.2761000 secs]
如果需要也可以同時使用兩個參數。推薦同時使用這兩個參數,因為這樣在關聯不同來源的GC日志時很有幫助
每一種收集器的日志形式都是由它們自身的實現所決定的,換而言之,每個收集器的日志格式都可以不一樣。
但虛擬機設計者為了方便用戶閱讀,將各個收集器的日志都維持一定的共性,
例如以下兩段典型的GC日志:
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K),
[Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
最前面的數字“33.125:”和“100.667:”代表了GC發生的時間,這個數字的含義是從Java虛擬機啟動以來經過的秒數。
GC日志開頭的“[GC”和“[Full GC”說明了這次垃圾收集的停頓類型,
而不是用來區分新生代GC還是老年代GC的。
如果有“Full”,說明這次GC是發生了Stop-The-World的,
例如下面這段新生代收集器ParNew的日志也會出現“[Full GC”(這一般是因為出現了分配擔保失敗之類的問題,所以才導致STW)。
如果是調用System.gc()方法所觸發的收集,那么在這里將顯示“[Full GC (System)”。
[Full GC 283.736: [ParNew: 261599K->261599K(261952K), 0.0000288 secs]
接下來的“[DefNew”、“[Tenured”、“[Perm”表示GC發生的區域,這里顯示的區域名稱與使用的GC收集器是密切相關的
例如上面樣例所使用的Serial收集器中的新生代名為“Default New Generation”,所以顯示的是“[DefNew”。
如果是ParNew收集器,新生代名稱就會變為“[ParNew”,意為“Parallel New Generation”。
如果采用Parallel Scavenge收集器,那它配套的新生代稱為“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。
后面方括號內部的“3324K->152K(3712K)”含義是“GC前該內存區域已使用容量-> GC后該內存區域已使用容量 (該內存區域總容量)”。
而在方括號之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆總容量)”。
再往后,“0.0025925 secs”表示該內存區域GC所占用的時間,單位是秒。
有的收集器會給出更具體的時間數據
如“[Times: user=0.01 sys=0.00, real=0.02 secs]”,
這里面的user、sys和real與Linux的time命令所輸出的時間含義一致,分別代表用戶態消耗的CPU時間、內核態消耗的CPU事件和操作從開始到結束所經過的墻鐘時間(Wall Clock Time)。
CPU時間與墻鐘時間的區別是,墻鐘時間包括各種非運算的等待耗時,例如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多線程操作會疊加這些CPU時間,所以讀者看到user或sys時間超過real時間是完全正常的。
分析工具
可以使用一些離線的工具來對GC日志進行分析
比如sun的gchisto( https://java.net/projects/gchisto)
gcviewer( https://github.com/chewiebug/GCViewer ),
這些都是開源的工具,用戶可以直接通過版本控制工具下載其源碼,進行離線分析
打印JVM參數
-XX:+PrintFlagsFinal and -XX:+PrintFlagsInitial
[Global flags]
uintx AdaptivePermSizeWeight = 20 {product}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AdaptiveSizePausePolicy = 0 {product}[...]
uintx YoungGenerationSizeSupplementDecay = 8 {product}
uintx YoungPLABSize = 4096 {product}
bool ZeroTLAB = false {product}
intx hashCode = 0 {product}
表格的每一行包括五列,來表示一個XX參數。第一列表示參數的數據類型,第二列是名稱,第四列為值,第五列是參數的類別。第三列”=”表示第四列是參數的默認值,而”:=” 表明了參數被用戶或者JVM賦值了。
-XX:+PrintCommandLineFlags
這個參數讓JVM打印出那些已經被用戶或者JVM設置過的詳細的XX參數的名稱和值。
換句話說,它列舉出 -XX:+PrintFlagsFinal的結果中第三列有":="的參數。
以這種方式,我們可以用-XX:+PrintCommandLineFlags作為快捷方式來查看修改過的參數
監控jvm
使用自帶工具就行,jstat,jmap,jstack
優化
- 選擇合適的GC collector
- 整個JVM heap的大小
- young generation在整個JVM heap中所占的比重
參數實例
public static void main(String[] args) throws InterruptedException{
//通過allocateDirect分配128MB直接內存
ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);
TimeUnit.SECONDS.sleep(10);
System.out.println("ok");
}
測試用例1:設置JVM參數-Xmx100m,運行異常,因為如果沒設置-XX:MaxDirectMemorySize,則默認與-Xmx參數值相同,分配128M直接內存超出限制范圍
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:658)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:306)
at com.stevex.app.nio.DirectByteBufferTest.main(DirectByteBufferTest.java:8)
為了避免Perm區滿引起的full gc,建議開啟CMS回收Perm區選項:
+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled
默認CMS是在tenured generation沾滿68%的時候開始進行CMS收集,如果你的年老代增長不是那么快,并且希望降低CMS次數的話,可以適當調高此值:
-XX:CMSInitiatingOccupancyFraction=80
遇到兩種fail引起full gc:
Prommotion failed和Concurrent mode failed時:
promotion failed是在進行Minor GC時,survivor space放不下、
對象只能放入舊生代,而此時old gen 的碎片太多為進行過內存重組和壓縮,無法提供一塊較大的、連續的內存空間存放來自新生代對象
Prommotion failed的日志輸出大概是這樣:
42576.951: [ParNew (promotion failed): 320138K->320138K(353920K), 0.2365970 secs]
42576.951: [CMS: 1139969K->1120688K( 166784K), 9.2214860 secs] 1458785K->1120688K(2520704K), 9.4584090 secs]
因為
解決這個問題的辦法有兩種完全相反的傾向:增大救助空間、增大年老代或者去掉救助空間。
解決方法可以通過設置參數
-XX:+UseCMSCompactAtFullCollection(打開對年老代的壓縮)
-XX:CMSFullGCsBeforeCompaction(設置運行多少次FULL GC以后對內存空間進行壓縮、整理)
直接關了servivor空間
-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0
concurrent mode failure
發生在當CMS已經在工作并處于concurrent階段中,而Java堆的內存不夠用需要做major GC(full GC)的時候。換句話說,old gen內存的消耗速度比CMS的收集速度要高,CMS收集器跟不上分配速度的時候會發生concurrent mode failure
Concurrent mode failed的日志大概是這樣的:
(concurrent mode failure): 1228795K->1228598K(1228800K), 7.6748280 secs] 1911483K->1681165K(1911488K),
[CMS Perm : 225407K->225394K(262144K)], 7.6751800 secs]
避免這個現象的產生就是調小-XX:CMSInitiatingOccupancyFraction參數的值,
讓CMS更早更頻繁的觸發,降低年老代被沾滿的可能。
full gc頻繁說明old區很快滿了。
如果是一次full gc后,剩余對象不多。那么說明你eden區設置太小,導致短生命周期的對象進入了old區
如果一次full gc后,old區回收率不大,那么說明old區太小
已知虛擬機的一些參數設置如下:
-Xms:1G;
-Xmx:2G;
-Xmn:500M;
-XX:MaxPermSize:64M;
-XX:+UseConcMarkSweepGC;
-XX:SurvivorRatio=3;
求Eden區域的大小?
分析這是網易2016年在線筆試題中的一道選擇題。
先分析一下里面各個參數的含義:
-Xms:1G , 就是說初始堆大小為1G
-Xmx:2G , 就是說最大堆大小為2G
-Xmn:500M ,就是說年輕代大小是500M(包括一個Eden和兩個Survivor)
-XX:MaxPermSize:64M , 就是說設置持久代最大值為64M
-XX:+UseConcMarkSweepGC , 就是說使用使用CMS內存收集算法
-XX:SurvivorRatio=3 , 就是說Eden區與Survivor區的大小比值為3:1:1
題目中所問的Eden區的大小是指年輕代的大小,直接根據-Xmn:500M和-XX:SurvivorRatio=3可以直接計算得出
解500M*(3/(3+1+1))
=500M*(3/5)
=500M*0.6
=300M
所以Eden區域的大小為300M。
參考資料
http://yinwufeng.iteye.com/blog/2157787
http://itindex.net/detail/47030-cms-gc-%E9%97%AE%E9%A2%98
http://www.cnblogs.com/ityouknow/p/5614961.html
歡迎關注