鍍金池/ 教程/ Java/ 內(nèi)存調(diào)優(yōu)
新生代垃圾回收
參數(shù)分類和即時(JIT)編譯器診斷
打印所有 XX 參數(shù)及值
GC 日志
JVM 類型以及編譯器模式
內(nèi)存調(diào)優(yōu)
CMS 收集器
吞吐量收集器

內(nèi)存調(diào)優(yōu)

原文地址譯文地址,作者:PATRICK PESCHLOW,譯者:鄭旭東 校對:梁海艦

理想的情況下,一個 Java 程序使用 JVM 的默認(rèn)設(shè)置也可以運(yùn)行得很好,所以一般來說,沒有必要設(shè)置任何 JVM 參數(shù)。然而,由于一些性能問題(很不幸的是,這些問題經(jīng)常出現(xiàn)),一些相關(guān)的 JVM 參數(shù)知識會是我們工作中得好伙伴。在這篇文章中,我們將介紹一些關(guān)于 JVM 內(nèi)存管理的參數(shù)。知道并理解這些參數(shù),將對開發(fā)者和運(yùn)維人員很有幫助。

所有已制定的 HotSpot 內(nèi)存管理和垃圾回收算法都基于一個相同的堆內(nèi)存劃分:新生代(young generation)里存儲著新分配的和較年輕的對象,老年代(old generation)里存儲著長壽的對象。在此之外,永久代(permanent generation)存儲著那些需要伴隨整個 JVM 生命周期的對象,比如,已加載的對象的類定義或者 String 對象內(nèi)部 Cache。接下來,我們將假設(shè)堆內(nèi)存是按照新生代、老年代和永久代這一經(jīng)典策略劃分的。然而,其他的一些堆內(nèi)存劃分策略也是可行的,一個突出的例子就是新的 G1 垃圾回收器,它模糊了新生代和老年代之間的區(qū)別。此外,目前的開發(fā)進(jìn)程似乎表明在未來的 HotSpot JVM 版本中,將不會區(qū)分老年代和永久代。

-Xms and -Xmx (or: -XX:InitialHeapSize and -XX:MaxHeapSize)

-Xms 和 - Xmx 可以說是最流行的 JVM 參數(shù),它們可以允許我們指定 JVM 的初始和最大堆內(nèi)存大小。一般來說,這兩個參數(shù)的數(shù)值單位是 Byte,但同時它們也支持使用速記符號,比如 “k” 或者 “K” 代表 “kilo”,“m” 或者 “M” 代表 “mega”,“g” 或者 “G” 代表 “giga”。舉個例子,下面的命令啟動了一個初始化堆內(nèi)存為 128M,最大堆內(nèi)存為 2G,名叫 “MyApp” 的 Java 應(yīng)用程序。

java -Xms128m -Xmx2g MyApp

在實(shí)際使用過程中,初始化堆內(nèi)存的大小通常被視為堆內(nèi)存大小的下界。然而 JVM 可以在運(yùn)行時動態(tài)的調(diào)整堆內(nèi)存的大小,所以理論上來說我們有可能會看到堆內(nèi)存的大小小于初始化堆內(nèi)存的大小。但是即使在非常低的堆內(nèi)存使用下,我也從來沒有遇到過這種情況。這種行為將會方便開發(fā)者和系統(tǒng)管理員,因?yàn)槲覀兛梢酝ㄟ^將 “-Xms” 和 “-Xmx” 設(shè)置為相同大小來獲得一個固定大小的堆內(nèi)存。 -Xms 和 - Xmx 實(shí)際上是 - XX:InitialHeapSize 和 - XX:MaxHeapSize 的縮寫。我們也可以直接使用這兩個參數(shù),它們所起得效果是一樣的:

$ java -XX:InitialHeapSize=128m -XX:MaxHeapSize=2g MyApp

需要注意的是,所有 JVM 關(guān)于初始 \ 最大堆內(nèi)存大小的輸出都是使用它們的完整名稱:“InitialHeapSize” 和 “InitialHeapSize”。所以當(dāng)你查詢一個正在運(yùn)行的 JVM 的堆內(nèi)存大小時,如使用 - XX:+PrintCommandLineFlags 參數(shù)或者通過 JMX 查詢,你應(yīng)該尋找 “InitialHeapSize” 和 “InitialHeapSize” 標(biāo)志而不是 “Xms” 和 “Xmx”。

-XX:+HeapDumpOnOutOfMemoryError and -XX:HeapDumpPath

如果我們沒法為 - Xmx(最大堆內(nèi)存)設(shè)置一個合適的大小,那么就有可能面臨內(nèi)存溢出(OutOfMemoryError)的風(fēng)險(xiǎn),這可能是我們使用 JVM 時面臨的最可怕的猛獸之一。就同另外一篇關(guān)于這個主題的博文說的一樣,導(dǎo)致內(nèi)存溢出的根本原因需要仔細(xì)的定位。通常來說,分析堆內(nèi)存快照(Heap Dump)是一個很好的定位手段,如果發(fā)生內(nèi)存溢出時沒有生成內(nèi)存快照那就實(shí)在是太糟了,特別是對于那種 JVM 已經(jīng)崩潰或者錯誤只出現(xiàn)在順利運(yùn)行了數(shù)小時甚至數(shù)天的生產(chǎn)系統(tǒng)上的情況。

