深入理解Java虛擬機筆記 – 自動內存管理機制(調優案例分析與實戰)

jopen 10年前發布 | 23K 次閱讀 Java Java開發
1、概述
2、案例分析
2.1、高性能硬件上的程序部署策略

一個部署問題 √

控制 Full GC 頻率的關鍵是看應用中絕大多數對象能否符合“朝生夕滅”的原則,即大多數對象的生存時間不應太長,尤其是不能有成批量的、長生存時間的大對象產生,這樣才能保障老年代空間的穩定。

如果讀者計劃使用 64 位 JDK 來管理大內存,還需要考慮下面可能面臨的問題:內存回收導致的長時間停頓;相同程序在 64 位 JDK 消耗的內存一般比 32 位 JDK 大,這是由于指針膨脹,以及數據類型對齊補白等因素導致的。

而使用無 Session 復制的親合式集群是一個相當不錯的選擇。

將一個固定的用戶請求永遠分配到固定的一個集群節點進行處理即可,這樣程序開發階段就基本不用為集群環境做什么特別的考慮了。

如果讀者計劃使用邏輯集群的方式來部署程序,可能會遇到下面一些問題:

  • 盡量避免節點競爭全局的資源,最典型的就是磁盤競爭,各個節點如果同時訪問某個磁盤文件的話(尤其是并發寫操作容易出現問題),很容易導致 IO 異常。
  • 很難最高效率地利用某些資源池,各個節點連接池使用率不一致,而如果使用集中式的JNDI,則會帶來個一定復雜性并且可能帶來額外的性能開銷。
  • 各個節點仍然不可避免地受到 32 位的內存限制。
  • 每個邏輯節點都有一份緩存,造成空間的浪費,可以考慮使用集中式緩存。

考慮到用戶對響應速度比較關心,并且文檔服務的主要壓力集中在磁盤和內存訪問, CPU 資源敏感度較低,因此改為 CMS 收集器進行垃圾回收。部署方式調整后,服務再沒有出現長時間停頓,速度比硬件升級前有較大提升。

2.2、集群間同步導致的內存溢出錯誤

例子 √

在服務使用過程中,往往一個頁面會產生數次乃至數十次的請求,因此這個過濾器導致集群各個節點之間網絡交互非常頻繁。當網絡情況不能滿足傳輸要求時,重發數據在內存中不斷堆積,很快就產生了內存溢出。

2.3、堆外內存導致的溢出錯誤

例子 √

沒有考慮到堆外內存占用比較高,32位windows平臺對每個進程內存限制2G,程序中使用到了CometD 1.1.1框架,有大量的NIO操作需要用到Direct Memory內存,Direct Memory分配不足導致的內存溢出。

從實踐經驗的角度出發,除了 Java 堆和永久代之外,我們注意到下面這些區域還會占用較多的內存:

  • Direct Memory: 可通過- XX: MaxDirectMemorySize 調整大小(HotSpot VM無此參數:http://rednaxelafx.iteye.com/blog/1098791,http://www.dongliu.net /post/504141),內存不足時拋出 OutOfMemoryError 或者 OutOfMemoryError: Direct buffer memory。
  • 線程堆棧:可通過- Xss 調整大小,內存不足時拋出 StackOverflowError( 縱向無法分配,即無法分配新的棧幀)或者 OutOfMemoryError: unable to create >>li:new native thread( 橫向無法分配,即無法建立新的線程)。
  • Socket 緩存區:每個 Socket 連接都 Receive 和 Send 兩個緩存區,分別占大約 37KB 和 25KB 內存,連接多的話這塊內存占用也比較可觀。如果無法分配,則可能會拋出 IOException: Too many open files 異常。
  • JNI 代碼:如果代碼中使用 JNI 調用本地庫,那本地庫使用的內存也不在堆中。
  • 虛擬機和 GC: 虛擬機、 GC 的代碼執行也要消耗一定的內存。
2.4、外部命令導致系統緩慢

圖片處理相關 √

大并發的時候,通過mpstat工具發現CPU使用率很高。

通過Solaris 10的Dtrace腳本可以查看當前情況下那些系統調用話費最多的CPU資源。

結果是fork,用來產生新進程的,Java中不應該有新的進程的產生。

最終找到了答案:每個用戶請求的處理都需要執行一個外部 shell 腳本來獲得系統的一些信息。執行這個 shell 腳本是通過 Java 的 Runtime. getRuntime(). exec() 方法來調用的。

Java 虛擬機執行這個命令的過程是:首先克隆一個和當前虛擬機擁有一樣環境變量的進程,再用這個新的進程去執行外部命令,最后再退出這個進程。如果頻繁執行這個操作,系統的消耗會很大,不僅是 CPU, 內存負擔也很重。

2.5、服務器JVM進程崩潰

跨系統集成的時候,使用到異步方式調用Web服務,由于兩邊服務速度不讀等,導致很多Web服務沒有調用完成,在等待的線程和Socket連接越來越多,超過JVM的承受范圍后JVM進程就崩潰了。

可以將異步調用改為生產者/消費者模式的消息隊列實現。

測試工具:SoapUI

2.6、不恰當數據結構導致內存占用過大

垃圾收集器: ParNew + CMS

在內存中存入了100萬個HashMap,就會GC造成停頓。

ParNew 收集器使用的是復制算法,這個算法的高效是建立在大部分對象都“朝生夕滅”的特性上的,如果存活對象過多,把這些對象復制到 Survivor 并維持這些對象引用的正確就成為一個沉重的負擔,因此導致 GC 暫停時間明顯變長。

如果不修改程序,僅從 GC 調優的角度去解決這個問題,可以考慮將 Survivor 空間去掉(加入參數- XX: SurvivorRatio= 65536、- XX: MaxTenuringThreshold= 0 或者- XX:+ AlwaysTenure), 讓新生代中存活的對象在第一次 Minor GC 后立即進入老年代,等到 Major GC 的時候再清理它們。這種措施可以治標,但也有很大副作用,治本的方案需要修改程序,因為這里的問題產生的根本原因是用 HashMap < Long, Long >結構來存儲數據文件空間效率太低。

HashMap<Long,Long>分別具有 8B 的 MarkWord、 8B 的 Klass 指針,在加 8B 存儲數據的 long 值。在這兩個 Long 對象組成 Map. Entry 之后,又多了 16B 的對象頭,然后一個 8B 的 next 字段和 4B 的 int 型的 hash 字段,為了對齊,還必須添加 4B 的空白填充,最后還有HashMap 中對這個 Entry 的 8B 的引用,這樣增加兩個長整型數字,實際耗費的內存為( Long( 24B) × 2)+ Entry( 32B)+ HashMap Ref( 8B)= 88B, 空間效率為 16B/ 88B= 18%, 實在太低了。

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

程序在最小化時它的工作內存被自動交換到磁盤的頁面文件之中了,這樣發生 GC 時就有可能因為恢復頁面文件的操作而導致不正常的 GC 停頓。

在 Java 的 GUI 程序中要避免這種現象,可以加入參數"- Dsun. awt.keepWorkingSetOnMinimize= true" 來解決。

3、實戰:Eclipse運行速度調優
3.1、調優前的程序運行狀態
3.2、升級JDK1.6的性能變化及兼容問題
3.3、編譯時間和類加載時間的優化
3.4、調整內存設置控制垃圾收集頻率
3.5、選擇收集器降低延遲
4、本章小結
 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!