弄清JVM(Java Virtual Machine)的內(nèi)存管理模型對(duì)了解java GC工作原理是很有必要的。最近正好看到一篇文檔寫的不錯(cuò),介紹了Java內(nèi)存管理的處理方式,包括JVM內(nèi)存分配各個(gè)區(qū)域的含義,以及如何監(jiān)測(cè)協(xié)調(diào)GC工作。翻譯后在此記錄。
原文傳送門
內(nèi)存分配區(qū)域
如上圖所示,JVM內(nèi)存被分成了幾個(gè)區(qū)域,粗略的看JVM的堆內(nèi)存分成了兩塊區(qū)域----新生域,年老域。
新生域內(nèi)存管理
剛創(chuàng)建的新對(duì)象會(huì)被分配到新生域。當(dāng)新生域使用殆盡,GC就開始工作了。這種狀況下促發(fā)的GC被稱作次級(jí)GC(Minor GC)。而新生域又被分成三個(gè)部分
- Eden Memory.就像是上帝(JVM)劃分出來的伊甸園區(qū)域,可以想象剛生成的對(duì)象在這里快快樂樂的生活著。
- 兩個(gè)Survivor Memory(幸存域),也就是上圖中的S0和S1區(qū)域。
新生域的主要特性:
- 大多數(shù)新創(chuàng)建的對(duì)象都被分配到了Eden Memory
- 當(dāng)Eden Memory被新分配的對(duì)象填充滿時(shí),次級(jí)GC開始工作,幸存下來的對(duì)象被移到其中一個(gè)Survivor Memory(幸存域)。
- 次級(jí)GC會(huì)同時(shí)檢查Survivor Memory(幸存域),將其中一個(gè)Survivor Memory(幸存域)中的對(duì)象移到另一個(gè)Survivor Memory(幸存域),保證一個(gè)Survivor Memory(幸存域)為空。
- 當(dāng)一個(gè)對(duì)象經(jīng)過次級(jí)GC多次掃描后,依然幸存,那么它就會(huì)被移到年老域(Old generation).JVM會(huì)設(shè)置一個(gè)閥值,當(dāng)一個(gè)對(duì)象被次級(jí)GC掃描過的次數(shù)達(dá)到這個(gè)閥值,而這個(gè)對(duì)象依然幸存,則該對(duì)象就被移到年老域(Old generation)。
年老域內(nèi)存管理
年老域的內(nèi)存存放的是一些長(zhǎng)生命周期和被次級(jí)GC多次掃描依然幸存下來的對(duì)象。GC在年老域內(nèi)存吃緊的情況下開始工作,這種GC稱為主級(jí)GC(Major GC),通常主級(jí)GC消耗的時(shí)間更長(zhǎng)。
世界暫停事件(Stop the World Event)
次級(jí)GC和主級(jí)GC開始工作時(shí),應(yīng)用線程就會(huì)停下來,因此整個(gè)Java程序也就會(huì)處于停止?fàn)顟B(tài),這種情況就是"Stop the World Event"。
新生域存放的是短生命周期的對(duì)象,因而次級(jí)GC的這個(gè)過程會(huì)很快結(jié)束,在程序看來幾乎不受影響。
主級(jí)GC需要掃描所有的幸存對(duì)象,因而它花費(fèi)的時(shí)間會(huì)較長(zhǎng)。一旦主級(jí)GC工作,應(yīng)用程序就會(huì)暫停下來,直觀感受就是程序不夠流暢,無法快速響應(yīng)業(yè)務(wù)事件處理,主級(jí)GC運(yùn)行次數(shù)過多,甚至?xí)l(fā)程序的超時(shí)錯(cuò)誤。要想應(yīng)用程序跑的流暢,就得少去促發(fā)主級(jí)GC工作,要少促發(fā)主級(jí)GC工作,就得盡可能的保證年老域的內(nèi)存空間不被填充滿。所以這提醒我們一定要珍惜內(nèi)存空間,尤其在Android移動(dòng)設(shè)備上。
不同的GC策略會(huì)影響GC工作消耗的時(shí)長(zhǎng)。為了讓應(yīng)用程序運(yùn)行流暢度最優(yōu),就有必要根據(jù)應(yīng)用程序的運(yùn)行場(chǎng)景運(yùn)用不同的GC策略。
持久域(Permanent Generation)
JVM通過元數(shù)據(jù)(metadata)來記錄應(yīng)用程序的類和方法,這些元數(shù)據(jù)就被放在持久域就,另外Java公共的庫文件和方法也被放在這里,持久域不屬于堆內(nèi)存。在內(nèi)存滿時(shí)該塊內(nèi)存中的對(duì)象也可能被回收。Java8中已經(jīng)刪除該區(qū)域。
方法域(Method Area)
方法域?qū)儆诔志糜蛑械囊粔K,它被用來存放定義方法的代碼和類文件結(jié)構(gòu)。
內(nèi)存池(Memory Pool)
內(nèi)存池由JVM創(chuàng)建出來存放不可變對(duì)象,比如String,它可能在堆內(nèi)存分配也可能在持久域分配,這取決于JVM的內(nèi)存管理機(jī)制。
運(yùn)行時(shí)常量池( Runtime Constant Pool)
運(yùn)行時(shí)常量池用于存放編譯期生成的各種字面量和符號(hào)引用以及靜態(tài)方法,這部分內(nèi)容將在類加載后存放,它屬于方法域的一部分。
棧內(nèi)存(Stack Memory)
棧內(nèi)存被執(zhí)行線程所用。它用來存放方法內(nèi)的變量,這些變量通常是一些指向堆內(nèi)存對(duì)象的引用,它們生命周期短。
堆內(nèi)存調(diào)節(jié)器(Heap Memory Switches)
| VM SWITCH | VM SWITCH DESCRIPTION |
|---|---|
| -Xms | 初始堆內(nèi)存大小 |
| -Xmx | 最大堆內(nèi)存 |
| -Xmn | 新生域大小,余下空間就是年老域 |
| -XX:PermGen | 設(shè)置持久域(Permanent Generation)大小 |
| -XX:MaxPermGen | 最大持久域 |
| -XX:SurvivorRatio | 伊甸園(Eden space)和幸存者(Survivor Space)比值, 比如新生域共分配了10M,-XX:SurvivorRatio=2,那么Eden Space就占5M,余下的5M被兩個(gè)Survivor spaces均分。默認(rèn)值為8。 |
| -XX:NewRatio | 年老域和新生域比值,默認(rèn)為2,也就是說年老域大小是新生域大小的2倍 |
Java提供了許多設(shè)置內(nèi)存大小以及各個(gè)內(nèi)存域占比大小的調(diào)節(jié)器。常用的調(diào)節(jié)器如下:
| VM SWITCH | VM SWITCH DESCRIPTION |
|---|---|
| -Xms | 初始堆內(nèi)存大小 |
| -Xmx | 最大堆內(nèi)存 |
| -Xmn | 新生域大小,余下空間就是年老域 |
| -XX:PermGen | 設(shè)置持久域(Permanent Generation)大小 |
| -XX:MaxPermGen | 最大持久域 |
| -XX:SurvivorRatio | 伊甸園(Eden space)和幸存者(Survivor Space)比值, 比如新生域共分配了10M,-XX:SurvivorRatio=2,那么Eden Space就占5M,余下的5M被兩個(gè)Survivor spaces均分。默認(rèn)值為8。 |
| -XX:NewRatio | 年老域和新生域比值,默認(rèn)為2,也就是說年老域大小是新生域大小的2倍 |
更詳細(xì)的調(diào)節(jié)器配置信息請(qǐng)查看JVM Options Official Page。
垃圾回收器
GC(Garbage Collection)是一個(gè)進(jìn)程,它專注于標(biāo)示和清理沒有引用的對(duì)象,釋放內(nèi)存空間給新分配的對(duì)象騰地方住。其他某些語言這個(gè)過程都是程序猿自己實(shí)現(xiàn)的,而Java自動(dòng)完成了這個(gè)過程。
GC作為一個(gè)后臺(tái)進(jìn)程,一直默默監(jiān)察著應(yīng)用程序的運(yùn)行過程,尋找--->標(biāo)記-->釋放那些沒有引用到的對(duì)象,為新對(duì)象騰空間。
典型的GC處理涉及到的過程如下:
- 標(biāo)記:GC標(biāo)記哪些對(duì)象在使用,哪些對(duì)象沒有地方使用
- 正常刪除:GC刪除無用的對(duì)象所占有的空間,這些空間可以被其他存活的對(duì)象所使用
- 刪除后匯集:為了提升性能,刪除無用對(duì)象后,所有幸存對(duì)象被匯集在一起,如此新對(duì)象分配內(nèi)存時(shí)效率會(huì)更高。
上述過程可能存在如下問題:
- 大多數(shù)新創(chuàng)建的對(duì)象都是很快就變?yōu)闊o用的,而GC的運(yùn)行頻率又不宜過高,針對(duì)這種caseGC顯得不夠高效。
- 長(zhǎng)生命周期的對(duì)象也會(huì)被GC多次掃描標(biāo)記。
正是為了規(guī)避上述問題,Java將堆內(nèi)存劃分成了不同的區(qū)域,也就是之前提到的新生域和年老域。
GC類型
垃圾回收策略共有5種類型,根據(jù)應(yīng)用程序業(yè)務(wù)場(chǎng)景的不同,可以設(shè)置差異化的內(nèi)存調(diào)節(jié)器。
- Serial GC (-XX:+UseSerialGC): Serial GC 采用簡(jiǎn)單的標(biāo)記-清楚-整理策略。比如副線GC和主線GC的模式,Serial GC 針對(duì)小型CPU上運(yùn)行的簡(jiǎn)單應(yīng)用很實(shí)用,特別是那些低內(nèi)存設(shè)備上運(yùn)行的小型應(yīng)用。
- Parallel GC (-XX:+UseParallelGC): Parallel GC 和Serial GC 很類似,但它可以有N個(gè)線程去處理新生域上的GC回收工作,這里的N值對(duì)應(yīng)CPU的核數(shù)??梢杂?br>
-XX:ParallelGCThreads=n 選項(xiàng)去設(shè)定。
Parallel GC 也是采用單線程模式在年老域上進(jìn)行GC工作的。 - Parallel Old GC (-XX:+UseParallelOldGC): 和Parallel GC類似,但該種配置可以采用多線程GC來處理年老域上的GC工作。
- Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC): 因?yàn)镃MS不會(huì)整理、壓縮堆空間,帶來的好處就是GC工作時(shí)暫停的時(shí)間很短暫,.它作用在年老域上,和工作線程并發(fā)執(zhí)行。針對(duì)新生域上的內(nèi)存處理,它采用的是Parallel GC的處理方式。該GC策略適用需要實(shí)時(shí)快速響應(yīng)的應(yīng)用程序上??梢酝ㄟ^
-XX:ParallelCMSThreads=n
來設(shè)置CMS的線程數(shù)。 - G1 Garbage Collector (-XX:+UseG1GC): G1 Garbage Collector是Java 7新加入的,其目的是用了代替CMS回收策略. 它同樣是并發(fā)執(zhí)行,但會(huì)逐步壓縮堆空間。
Garbage First Collector不用于其他其中GC策略,它沒有新生域年老域的概念。對(duì)它而言,堆內(nèi)存會(huì)被分成多個(gè)相同大小的區(qū)域,GC運(yùn)行,首先回收無用對(duì)象最多的小區(qū)域。Garbage-First Collector Oracle Documentation有更詳細(xì)的說明。
GC監(jiān)測(cè)
可以用命令行或者可視化工具監(jiān)測(cè)應(yīng)用程序背后的GC運(yùn)行情況。這里我用自己寫的一段簡(jiǎn)單測(cè)試代碼,用命令行來進(jìn)行GC監(jiān)控.
先看測(cè)試代碼,在一個(gè)10次的循環(huán)結(jié)構(gòu)中,每次去申請(qǐng)一個(gè)10M大小的空間,這里忽略掉string申請(qǐng)的空間。
package com.azhengye.test;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
String string = "i="+i;
byte[] bt = new byte[1024*1024*10];
System.out.print(string);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我們?cè)诿钚欣锞幾g運(yùn)行.
可以看到程序運(yùn)行正常。
接下來參考之前介紹的內(nèi)存調(diào)節(jié)器,我們?cè)谶\(yùn)行時(shí)加上一些參數(shù)。
問題出來了。
前兩次我們配置Xms為2m,運(yùn)行時(shí)爆出錯(cuò)誤提示了初始堆內(nèi)存過小。在最后一次將Xms設(shè)置為10m該錯(cuò)誤消失,但卻爆出了我們常見的OOM問題。
這個(gè)簡(jiǎn)單的例子就說明Java提供的這種內(nèi)存調(diào)節(jié)器作用,在某些應(yīng)用程序運(yùn)行時(shí),我們要根據(jù)不同的場(chǎng)景,配置不同的運(yùn)行參數(shù)。
jstat命令行監(jiān)測(cè)內(nèi)存
JDK提供了jstat命令用了監(jiān)測(cè)JVM內(nèi)存使用情況以及GC運(yùn)行狀態(tài)。
其使用規(guī)則如下:
jstat -gc <processid> <time>
進(jìn)程id可以通過ps命令查看到
192:~/Documents/eclipse_workspace/DemoTestJava $ ps -eaf | grep java
501 2538 1956 0 11:41下午 ttys000 0:02.50 /usr/bin/java -Xmx20m -Xms10m -Xmn3m -XX:PermSize=2m -XX:MaxPermSize=4m -XX:+UseSerialGC -cp bin/ com.azhengye.test.Test
501 2540 1613 0 11:41下午 ttys001 0:00.00 grep java
有了進(jìn)程號(hào),在用jstat命令查看詳細(xì)的內(nèi)存使用情況,每隔1s打印一次內(nèi)存情況。
jstat -gc 2538 1s
對(duì)照上圖介紹下每一欄的含義。
- S0C and S1C: 兩個(gè)幸存域的大小,之前介紹過它們總是相等的。這里也可以印證。它們的大小均為256KB.
- S0U and S1U: 兩個(gè)幸存域已經(jīng)使用的大小。
- EC and EU: 伊甸園區(qū)域的內(nèi)存大小和已經(jīng)占用的內(nèi)存大小,副線GC運(yùn)行后EU大小就會(huì)減少。
- OC and OU: 年老域內(nèi)存大小和已經(jīng)被使用的內(nèi)存大小。
- PC and PU: Perm Gen內(nèi)存大小和已經(jīng)被使用的內(nèi)存大小。
- YGC and YGCT: YGC 表示副線GC運(yùn)行的次數(shù),YGCT表示副線GC所消耗的時(shí)間。
- FGC and FGCT: FGC 表示主線GC運(yùn)行次數(shù). 相應(yīng)的FGCT打印的是主線GC消耗的時(shí)間.
- GCT: 副線GC和主線GC消耗時(shí)間總和。
jvisualvm可視化監(jiān)測(cè)
JDK同樣提供了jvisualvm可視化的監(jiān)測(cè)工具,首次打開需要先安裝Visual GC 插件。下圖是我截取的界面,功能比較多,這里不做過多介紹了。
內(nèi)存和GC調(diào)整
這一步輕易不要走,程序運(yùn)行的優(yōu)化更多的注意了應(yīng)該放在軟件實(shí)現(xiàn)上。除非很明顯的定位到程序運(yùn)行受到了GC的拖累,或者確實(shí)需要調(diào)整內(nèi)存的分配情況才能讓程序運(yùn)行起來最佳。
下面是幾點(diǎn)調(diào)整建議:
- 主線GC運(yùn)行太過頻繁,可以嘗試增大年老域內(nèi)存。
- OOM問題,可以適當(dāng)增加堆內(nèi)存大小。
- 采用不同的GC策略,監(jiān)測(cè)程序運(yùn)行情況,選取最合適的。
參考鏈接
了解CMS(Concurrent Mark-Sweep)垃圾回收器
Java 8新特性探究(9):跟OOM:Permgen說再見吧
Java8 Demos and Samples