天天看點

Java應用性能測試之堆記憶體

每一個性能工程師都需要知道Java中記憶體是如何工作的嗎?假如你想完全解決性能瓶頸的話,我的答案是“必須的”。Java的性能管理對每一個性能工程師以及Java開發者來說都是一個夢魇,但同時又是寫好Java應用必不可少的一部分。

這是一個申請新的對象和清除不使用對象(垃圾回收)的過程。Java有自動的記憶體管理,在背景有自動運作的垃圾回收機制來回收不使用的對象并釋放記憶體。假如沒有足夠的知識和經驗來了解JVM和垃圾回收是如何工作的,不知道Java的記憶體是如何建立的,我們工程師在執行Java應用程式的時候就很難發現對應的瓶頸是在哪裡。

當分析性能瓶頸的時候,了解Java記憶體子產品的運作是一個技術活。在我查閱了很多部落格,以及結合我自身的工作經驗來看,趟過了很多工作上的坑之後,慢慢了解了JVM各個部分都是如何工作的。當我開始做性能測試的時候,根本不知道什麼是Java的堆,我甚至不關注Java中對象都是如何建立的,更不用說GC是如何把不同類型的不使用的對象釋放的。

在我開始做Java性能測試的時候,我遇到了好幾個記憶體相關的錯誤,比如 java.lang.OutOfMemoryError,也就是在那時,我開始了解Java性能測試中JVM堆和棧所扮演的不同角色。當你想要獲得一些性能相關的工作時,很多公司和客戶都會檢查你對Java開發和Java性能調試上面的專業度,是以了解Java中記憶體是如何申請的是非常重要的,它可以讓你寫出高性能的應用,再也不會出現諸如OutOfMemoryError或者Memory Leaks的錯誤。

每一個性能工程師在調試JVM性能問題時都需要了解Java的性能管理内部是如何工作的。我們所建立一切,比如類,方法,對象,變量其實在JVM中都是記憶體。比如,我們建立一個局部變量,一個全局變量或者不同的類對象,他們都是存儲在JVM堆記憶體中。

JVM中有很大的記憶體,它被分成了兩大部分,一個是堆記憶體另一個是棧記憶體。首先,我們從堆記憶體開始來分析可能的讀寫記憶體問題。堆記憶體在Java中的定位已經目标是什麼,這大概是每一個性能測試工程師中心中的疑問。

對所有的性能問題來說,這是Java程式中非常關鍵的部分。其實有很多不同的模式,方法,資源以及技巧有關堆記憶體,我們可以用之來優化Java程式。

堆記憶體

關于Java中堆記憶體有很多圖表都對之進行了描述。每一個性能工程師都需要了解PermGen和Metaspace之間的差別,我們可以從下面的圖中粗略了解一二:

Java應用性能測試之堆記憶體
Java應用性能測試之堆記憶體
Java應用性能測試之堆記憶體

堆記憶體被分為兩代,一代是YOUNG,一代是OLD。YOUNG這一代的開始部分我們稱之為EDEN空間,緊接着其後的第二部分稱之為SURVIVOR,它有SURVIVOR0以及SURVIVOR1組成。現在我們來了解YOUNG每個部分的目的。無論何時,當我們建立一個新的對象時,他們都首先存放在EDEN空間。JVM中有一個自動的記憶體管理功能稱之為垃圾回收(這裡就不做詳細介紹了)。

假如應用非常重建立了成百上千的對象,EDEN的記憶體就會被這些對象占滿,這時候垃圾回收機制就會運作,把不使用或者沒有引用的對象删除了,這個過程就稱之為Minor GC。這個Minor GC會把所有的還存活的對象移動到SURVIVOR記憶體空間。Minor GC會自動在YOUNG中運作并釋放記憶體空間。Minor GC運作的周期很短并且速度很快。

不同的類可能會建立很多對象,是以當對象的數目增大時,EDEN空間的記憶體也會增加。假設現在有GC1, GC2, GC3一直到GCN在運作(他們是由JVM自動調用垃圾回收操作建立的),他們會不停地檢查不同的對象,并把他們立即轉移到SURVIVOR記憶體中。

YOUNG中存活的對象會被轉移到OLD中,當OLD也滿的時候,Major GC就會啟動。那什麼時候Major GC會被啟動呢?當OLD中記憶體完全滿了的時候,Major GC就會啟動。Major GC很長時候才會發生一次。當我們開發Java應用和Java自動架構的時候,假如程式員建立了很多的對象,那這時候就需要小心YOUNG和OLD這兩個不同的概念了。

程式員不應該建立任何沒有必要的對象,假如這種現象真的存在,那麼垃圾回收應當在任務結束的時候即使銷毀他們。Minor GC主要在YOUNG上運作,而Major GC則運作在OLD上。比如,你使用Amazon或者Walmart,我們知道将會有很多請求或者通路到這些頁面,并且我們看到了逾時的異常伴随着高的通路量。通常來說,在這種線上的商務網站,逾時的異常一般是由于Major GC使用了大量的記憶體去銷毀對象,進而使用相應的CPU以及記憶體的使用非常高。

這也表明了一些特定的類建立了太多了的對象。這種情況下,Major GC将會不停試圖銷毀不使用的對象。Major GC相比于Minor GC使用的時間會更長。

PermGen(Permanent Gneration) — JDK7之前支援

每個性能工程師都會好奇Permgen究竟是什麼?

  1. Permgen包含什麼?
  2. PermGen究竟在做什麼?
  3. 什麼樣的資料被存儲在這裡?
  4. 什麼樣的屬性會被存儲在這裡?

