GC及JVM參數(shù)

原文閱讀

前言

這段時(shí)間懈怠了,罪過!

最近看到有同事也開始用上了微信公眾號(hào)寫博客了,挺好的~給他們點(diǎn)贊,這博客我也不推廣,默默的靜靜的,主要是擔(dān)心自己堅(jiān)持不了。以前寫過時(shí)間事件日志現(xiàn)在也不寫了;寫過博客也不寫了;月記也不寫了。

堅(jiān)持平凡事就是偉大,本來計(jì)劃一周一篇的,這次沒有嚴(yán)格執(zhí)行。懈怠了

這個(gè)GC跟JVM內(nèi)容太多了,理論性東西多些,少年時(shí)還能記個(gè)八九成,好久沒弄,都忘記了。這次權(quán)當(dāng)整理溫習(xí),再看看《深入理解JVM虛擬機(jī)》,找些過去寫的博客挖點(diǎn)東西過來!

GC

Java GC(Garbage Collection,垃圾收集,垃圾回收)機(jī)制,是Java與C++/C的主要區(qū)別之一,作為Java開發(fā)者,一般不需要專門編寫內(nèi)存回收和垃圾清理代碼,對(duì)內(nèi)存泄露和溢出的問題,也不需要像C程序員那樣戰(zhàn)戰(zhàn)兢兢。這是因?yàn)樵贘ava虛擬機(jī)中,存在自動(dòng)內(nèi)存管理和垃圾清掃機(jī)制。概括地說,該機(jī)制對(duì)虛擬機(jī)中的內(nèi)存進(jìn)行標(biāo)記,并確定哪些內(nèi)存需要回收,根據(jù)一定的回收策略,自動(dòng)的回收內(nèi)存,永不停息(Nerver Stop)的保證虛擬機(jī)中的內(nèi)存空間,防止出現(xiàn)內(nèi)存泄露和溢出問題。

主要從這幾個(gè)問題入手,就差不多了

  1. Java內(nèi)存區(qū)域
  2. 哪些內(nèi)存需要回收?
  3. 什么時(shí)候回收
  4. 如何回收
  5. 監(jiān)控和優(yōu)化GC

Java內(nèi)存區(qū)域

image
  1. 程序計(jì)數(shù)器(Program Counter Register)

程序計(jì)數(shù)器是一個(gè)比較小的內(nèi)存區(qū)域,用于指示當(dāng)前線程所執(zhí)行的字節(jié)碼執(zhí)行到了第幾行,可以理解為是當(dāng)前線程的行號(hào)指示器。字節(jié)碼解釋器在工作時(shí),會(huì)通過改變這個(gè)計(jì)數(shù)器的值來取下一條語句指令。每個(gè)程序計(jì)數(shù)器只用來記錄一個(gè)線程的行號(hào),所以它是線程私有(一個(gè)線程就有一個(gè)程序計(jì)數(shù)器)的。
如果程序執(zhí)行的是一個(gè)Java方法,則計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址;如果正在執(zhí)行的是一個(gè)本地(native,由C語言編寫完成)方法,則計(jì)數(shù)器的值為Undefined,由于程序計(jì)數(shù)器只是記錄當(dāng)前指令地址,所以不存在內(nèi)存溢出的情況,因此,程序計(jì)數(shù)器也是所有JVM內(nèi)存區(qū) 域中唯一一個(gè)沒有定義OutOfMemoryError的區(qū)域。

  1. 虛擬機(jī)棧(JVM Stack)

一個(gè)線程的每個(gè)方法在執(zhí)行的同時(shí),都會(huì)創(chuàng)建一個(gè)棧幀(Statck Frame),棧幀中存儲(chǔ)的有局部變量表、操作站、動(dòng)態(tài)鏈接、方法出口等,當(dāng)方法被調(diào)用時(shí),棧幀在JVM棧中入棧,當(dāng)方法執(zhí)行完成時(shí),棧幀出棧。局部變量表中存儲(chǔ)著方法的相關(guān)局部變量,包括各種基本數(shù)據(jù)類型,對(duì)象的引用,返回地址等。在局部變量表中,只有l(wèi)ong和double類型會(huì)占用2個(gè)局部變量空間(Slot,對(duì)于32位機(jī)器,一個(gè)Slot就是32個(gè)bit),其它都是1個(gè)Slot。需要注意的是,局部變量表是在編譯時(shí)就已經(jīng)確定 好的,方法運(yùn)行所需要分配的空間在棧幀中是完全確定的,在方法的生命周期內(nèi)都不會(huì)改變。虛擬機(jī)棧中定義了兩種異常,如果線程調(diào)用的棧深度大于虛擬機(jī)允許的最大深度,則拋出StatckOverFlowError(棧溢出);不過多 數(shù)Java虛擬機(jī)都允許動(dòng)態(tài)擴(kuò)展虛擬機(jī)棧的大小(有少部分是固定長(zhǎng)度的),所以線程可以一直申請(qǐng)棧,知道內(nèi)存不足,此時(shí),會(huì)拋出 OutOfMemoryError(內(nèi)存溢出)。每個(gè)線程對(duì)應(yīng)著一個(gè)虛擬機(jī)棧,因此虛擬機(jī)棧也是線程私有的。

  1. 本地方法棧(Native Method Statck):

本地方法棧在作用,運(yùn)行機(jī)制,異常類型等方面都與虛擬機(jī)棧相同,唯一的區(qū)別是:虛擬機(jī)棧是執(zhí)行Java方法的,而本地方法棧是用來執(zhí)行native方法的,在很多虛擬機(jī)中(如Sun的JDK默認(rèn)的HotSpot虛擬機(jī)),會(huì)將本地方法棧與虛擬機(jī)棧放在一起使用。本地方法棧也是線程私有的。

  1. 堆區(qū)(Heap)

堆區(qū)是理解Java GC機(jī)制最重要的區(qū)域,沒有之一。在JVM所管理的內(nèi)存中,堆區(qū)是最大的一塊,堆區(qū)也是Java GC機(jī)制所管理的主要內(nèi)存區(qū)域,堆區(qū)由所有線程共享,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。堆區(qū)的存在是為了存儲(chǔ)對(duì)象實(shí)例,原則上講,所有的對(duì)象都在堆區(qū)上分配內(nèi)存(不過現(xiàn)代技術(shù)里,也不是這么絕對(duì)的,也有棧上直接分配的)。一般的,根據(jù)Java虛擬機(jī)規(guī)范規(guī)定,堆內(nèi)存需要在邏輯上是連續(xù)的(在物理上不需要),在實(shí)現(xiàn)時(shí),可以是固定大小的,也可以是可擴(kuò)展的,目前主流的虛擬機(jī)都是可擴(kuò)展的。如果在執(zhí)行垃圾回收之后,仍沒有足夠的內(nèi)存分配,也不能再擴(kuò)展,將會(huì)拋出OutOfMemoryError:Java heap space異常。

-Xms 參數(shù)設(shè)置最小值

-Xmx 參數(shù)設(shè)置最大值 
例:VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

