Java 內(nèi)存管理

弄清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ū)域

圖片來源網(wǎng)絡(luò)

如上圖所示,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處理涉及到的過程如下:

  1. 標(biāo)記:GC標(biāo)記哪些對(duì)象在使用,哪些對(duì)象沒有地方使用
  2. 正常刪除:GC刪除無用的對(duì)象所占有的空間,這些空間可以被其他存活的對(duì)象所使用
  3. 刪除后匯集:為了提升性能,刪除無用對(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • JAVA內(nèi)存管理 JVM結(jié)構(gòu) Class Loader類加載器的作用是根據(jù)給定的全限定名類名(如java.lang...
    聽歌的老頭閱讀 407評(píng)論 0 1
  • 本文引用自:深入理解Java虛擬機(jī)的第2章內(nèi)容 Java與C++之間有一堵由內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)所圍成的高墻...
    溜溜小毛驢兒閱讀 814評(píng)論 0 34
  • 2.9 JVM內(nèi)存管理 2.9.1 運(yùn)行時(shí)數(shù)據(jù)區(qū)域 JVM所管理的內(nèi)存可以分為一下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域: 其中方法區(qū)...
    jianhuih閱讀 420評(píng)論 0 0
  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虛擬機(jī)(JVM)垃圾回收器提供...
    簡(jiǎn)欲明心閱讀 90,374評(píng)論 17 311
  • 小顏你丫 仰望星空,那一角落是最美。 仰望星空,那一角落是最亮。 仰望星空,那一角落是真摯。 仰望星空,哪一角落是...
    小顏兒你丫閱讀 300評(píng)論 0 0

友情鏈接更多精彩內(nèi)容