1. 程式人生 > >JVM記憶體結構與垃圾回收總結

JVM記憶體結構與垃圾回收總結

1、JVM記憶體模型

  JVM只不過是執行在你係統上的另一個程序而已,這一切的魔法始於一個java命令。正如任何一個作業系統程序那樣,JVM也需要記憶體來完成它的執行時操作。記住:JVM本身是硬體的一層軟體抽象,在這之上才能夠執行Java程式,也才有了我們所吹噓的平臺獨立性以及“一次編寫,處處執行”

 Java虛擬機器在執行Java程式的過程中會把它說管理的記憶體劃分為若干個不同的資料區域,如下面兩圖所示:


  (1)程式計數器: 執行緒私有。程式計數器是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復登記處功能都需要依賴這個計數器的值來完成。為了執行緒切換後能恢復到正確的執行位置,每個執行緒都需要有一個獨立的程式計數器,各條執行緒之間的計數器互不影響,獨立儲存。這類記憶體區域稱為“執行緒私有”的記憶體。程式計數器,是唯一一個在java虛擬機器規範中沒有規定任何Out Of Memory Error的區域。

  (2)Java虛擬機器棧: 也是執行緒私有的,生命週期與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時,都會建立一個棧幀,用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。平常我們把java分為堆記憶體和棧記憶體,其中的“棧”就是現在講的虛擬機器棧,或者說是虛擬機器棧中區域性變量表部分。區域性變量表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧幀中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變量表的大小。

  對於java虛擬機器棧,有兩種異常情況:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常;如果虛擬機器棧在動態擴充套件時,無法申請到足夠的記憶體,就會丟擲OutOfMemoryError

  Java虛擬機器的解釋執行引擎被稱為“基於棧的執行引擎”,其中所指的“棧”就是運算元棧。因此我們也稱Java虛擬機器是基於棧的,這點不同於Android虛擬機器,Android虛擬機器是基於暫存器的。

  (3)本地方法棧: 執行緒私有。本地方法棧和虛擬機器棧所發揮的作用非常相似,它們之間的區別主要是,虛擬機器棧是為虛擬機器執行Java方法(也就是位元組碼)服務的,而本地方法棧則為虛擬機器使用到的Native方法服務。與虛擬機器棧類似,本地方法棧也會丟擲StackOverflowError和OutOfMemoryError異常。

  (4)Java堆: 所有執行緒共享。Java堆在虛擬機器啟動時建立,是Java虛擬機器所管理的記憶體中最大的一塊。Java堆的唯一目的就是存放物件例項和陣列。

  Java堆是垃圾收集器管理的主要區域,因此也被稱為“GC堆”。從記憶體回收的角度來看,由於現在的收集器大都採用分代收集演算法,所以Java堆可以細分為:新生代和老年代;再細分一點:Eden空間、From Survivor空間、To Survivor空間等。從記憶體分配角度來看,執行緒共享的Java堆可以劃分出多個執行緒私有的分配緩衝區。但是不管怎麼劃分,哪個區域,儲存的都是物件例項。

  Java堆物理上不需要連續的記憶體,只要邏輯上連續即可。如果堆中沒有記憶體完成例項分配,並且也無法再擴充套件時,將會丟擲OutOfMemoryError異常。

  (5)方法區: 所有執行緒共享。用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。方法區也有一個別名叫做Non-Heap(非堆),用於與Java堆區分。對於HotSpot虛擬機器來說,方法區又習慣稱為“永久代”(Permancent Generation),但這只是對於HotSpot虛擬機器來說的,其他虛擬機器的實現上並沒有這個概念。相對而言,垃圾收集行為在這個區域比較少出現,但也並非不會來收集,這個區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝上。

  記憶體區域模型的總結如下圖所示:

  

  執行緒私有的區域:程式計數器、虛擬機器棧、本地方法棧;
  所有執行緒共享的區域:Java堆、方法區;
  沒有異常的區域:程式計數器;
  StackOverflow Error異常:Java虛擬機器棧、本地方法棧;
  OutOfMemory Error異常:除程式計數器外的其他四個區域,Java虛擬機器棧、本地方法棧、Java堆、方法區;

  

  引申:為什麼JVM要選用基於棧的架構?

  JVM選用基於棧的架構,就是所有的運算元必須先入棧,然後根據指令中的操作碼選擇從棧頂彈出若干個元素進行計算後再將結果壓入棧中,在JVM中運算元可以存放在每一個棧幀中的一個本地變數集中,即在每個方法呼叫時就會給這個方法分配一個本地變數集,這個本地變數集在編譯時就已經確定,所以運算元入棧可以直接是常量入棧或者從本地變數中取一個變數壓入棧中。這和一般基於暫存器的操作有所不同,一個操作需要頻繁地入棧和出棧,如進行一個加法運算,如果兩個運算元都在本地變數中,那麼一個加法操作就要有5次棧操作,分別是將兩個運算元從本地變數入棧(2次),再將兩個操作數出棧(2次)用於加法運算,再將結果壓入棧頂(1次)。如果是基於暫存器的話,一般只需要將兩個運算元存入暫存器進行加法運算後再將結果存入其中一個暫存器即可,不需要那麼多的資料移動操作。那麼為什麼JVM還要選擇基於棧來設計呢?

  因為JVM要設計成與平臺無關,而平臺無關性就要在沒有或有很少暫存器的機器上也要同樣能正確執行Java程式碼。例如80x86的機器上暫存器就無規律,很難針對某一款機器設計通用的基於暫存器的指令,所以基於暫存器的架構很難做到通用。在手機作業系統方面,Google的Android平臺上的Dalvik VM就是基於特定晶片(ARM)設計的基於暫存器的架構,這樣在特定晶片上實現基於暫存器的架構可能更多考慮效能,但是也犧牲了跨平臺的移植性。

  棧是執行時的單位,而堆是儲存的單位

  棧解決程式的執行問題,即程式如何執行,或者說如何處理資料;堆解決的是資料儲存的問題,即資料怎麼放、放在哪兒。

  在Java中一個執行緒就會相應有一個執行緒棧與之對應,這點很容易理解,因為不同的執行緒執行邏輯有所不同,因此需要一個獨立的執行緒棧。而堆則是所有執行緒共享的。棧因為是執行單位,因此裡面儲存的資訊都是跟當前執行緒(或程式)相關資訊的。包括區域性變數、程式執行狀態、方法返回值等等;而堆只負責儲存物件資訊

  堆和棧兩者,棧是程式執行最根本的東西,程式執行可以沒有堆,但是不能沒有棧。而堆是為棧進行資料儲存服務,說白了堆就是一塊共享的記憶體。不過,正是因為堆和棧的分離的思想,才使得Java的垃圾回收成為可能

  在Java中,Main函式就是棧的起始點,也是程式的起始點

  程式要執行總是有一個起點的。同C語言一樣,java中的Main就是那個起點,無論什麼java程式,找到main就找到了程式執行的入口。

  程式執行永遠都是在棧中進行的,因而引數傳遞時,只存在傳遞基本型別和物件引用的問題。不會直接傳物件本身