若-Xms=-Xmx,則可避免堆自動(dòng)擴(kuò)展。
-XX:+HeapDumpOnOutOfMemoryError 可以讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出是dump出當(dāng)前的內(nèi)存堆轉(zhuǎn)儲(chǔ)快照。

  1. 方法區(qū)(Method Area)

在Java虛擬機(jī)規(guī)范中,將方法區(qū)作為堆的一個(gè)邏輯部分來對(duì)待,但事實(shí)上,方法區(qū)并不是堆(Non-Heap);另外,不少人的博客中,將Java GC的分代收集機(jī)制分為3個(gè)代:青年代,老年代,永久代,這些作者將方法區(qū)定義為“永久代”,這是因?yàn)?,?duì)于之前的HotSpot Java虛擬機(jī)的實(shí)現(xiàn)方式中,將分代收集的思想擴(kuò)展到了方法區(qū),并將方法區(qū)設(shè)計(jì)成了永久代。不過,除HotSpot之外的多數(shù)虛擬機(jī),并不將方法區(qū)當(dāng)做永久代,HotSpot本身,也計(jì)劃取消永久代。
方法區(qū)是各個(gè)線程共享的區(qū)域,用于存儲(chǔ)已經(jīng)被虛擬機(jī)加載的類信息(即加載類時(shí)需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態(tài)變量、編譯器即時(shí)編譯的代碼等。方法區(qū)在物理上也不需要是連續(xù)的,可以選擇固定大小或可擴(kuò)展大小,并且方法區(qū)比堆還多了一個(gè)限制:可以選擇是否執(zhí)行垃圾收集。一般的,方法區(qū)上 執(zhí)行的垃圾收集是很少的,這也是方法區(qū)被稱為永久代的原因之一(HotSpot),但這也不代表著在方法區(qū)上完全沒有垃圾收集,其上的垃圾收集主要是針對(duì)常量池的內(nèi)存回收和對(duì)已加載類的卸載。在方法區(qū)上進(jìn)行垃圾收集,條件苛刻而且相當(dāng)困難,效果也不令人滿意,所以一般不做太多考慮,可以留作以后進(jìn)一步深入研究時(shí)使用。在方法區(qū)上定義了OutOfMemoryError:PermGen space異常,
在內(nèi)存不足時(shí)拋出。
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分,用于存儲(chǔ)編譯期就生成的字面常量、符號(hào)引用、翻譯出來的直接引用(符號(hào)引用就是編碼是用字符串表示某個(gè)變量、接口的位置,直接引用就是根據(jù)符號(hào)引用翻譯出來的地址,將在類鏈接階段完成翻譯);運(yùn)行時(shí)常量池除了存儲(chǔ)編譯期常量外,也可以存儲(chǔ)在運(yùn)行時(shí)間產(chǎn)生的常量(比如String類的intern()方法,作用是String維護(hù)了一個(gè)常量池,如果調(diào)用的字符“abc”已經(jīng)在常量池中,則返回池中的字符串地址,否則,新建一個(gè)常量加入池中,并返回地址)。

-XX:MaxPermSize 設(shè)置上限
-XX:PermSize 設(shè)置最小值 
例:VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
  1. 直接內(nèi)存(Direct Memory)

直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn),所以我們放到這里一起講解。

Direct Memory滿了之后,系統(tǒng)不會(huì)自動(dòng)回收這段內(nèi)存; 而是要等Tenured Generation滿觸發(fā)GC時(shí),Direct Memory才會(huì)被跟著回收。

在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java堆里面的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)。

顯然,本機(jī)直接內(nèi)存的分配不會(huì)受到Java堆大小的限制,但是,既然是內(nèi)存,則肯定還是會(huì)受到本機(jī)總內(nèi)存(包括RAM及SWAP區(qū)或者分頁文件)的大小及處理器尋址空間的限制。服務(wù)器管理員配置虛擬機(jī)參數(shù)時(shí),一般會(huì)根據(jù)實(shí)際內(nèi)存設(shè)置-Xmx等參數(shù)信息,但經(jīng)常會(huì)忽略掉直接內(nèi)存,使得各個(gè)內(nèi)存區(qū)域的總和大于物理內(nèi)存限制(包括物理上的和操作系統(tǒng)級(jí)的限制),從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn)OutOfMemoryError異常。

-XX:MaxDirectMemorySize 設(shè)置最大值,默認(rèn)與java堆最大值一樣。

例 :-XX:MaxDirectMemorySize=10M -Xmx20M

哪些內(nèi)存被回收

根據(jù)運(yùn)行時(shí)數(shù)據(jù)區(qū)域的各個(gè)部分,程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧三個(gè)區(qū)域隨著線程而生,隨線程滅而滅。棧中的棧幀隨著方法的進(jìn)入和退出而進(jìn)棧出棧。每個(gè)棧幀分配多少內(nèi)存在類結(jié)構(gòu)確定下來的時(shí)候就基本已經(jīng)確定。所以這個(gè)三個(gè)區(qū)域內(nèi)存回收時(shí)方法或者線程結(jié)束而回收的,不需要太多關(guān)注;而java堆和方法區(qū)則不一樣,一個(gè)接口不同實(shí)現(xiàn)類,一個(gè)方法中不同的分支,在具體運(yùn)行的時(shí)候才能確定創(chuàng)建那些對(duì)象,所以這部分內(nèi)存是動(dòng)態(tài)的,也是需要垃圾回收機(jī)制來回收處理的。

  1. 堆內(nèi)存

判斷堆內(nèi)的對(duì)象是否可以回收,要判斷這個(gè)對(duì)象實(shí)例是否確實(shí)沒用,判斷算法有兩種:引用計(jì)數(shù)法和根搜索算法。

  • 引用計(jì)數(shù)法:就是給每個(gè)對(duì)象加一個(gè)計(jì)數(shù)器,如果有一個(gè)地方引用就加1,當(dāng)引用失效就減1;當(dāng)計(jì)數(shù)器為0,則認(rèn)為對(duì)象是無用的。這種算法最大的問題在于不能解決相互引用的對(duì)象,如:A.b=B;B.a=A,在沒有其他引用的情況下,應(yīng)該回收;但按照引用計(jì)數(shù)法來計(jì)算,他們的引用都不為0,顯然不能回收。

  • 根搜索算法:這個(gè)算法的思路是通過一系列名為“GC Roots”的對(duì)象作為起點(diǎn),
    從這個(gè)節(jié)點(diǎn)向下搜索,搜索所經(jīng)過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連(圖論的不可達(dá))時(shí),則證明該對(duì)象不可用。

java等一大部分商用語言是用根搜索算法來管理內(nèi)存的,java中可以做為GC Roots的對(duì)象有如下幾種:

虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對(duì)象;

方法區(qū)中的類靜態(tài)屬性引用的對(duì)象;

方法區(qū)中常量引用的對(duì)象;

本地方法棧JNI(Native)的引用對(duì)象;