幸運(yùn)的是,我們可以通過設(shè)置 - XX:+HeapDumpOnOutOfMemoryError 讓 JVM 在發(fā)生內(nèi)存溢出時自動的生成堆內(nèi)存快照。有了這個參數(shù),當(dāng)我們不得不面對內(nèi)存溢出異常的時候會節(jié)約大量的時間。默認(rèn)情況下,堆內(nèi)存快照會保存在 JVM 的啟動目錄下名為 java_pid.hprof 的文件里(在這里 就是 JVM 進(jìn)程的進(jìn)程號)。也可以通過設(shè)置 - XX:HeapDumpPath= 來改變默認(rèn)的堆內(nèi)存快照生成路徑, 可以是相對或者絕對路徑。

雖然這一切聽起來很不錯,但有一點(diǎn)我們需要牢記。堆內(nèi)存快照文件有可能很龐大,特別是當(dāng)內(nèi)存溢出錯誤發(fā)生的時候。因此,我們推薦將堆內(nèi)存快照生成路徑指定到一個擁有足夠磁盤空間的地方。

-XX:OnOutOfMemoryError

當(dāng)內(nèi)存溢發(fā)生時,我們甚至可以可以執(zhí)行一些指令,比如發(fā)個 E-mail 通知管理員或者執(zhí)行一些清理工作。通過 - XX:OnOutOfMemoryError 這個參數(shù)我們可以做到這一點(diǎn),這個參數(shù)可以接受一串指令和它們的參數(shù)。在這里,我們將不會深入它的細(xì)節(jié),但我們提供了它的一個例子。在下面的例子中,當(dāng)內(nèi)存溢出錯誤發(fā)生的時候,我們會將堆內(nèi)存快照寫到 /tmp/heapdump.hprof 文件并且在 JVM 的運(yùn)行目錄執(zhí)行腳本 cleanup.sh

$ java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:OnOutOfMemoryError =

-XX:PermSize and -XX:MaxPermSize

永久代在堆內(nèi)存中是一塊獨(dú)立的區(qū)域,它包含了所有 JVM 加載的類的對象表示。為了成功運(yùn)行應(yīng)用程序,JVM 會加載很多類(因?yàn)樗鼈円蕾囉诖罅康牡谌綆?,而這又依賴于更多的庫并且需要從里面將類加載進(jìn)來)這就需要增加永久代的大小。我們可以使用 - XX:PermSize 和 - XX:MaxPermSize 來達(dá)到這個目的。其中 - XX:MaxPermSize 用于設(shè)置永久代大小的最大值,-XX:PermSize 用于設(shè)置永久代初始大小。下面是一個簡單的例子:

$ java -XX:PermSize=128m -XX:MaxPermSize=256m MyApp

請注意,這里設(shè)置的永久代大小并不會被包括在使用參數(shù) - XX:MaxHeapSize 設(shè)置的堆內(nèi)存大小中。也就是說,通過 - XX:MaxPermSize 設(shè)置的永久代內(nèi)存可能會需要由參數(shù) - XX:MaxHeapSize 設(shè)置的堆內(nèi)存以外的更多的一些堆內(nèi)存。

-XX:InitialCodeCacheSize and -XX:ReservedCodeCacheSize

JVM 一個有趣的,但往往被忽視的內(nèi)存區(qū)域是 “代碼緩存”,它是用來存儲已編譯方法生成的本地代碼。代碼緩存確實(shí)很少引起性能問題,但是一旦發(fā)生其影響可能是毀滅性的。如果代碼緩存被占滿,JVM 會打印出一條警告消息,并切換到 interpreted-only 模式:JIT 編譯器被停用,字節(jié)碼將不再會被編譯成機(jī)器碼。因此,應(yīng)用程序?qū)⒗^續(xù)運(yùn)行,但運(yùn)行速度會降低一個數(shù)量級,直到有人注意到這個問題。就像其他內(nèi)存區(qū)域一樣,我們可以自定義代碼緩存的大小。相關(guān)的參數(shù)是 - XX:InitialCodeCacheSize 和 - XX:ReservedCodeCacheSize,它們的參數(shù)和上面介紹的參數(shù)一樣,都是字節(jié)值。

-XX:+UseCodeCacheFlushing

如果代碼緩存不斷增長,例如,因?yàn)闊岵渴鹨鸬膬?nèi)存泄漏,那么提高代碼的緩存大小只會延緩其發(fā)生溢出。為了避免這種情況的發(fā)生,我們可以嘗試一個有趣的新參數(shù):當(dāng)代碼緩存被填滿時讓 JVM 放棄一些編譯代碼。通過使用 - XX:+UseCodeCacheFlushing 這個參數(shù),我們至少可以避免當(dāng)代碼緩存被填滿的時候 JVM 切換到 interpreted-only 模式。不過,我仍建議盡快解決代碼緩存問題發(fā)生的根本原因,如找出內(nèi)存泄漏并修復(fù)它。