Java EE應用中的性能問題解決方案 — 第一部分 內存溢出和JVM內幕(A)

聲明:本文禁止未經本人同意的任何形式轉載!如有轉載需求,可與本人通過個人資料中的電子郵箱聯繫。對於未經同意的轉載,本人將保留進一步行動的權利!
Java EE的應用,無論部署至哪種應用服務器上,都可能遇到一些性能的問題。在此,我們將介紹一些通用的性能問題及解決方案。在性能調整和優化中,首先需要了解客戶的問題。通過架構、現象等方面尋找可能影響Java EE性能的可能方面。所以本文中列舉的是一些通用的問題,在實際的性能調整和優化中需要結合具體的情況來進行分析判斷並做出相應調整。
  
內存溢出錯誤
最經常遇到的問題可能就是OutOfMemoryError了。這個錯誤經常纏繞着一些企業應用,讓系統管理員覺得很可怕。通常,這個錯誤是以下一些現象的併發症:
  • 應用服務器宕機
  • 性能大幅降低
  • 表象上出現無休止重複的垃圾收集,致使正常處理中斷,並還可能造成應用服務器宕機
無論是什麼現象,可能都需要重啓應用服務器可能才能恢復正常。
 
導致內存溢出的原因
在解決內存溢出的錯誤前,首先要理解這是如何產生的。如果JVM在其進程內存空間沒有內存可用(包括堆的各個區域、永久內存空間),而一個進程試圖創建一個新的對象實例,垃圾收集線程就開始嘗試釋放足夠的內存空間來放置新的對象。當垃圾收集線程無法釋放足夠的內存時,就拋出OutOfMemoryError。
 
內存溢出錯誤最有可能是由於Java的內存泄露造成的。有很多關於Java內存泄露的討論,最後的定論就是Java的內存泄露來源於對未使用對象引用的延遲釋放:也就是說已經完成了對該對象的使用,但是因爲還存在着一個或者多個對象引用着該對象,所以垃圾收集線程就不能收回該片內存。由該對象佔有着的內存即導致了可用堆空間的流失。這樣的內存泄露通常發生在Web請求階段,例如10000個用戶併發訪問,而每個請求中有一個或兩個存在內存泄露的對象就有可能導致服務器崩潰。更爲甚者的是,一般來說,發生內存泄露的對象都不是簡單的對象(例如Integer、Double),而是在堆中要佔據很多“子圖”的對象。例如,有可能在不經意間引用了Person的對象,而Person的對象又引用了Profile的對象,而Profile的對象又引用了好幾個PerformanceReview的對象。比起Person對象佔據的100字節內存來說,在這個情況下完整的“子圖”對象可能佔據500K或者更多的內存。
 
爲了從根本上解決問題,需要判斷是真實存在內存泄露還是別的內容引起的OutOfMemoryError。有如下幾個判斷方法:
  • 分析底層的內存統計信息
  • 檢查堆增長的方式
雖然Sun和IBM的JVM不一樣,JVM的調優過程卻有很多相同之處。
 
SUN JVM的內存管理
Sun的JVM是有“代溝”的(開玩笑,應該說是按代分片的),也就是說在一個空間(分片)中創建的對象在真正進入長期空間前都有好幾次銷燬的機會。Sun的JVM被明確地分成以下幾個空間(分片):
  • 新生代,包括Eden和兩個生存空間(From空間和To空間)
  • 老生代
  • 永生代
 
如下圖:
 
 
對象在Eden空間中創建,當Eden滿後,垃圾收集線程遍歷Eden中的所有對象,將活動對象拷貝至第一個生存空間,並將廢對象的空間回收釋放。當Eden再次充滿時,垃圾收集線程就再來一遍,但將活動的對象拷貝至第二個生存空間,並將原來在第一個生存空間中的對象拷貝到第二個生存空間。如果第二個生存空間衝滿後,一些留在Eden和第一個生存空間的對象就會被拷貝到老生代。當垃圾收集通過這種輕量級的方法(也稱爲“拷貝收集法”)不能獲得足夠內存的話,垃圾收集就會執行一次大規模收集,也叫“停滯收集法(stop-the-world collection)”。在這種收集法下,垃圾收集將所有線程掛起並在全堆範圍內執行標記清掃收集,騰空整個新生代並準備重新開始進程。
 
拷貝收集法的運行方式圖:
 
 
 
停滯收集法的運行圖:
 
總的來說,對象在新生代創建,大部分都在此進行輪迴,並不需要執行標記清掃收集。有一些對象上升至了老生代,因爲它們活動的時間太長了,如果老生代也滿了,就必須來一次標記清掃收集了。
可以看出,在Sun的實現中,老生代中的對象只能通過大規模的收集活動來收集,所以代價很高。所以需要確保對象生存的時間較短,這樣在其還未進入老生代的時候就將其收集。
聲明:本文禁止未經本人同意的任何形式轉載!如有轉載需求,可與本人通過個人資料中的電子郵箱聯繫。對於未經同意的轉載,本人將保留進一步行動的權利!
發佈了13 篇原創文章 · 獲贊 0 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章