無論是通過引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量,還是通過可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá),判定對(duì)象是否存活都與“引用”有關(guān)。在JDK1.2以前,Java中的引用的定義很傳統(tǒng)如果reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個(gè)引用。這種定義很純粹,但是太過狹隘,一個(gè)對(duì)象在這種定義下只有被引用或者沒有被引用兩種狀態(tài),對(duì)于如何描述一些“食之無味,棄之可惜”的對(duì)象就顯得無能為力。我們希望能描述這樣一類對(duì)象:當(dāng)內(nèi)存空間還足夠時(shí),則能保留在內(nèi)存之中;如果內(nèi)存空間在進(jìn)行垃圾收集后還是非常緊張,則可以拋棄這些對(duì)象。很多系統(tǒng)的緩存功能都符合這樣的應(yīng)用場(chǎng)景。

在JDK 1.2之后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強(qiáng)度依次逐漸減弱。

  • 強(qiáng)引用

只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)收掉被引用的對(duì)象

  • 軟引用

在系統(tǒng)將要發(fā)生內(nèi)存異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收。

  • 弱引用

被弱引用關(guān)聯(lián)的對(duì)象只能生存道下一次垃圾收集發(fā)生之前。

  • 虛引用

一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對(duì)象的實(shí)例。

finalize()方法

在Object類中

protected void finalize() throws Throwable { }

注意下這個(gè)訪問控制符是protected

finalize()在什么時(shí)候被調(diào)用?

有三種情況

  1. 所有對(duì)象被Garbage Collection時(shí)自動(dòng)調(diào)用,比如運(yùn)行System.gc()的時(shí)候.
  2. 程序退出時(shí)為每個(gè)對(duì)象調(diào)用一次finalize方法。
  3. 顯式的調(diào)用finalize方法

當(dāng)一個(gè)對(duì)象不可到達(dá)時(shí),并不是馬上就被回收的。

image

當(dāng)對(duì)象沒有覆蓋finalize()方法,或者finalized()已經(jīng)被JVM調(diào)用過,那就是沒有必要執(zhí)行finalzied()
;Finalizer線程執(zhí)行它,但并不保證等待它執(zhí)行結(jié)束,這主要是防止finalize()出現(xiàn)問題,導(dǎo)致Finalizer線程無限等待,整個(gè)內(nèi)存回收系統(tǒng)崩潰

具體的finalize流程:

對(duì)象可由兩種狀態(tài),涉及到兩類狀態(tài)空間,一是終結(jié)狀態(tài)空間 F = {unfinalized, finalizable, finalized};二是可達(dá)狀態(tài)空間 R = {reachable, finalizer-reachable, unreachable}。各狀態(tài)含義如下:
unfinalized: 新建對(duì)象會(huì)先進(jìn)入此狀態(tài),GC并未準(zhǔn)備執(zhí)行其finalize方法,因?yàn)樵搶?duì)象是可達(dá)的
finalizable: 表示GC可對(duì)該對(duì)象執(zhí)行finalize方法,GC已檢測(cè)到該對(duì)象不可達(dá)。正如前面所述,GC通過F-Queue隊(duì)列和一專用線程完成finalize的執(zhí)行
finalized: 表示GC已經(jīng)對(duì)該對(duì)象執(zhí)行過finalize方法
reachable: 表示GC Roots引用可達(dá)
finalizer-reachable(f-reachable):表示不是reachable,但可通過某個(gè)finalizable對(duì)象可達(dá)
unreachable:對(duì)象不可通過上面兩種途徑可達(dá)


image
  1. 新建對(duì)象首先處于[reachable, unfinalized]狀態(tài)(A)
  2. 隨著程序的運(yùn)行,一些引用關(guān)系會(huì)消失,導(dǎo)致狀態(tài)變遷,從reachable狀態(tài)變遷到f-reachable(B, C, D)或unreachable(E, F)狀態(tài)
  3. 若JVM檢測(cè)到處于unfinalized狀態(tài)的對(duì)象變成f-reachable或unreachable,JVM會(huì)將其標(biāo)記為finalizable狀態(tài)(G,H)。若對(duì)象原處于[unreachable, unfinalized]狀態(tài),則同時(shí)將其標(biāo)記為f-reachable(H)。
  4. 在某個(gè)時(shí)刻,JVM取出某個(gè)finalizable對(duì)象,將其標(biāo)記為finalized并在某個(gè)線程中執(zhí)行其finalize方法。由于是在活動(dòng)線程中引用了該對(duì)象,該對(duì)象將變遷到(reachable, finalized)狀態(tài)(K或J)。該動(dòng)作將影響某些其他對(duì)象從f-reachable狀態(tài)重新回到reachable狀態(tài)(L, M, N), 這就是對(duì)象重生
  5. 處于finalizable狀態(tài)的對(duì)象不能同時(shí)是unreahable的,由第4點(diǎn)可知,將對(duì)象finalizable對(duì)象標(biāo)記為finalized時(shí)會(huì)由某個(gè)線程執(zhí)行該對(duì)象的finalize方法,致使其變成reachable。這也是圖中只有八個(gè)狀態(tài)點(diǎn)的原因
  6. 程序員手動(dòng)調(diào)用finalize方法并不會(huì)影響到上述內(nèi)部標(biāo)記的變化,因此JVM只會(huì)至多調(diào)用finalize一次,即使該對(duì)象“復(fù)活”也是如此。程序員手動(dòng)調(diào)用多少次不影響JVM的行為
  7. 若JVM檢測(cè)到finalized狀態(tài)的對(duì)象變成unreachable,回收其內(nèi)存(I)
  8. 若對(duì)象并未覆蓋finalize方法,JVM會(huì)進(jìn)行優(yōu)化,直接回收對(duì)象(O)

注:System.runFinalizersOnExit()等方法可以使對(duì)象即使處于reachable狀態(tài),JVM仍對(duì)其執(zhí)行finalize方法

對(duì)finalize()的一句話概括:
JVM能夠保證一個(gè)對(duì)象在回收以前一定會(huì)調(diào)用一次它的finalize()方法。這句話中兩個(gè)陷阱:回收以前一定和一次

但有很多地方是講,JVM不承諾這一定調(diào)用finalize(),這就是上面的陷阱造成的

你永遠(yuǎn)不知道它什么時(shí)候被調(diào)用甚至?xí)粫?huì)調(diào)用(因?yàn)橛行?duì)象是永遠(yuǎn)不會(huì)被回收的,或者被回收以前程序就結(jié)束了),但如果他是有必要執(zhí)行finalize()的,那在GC前一定調(diào)用一次且僅一次,如果在第一次GC時(shí)沒有被回收,那以后再GC時(shí),就不再調(diào)用finalize()

  1. 方法區(qū)

很多人認(rèn)為方法區(qū)(或者HotSpot虛擬機(jī)中的永久代)是沒有垃圾收集的,Java虛擬機(jī)規(guī)范中確實(shí)說過可以不要求虛擬機(jī)在方法區(qū)實(shí)現(xiàn)垃圾收集,而且在方法區(qū)進(jìn)行垃圾收集的“性價(jià)比”一般比較低:在堆中,尤其是在新生代中,常規(guī)應(yīng)用進(jìn)行一次垃圾收集*++一般可以回收70%~95%的空間++,而永久代的垃圾收集效率遠(yuǎn)低于此。

