5.深入理解java虛擬機--第二部分--- 調優案例分析與實戰

5.1概述

上文介紹了處理Java虛擬機內存問題的知識與工具,在處理實際項目的問題時,除了知識與工具外,經驗同樣是一個很重要的因素。因此本章將與讀者分享幾個比較有代表性的實際案例。考慮到虛擬機故障處理和調優主要面向各類服務端應用,而大部分Java程序員較少有機會直接接觸生產環境的服務器,因此本章還準備了一個所有開發人員都能夠進行“親身實戰”的練習,希望通過實踐使讀者獲得故障處理和調優的經驗。

5.2案例分析

5.2.1 高性能硬件上的程序部署策略

在高性能硬件上部署程序,目前主要有兩種方式:

通過64位JDK來使用大內存。

使用若干個32位虛擬機建立邏輯集羣來利用硬件資源。

此案例中的管理員採用了第一種部署方式。對於用戶交互性強、對停頓時間敏感的系統,可以給Java虛擬機分配超大堆的前提是有把握把應用程序的Full GC頻率控制得足夠低,至少要低到不會影響用戶使用,譬如十幾個小時乃至一天才出現一次Full GC,這樣可以通過在深夜執行定時任務的方式觸發Full GC甚至自動重啓應用服務器來保持內存可用空間在一個穩定的水平。控制Full GC頻率的關鍵是看應用中絕大多數對象能否符合“朝生夕滅”的原則,即大多數對象的生存時間不應太長,尤其是不能有成批量的、長生存時間的大對象產生,這樣才能保障老年代空間的穩定。

相同程序在64位JDK消耗的內存一般比32位JDK大,這是由於指針膨脹,以及數據類型對齊補白等因素導致的。

上面的問題聽起來有點嚇人,所以現階段不少管理員還是選擇第二種方式:使用若干個32位虛擬機建立邏輯集羣來利用硬件資源。具體做法是在一臺物理機器上啓動多個應用服務器進程,每個服務器進程分配不同端口,然後在前端搭建一個負載均衡器,以反向代理的方式來分配訪問請求。讀者不需要太過在意均衡器轉發所消耗的性能,即使使用64位JDK,許多應用也不止有一臺服務器,因此在許多應用中前端的均衡器總是要存在的。

5.2.2 集羣間同步導致的內存溢出

5.3.3堆外內存溢出

Direct Memory

JVM可以使用的內存分外2種:堆內存和堆外內存.

    堆內存完全由JVM負責分配和釋放,如果程序沒有缺陷代碼導致內存泄露,那麼就不會遇到java.lang.OutOfMemoryError這個錯誤。

    使用堆外內存,就是爲了能直接分配和釋放內存,提高效率。JDK5.0之後,代碼中能直接操作本地內存的方式有2種:使用未公開的Unsafe和NIO包下ByteBuffer。

作爲JAVA開發者我們經常用java.nio.DirectByteBuffer對象進行堆外內存的管理和使用,它會在對象創建的時候就分配堆外內存。

DirectByteBuffer類是在Java Heap外分配內存,對堆外內存的申請主要是通過成員變量unsafe來操作

從實踐經驗的角度出發,除了Java堆和永久代之外,我們注意到下面這些區域還會佔用較多的內存,這裏所有的內存總和受到操作系統進程最大內存的限制。[插圖]Direct Memory:可通過-XX:MaxDirectMemorySize調整大小,內存不足時拋出OutOfMemoryError或者OutOfMemoryError:Direct buffer memory

5.2.4 外部命令導致系統緩慢

有趣,調用系統的進程

