java基礎-JVM(堆內存)

前言

        與C語言不同,Java內存(堆內存)的分配與回收由JVM垃圾收集器自動完成,這個特性深受大家歡迎,能夠幫助程序員更好的編寫代碼,本文以HotSpot虛擬機爲例,說一說Java GC的那些事。

 

Java堆內存

        我們知道Java堆是被所有線程共享的一塊內存區域,所有對象實例和數組都在堆上進行內存分配。爲了進行高效的垃圾回收,虛擬機把堆內存劃分成新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)3個區域。

 

新生代

        新生代由 Eden 與 Survivor Space(S0,S1)構成,大小通過-Xmn參數指定,Eden 與 Survivor Space 的內存大小比例默認爲8:1,可以通過-XX:SurvivorRatio 參數指定,比如新生代爲10M 時,Eden分配8M,S0和S1各分配1M。

 

        Eden:希臘語,意思爲伊甸園,在聖經中,伊甸園含有樂園的意思,根據《舊約·創世紀》記載,上帝耶和華照自己的形像造了第一個男人亞當,再用亞當的一個肋骨創造了一個女人夏娃,並安置他們住在了伊甸園。

 

        大多數情況下,對象在Eden中分配,當Eden沒有足夠空間時,會觸發一次Minor GC,虛擬機提供了-XX:+PrintGCDetails參數,告訴虛擬機在發生垃圾回收時打印內存回收日誌。

 

Survivor:意思爲倖存者,是新生代和老年代的緩衝區域。

 

        當新生代發生GC(Minor GC)時,會將存活的對象移動到S0內存區域,並清空Eden區域,當再次發生Minor GC時,將Eden和S0中存活的對象移動到S1內存區域。

 

        存活對象會反覆在S0和S1之間移動,當對象從Eden移動到Survivor或者在Survivor之間移動時,對象的GC年齡自動累加,當GC年齡超過默認閾值15時,會將該對象移動到老年代,可以通過參數-XX:MaxTenuringThreshold 對GC年齡的閾值進行設置。

 

老年代

        老年代的空間大小即-Xmx 與-Xmn 兩個參數之差,用於存放經過幾次Minor GC之後依舊存活的對象。當老年代的空間不足時,會觸發Major GC/Full GC,速度一般比Minor GC慢10倍以上。

 

永久代

        在JDK8之前的HotSpot實現中,類的元數據如方法數據、方法信息(字節碼,棧和變量大小)、運行時常量池、已確定的符號引用和虛方法表等被保存在永久代中,32位默認永久代的大小爲64M,64位默認爲85M,可以通過參數-XX:MaxPermSize進行設置,一旦類的元數據超過了永久代大小,就會拋出OOM異常。

 

        虛擬機團隊在JDK8的HotSpot中,把永久代從Java堆中移除了,並把類的元數據直接保存在本地內存區域(堆外內存),稱之爲元空間。

 

這樣做有什麼好處?

        有經驗的同學會發現,對永久代的調優過程非常困難,永久代的大小很難確定,其中涉及到太多因素,如類的總數、常量池大小和方法數量等,而且永久代的數據可能會隨着每一次Full GC而發生移動。

 

        而在JDK8中,類的元數據保存在本地內存中,元空間的最大可分配空間就是系統可用內存空間,可以避免永久代的內存溢出問題,不過需要監控內存的消耗情況,一旦發生內存泄漏,會佔用大量的本地內存。

 

ps:JDK7之前的HotSpot,字符串常量池的字符串被存儲在永久代中,因此可能導致一系列的性能問題和內存溢出錯誤。在JDK8中,字符串常量池中只保存字符串的引用。

 

如何判斷對象是否存活

        GC動作發生之前,需要確定堆內存中哪些對象是存活的,一般有兩種方法:引用計數法和可達性分析法。

 

1、引用計數法

        在對象上添加一個引用計數器,每當有一個對象引用它時,計數器加1,當使用完該對象時,計數器減1,計數器值爲0的對象表示不可能再被使用。

 

引用計數法實現簡單,判定高效,但不能解決對象之間相互引用的問題。

 

public class GCtest {

    private Object instance = null;

    private static final int _10M = 10 * 1 << 20;

    // 一個對象佔10M,方便在GC日誌中看出是否被回收

    private byte[] bigSize = new byte[_10M];

 

    public static void main(String[] args) {

        GCtest objA = new GCtest();

        GCtest objB = new GCtest();

 

        objA.instance = objB;

        objB.instance = objA;

 

        objA = null;

        objB = null;

 

        System.gc();

    }

}

 

通過添加-XX:+PrintGC參數,運行結果:

[GC (System.gc()) [PSYoungGen: 26982K->1194K(75776K)] 26982K->1202K(249344K), 0.0010103 secs]

 

從GC日誌中可以看出objA和objB雖然相互引用,但是它們所佔的內存還是被垃圾收集器回收了。

 

2、可達性分析法

        通過一系列稱爲 “GC Roots” 的對象作爲起點,從這些節點開始向下搜索,搜索路徑稱爲 “引用鏈”,以下對象可作爲GC Roots:

        本地變量表中引用的對象

        方法區中靜態變量引用的對象

        方法區中常量引用的對象

        Native方法引用的對象

 

當一個對象到 GC Roots 沒有任何引用鏈時,意味着該對象可以被回收。

 

在可達性分析法中,判定一個對象objA是否可回收,至少要經歷兩次標記過程:

 

        1、如果對象objA到 GC Roots沒有引用鏈,則進行第一次標記。

        2、如果對象objA重寫了finalize()方法,且還未執行過,那麼objA會被插入到F-Queue隊列中,由一個虛擬機自動創建的、低優先級的Finalizer線程觸發其finalize()方法。finalize()方法是對象逃脫死亡的最後機會,GC會對隊列中的對象進行第二次標記,如果objA在finalize()方法中與引用鏈上的任何一個對象建立聯繫,那麼在第二次標記時,objA會被移出“即將回收”集合。

 

看看具體實現

 

public class FinalizerTest {

    public static FinalizerTest object;

    public void isAlive() {

        System.out.println("I'm alive");

    }

 

    @Override

    protected void finalize() throws Throwable {

        super.finalize();

        System.out.println("method finalize is running");

        object = this;

    }

 

    public static void main(String[] args) throws Exception {

        object = new FinalizerTest();

 

        // 第一次執行,finalize方法會自救

        object = null;

        System.gc();

 

        Thread.sleep(500);

        if (object != null) {

            object.isAlive();

        } else {

            System.out.println("I'm dead");

        }

 

        // 第二次執行,finalize方法已經執行過

        object = null;

        System.gc();

 

        Thread.sleep(500);

        if (object != null) {

            object.isAlive();

        } else {

            System.out.println("I'm dead");

        }

    }

}

 

 

執行結果:

 

method finalize is runningI'm aliveI'm dead

 

從執行結果可以看出:

 

第一次發生GC時,finalize方法的確執行了,並且在被回收之前成功逃脫;

第二次發生GC時,由於finalize方法只會被JVM調用一次,object被回收。

當然了,在實際項目中應該儘量避免使用finalize方法。

 

拓展:

   利用JConsole工具監控java程序內存和JVM

一.找到java應用程序對應的進程PID

linux:   ps -ef|grep java
windows: netstat -ano|findstr "java"

二.啓動JConsole監控工具

方法一:

打開cmd命令窗口,進入jdk安裝路徑下/bin目錄,

輸入命令:JConsole “PID號” 如圖:

如圖自動啓動並打開JConsole監控界面:

方法二:進入jdk安裝目錄bin目錄下,雙擊運行JConsole.exe程序,選擇應用程序對應的PID程序連接或雙擊即可

三.對圖表進行性能分析

JConsole主要是監控java應用程序,它是jdk自帶的工具,一個基於JMX用於連接正在運行的JVM,會啓動com.sun.management.jmxremote實現默認地JMX管理客戶端。

1)  概要

概要界面可以實時查看java應用程序的堆內存使用情況、線程、類以及CPU使用情況,如圖:

2)  內存

內存界面可以在圖表選擇“堆內存使用情況”和“非堆內存使用情況”實時圖,並顯示內存詳細信息:使用內存、分配:最大值等,如圖:

PS:重點關注使用內存的佔比,使用內存與最大值之間的合理比值爲1:3,已使用內存不能大於1/2最大值,否則內存存在瓶頸。

3)  線程

線程圖不是重點關注,只關注該線程情況,並可以檢測是否有死鎖線程。

4)  類

類圖並不是重點關注圖,與應用程序類的多少有關,無固定值。

1)  VM摘要

VM摘要圖是觀察JVM使用情況圖

堆是由Java虛擬機(JVM,下文提到的JVM特指Sun hotspot JVM)用來存放Java類、對象和靜態成員的內存空間,Java程序中創建的所有對象都在堆中分配空間,堆只用來存儲對象,應用程序通過存放在堆棧(Stack)內的引用來訪問堆數據,一個JVM進程只能擁有一個堆。JVM通過-Xms和-Xmx參數分別設置堆的初始值和最大值。

此圖需要關注分析當前堆大小、堆大小的最大值、分配的內存,以及物理總內存和可用物理內存。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章