方法區(qū)回收主要有兩部分:廢棄的常量和無用的類。廢棄的常量判斷方法和堆中的對(duì)象類似,只要判斷沒有地方引用就可以回收。相比之下,判斷一個(gè)類是否無用,條件就比較苛刻,需要同時(shí)滿足下面3個(gè)條件才能算是“無用的類”:

  • 該類的所有實(shí)例都已經(jīng)被回收,也就是java堆中不存在該類的任何實(shí)例;
  • 加載該類的ClassLoader已經(jīng)被回收;
  • 該類對(duì)應(yīng)的java.lang.Class對(duì)象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機(jī)可以對(duì)滿足上述3個(gè)條件的無用類進(jìn)行回收,這里說的僅僅是“可以”,而不是和對(duì)象一樣,不使用了就必然會(huì)回收。是否對(duì)類進(jìn)行回收,

HotSpot虛擬機(jī)提供了
-Xnoclassgc參數(shù)進(jìn)行控制,
還可以使用
-verbose:class
-XX:+TraceClassLoading
-XX:+TraceClassUnLoading查看類的加載和卸載信息。

在大量使用反射、動(dòng)態(tài)代理、CGLib等bytecode框架的場(chǎng)景,以及動(dòng)態(tài)生成JSP和OSGi這類頻繁自定義ClassLoader的場(chǎng)景都需要虛擬機(jī)具備類卸載的功能,以保證永久代不會(huì)溢出

如何回收

選擇合適的GC collector是JVM調(diào)優(yōu)最重要的一項(xiàng),前提是先了解回收算法

“標(biāo)記-清除”(Mark-Sweep)

image

算法分為“標(biāo)記”和“清除”兩個(gè)階段:
首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象

主要缺點(diǎn)有兩個(gè)

  1. 一個(gè)是效率問題,標(biāo)記和清除過程的效率都不高
  2. 一個(gè)是空間問題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致,當(dāng)程序在以后的運(yùn)行過程中需要分配較大對(duì)象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作

“復(fù)制”(Copying)

image

它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對(duì)其中的一塊進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。

只是這種算法的代價(jià)是將內(nèi)存縮小為原來的一半,未免太高了一點(diǎn)

現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來回收新生代,IBM的專門研究表明,新生代中的對(duì)象98%是朝生夕死的,所以并不需要按照1∶1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當(dāng)回收時(shí),將Eden和Survivor中還存活著的對(duì)象一次性地拷貝到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor的空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(80%+10%),只有10%的內(nèi)存是會(huì)被“浪費(fèi)”的。當(dāng)然,98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于10%的對(duì)象存活,當(dāng)Survivor空間不夠用時(shí),需要依賴其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保(Handle Promotion)。

在對(duì)象存活率較高時(shí)就要執(zhí)行較多的復(fù)制操作,效率將會(huì)變低。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。

-XX:SurvivorRatio=4
設(shè)置年輕代中Eden區(qū)與Survivor區(qū)的大小比值。
設(shè)置為4,則Eden區(qū)與兩個(gè)Survivor區(qū)的比值為4:1:1,一個(gè)Survivor區(qū)占整個(gè)年輕代的1/6 

為什么新生代有兩個(gè)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

  1. Try to fit the survivors from eden into the holes in the survivor space that were cleared by the GC?
  2. Shift all the objects in the survivor space down to eliminate the fragmentation, and then move the survivors into it?
  3. 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.

“標(biāo)記-整理”(Mark-Compact)

image

此算法結(jié)合了“標(biāo)記-清除”和“復(fù)制”兩個(gè)算法的優(yōu)點(diǎn)。也是分兩階段,

  1. 第一階段從根節(jié)點(diǎn)開始標(biāo)記所有被引用對(duì)象,
  2. 第二階段遍歷整個(gè)堆,把清除未標(biāo)記對(duì)象并且把存活對(duì)象“壓縮”到堆的其中一塊,按順序排放。此算法避免了“標(biāo)記-清除”的碎片問題,同時(shí)也避免了“復(fù)制”算法的空間問題。

“分代收集”(Generational Collection)

當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”(Generational Collection)算法,
這種算法并沒有什么新的思想,只是根據(jù)對(duì)象的存活周期的不同將內(nèi)存劃分為幾塊。
一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴āT谛律?,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法來進(jìn)行回收。

image
  • 新生代 GC(Minor GC):指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)?Java 對(duì)象大多都具
    備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。

  • 老年代 GC(Major GC):指發(fā)生在老年代的 GC,出現(xiàn)了 Major GC,經(jīng)常
    會(huì)伴隨至少一次的 Minor GC(但非絕對(duì)的,在 ParallelScavenge 收集器的收集策略里
    就有直接進(jìn)行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會(huì)比 Minor GC 慢 10
    倍以上。
    虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器。如果對(duì)象在 Eden 出生并經(jīng)過第一次 Minor GC 后仍然存活,并且能被 Survivor 容納的話,將被移動(dòng)到 Survivor 空間中,并將對(duì)象年齡設(shè)為 1。對(duì)象在 Survivor 區(qū)中每熬過一次 Minor GC,年齡就增加 1 歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為 15 歲)時(shí),就會(huì)被晉升到老年代中。

對(duì)象晉升老年代的年齡閾值,可以通過參數(shù) -XX:MaxTenuringThreshold 來設(shè)置。

垃圾收集器

image

按系統(tǒng)線程分

image

注意并發(fā)(Concurrent)和并行(Parallel)的區(qū)別

  1. 并發(fā)是指用戶線程與GC線程同時(shí)執(zhí)行(不一定是并行,可能交替,但總體上是在同時(shí)執(zhí)行的),不需要停頓用戶線程(其實(shí)在CMS中用戶線程還是需要停頓的,只是非常短,GC線程在另一個(gè)CPU上執(zhí)行);
  2. 并行收集是指多個(gè)GC線程并行工作,但此時(shí)用戶線程是暫停的;

這個(gè)跟傳統(tǒng)的并發(fā)并行概念不同
并行是物理的,并發(fā)是邏輯的。
并行是和串行對(duì)立。

Serial收集器

image

Serial是最基本、歷史最悠久的垃圾收集器,使用復(fù)制算法,曾經(jīng)是JDK1.3.1之前新生代唯一的垃圾收集器。目前也是ClientVM下 ServerVM 4核4GB以下機(jī)器的默認(rèn)垃圾回收器。
串行收集器并不是只能使用一個(gè)CPU進(jìn)行收集,而是當(dāng)JVM需要進(jìn)行垃圾回收的時(shí)候,需要中斷所有的用戶線程,知道它回收結(jié)束為止,因此又號(hào)稱“Stop The World” 的垃圾回收器。注意,JVM中文名稱為java虛擬機(jī),因此它就像一臺(tái)虛擬的電腦一樣在工作,而其中的每一個(gè)線程就被認(rèn)為是JVM的一個(gè)處理器,因此大家看到圖中的CPU0、CPU1實(shí)際為用戶的線程,而不是真正機(jī)器的CPU,大家不要誤解哦。

串行回收方式適合低端機(jī)器,是Client模式下的默認(rèn)收集器,對(duì)CPU和內(nèi)存的消耗不高,適合用戶交互比較少,后臺(tái)任務(wù)較多的系統(tǒng)。