2、Java的物件訪問定位

  建立了物件是為了使用物件,我們對資料的使用是通過棧上的reference資料來操作堆上的具體物件,對於不同的虛擬機器實現,reference資料型別有不同的定義,主要是如下兩種訪問方式:

  1)使用控制代碼訪問。此時,Java堆中將會劃出一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊,如下圖:

  2)使用直接指標訪問。此時reference中儲存的就是物件的地址。如下圖:

  上面兩種物件訪問方式各有優勢,使用控制代碼訪問的最大好處就是reference中儲存的是穩定的控制代碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時,只會改變控制代碼中的例項資料指標,而reference本身不需要修改;使用直接指標訪問方式的最大好處就是速度更快,它節省了一次指標定位的時間開銷(根據上圖,節省的是物件例項資料的指標定位),由於物件的訪問在Java中非常頻繁,因此,這類開銷積少成多後也是一項非常可觀的執行成本。對於HotSpot虛擬機器而言,選擇的是第二種方式。

3、常見的OOM和SOF

  OOM表示Out Of Memory Error異常,OOM分為兩種情況:記憶體溢位(Memory Overflow)和記憶體洩漏(Memory Leak)。

  記憶體溢位,是指程式在申請記憶體時,沒有足夠的空間供其使用,出現了Out Of Memory,也就是要求分配的記憶體超出了系統能給你的,系統不能滿足需求,於是產生溢位。記憶體溢位分為上溢和下溢,比方說棧,棧滿時再做進棧必定產生空間溢位,叫上溢,棧空時再做退棧也產生空間溢位,稱為下溢。

  有時候記憶體洩露會導致記憶體溢位,所謂記憶體洩露(memory leak),是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩露危害可以忽略,但記憶體洩露堆積後果很嚴重,無論多少記憶體,遲早會被佔光,舉個例子,就是說系統的籃子(記憶體)是有限的,而你申請了一個籃子,拿到之後沒有歸還(忘記還了或是丟了),於是造成一次記憶體洩漏。在你需要用籃子的時候,又去申請,如此反覆,最終系統的籃子無法滿足你的需求,最終會由記憶體洩漏造成記憶體溢位。

  經常遇到的OOM有兩種,
  1)Java Heap溢位
  Java堆用於儲存物件例項,我們只要不斷的建立物件,而又沒有及時回收這些物件(即記憶體洩漏),就會在物件數量達到最大堆容量限制後產生記憶體溢位異常。
  2)方法區溢位
  方法區用於存放Class的相關資訊,如類名、訪問修飾符、常量池、欄位描述、方法描述等。方法區溢位也是一種常見的記憶體溢位異常,一個類如果要被垃圾收集器回收,判定條件是很苛刻的。在經常動態生成大量Class的應用中,要特別注意這點。異常資訊:Java.lang.OutOfMemoryError:PermGen space。

  SOF表示Stack Overflow,即堆疊溢位。當應用程式遞迴太深而發生堆疊溢位時,丟擲該錯誤。因為棧一般預設為1—2M,一旦出現死迴圈或者是大量的遞迴呼叫,在不斷的壓棧過程中,造成棧容量超過1M而導致溢位。棧溢位的原因總結:a.遞迴呼叫;b.大量迴圈或死迴圈;c.全域性變數是否過多;d.陣列、List、Map資料過大