這是一個來自網絡的案例:一個數字校園應用系統,運行在一臺4個CPU的Solaris 10操作系統上,中間件爲GlassFish服務器。系統在做大併發壓力測試的時候,發現請求響應時間比較慢,通過操作系統的mpstat工具發現CPU使用率很高,並且系統佔用絕大多數的CPU資源的程序並不是應用系統本身。這是個不正常的現象,通常情況下用戶應用的CPU佔用率應該佔主要地位,才能說明系統是正常工作的。通過Solaris 10的Dtrace腳本可以查看當前情況下哪些系統調用花費了最多的CPU資源,Dtrace運行後發現最消耗CPU資源的竟然是“fork”系統調用。衆所周知,“fork”系統調用是Linux用來產生新進程的,在Java虛擬機中,用戶編寫的Java代碼最多隻有線程的概念,不應當有進程的產生。這是個非常異常的現象。通過本系統的開發人員,最終找到了答案:每個用戶請求的處理都需要執行一個外部shell腳本來獲得系統的一些信息。執行這個shell腳本是通過Java的Runtime.getRuntime().exec()方法來調用的。這種調用方式可以達到目的,但是它在Java虛擬機中是非常消耗資源的操作,即使外部命令本身能很快執行完畢,頻繁調用時創建進程的開銷也非常可觀。Java虛擬機執行這個命令的過程是:首先克隆一個和當前虛擬機擁有一樣環境變量的進程,再用這個新的進程去執行外部命令,最後再退最後再退出這個進程。如果頻繁執行這個操作,系統的消耗會很大,不僅是CPU,內存負擔也很重。用戶根據建議去掉這個Shell腳本執行的語句,改爲使用Java的API去獲取這些信息後,系統很快恢復了正常。

5.2.5 服務器JVM進程崩潰

改異步通知爲生產消費模式的消息隊列

5.2.6 不恰當數據結構導致內存佔用過大


5.2.7 由Windows虛擬內存導致的長時間停頓

5.3 實戰:Eclipse運行速度調優

5.3.1 調優前的程序運行狀態

5.3.2 升級JDK 1.6的性能變化及兼容問題

5.3.3 編譯時間和類加載時間的優化

而編譯時間是什麼呢?程序在運行之前不是已經編譯了嗎?虛擬機的JIT編譯與垃圾收集一樣,是本書的一個重要部分,後面有專門章節講解,這裏先簡單介紹一下:編譯時間是指虛擬機的JIT編譯器(Just In Time Compiler)編譯熱點代碼(Hot Spot Code)的耗時。我們知道Java語言爲了實現跨平臺的特性,Java代碼編譯出來後形成的Class文件中存儲的是字節碼(ByteCode),虛擬機通過解釋方式執行字節碼命令,比起C/C++編譯成本地二進制代碼來說,速度要慢不少。爲了解決程序解釋執行的速度問題,JDK 1.2以後,虛擬機內置了兩個運行時編譯器[插圖],如果一段Java方法被調用次數達到一定程度,就會被判定爲熱代碼交給JIT編譯器即時編譯爲本地代碼,提高運行速度(這就是HotSpot虛擬機名字的由來)。甚至有可能在運行期動態編譯比C/C++的編譯期靜態譯編出來的代碼更優秀,因爲運行期可以收集很多編譯器無法知道的信息,甚至可以採用一些很激進的優化手段,在優化條件不成立的時候再逆優化退回來。所以Java程序只要代碼沒有問題(主要是泄漏問題,如內存泄漏、連接泄漏),隨着代碼被編譯得越來越徹底,運行速度應當是越運行越快的。Java的運行期編譯最大的缺點就是它進行編譯需要消耗程序正常的運行時間,這也就是上面所說的“編譯時間”。

5.3.4 調整內存設置控制垃圾收集頻率

整個分析過程只能說是大寫的牛逼!

通過各種分析公爵得到minorGC和fullGC 的頻率相當的多,進一步分析得出新生代和老生代的內存容量小了,老年代一次次的gc一次次的擴容,調整容量大小!

5.3.5 選擇收集器降低延遲

作者選擇了支持併發多線程的parNew收集器和CMS收集qi,大大降低了垃圾收集的延遲

5.4 本章小結Java虛擬機的內存管理與垃圾收集是虛擬機結構體系中最重要的組成部分,對程序的性能和穩定性有非常大的影響,在本書的第2~5章中,筆者從理論知識、異常現象、代碼、工具、案例、實戰等幾個方面對其進行了講解,希望讀者有所收穫。本書關於虛擬機內存管理部分到此爲止就結束了,後面將開始介紹Class文件與虛擬機執行子系統方面的知識。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章