Serial收集器默認(rèn)新舊生代的回收器搭配為Serial+ SerialOld

新生代、老年代使用串行回收;新生代復(fù)制算法、老年代標(biāo)記-壓縮

在J2SE5.0上,在非server模式下,JVM自動(dòng)選擇串行收集器。
也可以顯示進(jìn)行選擇,在Java啟動(dòng)參數(shù)中增加: 
-XX:+UseSerialGC

Serial Old收集器

SerialOld是舊生代Client模式下的默認(rèn)收集器,單線程執(zhí)行,使用“標(biāo)記-整理”算法
在Server模式下,主要有兩個(gè)用途:

  1. 在JDK1.5之前版本中與新生代的Parallel Scavenge收集器搭配使用。
  2. 作為年老代中使用CMS收集器的后備垃圾收集方案。

ParNew收集器

image

ParNew收集器其實(shí)就是多線程版本的Serial收集器,

Stop The World

他是多CPU模式下的首選回收器(該回收器在單CPU的環(huán)境下回收效率遠(yuǎn)遠(yuǎn)低于Serial收集器,所以一定要注意場(chǎng)景哦)

Server模式下的默認(rèn)收集器。

新生代并行,老年代串行;新生代復(fù)制算法、老年代標(biāo)記-壓縮

-XX:+UseParNewGC  ParNew收集器

ParNew收集器默認(rèn)開啟和CPU數(shù)目相同的線程數(shù)
-XX:ParallelGCThreads 限制線程數(shù)量

Parallel Scavenge收集器

Parallel Scavenge收集器也是一個(gè)新生代垃圾收集器,同樣使用復(fù)制算法,也是一個(gè)多線程的垃圾收集器,也稱吞吐量?jī)?yōu)先的收集器

所提到的吞吐量=程序運(yùn)行時(shí)間/(JVM執(zhí)行回收的時(shí)間+程序運(yùn)行時(shí)間),假設(shè)程序運(yùn)行了100分鐘,JVM的垃圾回收占用1分鐘,那么吞吐量就是99%。在當(dāng)今網(wǎng)絡(luò)告訴發(fā)達(dá)的今天,良好的響應(yīng)速度是提升用戶體驗(yàn)的一個(gè)重要指標(biāo),多核并行云計(jì)算的發(fā)展要求程序盡可能的使用CPU和內(nèi)存資源,盡快的計(jì)算出最終結(jié)果,因此在交互不多的云端,比較適合使用該回收器。

可以通過參數(shù)來打開自適應(yīng)調(diào)節(jié)策略,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或最大的吞吐量;也可以通過參數(shù)控制GC的時(shí)間不大于多少毫秒或者比例

新生代復(fù)制算法、老年代標(biāo)記-壓縮

-XX:+UseParallelGC  使用Parallel收集器+ 老年代串行

Parallel Scavenge收集器提供了兩個(gè)參數(shù)用于精準(zhǔn)控制吞吐量:
a.-XX:MaxGCPauseMillis:控制最大垃圾收集停頓時(shí)間,是一個(gè)大于0的毫秒數(shù)。
b.-XX:GCTimeRation:直接設(shè)置吞吐量大小,是一個(gè)大于0小于100的整數(shù),
也就是程序運(yùn)行時(shí)間占總時(shí)間的比率,默認(rèn)值是99,即垃圾收集運(yùn)行最大1%(1/(1+99))的垃圾收集時(shí)間

-XX:+UseAdaptiveSizePolicy,這是個(gè)開關(guān)參數(shù),
打開之后就不需要手動(dòng)指定新生代大小(-Xmn)、Eden與Survivor區(qū)的比例(-XX:SurvivorRation)、
新生代晉升年老代對(duì)象年齡(-XX:PretenureSizeThreshold)等細(xì)節(jié)參數(shù)

Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標(biāo)記-整理”算法。這個(gè)收集器是在JDK 1.6中才開始提供

在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保證新生代的吞吐量?jī)?yōu)先,無法保證整體的吞吐量,Parallel Old正是為了在年老代同樣提供吞吐量?jī)?yōu)先的垃圾收集器,如果系統(tǒng)對(duì)吞吐量要求比較高,可以優(yōu)先考慮新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略

參數(shù)控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行

新生代Parallel Scavenge和年老代Parallel Old收集器搭配運(yùn)行過程圖:


image

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。目前很大一部分的Java應(yīng)用都集中在互聯(lián)網(wǎng)站或B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間最短,以給用戶帶來較好的體驗(yàn)。

由于整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)地執(zhí)行。老年代收集器(新生代使用ParNew)

優(yōu)點(diǎn):并發(fā)收集、低停頓

缺點(diǎn):產(chǎn)生大量空間碎片、并發(fā)階段會(huì)降低吞吐量

從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“標(biāo)記-清除”算法實(shí)現(xiàn)的,它的運(yùn)作過程相對(duì)于前面幾種收集器來說要更復(fù)雜一些,整個(gè)過程分為4個(gè)步驟,包括:

  1. 初始標(biāo)記(CMS initial mark)

  2. 并發(fā)標(biāo)記(CMS concurrent mark)

  3. 重新標(biāo)記(CMS remark)

  4. 并發(fā)清除(CMS concurrent sweep)

a.初始標(biāo)記:只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)的對(duì)象,速度很快,仍然需要暫停所有的工作線程。

b.并發(fā)標(biāo)記:進(jìn)行GC Roots跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。

c.重新標(biāo)記:為了修正在并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,仍然需要暫停所有的工作線程。

d.并發(fā)清除:清除GC Roots不可達(dá)對(duì)象,和用戶線程一起工作,不需要暫停工作線程。
由于耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過程中,垃圾收集線程可以和用戶現(xiàn)在一起并發(fā)工作,所以總體上來看CMS收集器的內(nèi)存回收和用戶線程是一起并發(fā)地執(zhí)行。

CMS收集器工作過程:

image

其中初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要“Stop The World”。

