本文詳細描述了 Java 堆內存模型,垃圾回收算法以及處理內存泄露的最佳方案,并輔之以圖表,希望能對理解 Java 內存結構有所幫助。原文作者 Sumith Puri,本文系 OneAPM 工程師編譯整理。
下圖展示了 Java 堆內存模型,以及運行在 Java 虛擬機中任意 Java 應用的 PermGen (內存永久保存區域),下面的比率展示了 JVM 各代類型允許的內存大小分配情況,所有的數據均適用于 Java 1.7 及以下版本。該圖也被稱為 Java 內存模型的“管理區(Managed Area)”。
Java 內存結構(Java 內存模型)
除此之外,還有一塊堆棧區(Stack Area),可通過 -Xss 選項進行配置。該區域存儲了所有線程的堆引用、本地引用、程序計數器寄存器、代碼緩存以及本地變量。該區域也稱為內存模型的本地區(Native Area)。
Java 內存模型(結構)的管理區
[Young Generation/Nursery] 伊甸園區(Eden Space)
所有新對象都首先在 Eden Space 創建。一旦該區達到由 JVM 設定的任意閾值,新生代垃圾回收機制(Minor GC)就會啟動。它會首先清除所有的非引用對象,并將引用對象從 'eden' 與 'from' 區移至 'to' 幸存者區。垃圾回收一結束,'from' 與 'to' 的角色(名字)就會對換。
[Young Generation/Nursery] 幸存者 1 區 (From)
這是幸存者區的一部分。或者視為幸存者區中的一個角色。這兒就是之前垃圾回收中的 'to' 角色。
[Young Generation/Nursery] 幸存者 2 區 (To)
這也是幸存者區的一部分。也可以視為幸存者區中的一個角色。垃圾回收過程中的所有引用對象都會從 'from' 與 'eden' 區移至此處。
[Old Generation] 年老代區(Tenured)
根據閾值限定的不同,對象們會從 'to' 幸存者區移至年老代區。你可以使用 -XX:+PrintTenuringDistribution 檢查閾值,該指令會按照年齡顯示對象(占用的字節空間)。年齡是指對象在幸存者區內移動的次數。
其他重要的標記還有 -XX:InitialTenuringThreshold、-XX:MaxTenuringThreshold 與 -XX:TargetSurvivorRatio ,這些標記能幫你實現最佳的年老代區與幸存者區使用方案。
通過設置 -XX:InitialTenuringThreshold 與 -XX:MaxTenuringThreshold,可以指定年齡的最初值與最大值,而幸存者區 (To) 的使用率則由 -XX:+NeverTenure 與 -XX:+AlwaysTenure 決定。前者是指永遠不將對象存儲到年老代區,而后者恰恰相反,總是將對象存儲到年老代區。
此處進行的垃圾回收是年老代垃圾回收(Major GC)。當堆空間已滿或者年老代區占滿時,就會觸發 Major GC。此時,通常會由一個“停止一切(Stop-the-World)”事件或線程執行垃圾回收。此外,還有另一種稱為全垃圾回收(Full GC)的垃圾回收機制,會涉及諸如永久內存區域。
與整體堆內存相關的另兩個重要且有趣的標記是 -XX:SurvivorRatio 與 -XX:NewRatio,前者指定伊甸園區相對幸存者區的比率,后者指定年老代區相對新生代區的比率。
[Permanent Generation] 永久代區(Permgen space)
永久代區(Permgen)用于存儲以下信息:常量池 (內存池),字段與方法數據及代碼。
垃圾回收算法
串行 GC(Serial GC) (-XX:UseSerialGC): 針對年輕代與年老代的垃圾回收
該算法使用簡單的“標記-清掃-壓縮(mark-sweep-compact)”循環清理年輕代與年老代,適合內存占用較低、CPU 使用量較少的客戶端系統。
并行 GC(Parallel GC) (-XX:UseParallelGC): 針對年輕代與年老代的垃圾回收
該算法使用 N 個線程(N 的值可以通過 ** -XX:ParallelGCThreads=N** 設定,N 同時代表垃圾回收占用的 CPU 內核數)。其中,年輕代垃圾回收會使用 N 個線程,而年老代只用一個線程。
并行 Old GC (-XX:UseParallelOldGC): 針對年輕代與年老代的垃圾回收
該算法對年輕代與年老代均使用 N 個線程,其他方面與并行 GC 完全一致。
并發 Mark and Sweep GC (-XX:ConcMarkSweepGC): 針對年老代的垃圾回收
顧名思義,CMS GC 會最小化垃圾回收所需的停頓時間。該算法最適于創建高響應度的應用,且只作用于年老代。它會創建多條垃圾回收的線程,與應用線程同時工作。垃圾回收的線程數量可以使用 -XX:ParallelCMSThreads=n 標記指定。
G1 GC (-XX:UseG1GC): 針對年輕代與年老代的垃圾回收 (將堆內存等分為大小相同的區塊)
這是一種并行、并發、不斷壓縮的低停頓垃圾回收器。G1 是在 Java 7 中引入以取代 CMS GC 的,它會先將堆內存分為多個大小相等的區塊,繼而執行垃圾回收。通常,從活動數據最少的區塊開始,因此以垃圾為先。
最常見的內存溢出問題
所有 Java 程序員都應該知道的最常見的內存溢出問題:
Exception in thread "main": java.lang.OutOfMemoryError: Java heap space( Java 堆內存)。這并不一定意味著內存泄露,也可能是分配的堆內存空間太小。此外,在運行時間較長的應用中,也可能是因為一個無意識的引用被指向堆對象(內存泄露)。即便是應用本身調用的 APIs,也可能保存著指向無依據對象的引用。而且,在大量使用終結器的應用中,對象們有時可能正排在終結隊列中。當這樣的應用創建高優先級的線程時,會導致越來越多的對象排在終結隊列中,最終導致內存溢出。
Exception in thread "main": java.lang.OutOfMemoryError: PermGen space(永久存儲空間)。如果加載了很多類與方法,或者創建了很多字符串常量,特別是使用 intern() 方法進行創建(從 JDK 7 開始,interned 字符串就不再存儲在 PermGen 中),這類錯誤就會出現。當出現這類錯誤時,打印的堆棧跟蹤附近可能會出現如下文本:ClassLoader.defineClass。
Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit (請求的數組大小超出 VM 限制)。當請求的數組大小超過可用的堆空間時,這類報錯就會出現。這類錯誤通常歸咎于編程錯誤,在運行時請求了極大的數組大小。
Exception in thread "main": java.lang.OutOfMemoryError:
request <s> bytes for <r>
,交換空間溢出?這是內存泄露最常見的根源。通常,當操作系統沒有足夠的交換空間,或另一個進程占用了系統中的所有可用內存,就會導致內存泄露。簡而言之,由于空間用盡,堆內存無法提供所請求的空間大小。該信息中的 's' 代表失敗的請求所需的內存大小(以字節為單位),而 'r' 代表內存請求的原因。在大多數情況下,此處的 'r' 是報告分配失敗的源模塊,有時也會是具體的原因。Exception in thread "main": java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)(<原因><堆棧跟蹤><本地方法>)。該報錯意味著一個本地方法遇到內存分配失敗。問題的根源在于 Java Native Interface(Java 本地接口) 中存在的錯誤,而非 JVM 中運行的代碼錯誤。若是本地代碼不檢查內存分配錯誤,應由會直接崩潰,而不會出現內存溢出。
內存泄露的定義
你可以將內存泄露看做一種疾病,而內存溢出錯誤為一種征兆。但不是所有的內存溢出錯誤都意味著內存泄露,不是所有的內存泄露都以內存溢出為征兆。
維基百科的定義:在計算機科學中,內存泄露是一種以如下方式發生的資源泄露——計算機程序錯誤地分配內存,導致不再需要的內存得不到釋放。在面向對象的編程語言中,一個對象若存儲在內存中,卻無法由運行的代碼獲取,即為內存泄露。
Java 中常用的內存泄露定義
當不再需要的對象引用仍舊多余地予以保存,即為內存泄露。
在 Java 中,內存泄露是指對象已不再使用,但垃圾回收未能將他們視做不使用對象予以回收。
當程序不再使用某個對象,但在一些無法觸及的位置該對象仍舊被引用,即為內存泄露。也因此,垃圾回收器無法刪除它。該對象占用的內存空間無法釋放,程序所需的總內存就會增加。久而久之,應用的性能就會下降,JVM 可能會耗盡所有內存。
在某種程度上,當無法再給年老區分配內存時,內存泄露就會發生。
內存泄露最常見的一些情況:
- ThreadLocal 變量
- 循環與復雜的雙向引用
- JNI 內存泄露
- 可變的靜態域(最為常見)
我建議結合使用 Visual VM 與 JDK,對內存泄露問題進行調試。
常見的內存泄露調試方法
- NetBeans 分析器
- 使用 jhat Utility
- 創建 Heap Dump
- 獲取當下運行進程的堆內存柱狀圖
- 獲取內存溢出錯誤的堆內存柱狀圖
- 監控在等待終結的對象數量
- 第三方內存調試器
調試內存泄露問題的常用策略或步驟:
- 確認征兆
- 啟用詳細的垃圾回收機制(verbose GC)
- 啟用性能分析
- 分析堆棧跟蹤
原文地址:https://dzone.com/articles/java-memory-architecture-model-garbage-collection
OneAPM for Java 能夠深入到所有 Java 應用內部完成應用性能管理和監控,包括代碼級別性能問題的可見性、性能瓶頸的快速識別與追溯、真實用戶體驗監控、服務器監控和端到端的應用性能管理。想閱讀更多技術文章,請訪問 OneAPM 官方博客。