Java內(nèi)存與垃圾回收調(diào)優(yōu)
要了解Java垃圾收集機制,先理解JVM內(nèi)存模式是非常重要的。今天我們將會了解JVM內(nèi)存的各個部分、如何監(jiān)控以及垃圾收集調(diào)優(yōu)。
Java(JVM)內(nèi)存模型
正如你從上面的圖片看到的,JVM內(nèi)存被分成多個獨立的部分。廣泛地說,JVM堆內(nèi)存被分為兩部分——年輕代(Young Generation)和老年代(Old Generation)。
年輕代
年輕代是所有新對象產(chǎn)生的地方。當年輕代內(nèi)存空間被用完時,就會觸發(fā)垃圾回收。這個垃圾回收叫做Minor GC。年輕代被分為3個部分——Enden區(qū)和兩個Survivor區(qū)。
年輕代空間的要點:
- 大多數(shù)新建的對象都位于Eden區(qū)。
- 當Eden區(qū)被對象填滿時,就會執(zhí)行Minor GC。并把所有存活下來的對象轉(zhuǎn)移到其中一個survivor區(qū)。
- Minor GC同樣會檢查存活下來的對象,并把它們轉(zhuǎn)移到另一個survivor區(qū)。這樣在一段時間內(nèi),總會有一個空的survivor區(qū)。
- 經(jīng)過多次GC周期后,仍然存活下來的對象會被轉(zhuǎn)移到年老代內(nèi)存空間。通常這是在年輕代有資格提升到年老代前通過設(shè)定年齡閾值來完成的。
年老代
年老代內(nèi)存里包含了長期存活的對象和經(jīng)過多次Minor GC后依然存活下來的對象。通常會在老年代內(nèi)存被占滿時進行垃圾回收。老年代的垃圾收集叫做Major GC。Major GC會花費更多的時間。
Stop the World事件
所有的垃圾收集都是“Stop the World”事件,因為所有的應用線程都會停下來直到操作完成(所以叫“Stop the World”)。
因為年輕代里的對象都是一些臨時(short-lived )對象,執(zhí)行Minor GC非常快,所以應用不會受到(“Stop the World”)影響。
由于Major GC會檢查所有存活的對象,因此會花費更長的時間。應該盡量減少Major GC。因為Major GC會在垃圾回收期間讓你的應用反應遲鈍,所以如果你有一個需要快速響應的應用發(fā)生多次Major GC,你會看到超時錯誤。
垃圾回收時間取決于垃圾回收策略。這就是為什么有必要去監(jiān)控垃圾收集和對垃圾收集進行調(diào)優(yōu)。從而避免要求快速響應的應用出現(xiàn)超時錯誤。
***代
***代或者“Perm Gen”包含了JVM需要的應用元數(shù)據(jù),這些元數(shù)據(jù)描述了在應用里使用的類和方法。注意,***代不是Java堆內(nèi)存的一部分。
***代存放JVM運行時使用的類。***代同樣包含了Java SE庫的類和方法。***代的對象在full GC時進行垃圾收集。
方法區(qū)
方法區(qū)是***代空間的一部分,并用來存儲類型信息(運行時常量和靜態(tài)變量)和方法代碼和構(gòu)造函數(shù)代碼。
內(nèi)存池
如果JVM實現(xiàn)支持,JVM內(nèi)存管理會為創(chuàng)建內(nèi)存池,用來為不變對象創(chuàng)建對象池。字符串池就是內(nèi)存池類型的一個很好的例子。內(nèi)存池可以屬于堆或者***代,這取決于JVM內(nèi)存管理的實現(xiàn)。
運行時常量池
運行時常量池是每個類常量池的運行時代表。它包含了類的運行時常量和靜態(tài)方法。運行時常量池是方法區(qū)的一部分。
Java棧內(nèi)存
Java棧內(nèi)存用于運行線程。它們包含了方法里的臨時數(shù)據(jù)、堆里其它對象引用的特定數(shù)據(jù)。你可以閱讀棧內(nèi)存和堆內(nèi)存的區(qū)別。
Java 堆內(nèi)存開關(guān)
Java提供了大量的內(nèi)存開關(guān)(參數(shù)),我們可以用它來設(shè)置內(nèi)存大小和它們的比例。下面是一些常用的開關(guān):
VM 開關(guān) | VM 開關(guān)描述 |
---|---|
-Xms | 設(shè)置JVM啟動時堆的初始化大小。 |
-Xmx | 設(shè)置堆***值。 |
-Xmn | 設(shè)置年輕代的空間大小,剩下的為老年代的空間大小。 |
-XX:PermGen | 設(shè)置***代內(nèi)存的初始化大小。 |
-XX:MaxPermGen | 設(shè)置***代的***值。 |
-XX:SurvivorRatio | 提供Eden區(qū)和survivor區(qū)的空間比例。比如,如果年輕代的大小為10m并且VM開關(guān)是-XX:SurvivorRatio=2,那么將會保留5m內(nèi)存給Eden區(qū)和每個Survivor區(qū)分配2.5m內(nèi)存。默認比例是8。 |
-XX:NewRatio | 提供年老代和年輕代的比例大小。默認值是2。 |
Java垃圾回收大多數(shù)時候,上面的選項已經(jīng)足夠使用了。但是如果你還想了解其他的選項,那么請查看JVM選項官方網(wǎng)頁。
Java垃圾回收會找出沒用的對象,把它從內(nèi)存中移除并釋放出內(nèi)存給以后創(chuàng)建的對象使用。Java程序語言中的一個***優(yōu)點是自動垃圾回收,不像其他的程序語言那樣需要手動分配和釋放內(nèi)存,比如C語言。
垃圾收集器是一個后臺運行程序。它管理著內(nèi)存中的所有對象并找出沒被引用的對象。所有的這些未引用的對象都會被刪除,回收它們的空間并分配給其他對象。
一個基本的垃圾回收過程涉及三個步驟:
- 標記:這是***步。在這一步,垃圾收集器會找出哪些對象正在使用和哪些對象不在使用。
- 正常清除:垃圾收集器清會除不在使用的對象,回收它們的空間分配給其他對象。
- 壓縮清除:為了提升性能,壓縮清除會在刪除沒用的對象后,把所有存活的對象移到一起。這樣可以提高分配新對象的效率。
簡單標記和清除方法存在兩個問題:
- 效率很低。因為大多數(shù)新建對象都會成為“沒用對象”。
- 經(jīng)過多次垃圾回收周期的對象很有可能在以后的周期也會存活下來。
上面簡單清除方法的問題在于Java垃圾收集的分代回收的,而且在堆內(nèi)存里有年輕代和年老代兩個區(qū)域。我已經(jīng)在上面解釋了Minor GC和Major GC是怎樣掃描對象,以及如何把對象從一個分代空間移到另外一個分代空間。
Java垃圾回收類型
這里有五種可以在應用里使用的垃圾回收類型。僅需要使用JVM開關(guān)就可以在我們的應用里啟用垃圾回收策略。讓我們一起來逐一了解:
- Serial GC(-XX:+UseSerialGC):Serial GC使用簡單的標記、清除、壓縮方法對年輕代和年老代進行垃圾回收,即Minor GC和Major GC。Serial GC在client模式(客戶端模式)很有用,比如在簡單的獨立應用和CPU配置較低的機器。這個模式對占有內(nèi)存較少的應用很管用。
- Parallel GC(-XX:+UseParallelGC):除了會產(chǎn)生N個線程來進行年輕代的垃圾收集外,Parallel GC和Serial GC幾乎一樣。這里的N是系統(tǒng)CPU的核數(shù)。我們可以使用 -XX:ParallelGCThreads=n 這個JVM選項來控制線程數(shù)量。并行垃圾收集器也叫throughput收集器。因為它使用了多CPU加快垃圾回收性能。Parallel GC在進行年老代垃圾收集時使用單線程。
- Parallel Old GC(-XX:+UseParallelOldGC):和Parallel GC一樣。不同之處,Parallel Old GC在年輕代垃圾收集和年老代垃圾回收時都使用多線程收集。
- 并發(fā)標記清除(CMS)收集器(-XX:+UseConcMarkSweepGC):CMS收集器也被稱為短暫停頓并發(fā)收集器。它是對年老代進行垃圾收集的。CMS收集器通過多線程并發(fā)進行垃圾回收,盡量減少垃圾收集造成的停頓。CMS收集器對年輕代進行垃圾回收使用的算法和Parallel收集器一樣。這個垃圾收集器適用于不能忍受長時間停頓要求快速響應的應用。可使用 -XX:ParallelCMSThreads=n JVM選項來限制CMS收集器的線程數(shù)量。
- G1垃圾收集器(-XX:+UseG1GC) G1(Garbage First):垃圾收集器是在Java 7后才可以使用的特性,它的長遠目標時代替CMS收集器。G1收集器是一個并行的、并發(fā)的和增量式壓縮短暫停頓的垃圾收集器。G1收集器和其他的收集器運行方式不一樣,不區(qū)分年輕代和年老代空間。它把堆空間劃分為多個大小相等的區(qū)域。當進行垃圾收集時,它會優(yōu)先收集存活對象較少的區(qū)域,因此叫“Garbage First”。你可以在Oracle Garbage-FIrst收集器文檔找到更多詳細信息。
Java垃圾收集監(jiān)控
我們可以使用命令行和圖形工具來監(jiān)控監(jiān)控應用垃圾回收。例如,我使用Java SE下載頁中的一個demo來實驗。
如果你想使用同樣的應用,可以到Java SE下載頁面下載JDK 7和JavaFX演示和示例。我使用的示例應用是Java2Demo.jar,它位于 jdk1.7.0_55/demo/jfc/Java2D 目錄下。這只是一個可選步驟,你可以運行GC監(jiān)控命令監(jiān)控任何Java應用。
我打開演示應用使用的命令是:
- pankaj@Pankaj:~/Downloads/jdk1.7.0_55/demo/jfc/Java2D$ java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar Java2Demo.jar
jsat
可以使用jstat命令行工具監(jiān)控JVM內(nèi)存和垃圾回收。標準的JDK已經(jīng)附帶了jstat,所以不需要做任何額外的事情就可以得到它。
要運行jstat你需要知道應用的進程id,你可以使用 ps -eaf | grep java 命令獲取進程id。
- pankaj@Pankaj:~$ ps -eaf | grep Java2Demo.jar
- 501 9582 11579 0 9:48PM ttys000 0:21.66 /usr/bin/java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseG1GC -jar Java2Demo.jar
- 501 14073 14045 0 9:48PM ttys002 0:00.00 grep Java2Demo.jar
從上面知道,我的Java應用進程id是9582。現(xiàn)在可以運行jstat命令了,就像下面展示的一樣:
- pankaj@Pankaj:~$ jstat -gc 9582 1000
- S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
- 1024.0 1024.0 0.0 0.0 8192.0 7933.3 42108.0 23401.3 20480.0 19990.9 157 0.274 40 1.381 1.654
- 1024.0 1024.0 0.0 0.0 8192.0 8026.5 42108.0 23401.3 20480.0 19990.9 157 0.274 40 1.381 1.654
- 1024.0 1024.0 0.0 0.0 8192.0 8030.0 42108.0 23401.3 20480.0 19990.9 157 0.274 40 1.381 1.654
- 1024.0 1024.0 0.0 0.0 8192.0 8122.2 42108.0 23401.3 20480.0 19990.9 157 0.274 40 1.381 1.654
- 1024.0 1024.0 0.0 0.0 8192.0 8171.2 42108.0 23401.3 20480.0 19990.9 157 0.274 40 1.381 1.654
- 1024.0 1024.0 48.7 0.0 8192.0 106.7 42108.0 23401.3 20480.0 19990.9 158 0.275 40 1.381 1.656
- 1024.0 1024.0 48.7 0.0 8192.0 145.8 42108.0 23401.3 20480.0 19990.9 158 0.275 40 1.381 1.656
jstat命令的***一個參數(shù)是每個輸出的時間間隔。每隔一秒就會打印出內(nèi)存和垃圾收集數(shù)據(jù)。
讓我們一起來對每一列的意義進行逐一了解:
- S0C和S1C:這一列展示了Survivor0和Survivor1區(qū)的當前大小(單位KB)。
- S0U和S1U:這一列展示了當前Survivor0和Survivor1區(qū)的使用情況(單位KB)。注意:無論任何時候,總會有一個Survivor區(qū)是空著的。
- EC和EU:這些列展示了Eden區(qū)當前空間大小和使用情況(單位KB)。注意:EU的大小一直在增大。而且只要大小接近EC時,就會觸發(fā)Minor GC并且EU將會減小。
- OC和OU:這些列展示了年老代當前空間大小和當前使用情況(單位KB)。
- PC和PU:這些列展示了Perm Gen(***代)當前空間大小和當前使用情況(單位KB)。
- YGC和YGCT:YGC這列顯示了發(fā)生在年輕代的GC事件的數(shù)量。YGCT這列顯示了在年輕代進行GC操作的累計時間。注意:在EU的值由于minor GC導致下降時,同一行的YGC和YGCT都會增加。
- FGC和FGCT:FGC列顯示了發(fā)生Full GC事件的次數(shù)。FGCT顯示了進行Full GC操作的累計時間。注意:相對于年輕代的GC使用時間,F(xiàn)ull GC所用的時間長很多。
- GCT:這一列顯示了GC操作的總累計時間。注意:總累計時間是YGCT和FGCT兩列所用時間的總和(GCT=YGCT+FGCT)。
jstat的優(yōu)點,我們同樣可以在沒有GUI的遠程服務器上運行jstat。注意:我們是通過 -Xmn10m 選項來指定S0C、S1C和EC的總和為10m的。
Java VisualVM及Visual GC插件
如果你想在GUI里查看內(nèi)存和GC,那么可以使用jvisualvm工具。Java VisualVM同樣是JDK的一部分,所以你不需要單獨去下載。
在終端運行jvisualvm命令啟動Java VisualVM程序。一旦啟動程序,你需要從Tools->Plugins選項安裝Visual GC插件,就像下面圖片展示的。
安裝完Visual GC插件后,從左邊欄打開應用并把視角轉(zhuǎn)到Visual GC部分。你將會得到關(guān)于JVM內(nèi)存和垃圾收集詳情,如下圖所示。
Java垃圾回收調(diào)優(yōu)
Java垃圾回收調(diào)優(yōu)應該是提升應用吞吐量的***一個選擇。在你發(fā)現(xiàn)應用由于長時間垃圾回收導致了應用性能下降、出現(xiàn)超時的時候,應該考慮Java垃圾收集調(diào)優(yōu)。
如果你在日志里看到 java.lang.OutOfMemoryError: PermGen space錯誤,那么可以嘗試使用 -XX:PermGen 和 -XX:MaxPermGen JVM選項去監(jiān)控并增加Perm Gen內(nèi)存空間。你也可以嘗試使用-XX:+CMSClassUnloadingEnabled并查看使用CMS垃圾收集器的執(zhí)行性能。
如果你看到了大量的Full GC操作,那么你應該嘗試增大老年代的內(nèi)存空間。
全面垃圾收集調(diào)優(yōu)要花費大量的努力和時間,這里沒有一塵不變的硬性調(diào)優(yōu)規(guī)則。你需要去嘗試不同的選項并且對這些選項進行對比,從而找出最適合自己應用的方案。
這就是所有的Java內(nèi)存模型和垃圾回收內(nèi)容。希望對你理解JVM內(nèi)存和垃圾收集過程有所幫助。
原文鏈接: journaldev 翻譯: ImportNew.com - 進林
譯文鏈接: http://www.importnew.com/14086.html