JVM記憶體管理
原文:Java Memory Management for Java Virtual Machine (JVM) by Justin Gesso. June 2, 2017
翻譯:陳同學 歡迎通路譯者部落格原文,閱讀體驗更佳
Java記憶體管理是一項持續的挑戰,同時也是鍛造出可拓展應用的必備技能。本質上,Java記憶體管理就是一個為新對象配置設定記憶體和釋放無用對象記憶體的過程。
準備好深入Java記憶體管理之旅吧!
在本文中,我們将讨論JVM、了解記憶體管理、學習記憶體監控工具、監控記憶體使用和垃圾回收活動。
接下來你會發現,有許多模型、方法、工具和建議可用于記憶體優化。
JVM
JVM是一種可以讓計算機運作Java程式的抽象計算機器。JVM有三個概念:
- 規範(specification):為JVM的運作提供規範,由Sun公司或其他公司提供實作
- 實作(implementation):也叫JRE,即Java運作時環境
- 執行個體(instance):在編寫Java代碼之後,運作一個Java類,将會建立一個JVM執行個體
JVM加載代碼,驗證代碼,執行代碼,管理記憶體(包含從作業系統配置設定記憶體,管理Java記憶體配置設定即堆記憶體壓縮和垃圾對象清除) 和提供運作時環境。
JVM記憶體結構
JVM記憶體被劃分為多個部分:堆記憶體(Heap)、非堆記憶體(Non-Heap)和其他記憶體。
Fig 1.1 source https://www.yourkit.com/docs/kb/sizes.jsp
- 堆記憶體(Heap Memory)
堆記憶體是為所有Java類執行個體和數組配置設定記憶體的運作時資料區。堆記憶體在JVM啟動時被建立,随着應用程式的運作,堆記憶體可能會變大或減小。可以使用JVM參數 -Xms 來指定堆的size,堆size是固定的,也可以是可變的,這取決于垃圾回收政策。最大堆size可以使用 -Xmx 來配置,堆的預設大小是62M。
- 非堆記憶體(Non-Heap Memory)
JVM堆以外的記憶體稱之為非堆記憶體,非堆記憶體在JVM啟動時建立,它存儲了每個類的結構,例如:運作時常量池,字段和方法的資料,方法和構造函數的代碼,以及被加入到字元串常量池的字元串。非堆記憶體預設最大size是64M,可以通過 –XX:MaxPermSize 重新設定。
- 其他記憶體(Other Memory)
JVM使用這塊記憶體來存儲JVM自身的代碼和内部結構,以及已加載的Profiler agent的代碼和資料等。
JVM堆記憶體結構
Fig 1.2 source http://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java
JVM堆在實體上被劃分為2個部分(或稱為2代):nursery(或稱為年輕空間/年輕代) 或 old space(或稱為老年代)。
譯者注:nursery和old space不做翻譯。幾個單詞直譯說明:nursery:幼稚園, Minor:年輕的, Major:成年的/老年的。
nursery是堆記憶體中配置設定新對象的地方。在nursery滿了之後,會運作一個特殊的 yong collection 來進行垃圾收集,當nursery中對象生存的時間足夠長時,這些對象會被移動到 old space,以騰出nursery的空間來為新對象配置設定記憶體,這種垃圾收集叫做 Minor GC。nursery分成3部分:Eden(伊甸園) 和 2個 Survivor Space(幸存者區).
關于nursery空間的幾個重點:
- 大多數新建立的對象都位于 Eden區
- Eden區 滿了之後會觸發 Minor GC,Eden中所有存活的對象會被移動到 Survivor區 中目前未使用的一個區.
- Minor GC 也會檢查另一個正在使用 Survivor區 中存活的對象并把它們移動到未使用的 Survivor區. 是以任何時刻,2個 Survivor區其中有一個一定是空的
- 在多次GC之後依然存活的對象将被移動到 老年代空間,這通常是在nursery中的對象有資格進入老年代空間之前,為這些對象設定一個年齡閥值實作。
在老年代滿後,會進行垃圾收集,這個過程稱為 old collectio。老年代空間包含那些存活已經且在多次 Minor GC之後依然幸存的對象,通常隻有在老年代滿後才會觸發老年代垃圾收集,老年代垃圾收集也叫 Major GC,Major GC通常會消耗很長的時間。
Nursery中大多是臨時對象而且生存時間很短,yong collection 被設計成能夠快速找到依然存活的對象并将他們移出nursery. 通常情況下,和 old collection 以及 單代堆(single-generational heap, 一個沒有nursery的堆) 相比,一次 yong collection 會快速釋放大量記憶體。
最近的釋出版本中,nursery中有一個叫做 keep area(保留區) 的地方,keep area中包含大多數新建立的對象,而且在垃圾回收時不會處理 keep area中的資料,直到下一次年輕代垃圾收集才會被處理。這種方式可以阻止在垃圾收集啟動不久前建立的對象被移動。
Java記憶體模型
永久代(Permanent Generation)
該區在Java8中已被Metaspace替換
永久代(Perm Gen)包含了JVM用于描述應用中的類和方法的中繼資料,Perm Gen由JVM在運作時根據應用程式所需要類進行建構,同時它也包含了Java SE Library中的類和方法。Perm Gen中的對象隻在Full GC時進行垃圾收集。
元空間(Metaspace)
在Java8中已經沒有Perm Gen,也就是說不會再有
java.lang.OutOfMemoryError:Perm Gen
問題。不像Perm Gen是屬于堆記憶體中的一部分,Metaspace不屬于堆記憶體,類的中繼資料現在大多配置設定在機器的記憶體之外。和Perm Gen會使用固定的最大記憶體大小相比,Metaspace預設會自增長(大小取決于作業系統),可以使用 -XX:MetaspaceSize and -XX:MaxMetaspaceSize 來進行配置。采用Metaspace之後,類和類的中繼資料的生存時間和和它們的ClassLoaders保持一緻,隻要它們的ClassLoaders存活,這些中繼資料也會一直存活而且不能被釋放。
代碼緩存(Code Cache)
當Java程式運作時,它會以分層的方式執行代碼。在第一層,它使用用戶端編譯器(C1 編譯器)編譯代碼,相關統計分析資料将用于第二層的服務端編譯(C2編譯器)以便優化代碼。分層編譯在Java7中預設未啟用,在Java8中已啟用。
即時編譯器(JIT)将編譯後的代碼存儲到一個叫 Code Cache 的區域中,這是一個儲存編譯後代碼的特殊堆。如果這個區域的大小超出閥值,将會重新整理這個區域,而且這個區域的對象不能被GC重新定位。
Java8解決了一些性能問題和編譯器未重新啟用的問題,Java7中為了避免這些問題的解決方案是将 Code Cache 區域的大小增加到永遠不可能達到的程度。
方法區(Method Area)
方法區是Perm Gen的一部分記憶體,用來儲存類的結構(運作時常量和靜态變量)和方法以及構造函數的代碼。
記憶體池(Memory Pool)
記憶體池由JVM管理器建立,它用來建立不可變對象池(immutable object)。記憶體池可以屬于堆或者Perm Gen,到底屬于哪部分取決于JVM記憶體管理器的實作。
運作時常量池(Runtime Constant Pool)
運作時常量池是方法區的一部分,包含類的運作時常量和靜态方法。
棧記憶體(Java Stack Memory)
Java棧記憶體用于線程執行,包含方法執行所需要的一些生存時間很短的特殊資料和一些指向堆記憶體對象的引用。
堆記憶體配置
配置項 | 說明 |
---|---|
**–**Xms | 設定JVM啟動時的初始堆記憶體 |
-Xmx | 設定最大堆記憶體 |
-Xmn | 設定年輕代的大小,剩下的就是老年代的空間 |
-XX:PermGen | 設定永久代的初始記憶體大小 |
-XX:MaxPermGen | 設定永久代最大記憶體 |
-XX:SurvivorRatio | 設定Eden空間的比率,例如:如果年輕代空間是10M,-XX:SurvivoRatio=2,那麼5M将用于Eden區,剩餘的5M平均分給2個Survivor區(每個2.5M)。預設值為8 |
-XX:NewRatio | 設定老年代/新生代比率,預設值為2 |
垃圾收集
垃圾收集是一個釋放堆空間以配置設定新對象的過程,Java中最大的功能之一就是自動垃圾收集。垃圾收集器是一個在背景運作的程式,它會檢視記憶體中的所有對象,找出那些不被其他程式引用的對象。所有這些未被引用的對象将被清除,空間被回收以配置設定給其他對象。一種垃圾回收的基本方式包含三個步驟:
- Marking(标記):這是垃圾收集器識别哪些對象正被使用哪些對象不再被使用的第一步
- Normal Deletion(正常清除):垃圾收集器清除不再被使用的對象,回收記憶體以配置設定給其他對象
- Deletion with compacting(壓縮清除):為了更好的性能,在清除不再使用的對象後,所有存活的對象将被移動到一起,這會提高新對象記憶體配置設定的性能。
标記/清除垃圾收集模式
JVM使用 标記/清除垃圾收集模式 來進行整個堆區的垃圾收集工作,這種模式包含兩個階段:标記階段和清除階段。
在标記階段,所有Java線程、本地處理程式以及其他ROOT資源可達的對象都會被标記為 存活,同時也包含這些存活對象可達的對象。這個過程識别和标記了所有正在使用的對象,剩下的所有對象将被視為垃圾。
在清除階段,将周遊堆以找出存活對象之間的記憶體碎片并記錄到一個list中,這些空間将用于新對象的記憶體配置設定。
Java垃圾收集類型
在應用程式中有5種垃圾收集方式可以使用,可以通過配置來調整JVM的垃圾收集政策。
- Serial GC (-XX:+UseSerialGC):Serial GC 使用簡單的 标記-清除-壓縮 方式來進行年輕代和老年代的垃圾收集,Minor GC和Major GC就是這種方式
- Parallel GC (-XX:+UseParallelGC):Parallel GC 和 Serial GC 相同,隻不過它使用N個線程來進行 年輕代垃圾收集,N是系統CPU的核心數。可以使用 –XX:ParallelGCThreads=n 來控制線程數量
- Parallel Old GC (-XX:+UseParallelOldGC):和Parallel GC一樣,不過它 年輕代 和 老年代 都使用多個線程來進行垃圾收集
- Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC):CMS也稱為 并發的低停頓(concurrent low-pause)收集器,它進行老年代垃圾回收。CMS收集器嘗試使用線程并行收集大多數垃圾,以最大限度的減少因垃圾回收造成的應用暫停。CMS年輕代垃圾回收器使用了和并行收集器一樣的算法,這種收集器适合于那些不能承擔長時間停頓的響應式應用。可以使用 -XX:ParallelCMSThreads=n 來設定CMS收集器的線程數量。
譯者注:暫停/停頓指執行垃圾回收時會stop-the-world,即暫停其他線程的工作,隻做垃圾收集
- G1 Garbage Collector (-XX:+UseG1GC):Java7中已經有G1(The Garbage First)收集器,它的最終目标是替換CMS收集器。G1是一個 parallel, concurrent and incrementally compact low-pause 的垃圾收集器,G1和其他收集器不一樣,它沒有年輕代和老年代的概念,而是将堆劃分成多個大小相等的區域,當G1執行時,它首先收集那些存活資料(live data)較少的區域,是以叫 垃圾優先。
記憶體使用和GC活動監控
記憶體不足常常是導緻Java應用不穩定和無響應的原因,是以,為了確定穩定性和性能,我們需要監控垃圾收集對響應時間和記憶體使用的影響。然後,監控記憶體使用和垃圾收集還不夠,因為這兩個因素并不能告訴我們響應時間是否受到它們的影響。隻有垃圾GC造成的程式 暫停 會直接影響響應時間,而且GC可以和應用程式同時運作。是以,需要将垃圾回收造成的暫停和應用響應時間關聯起來,基于此,我們需要監控以下内容:
- 各個記憶體池(Eden、Survivor和老年代)的使用情況,因為記憶體不足是造成GC活動的首要原因
- 如果進行垃圾回收,整體記憶體的使用率也在不斷攀升,說明發生了記憶體洩漏,這不可避免的會導緻記憶體溢出。在這種情況下,進行一次堆記憶體分析十分必要
- 年輕代垃圾收集的次數反映了對象配置設定率資訊,次數越多,配置設定的對象就越多。年輕代垃圾頻繁回收可能是影響響應時間的原因,同時也是導緻老年代不斷增長的原因。
- 如果在GC之後老年代的使用率波動很大,但是其記憶體大小卻沒有上升,說明很多不必要的對象從年輕代拷貝到了老年代,這可能有三個原因:年輕代太小、流失率高或太多事務使用了記憶體。
- 高頻GC活動對CPU的使用會造成負面影響,然而,隻有 暫停(stop-the-world-event) 對響應時間有直接影響。與主流觀點相反,暫停不一定隻作用于Major GC,是以,監控暫停和應用的響應時間非常重要。
jstat
jstat 是Java HotSpot VM的内置工具,用于擷取運作中應用的性能和資源消耗資訊。該工具可用于診斷性能問題,特别是與堆大小和垃圾收集相關的問題。jstat 不需要設定任何JVM啟動參數,Java HotSpot VM中的内置指令已預設啟用。任何可下載下傳JDK版本中都包含了這個工具,jstat 使用虛拟機辨別符(VMID)來識别目标程序。
使用 帶有 gc選項的jstat 指令來檢視JVM堆記憶體使用情況
<JAVA_HOME>/bin/jstat –gc <JAVA_PID>
譯者注:下述表格非常直白,不做任何翻譯
項 | 說明 |
---|---|
S0C | Current survivor space 0 capacity (KB) |
S1C | Current survivor space 1 capacity (KB) |
S0U | Survivor space 0 utilization (KB) |
S1U | Survivor space 1 utilization (KB) |
EC | Current eden space capacity (KB) |
EU | Eden space utilization (KB) |
OC | Current old space capacity (KB) |
OU | Old space utilization (KB) |
MC | Metasapce capacity (KB) |
MU | Metaspace utilization (KB) |
CCSC | Compressed class space capacity (KB) |
CCSU | Compressed class space used (KB) |
YGC | Number of young generation garbage collection events |
YGCT | Young generation garbage collection time |
FGC | Number of full GC events |
FGCT | Full garbage collection time |
GCT | Total garbage collection time |
jmap
jmap 工具用于列印運作中的VM和核心檔案的記憶體相關的統計資料。JDK8 引入了 Java Mission Control、Java Flight Recorder 和 jcmd 工具 用于診斷JVM和Java應用程式的問題。推薦使用最新的 jcmd 工具代替 jmap 工具以增強診斷能力、減少性能開銷。
可以使用 -heap 選型來擷取下列Java 堆資訊:
- GC算法的特殊資訊,包含了GC算法的名字(如:Parallel GC)和特定算法的詳細資料(如: Parallel GC的線程數)
- 檢視 通過指令行配置的JVM參數或JVM根據機器配置自動選擇的配置資訊
- 堆記憶體使用概要:對于堆記憶體的每一個區,這個工具可以列印出堆總容量、正在使用的記憶體、可用的空閑記憶體。如果一個記憶體區正在作為垃圾收集區(如新生代),指令的輸出結果中将會包含一個特定記憶體大小的概要資訊。
<JAVA_HOME>/bin/jmap –heap <JAVA_PID>
jcmd
jcmd 指令用于向JVM發送診斷請求,這些請求對于控制Java Flight Recording、故障排除、JVM和應用診斷非常有用。該指令必須在JVM運作的機器上使用,而且具有和啟動JVM的使用者/使用者組一樣的權限。
使用以下指令建立 heap dump.
jcmd <JAVA_PID> GC.heap_dump filename=<FILE>
上述指令的效果和以下指令一樣:
jmap –dump:file=<FILE> <JAVA_PID>
譯者注:原文還有jhat/hprof/javac等指令以及VirtualVM的使用,可自行檢視。本文量太大,譯者要累崩了。對于VirtualVM的使用,可以檢視譯者以前的一篇文章 VisualVM遠端監控Tomcat中應用
Java垃圾收集優化
當我們看到因長時間GC導緻應用逾時而引起的性能下降時,Java垃圾收集優化可能是我們提高應用吞吐量的最後手段。
如果出現
java.lang.OutOfMemoryError:PermGen Space
錯誤,可以使用 –XX:PermGen 和 –XX:MaxPermGen 參數增加 Perm Gen記憶體的大小,同時也需要監控記憶體。不過在Java8中将我們将看不到這個錯誤。 如果我們發現頻繁的 Full GC 活動,可以嘗試增加老年代的記憶體大小。總的來說,垃圾收集優化需要付出很大的精力,而且沒有捷徑可走,我們隻能不斷嘗試不同的JVM配置并進行比較,最終找到适合我們應用的最佳配置。
這兒有一些性能方案:
- 應用采樣和分析
- 伺服器和JVM調優
- 使用合适的硬體和作業系統
- 根據應用行為和采樣結果改進代碼(說起來容易說起來難!)
- 正确的使用JVM(使用最佳的JVM參數配置)
- 在多核機器中使用 -XX:+UseParallelGC
了解下這些有用的點:
- 不要限制JVM的記憶體,除非我們遇到 暫停 問題
- 将 -Xms 和 -Xmx 配置成一樣的值
- 當處理器的數量增加時,一定要加大記憶體,因為可以并行配置設定記憶體
- 不要忘記優化Perm Gen
- 盡可能少的使用同步
- 如果有用的話,盡可能使用多線程,但是也要注意線程的性能開銷,同時要確定在不同環境下多線程能夠正常運作
- 避免過早的建立對象,最好在真正要使用它時才建立。這是我們常常忽略的一個基本概念
- JSP往往比Servlet更慢
- 使用StringBuilder代替字元串拼接
- 使用基礎資料類型而不是包裝的對象(例如使用long而不是Long)
- 盡可能的重用對象,同時避免建立不必要的對象
- 在測試empty字元串時,equals是非常昂貴的,使用length屬性代替
- “==”比equals更快
- n += 5比 n = n +5 更快,第一種情況産生的位元組更少
- 周期性的flush & clear Hibernate Session
- 批量更新和删除