4、哪些記憶體需要被回收

  根據Java記憶體模型,其中,程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的,因此這幾個區域的記憶體分配和回收都具備確定性,故這幾個區域就不需要過多考慮回收的問題,因為方法結束或者執行緒結束時,記憶體自然就跟著回收了。

  對於java堆和方法區則不一樣,java堆是存放例項物件的地方,我們只有在程式執行期間才能知道會建立哪些物件,這部分記憶體的分配和回收是動態的,因此,垃圾收集器所關注的就是這一部分。

  對於方法區(或者說HotSpot虛擬機器中的永久代),垃圾回收的主要是回收兩部分內容:廢棄常量和無用的類。對於廢棄常量,主要是判斷當前系統中有沒有物件引用這個常量;對於無用的類則比較嚴格,需要滿足下面三個條件:
  (1)該類的所有例項都已經被回收,即堆中不存在該類任何例項
  (2)載入該類的ClassLoader已經被回收
  (3)對類對應的java.lang.Class物件沒有在任何地方被引用,無法再任何地方通過反射訪問該類的方法;

  滿足了上面三個條件也僅僅是“可以”進行回收了,還要根據HotSpot的一些配置引數綜合考慮。

  在Java中靜態記憶體分配是指在Java被編譯時就已經能夠確定需要的記憶體空間,當程式被載入時系統把記憶體一次性分配給它。這些記憶體不會在程式執行時發生變化,直到程式執行結束時記憶體才被回收。在Java的類和方法中的區域性變數包括原生資料型別(int、long、char等)和物件的引用都是靜態分配記憶體的,如下面這段程式碼:

