前言
學以致用,至此JVM中關於內存部分的理論知識與工具的使用,我們已經搞定了。
那麼接下來,本文,將重點就如何運用這些知識與工具來進行故障處理和調優進行敘述。
64位Java虛擬機
採用64位JDK來管理大內存,需要注意的問題:
- 產生堆溢出幾乎無法產生堆轉儲快照,哪怕產生了快照也幾乎無法進行分析,因爲dump文件會很大,達到十幾個G。
- 相同程序在64位JDK消耗的內存一般比32位JDK大,這是由於指針膨脹,以及數據類型對其補白等因素導致的。
- 我們可以採用在一臺物理機上啓動多個應用服務器進程,若干個32位虛擬機建立的邏輯集羣來利用硬件資源。可以採用無Session複製的親和式集羣,也就是將一個固定的用戶請求永遠分配到固定的一個集羣節點進行處理即可。
當然這種一臺物理機上建立無Session複製的親和式集羣的多個應用的方式,也有它的缺點:
- 儘量避免節點競爭全局的資源,最典型的就是磁盤競爭,尤其是併發寫同一文件,很容易導致IO異常。
- 很難最高效率的利用某些資源池,譬如連接池,一般都是在各個節點建立自己獨立的連接池,這樣有可能導致一些節點連接池滿了,另外一些節點仍有較多空餘。
- 各個節點仍然不可避免的收到32位的內存限制,每個進程最多隻能使用4GB的內存,考慮到堆以外的開銷,每個進程肯定實際使用的內存比4GB小。
- 大量使用本地緩存,在邏輯集羣中會造成較大的浪費,因爲每個邏輯節點上都有一份緩存,這時候可以考慮把本地緩存改爲集中式緩存。
被集羣共享的數據要使用類似JBossCache這種集羣緩存來同步的話,可以允許讀操作頻繁,因爲數據在本地內存有一份副本,讀取的動作不會耗費多少資源,但不應當有過於頻繁的寫操作,那樣會帶來很大的網絡同步的開銷。
某些情況下-XX:HeapDumpOnOutOfMemoryError沒反應,拋出內存溢出異常時什麼文件都沒有生成。我們可以使用原生的命令jstat -gcutil vmid 間隔 次數來監控GC的情況。
Direct Memory
垃圾收集時,虛擬機雖然會對Direct Memory進行回收,但是Direct Memory卻不能像新生代、老年代那樣,發現空間不足了就通知收集器進行垃圾回收。
它只能等待老年代滿了後Full GC,然後”順便的“幫它清理掉內存的廢棄對象。
否則它只能一直等到拋出內存溢出異常時,先catch掉,再在catch塊裏面”大喊“一聲:”System.gc( )!“。
要是虛擬機打開了**-XX:+DisableExplictGC**開關,那就只能靜靜的看着堆中還有很多空閒內存,自己卻不得不拋出內存溢出異常了。
堆外內存之總結
從實踐的角度出發,除了Java堆和永久代之外,我們注意到下面這些區域還會佔用較多的內存。這裏所有內存的總和受到操作系統進程最大內存的限制:
- Direct Memory:可以使用**-XX:MaxDirectMemorySize**調整大小,內存不足時拋出OutOfMemoryError或者OutOfMemoryError: Direct buffer memory。
- 線程堆棧:可以通過**-Xss**調整大小,內存不足時,拋出StackOverflowError(縱向無法分配,即無法分配新的棧幀)或者OutOfMemoryError:unable to create new native thread(橫向無法分配,即無法建立新的線程)。
- Socket 緩存區:每個Socket連接都有Receive和Send兩個緩衝區,連接多的話這塊內存佔用也比較樂觀。如果無法分配,則可能會拋出IOException:Too many open files異常。
- JNI代碼:如果代碼中使用JNI調用本地庫,那本地庫使用的內存也不在堆中。
- 虛擬機和GC:虛擬機、GC的代碼執行也要消耗一定的內存。
Connection reset
對方接口無法正常調用,如果採用異步的方式,會使我們的服務不斷的積累着等待的線程,最後超過虛擬機的承受能力後使得虛擬機進程崩潰,解決辦法,通知對方修復接口,並將異步調用改爲生產者/消費者模式的消息隊列實現後,系統恢復正常。
-XX:+PrintGCApplicationStoppedTime:打印GC過程中應用程序被阻塞的時間。
-XX:+PrintGCDateStamps
-Xloggc:gclog.log
從GC日誌文件中確認停頓是否有GC導致的。
-XX:+PrintReferenceGC:可以使我們從GC日誌中找到長時間停頓的具體日誌信息。
什麼是JIT即時編譯器
我們看到的上圖中的編譯時間到底是什麼呢?程序在運行之前不是已經編譯好了嗎?
這裏的編譯時間就是指虛擬機的JIT編譯器編譯熱點代碼的耗時,我們知道Java語言爲了實現跨平臺的特性,Java編譯出來的是字節碼,虛擬機通過解釋的方式執行字節碼指令。
爲了解決程序解釋執行的速度問題,虛擬機內置了兩個運行時編譯器。
如果一段Java代碼方法被調用次數達到一定程度,就會被判定爲熱代碼交給JIT編譯器即時編譯爲本地代碼,提高運行速度,這也就是HotSpot虛擬機名字的由來。
Java的運行期編譯最大的缺點就是它進行編譯需要消耗程序正常的運行時間,這也就是上面所說的編譯時間。
屏蔽掉System.gc( ),-XX:+DisableExplictGC。
VisualVM的實戰分析
如下圖所示,我們分析下VisualVM中的VisualGC插件中的元素代表的意義:
- 這是我們的插件的名字。
- 我們變異了629次,花費了514ms。
- 加載了1610個類,花費了444ms。
- 共進行了1次垃圾收集,耗費了7ms。上一次發生GC的原因是內存不夠使用。
- Eden區共8M,使用了7.62M,發生了一次Minor GC,花費了7ms。
- 倖存區1共1M,還沒被佔用。
- 倖存區1共1M,佔用1K。
- 老年代共10M,已被佔用了8M,沒有發生Full GC。
- 代碼區1G,佔用了6M。
新生代共10M,其中非用戶時間由三部分組成:
- GC時間
- JIT編譯時間
- 類加載時間
內存溢出排查之總結
通過上面的工具的學習,以及一節理論結合的實踐,當虛擬機出現反應緩慢時,我們可以進行以下的排查:
- 採用的什麼垃圾收集器?
- 然後看下GC的情況,如Full GC的次數以及停頓的時間等。
- 以及是否有大對象,一直存活的情況等。
- 如果我們可以將應用程序的Full GC頻率控制得足夠低,譬如幾天出現一次Full GC,那麼我們就可以通過在深夜執行定時任務的方式觸發Full GC、甚至自動重啓應用服務器來保持內存可用空間在一個穩定的水平。
- 如果CPU資源敏感度低,可以考慮CMS收集器進行垃圾回收,能夠併發的進行垃圾回收。
如果感覺比較零散,不好記住的話,可以參考以下幾點:
- 看虛擬機參數,有個大概的瞭解配置的情況。
- 看內存快照,分析其中內存的佔用情況,是否有大量大對象存活。
- 分析GC的過程,看是否不是正常的內存回收。
- 根據線程快照,看線程的運行情況,線程是否有死鎖或者被某些資源阻塞的情況。
- 分析編譯時間與類加載時間是否比較正常。