PermGen是一個特殊的對空間,和主記憶體堆是分開的。事實上Permgen不是堆記憶體的一部分,他們是非堆記憶體。所有的靜态變量和常量會存儲在方法的空間,而方法空間是Permgen的一部分。Permgen唯一的缺點是他的存儲空間有限,是以經常會産生OutOfMemoryError。

Perm Gen中的類加載器是不會被垃圾回收的,是以會經常産生記憶體洩漏。32bit的JVM的預設最大記憶體是64MB,64bit是84MB。JVM中的-XX:PermSize 和 -XX:MaxPermSize在JDK8中已經不再支援。

MetaSpace — JDK8開始支援

MetaSpace是JDK8引入的用來取代之前的PermGen記憶體。兩者最大的差別就在于他們對于記憶體申請的處理。尤其特别的是,這塊記憶體空間會自動增加。metaspace的優點就是垃圾回收會在記憶體使用到達最大大小時自動運作。

有了這樣的改進,在MetaSpace中OutOfMemoryError就離我們漸漸遠去了。我們也有類似的參數來調整相關的設定 XX:MetaspaceSize以及XX:MaxMetaspaceSize

棧記憶體 – 簡單介紹一下棧

Java的棧記憶體是用來為程序執行服務的,它包含了特定的方法值。使用的資料結構是LIFO(後進先出)。棧在java是一塊包含方法,局部變量以及引用變量的記憶體。存儲在堆中的變量是可以全局通路的,而在棧中的變量别的程序是不能通路的。當一個方法被調用的時候,會在棧中為這個方法建立一個新的塊。

這個新的塊包含了所有的局部變量,以及這個方法使用的别的對象的引用。當這個方法結束的時候,這個塊就會被擦除,然後它就可以被後面的别的方法所使用。這裡的對象隻會在那個特定的函數生命周期中存在。棧的大小和堆比起來小很多。

當棧的空間不足時,會報Java.lang.StackOverFlowError,我們可以使用-Xss來定義棧的大小。

記憶體相關的錯誤

你需要了解的是這些輸出其實隻是表達JVM所受到的影響,而不是說真正的錯誤。真正的錯誤或者根源可能來自于你代碼的某一個地方,比如記憶體洩漏,GC的問題,同步的問題,資源的申請或者甚至是硬體的設定。解決這些問題的最簡單的方法就是增大受影響的記憶體大小。

我們需要監測資源的使用情況,分析一個類,得到多個堆的dump,分析這些dump,檢查分析、優化我們的代碼。假如這些方法都沒有作用,那麼可能需要配置設定更大的空間。

java.lang.StackOverFlowError — 這個錯誤表明棧記憶體滿了

java.lang.OutOfMemoryError — 這個錯誤表明堆記憶體滿了

java.lang.OutOfMemoryError: GC Overhead limit exceeded — 這個錯誤表明GC超過了限制

java.lang.OutOfMemoryError: Permgen space — 這個錯誤表明Permanent Generation滿了

java.lang.OutOfMemoryError: Metaspace — 這個錯誤表明 Metaspace 滿了

java.lang.OutOfMemoryError: Unable to create new native thread — 這個錯誤表明JVM本地代碼不能在建立程序了,因為已經建立了很多程序,所有可用的資源都被消耗了。

java.lang.OutOfMemoryError: request size bytes for reason —這個錯誤表明交換空間滿了java.lang.OutOfMemoryError: Requested array size exceeds VM limit — 這個錯誤表明數組的大小超了目前平台的限制

怎樣設定初始和最大的堆

初始堆大小 — XMS

比1/64的實體記憶體大,或者别的合理的最小值。在J2SE 5.0之前,一個合理的最小值就是預設的初始堆大小。可以使用指令行中的-Xms來設定。

最大的堆大小 — XMX

比1/4的實體記憶體小或者1GB。在J2SE 5.0之前,預設的最大堆大小是64MB。可以使用指令行的-Mxm來設定。當Jenkins運在vm或者Docker Container中時這個門檻值需要延長。把最小的堆大小和最大的值設定愛成一樣。

推薦的JVM 設定

下面是一些推薦的值:

-server -Xms24G -Xmx24G -XX:PermSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=20 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70

對一些副本伺服器,可以設定成這樣:

-server -Xms4G -Xmx4G -XX:PermSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=20 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70

對獨立的安裝,使用這個值:

-server -Xms32G -Xmx32G -XX:PermSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=20 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70

上述JVM設定的詳解:

-Xms, -Xmx: 用來限制堆的大小。堆大小在副本伺服器上比較小是因為即使全GC也不會導緻SIP的轉播。

-XX:+UseG1GC: 使用G1回收

-XX:MaxGCPauseMillis: 設定最大的GC暫停時間,這個隻是一個軟體的設定,JVM會盡力達到這一點。

-XX:ParallelGCThreads: 設定并行垃圾回收的程序數目。預設值在不同的系統上不同。

-XX:ConcGCThreads: 垃圾回收使用的并發程序資料。預設值在不同的系統上不同。

-XX:InitiatingHeapOccupancyPercent: 開始并發GC的堆使用百分比。預設值是45。

總結

有超過600參數可以傳遞給JVM去調整相應的垃圾回收和記憶體。假如在加上别的方面,這個資料很容易就到達1000+。我們隻是簡單介紹了一下性能調試中經常會遇到的參數。感覺閱讀這篇文章。

原文位址:

http://donggeitnote.com/2020/07/01/java%e5%ba%94%e7%94%a8%e6%80%a7%e8%83%bd%e6%b5%8b%e8%af%95%e4%b9%8b%e5%a0%86%e5%86%85%e5%ad%98/

更多原創,敬請關注微信公衆号,每日更新業界最新資訊:

Java應用性能測試之堆記憶體

歡迎通路個人小站: www.donggeitnote.com