public void staticData(int arg) {
    String s = “String”;
    long l = 1;
    Long lg = 1L;
    Object o = new Object();
    Integer i = 0;
}

  其中引數arg、1是原生的資料型別,s、o和i是指向物件的引用。在Javac編譯時就已經確定了這些變數的靜態記憶體空間。其中arg會分配4個位元組,long會分配8個位元組,String、Long、Object和Integer是物件的型別,它們的引用會佔用4個位元組空間,這個方法佔用的靜態記憶體空間是4 + 4 + 8 + 4 + 4 + 4 = 28位元組。

  靜態記憶體空間是在Java棧上分配的,當這個方法執行結束時,對應的棧幀也就撤銷,所以分配的記憶體空間也就回收了

  在上面的程式碼中,變數lg和i儲存的值雖然與l和arg變數一樣,但是它們的儲存位置是不一樣的,後者是原生的資料型別,它們儲存在Java棧中,方法執行結束就會消失,而前者是物件型別,它們儲存在Java堆中,它們是可以被共享的,也不一定隨著方法執行結束而消失。變數l和lg的記憶體空間大小顯然也是不一樣的,l在Java棧中被分配8個位元組空間,而lg被分配4個位元組的地址指標空間這個地址指標指向lg物件在堆中的地址。很顯然在堆中Long型別數字1肯定不只8個位元組,所以Long代表的數字肯定比long型別佔用的空間大很多。

  在Java中,物件的記憶體空間是動態分配的,所謂的動態分配就是在程式執行時才知道要分配的儲存空間大小,而不是在編譯時就能夠確定的。lg代表的Long物件,只有JVM在解析Long類時才知道這個類中有哪些資訊,這些資訊都是哪些型別,然後再為這些資訊分配相應的儲存空間儲存相應的值。而這個物件什麼時候被回收也是不確定的,只有等到這個物件不再使用時才會被回收。

5、基於分代策略的垃圾回收

  分代的垃圾回收策略,是基於這樣一個事實:不同的物件的生命週期是不一樣的。因此,不同生命週期的物件可以採取不同的收集方式,以便提高回收效率

  在Java程式執行的過程中,會產生大量的物件,其中有些物件是與業務資訊相關,比如Http請求中的Session物件、執行緒、Socket連線,這類物件跟業務直接掛鉤,因此生命週期比較長。但是還有一些物件,主要是程式執行過程中生成的臨時變數,這些物件生命週期會比較短,比如:String物件,由於其不可變類的特性,系統會產生大量的這些物件,有些物件甚至只用一次即可回收。

  試想,在不進行物件存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因為每次回收都需要遍歷所有存活物件,但實際上,對於生命週期長的物件而言,這種遍歷是沒有效果的,因為可能進行了很多次遍歷,但是他們依舊存在。因此,分代垃圾回收採用分治的思想,進行代的劃分,把不同生命週期的物件放在不同代上,不同代上採用最適合它的垃圾回收方式進行回收


  如上圖所示、虛擬機器中的共劃分為三個代:年輕代(Young Generation)、年老點(Old Generation)和持久代(Permanent Generation)。其中持久代屬於方法區(也稱非堆),主要存放的是Java類的類資訊,與垃圾收集器要收集的Java物件關係不大。年輕代和年老代的劃分是對垃圾收集影響比較大的

  這三個區存放的內容有如下區別:

  Young區又分為Eden區和兩個Survivor區,其中新建立的物件都在Eden區,當Eden區滿後會觸發minor GC將Eden區仍然存活的物件複製到其中一個Survivor區中,另外一個Survivor區中的存活物件也複製到這個Survivor區,以保證始終有一個Survivor區是空的。需要注意,兩個Survivor區是對稱的,沒有先後關係,所以同一個區中可能同時存在從Eden複製過來物件,和從前一個Survivor複製過來的物件,而複製到年老區的只有從第一個Survivor去過來的物件。根據程式需要,Survivor區是可以配置為多個的(多於兩個),這樣可以增加物件在年輕代中的存在時間,減少被放到年老代的可能。

  Old區存放的Young區的Survivor滿後觸發minor GC後仍然存活的物件,當Eden區滿後會將物件存放到Survivor區中,如果Survivor區仍然存不下這些物件,GC收集器會將這些物件直接存放到Old區。如果Survivor區中的物件足夠老,也直接存放到Old區。如果Old區也滿了,將會觸發Full GC,回收整個堆記憶體。在年輕代中經歷了N次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。

  Perm區存放的主要是類的Class物件,Class物件就像其他儲存在堆中的物件一樣,Class物件也和Object物件一樣被儲存和GC。如果一個類被頻繁地載入,也可能導致Perm區滿,Perm區的垃圾回收也是由Ful GC觸發的。Full GC對整個記憶體進行整理,包括Young、Old和Perm,持久代大小通過-XX:MaxPermSize=<N>進行設定

  Full GC因為需要對整個堆進行回收,所以很慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於Full GC的調節。有如下原因可能導致Full GC:
  1)年老代(Tenured)被寫滿;
  2)持久代(Perm)被寫滿;
  3)System.gc()被顯示呼叫;
  4)上一次GC之後Heap的各域分配策略動態變化。

  Minor GC,即新生代GC,指發生在新生代的垃圾收集動作,因為Java物件大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快

  Major GC/Full GC,即老年代GC,指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(非絕對),Major GC的速度一般會比Minor GC慢10倍以上