CMS收集器有以下三個(gè)不足:

  • CMS收集器對(duì)CPU資源非常敏感,其默認(rèn)啟動(dòng)的收集線程數(shù)=(CPU數(shù)量+3)/4,在用戶程序本來CPU負(fù)荷已經(jīng)比較高的情況下,如果還要分出CPU資源用來運(yùn)行垃圾收集器線程,會(huì)使得CPU負(fù)載加重。

  • CMS無法處理浮動(dòng)垃圾(Floating Garbage),可能會(huì)導(dǎo)致Concurrent ModeFailure失敗而導(dǎo)致另一次Full GC。由于CMS收集器和用戶線程并發(fā)運(yùn)行,因此在收集過程中不斷有新的垃圾產(chǎn)生,這些垃圾出現(xiàn)在標(biāo)記過程之后,CMS無法在本次收集中處理掉它們,只好等待下一次GC時(shí)再將其清理掉,這些垃圾就稱為浮動(dòng)垃圾。
    CMS垃圾收集器不能像其他垃圾收集器那樣等待年老代機(jī)會(huì)完全被填滿之后再進(jìn)行收集,需要預(yù)留一部分空間供并發(fā)收集時(shí)的使用,可以通過參數(shù)-XX:CMSInitiatingOccupancyFraction來設(shè)置年老代空間達(dá)到多少的百分比時(shí)觸發(fā)CMS進(jìn)行垃圾收集,默認(rèn)是68%。
    如果在CMS運(yùn)行期間,預(yù)留的內(nèi)存無法滿足程序需要,就會(huì)出現(xiàn)一次ConcurrentMode Failure失敗,此時(shí)虛擬機(jī)將啟動(dòng)預(yù)備方案,使用Serial Old收集器重新進(jìn)行年老代垃圾回收。

  • CMS收集器是基于標(biāo)記-清除算法,因此不可避免會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,如果無法找到一塊足夠大的連續(xù)內(nèi)存存放對(duì)象時(shí),將會(huì)觸發(fā)因此Full GC。CMS提供一個(gè)開關(guān)參數(shù)-XX:+UseCMSCompactAtFullCollection,用于指定在Full GC之后進(jìn)行內(nèi)存整理,內(nèi)存整理會(huì)使得垃圾收集停頓時(shí)間變長(zhǎng),CMS提供了另外一個(gè)參數(shù)-XX:CMSFullGCsBeforeCompaction,用于設(shè)置在執(zhí)行多少次不壓縮的Full GC之后,跟著再來一次內(nèi)存整理

-XX:+UseConcMarkSweepGC  使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC后,進(jìn)行一次碎片整理;整理過程是獨(dú)占的,會(huì)引起停頓時(shí)間變長(zhǎng)
-XX:+CMSFullGCsBeforeCompaction  設(shè)置進(jìn)行幾次Full GC后,進(jìn)行一次碎片整理
-XX:ParallelCMSThreads  設(shè)定CMS的線程數(shù)量(一般情況約等于可用CPU數(shù)量)

G1收集器

G1可謂博采眾家之長(zhǎng),力求到達(dá)一種完美。他吸取了增量收集優(yōu)點(diǎn),把整個(gè)堆劃分為一個(gè)一個(gè)等大小的區(qū)域(region)。內(nèi)存的回收和劃分都以region為單位;同時(shí),他也吸取了CMS的特點(diǎn),把這個(gè)垃圾回收過程分為幾個(gè)階段,分散一個(gè)垃圾回收過程;而且,G1也認(rèn)同分代垃圾回收的思想,認(rèn)為不同對(duì)象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。為了達(dá)到對(duì)回收時(shí)間的可預(yù)計(jì)性,G1在掃描了region以后,對(duì)其中的活躍對(duì)象的大小進(jìn)行排序,首先會(huì)收集那些活躍對(duì)象小的region,以便快速回收空間(要復(fù)制的活躍對(duì)象少了),因?yàn)榛钴S對(duì)象小,里面可以認(rèn)為多數(shù)都是垃圾,所以這種方式被稱為Garbage First(G1)的垃圾回收算法,即:垃圾優(yōu)先的回收。

image

與CMS收集器相比G1收集器有以下特點(diǎn):

  1. 空間整合,G1收集器采用標(biāo)記整理算法,不會(huì)產(chǎn)生內(nèi)存空間碎片。分配大對(duì)象時(shí)不會(huì)因?yàn)闊o法找到連續(xù)空間而提前觸發(fā)下一次GC。

  2. 可預(yù)測(cè)停頓,這是G1的另一大優(yōu)勢(shì),降低停頓時(shí)間是G1和CMS的共同關(guān)注點(diǎn),但G1除了追求低停頓外,還能建立可預(yù)測(cè)的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長(zhǎng)度為N毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過N毫秒,這幾乎已經(jīng)是實(shí)時(shí)Java(RTSJ)的垃圾收集器的特征了。

收集的范圍都是整個(gè)新生代或者老年代,而G1不再是這樣。使用G1收集器時(shí),Java堆的內(nèi)存布局與其他收集器有很大差別,它將整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,它們都是一部分(可以不連續(xù))Region的集合。

G1的新生代收集跟ParNew類似,當(dāng)新生代占用達(dá)到一定比例的時(shí)候,開始出發(fā)收集。

和CMS類似,G1收集器收集老年代對(duì)象會(huì)有短暫停頓。

  1. 標(biāo)記階段,首先初始標(biāo)記(Initial-Mark),這個(gè)階段是停頓的(Stop the World Event),并且會(huì)觸發(fā)一次普通Mintor GC。對(duì)應(yīng)GC log:GC pause (young) (inital-mark)

  2. Root Region Scanning,程序運(yùn)行過程中會(huì)回收survivor區(qū)(存活到老年代),這一過程必須在young GC之前完成。

  3. Concurrent Marking,在整個(gè)堆中進(jìn)行并發(fā)標(biāo)記(和應(yīng)用程序并發(fā)執(zhí)行),此過程可能被young GC中斷。在并發(fā)標(biāo)記階段,若發(fā)現(xiàn)區(qū)域?qū)ο笾械乃袑?duì)象都是垃圾,那個(gè)這個(gè)區(qū)域會(huì)被立即回收(圖中打X)。同時(shí),并發(fā)標(biāo)記過程中,會(huì)計(jì)算每個(gè)區(qū)域的對(duì)象活性(區(qū)域中存活對(duì)象的比例)。


    image
  4. Remark, 再標(biāo)記,會(huì)有短暫停頓(STW)。再標(biāo)記階段是用來收集 并發(fā)標(biāo)記階段 產(chǎn)生新的垃圾(并發(fā)階段和應(yīng)用程序一同運(yùn)行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

  5. Copy/Clean up,多線程清除失活對(duì)象,會(huì)有STW。G1將回收區(qū)域的存活對(duì)象拷貝到新區(qū)域,清除Remember Sets,并發(fā)清空回收區(qū)域并把它返回到空閑區(qū)域鏈表中。


    image
  6. 復(fù)制/清除過程后。回收區(qū)域的活性對(duì)象已經(jīng)被集中回收到深藍(lán)色和深綠色區(qū)域。


    image
-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC        #開啟
-XX:MaxGCPauseMillis =50                  #暫停時(shí)間目標(biāo)
-XX:GCPauseIntervalMillis =200          #暫停間隔目標(biāo)
-XX:+G1YoungGenSize=512m            #年輕代大小
-XX:SurvivorRatio=6                            #幸存區(qū)比例

什么時(shí)候回收

Minor GC觸發(fā)

  1. Eden區(qū)域滿了,或者新創(chuàng)建的對(duì)象大小 > Eden所??臻g
  2. CMS設(shè)置了CMSScavengeBeforeRemark參數(shù),這樣在CMS的Remark之前會(huì)先做一次Minor GC來清理新生代,加速之后的Remark的速度。這樣整體的stop-the world時(shí)間反而短
  3. Full GC的時(shí)候會(huì)先觸發(fā)Minor GC

啥時(shí)候會(huì)觸發(fā)CMS GC?

CMS不等于Full GC,很多人會(huì)認(rèn)為CMS肯定會(huì)引發(fā)Minor GC。CMS是針對(duì)老年代的GC策略,原則上它不會(huì)去清理新生代,只有設(shè)置CMSScavengeBeforeRemark優(yōu)化時(shí),或者是concurrent mode failure的時(shí)候才會(huì)去做Minor GC

1、舊生代或者持久代已經(jīng)使用的空間達(dá)到設(shè)定的百分比時(shí)(CMSInitiatingOccupancyFraction這個(gè)設(shè)置old區(qū),perm區(qū)也可以設(shè)置);

2、JVM自動(dòng)觸發(fā)(JVM的動(dòng)態(tài)策略,也就是悲觀策略)(基于之前GC的頻率以及舊生代的增長(zhǎng)趨勢(shì)來評(píng)估決定什么時(shí)候開始執(zhí)行),如果不希望JVM自行決定,可以通過-XX:UseCMSInitiatingOccupancyOnly=true來制定;

3、設(shè)置了 -XX:CMSClassUnloadingEnabled 這個(gè)則考慮Perm區(qū);

啥時(shí)候會(huì)觸發(fā)Full GC?

一、舊生代空間不足:java.lang.outOfMemoryError:java heap space;

二、Perm空間滿:java.lang.outOfMemoryError:PermGen space;

三、CMS GC時(shí)出現(xiàn)promotion failed 和concurrent mode failure(Concurrent mode failure發(fā)生的原因一般是CMS正在進(jìn)行,但是由于old區(qū)內(nèi)存不足,需要盡快回收old區(qū)里面的死的java對(duì)象,這個(gè)時(shí)候foreground gc需要被觸發(fā),停止所有的java線程,同時(shí)終止CMS,直接進(jìn)行MSC。);

四、統(tǒng)計(jì)得到的minor GC晉升到舊生代的平均大小大于舊生代的剩余空間;

五、主動(dòng)觸發(fā)Full GC(執(zhí)行jmap -histo:live [pid])來避免碎片問題;

六、調(diào)用System.gc時(shí),系統(tǒng)建議執(zhí)行Full GC,但是不必然執(zhí)行,-XX:+DisableExplicitGC 禁用System.gc()調(diào)用

GC策略選擇總結(jié)

jvm有client和server兩種模式,這兩種模式的gc默認(rèn)方式是不同的:

client模式下,新生代選擇的是串行g(shù)c,舊生代選擇的是串行g(shù)c

server模式下,新生代選擇的是并行回收gc,舊生代選擇的是并行g(shù)c

一般來說我們系統(tǒng)應(yīng)用選擇有兩種方式:吞吐量?jī)?yōu)先和暫停時(shí)間優(yōu)先,對(duì)于吞吐量?jī)?yōu)先的采用server默認(rèn)的并行g(shù)c方式,對(duì)于暫停時(shí)間優(yōu)先的選用并發(fā)gc(CMS)方式。