6、理解GC日誌

  閱讀GC日誌是處理Java虛擬機器記憶體問題的基礎技能,它只是一些人為確定的規則,沒有太多技術含量。

  每一種收集器的日誌形式都是由它們自身的實現所決定的,換而言之,每個收集器的日誌格式都可以不一樣。但虛擬機器設計者為了方便使用者閱讀,將各個收集器的日誌都維持一定的共性,例如以下兩段典型的GC日誌:

33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs] 
3324K->152K(11904K),0.0031680 secs]

100.667:[Full GC[Tenured:0 K->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的(stop-the-world停頓型別表示垃圾收集器在收集垃圾過程中暫停了所有其他的工作執行緒,直到它收集結束)

  接下來的“[DefNew”、“[Tenured”、“[Perm”表示GC發生的區域,這裡顯示的區域名稱與使用的GC收集器是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名為“DefaultNew Generation”,所以顯示的是“[DefNew”。如果是ParNew收集器,新生代名稱就會變為“[ParNew”,意為“Parallel New Generation”。如果採用Parallel Scavenge收集器,那它配套的新生代稱為“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。

  後面方括號內部的“3324K->152K(3712K)”含義是“GC前該記憶體區域已使用容量->GC後該記憶體區域已使用容量(該記憶體區域總容量)”。而在方括號之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC後Java堆已使用容量(Java堆總容量)”。

  再往後,“0.0031680 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時間是完全正常的。

  

7、經驗及JVM引數分析

  大物件會直接進入老年代,所謂大物件就是指,需要大量連續記憶體空間的Java物件,最典型的大物件就是那種很長的字串及陣列(下面例子中的byte[]陣列就是典型的大物件)。大物件對虛擬機器的記憶體分配來說就是一個壞訊息,替Java虛擬機器抱怨一句,比遇到一個大物件更加壞的訊息就是遇到一群“朝生夕滅”的“短命大物件”,寫程式的時候應當避免。經常出現大物件容易導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們

  虛擬機器提供了一個-XX:PretenureSizeThreshold引數,令大於這個設定值的物件直接在老年代中分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體拷貝。

private static final int _1MB = 1024 * 1024;   
/**  
 1. VM引數:-verbose:gc -Xms20M -Xmx20M -Xmn10M 
 2. -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 
*/  
public static void testPretenureSizeThreshold() {  
 byte[] allocation;  
 allocation = new byte[4 * _1MB];  //直接分配在老年代中  
} 

  執行結果:

Heap  
def new generation   total 9216K, used 671K
[0x029d0000, 0x033d0000, 0x033d0000)  
 eden space 8192K,   8% used [0x029d0000, 0x02a77e98, 0x031d0000)  
 from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
 to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
tenured generation   total 10240K, used 4096K 
[0x033d0000, 0x03dd0000, 0x03dd0000)  
  the space 10240K,  40% used [0x033d0000, 
0x037d0010, 0x037d0200, 0x03dd0000)  
compacting perm gen  total 12288K, used 2107K 
[0x03dd0000, 0x049d0000, 0x07dd0000)  
  the space 12288K,  17% used [0x03dd0000, 
0x03fdefd0, 0x03fdf000, 0x049d0000)  
No shared spaces configured. 

  執行程式碼中的testPretenureSizeThreshold()方法後,我們看到Eden空間幾乎沒有被使用,而老年代10MB的空間被使用了40%,也就是4MB的allocation物件直接就分配在老年代中,這是因為PretenureSizeThreshold被設定為3MB(就是3145728B,這個引數不能與-Xmx之類的引數一樣直接寫3MB),因此超過3MB的物件都會直接在老年代中進行分配。

  不管是Minor GC還是Full GC,GC過程中都會導致程式執行中中斷,正確的選擇不同的GC策略,調整JVM的GC引數,可以極大的減少由於GC工作而導致的程式執行中斷方面的問題,進而適當的提高Java程式的工作效率。但是調整GC是一個極為複雜的過程,由於各個程式具備不同的特點,如:web和GUI程式就有很大區別(Web可以適當的停頓,但GUI的停頓是客戶無法接受的),而且由於跑在各個機器上的配置不同(主要是cup個數及記憶體容量不同),所以使用的GC種類也會不同。

  JVM主要引數及其含義解釋如下圖所示:




  當發生Minor GC時,除了將Eden區的非活動物件回收以外,還會把一些老物件也複製到Old區中。這個老物件的定義是通過配置引數MaxTenuringThrehold來控制的,如-XX:MaxTenuringThrehold=10,則表示如果這個物件已經被Minor GC回收過10次後仍然存活,那麼這個物件在這次Minor GC後直接進入Old區。

  JVM提供了大量命令列引數,列印資訊,供除錯使用。主要有以下一些:

  -XX:+PrintGC的輸出形式如下:

[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 
121376K->10414K(130112K), 0.0650971 secs]


  -XX:+PrintGCDetails 輸出形式:

[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K-
>113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K-
>8614K(9088K), 0.0000665 secs][Tenured: 112761K-
>10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]


  -XX:+PrintGCApplicationConcurrentTime:列印每次垃圾回收前,程式未中斷的執行時間。可與上面混合使用。輸出形式:

Application time: 0.5291524 seconds

  -XX:+PrintGCApplicationStoppedTime:列印垃圾回收期間程式暫停的時間。可與上面混合使用。輸出形式:

Total time for which application threads were stopped: 0.0468229 seconds

8、JVM記憶體設定指導原則

  年輕代大小選擇

  響應時間優先的應用:儘可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代垃圾收集發生的頻率也是最小的。同時,減少到達年老代的物件。

  吞吐量(不算gc時間後的工作時間佔總時間的比值)優先的應用:儘可能設定大,可能到達Gbit的程度。因為對響應時間沒有要求,垃圾收集可以並行進行,一般適合8CPU以上的應用。

  避免設定過小的年輕代,當年輕代設定過小時會導致:①年輕代GC次數更加頻繁,②可能導致年輕代GC後的存活物件直接進入老年代,如果此時老年代滿了,會觸發Full GC。

  年老代大小選擇

  響應時間優先的應用:年老代使用併發收集器,所以其大小需要小心設定,一般要考慮併發會話率和會話持續時間等一些引數。如果堆設定小了,可以會造成記憶體碎片,高回收頻率以及應用暫停而使用傳統的標記-清除方式;如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下資料獲得:併發垃圾收集資訊、持久代併發收集次數、傳統GC資訊、花在年輕代和年老代回收上的時間比例。

  較小堆引起的碎片問題

  因為年老代的併發收集器使用標記-清除演算法,所以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合併,這樣可以分配給較大的物件。但是,當堆空間較小時,執行一段時間以後,就會出現“碎片”,如果併發收集器找不到足夠的空間,那麼併發收集器將會停止,然後使用傳統的標記-清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:

  -XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啟對年老代的壓縮。
  -XX:CMSFullGCsBeforeCompaction=0:上面配置開啟的情況下,這裡可設定多少次Full GC後,對年老代進行壓縮。