監(jiān)控與調(diào)優(yōu)

GC日志

-XX:+PrintGC 輸出GC日志
-XX:+PrintGCDetails 輸出GC的詳細(xì)日志
-XX:+PrintGCTimeStamps 輸出GC的時(shí)間戳(以基準(zhǔn)時(shí)間的形式)
-XX:+PrintGCDateStamps 輸出GC的時(shí)間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進(jìn)行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的輸出路徑
-verbose.gc開關(guān)可顯示GC的操作內(nèi)容。
打開它,可以顯示最忙和最空閑收集行為發(fā)生的時(shí)間、收集前后的內(nèi)存大小、收集需要的時(shí)間等

-XX:+PrintGCTimeStamps和-XX:+PrintGCDateStamps

使用-XX:+PrintGCTimeStamps可以將時(shí)間和日期也加到GC日志中。表示自JVM啟動(dòng)至今的時(shí)間戳?xí)惶砑拥矫恳恍兄?。例子如下?/p>

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,每一行就添加上了絕對(duì)的日期和時(shí)間。

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]

如果需要也可以同時(shí)使用兩個(gè)參數(shù)。推薦同時(shí)使用這兩個(gè)參數(shù),因?yàn)檫@樣在關(guān)聯(lián)不同來源的GC日志時(shí)很有幫助

每一種收集器的日志形式都是由它們自身的實(shí)現(xiàn)所決定的,換而言之,每個(gè)收集器的日志格式都可以不一樣。

但虛擬機(jī)設(shè)計(jì)者為了方便用戶閱讀,將各個(gè)收集器的日志都維持一定的共性,

例如以下兩段典型的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]

最前面的數(shù)字“33.125:”和“100.667:”代表了GC發(fā)生的時(shí)間,這個(gè)數(shù)字的含義是從Java虛擬機(jī)啟動(dòng)以來經(jīng)過的秒數(shù)。

GC日志開頭的“[GC”和“[Full GC”說明了這次垃圾收集的停頓類型,

而不是用來區(qū)分新生代GC還是老年代GC的。

如果有“Full”,說明這次GC是發(fā)生了Stop-The-World的,

例如下面這段新生代收集器ParNew的日志也會(huì)出現(xiàn)“[Full GC”(這一般是因?yàn)槌霈F(xiàn)了分配擔(dān)保失敗之類的問題,所以才導(dǎo)致STW)。

如果是調(diào)用System.gc()方法所觸發(fā)的收集,那么在這里將顯示“[Full GC (System)”。

[Full GC 283.736: [ParNew: 261599K->261599K(261952K), 0.0000288 secs] 

接下來的“[DefNew”、“[Tenured”、“[Perm”表示GC發(fā)生的區(qū)域,這里顯示的區(qū)域名稱與使用的GC收集器是密切相關(guān)的

例如上面樣例所使用的Serial收集器中的新生代名為“Default New Generation”,所以顯示的是“[DefNew”。

如果是ParNew收集器,新生代名稱就會(huì)變?yōu)椤埃跴arNew”,意為“Parallel New Generation”。

如果采用Parallel Scavenge收集器,那它配套的新生代稱為“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。

后面方括號(hào)內(nèi)部的“3324K->152K(3712K)”含義是“GC前該內(nèi)存區(qū)域已使用容量-> GC后該內(nèi)存區(qū)域已使用容量 (該內(nèi)存區(qū)域總?cè)萘?”。

而在方括號(hào)之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆總?cè)萘?”。

再往后,“0.0025925 secs”表示該內(nèi)存區(qū)域GC所占用的時(shí)間,單位是秒。

有的收集器會(huì)給出更具體的時(shí)間數(shù)據(jù)

如“[Times: user=0.01 sys=0.00, real=0.02 secs]”,
這里面的user、sys和real與Linux的time命令所輸出的時(shí)間含義一致,分別代表用戶態(tài)消耗的CPU時(shí)間、內(nèi)核態(tài)消耗的CPU事件和操作從開始到結(jié)束所經(jīng)過的墻鐘時(shí)間(Wall Clock Time)。

CPU時(shí)間與墻鐘時(shí)間的區(qū)別是,墻鐘時(shí)間包括各種非運(yùn)算的等待耗時(shí),例如等待磁盤I/O、等待線程阻塞,而CPU時(shí)間不包括這些耗時(shí),但當(dāng)系統(tǒng)有多CPU或者多核的話,多線程操作會(huì)疊加這些CPU時(shí)間,所以讀者看到user或sys時(shí)間超過real時(shí)間是完全正常的。

分析工具

可以使用一些離線的工具來對(duì)GC日志進(jìn)行分析

比如sun的gchisto( https://java.net/projects/gchisto)

gcviewer( https://github.com/chewiebug/GCViewer ),

這些都是開源的工具,用戶可以直接通過版本控制工具下載其源碼,進(jìn)行離線分析

打印JVM參數(shù)

-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}

表格的每一行包括五列,來表示一個(gè)XX參數(shù)。第一列表示參數(shù)的數(shù)據(jù)類型,第二列是名稱,第四列為值,第五列是參數(shù)的類別。第三列”=”表示第四列是參數(shù)的默認(rèn)值,而”:=” 表明了參數(shù)被用戶或者JVM賦值了。

-XX:+PrintCommandLineFlags

這個(gè)參數(shù)讓JVM打印出那些已經(jīng)被用戶或者JVM設(shè)置過的詳細(xì)的XX參數(shù)的名稱和值。

換句話說,它列舉出 -XX:+PrintFlagsFinal的結(jié)果中第三列有":="的參數(shù)。

以這種方式,我們可以用-XX:+PrintCommandLineFlags作為快捷方式來查看修改過的參數(shù)

監(jiān)控jvm

使用自帶工具就行,jstat,jmap,jstack

優(yōu)化

  1. 選擇合適的GC collector
  2. 整個(gè)JVM heap的大小
  3. young generation在整個(gè)JVM heap中所占的比重

參數(shù)實(shí)例

 public static void main(String[] args) throws InterruptedException{
        //通過allocateDirect分配128MB直接內(nèi)存
        ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);
         
        TimeUnit.SECONDS.sleep(10);
        System.out.println("ok");
    }

測(cè)試用例1:設(shè)置JVM參數(shù)-Xmx100m,運(yùn)行異常,因?yàn)槿绻麤]設(shè)置-XX:MaxDirectMemorySize,則默認(rèn)與-Xmx參數(shù)值相同,分配128M直接內(nèi)存超出限制范圍

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區(qū)滿引起的full gc,建議開啟CMS回收Perm區(qū)選項(xiàng):

+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled

默認(rèn)CMS是在tenured generation沾滿68%的時(shí)候開始進(jìn)行CMS收集,如果你的年老代增長(zhǎng)不是那么快,并且希望降低CMS次數(shù)的話,可以適當(dāng)調(diào)高此值:

-XX:CMSInitiatingOccupancyFraction=80

遇到兩種fail引起full gc:

Prommotion failed和Concurrent mode failed時(shí):

promotion failed是在進(jìn)行Minor GC時(shí),survivor space放不下、
對(duì)象只能放入舊生代,而此時(shí)old gen 的碎片太多為進(jìn)行過內(nèi)存重組和壓縮,無法提供一塊較大的、連續(xù)的內(nèi)存空間存放來自新生代對(duì)象

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]

因?yàn)?br> 解決這個(gè)問題的辦法有兩種完全相反的傾向:增大救助空間、增大年老代或者去掉救助空間。

解決方法可以通過設(shè)置參數(shù)
-XX:+UseCMSCompactAtFullCollection(打開對(duì)年老代的壓縮)
-XX:CMSFullGCsBeforeCompaction(設(shè)置運(yùn)行多少次FULL GC以后對(duì)內(nèi)存空間進(jìn)行壓縮、整理) 
直接關(guān)了servivor空間
-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0

concurrent mode failure

發(fā)生在當(dāng)CMS已經(jīng)在工作并處于concurrent階段中,而Java堆的內(nèi)存不夠用需要做major GC(full GC)的時(shí)候。換句話說,old gen內(nèi)存的消耗速度比CMS的收集速度要高,CMS收集器跟不上分配速度的時(shí)候會(huì)發(fā)生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]

避免這個(gè)現(xiàn)象的產(chǎn)生就是調(diào)小-XX:CMSInitiatingOccupancyFraction參數(shù)的值,
讓CMS更早更頻繁的觸發(fā),降低年老代被沾滿的可能。

full gc頻繁說明old區(qū)很快滿了。

如果是一次full gc后,剩余對(duì)象不多。那么說明你eden區(qū)設(shè)置太小,導(dǎo)致短生命周期的對(duì)象進(jìn)入了old區(qū)

如果一次full gc后,old區(qū)回收率不大,那么說明old區(qū)太小

已知虛擬機(jī)的一些參數(shù)設(shè)置如下: 
-Xms:1G; 
-Xmx:2G; 
-Xmn:500M; 
-XX:MaxPermSize:64M; 
-XX:+UseConcMarkSweepGC; 
-XX:SurvivorRatio=3; 
求Eden區(qū)域的大???
分析這是網(wǎng)易2016年在線筆試題中的一道選擇題。 
先分析一下里面各個(gè)參數(shù)的含義: 
-Xms:1G , 就是說初始堆大小為1G 
-Xmx:2G , 就是說最大堆大小為2G 
-Xmn:500M ,就是說年輕代大小是500M(包括一個(gè)Eden和兩個(gè)Survivor) 
-XX:MaxPermSize:64M , 就是說設(shè)置持久代最大值為64M 
-XX:+UseConcMarkSweepGC , 就是說使用使用CMS內(nèi)存收集算法 
-XX:SurvivorRatio=3 , 就是說Eden區(qū)與Survivor區(qū)的大小比值為3:1:1

題目中所問的Eden區(qū)的大小是指年輕代的大小,直接根據(jù)-Xmn:500M和-XX:SurvivorRatio=3可以直接計(jì)算得出
解500M*(3/(3+1+1)) 
=500M*(3/5) 
=500M*0.6 
=300M 
所以Eden區(qū)域的大小為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

歡迎關(guān)注
【碼農(nóng)戲碼】
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • JVM架構(gòu) 當(dāng)一個(gè)程序啟動(dòng)之前,它的class會(huì)被類裝載器裝入方法區(qū)(Permanent區(qū)),執(zhí)行引擎讀取方法區(qū)的...
    cocohaifang閱讀 1,822評(píng)論 0 7
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡(luò)上收集的一些資料的整理,因此不免有一些不準(zhǔn)確的地方,同時(shí)不同JDK版本的...
    高廣超閱讀 16,040評(píng)論 3 83
  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虛擬機(jī)(JVM)垃圾回收器提供...
    簡(jiǎn)欲明心閱讀 90,341評(píng)論 17 311
  • 作者:一字馬胡 轉(zhuǎn)載標(biāo)志 【2017-11-12】 更新日志 日期更新內(nèi)容備注 2017-11-12新建文章初版 ...
    beneke閱讀 2,322評(píng)論 0 7
  • Java 虛擬機(jī)有自己完善的硬件架構(gòu), 如處理器、堆棧、寄存器等,還具有相應(yīng)的指令系統(tǒng)。JVM 屏蔽了與具體操作系...
    尹小凱閱讀 1,746評(píng)論 0 10

友情鏈接更多精彩內(nèi)容