JVM 基礎(chǔ)、堆內(nèi)存分析和垃圾回收算法

文章首發(fā)我的博客,歡迎訪問:https://blog.itzhouq.cn/jvm

首先基本的面試題都是下面的奪命連環(huán)問,感受一下。

  • 請你談?wù)勀銓?JVM 的理解。java8 虛擬機(jī)和之前有什么變化?
  • 什么是 OOM, 什么是棧溢出 StackOverFlowError? 怎么分析?
  • JVM 的常用調(diào)優(yōu)參數(shù)有哪些?
  • 內(nèi)存快照如何抓取,怎么分析 Dump 文件?你知道嗎?
  • 談?wù)?JVM 中,你對類加載器的認(rèn)識?

這篇文章先大體梳理一下相關(guān)的知識點(diǎn),后面再整理一篇基本面試題相關(guān)的,先挖個坑。要說明的是,文章中很多地方關(guān)于概念是一帶而過的,難免有部分內(nèi)容沒有說明白。對于不明白的點(diǎn),建議自己動手查查相關(guān)資料,決不能指望一篇筆記就能把 JVM 搞明白,這顯然也是不可能的。

1、JVM 的位置

image

可以看到 JVM 是 JRE 的一部分。主要工作是解釋自己的字節(jié)碼并映射到本地的 CPU 指令集和 OS 的系統(tǒng)調(diào)用。Java 語言是跨平臺的,不同的操作系統(tǒng)會有不同的 JVM 映射規(guī)則,這就使得 Java 語言與操作系統(tǒng)無關(guān)。

2、JVM 的體系結(jié)構(gòu)

image
image
image

3、類加載器

作用:加載 Class 文件,比如我們 new Student() 的時候,Student 是類,是抽象的,使用 new 關(guān)鍵詞創(chuàng)建對象實(shí)例,實(shí)例的引用是在棧中,而具體的人是放在堆中。

3.1、類的實(shí)例化和雙親委派機(jī)制

public class Car {
    public static void main(String[] args) {
        // 類是模板,對象是具體的
        Car car1 = new Car();
        Car car2 = new Car();
        Car car3 = new Car();

        System.out.println(car1.hashCode()); // 460141958
        System.out.println(car2.hashCode()); // 1163157884
        System.out.println(car3.hashCode()); // 1956725890

        Class<? extends Car> aClass1 = car1.getClass();
        Class<? extends Car> aClass2 = car2.getClass();
        Class<? extends Car> aClass3 = car3.getClass();

        System.out.println(aClass1.hashCode()); // 685325104
        System.out.println(aClass2.hashCode()); // 685325104
        System.out.println(aClass3.hashCode()); // 685325104
    }
}
image

3.2、類加載

類加載器:

  1. 虛擬機(jī)自帶的加載器
  2. 啟動類(根)加載器
  3. 擴(kuò)展類加載器
  4. 應(yīng)用程序加載器

試驗(yàn):自己定義一個String類,看是否能執(zhí)行

package java.lang;

public class String {
    // 雙親委派機(jī)制:安全
    // BOOT --> EXT --> APP (最終執(zhí)行)
    // BOOT
    // EXT
    // APP
    public String toString () {
        return "Hello";
    }

    public static void main(String[] args) {
        String s = new String();
        System.out.println(s.toString());
    }
    
    // 錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為:
    //   public static void main(String[] args)
    //否則 JavaFX 應(yīng)用程序類必須擴(kuò)展javafx.application.Application
    
    /**
     * 類加載的流程
     * 1. 類加載器收到類加載的請求
     * 2. 將這個請求向上委托給父類加載器去完成,一直向上委托,直到啟動類加載器
     * 3. 啟動節(jié)加載器檢查是否能夠加載當(dāng)前這個類,能加載就結(jié)束,使用當(dāng)前的加載器,
        否則,拋出異常通知子加載器進(jìn)行加載
     * 4. 重復(fù)步驟3
     */

}

百度:雙親委派機(jī)制

4、沙盒安全機(jī)制

Java安全的模型的核心就是 Java 沙箱 (sandbox),什么是沙箱?沙箱是一個限制程序運(yùn)行的環(huán)境。沙箱機(jī)制就是將 Java 代碼限制在虛擬機(jī)特定的運(yùn)行環(huán)境中,并且嚴(yán)格限制代碼對本地系統(tǒng)資源訪問,通過這樣的措施來保證對代碼的有效隔離,防止對本地系統(tǒng)操作破壞。沙箱主要限制系統(tǒng)資源訪問。

5、Native

開啟一個多線程啟動類:

public static void main(String[] args) {
    new Thread(() -> {

    }, "my thread name").start();
}

點(diǎn)進(jìn)去查看start()方法的源碼:

public synchronized void start() {
    /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0(); // 調(diào)用start0()方法
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}

private native void start0();

可以看到源碼中使用了特殊的方法 start0(),使用了 native關(guān)鍵詞。

native :凡是帶了 native 關(guān)鍵詞,說明 java 的作用范圍達(dá)不到了,會調(diào)用底層 C 語言的庫!
native 的方法會進(jìn)入本地方法棧,調(diào)用本地方法接口 JNI(Java Native Interface 本地方法接口),其他的就是 Java 棧。

JNI的作用:擴(kuò)展 java 的使用,融合不同的編程語言為 java 所用!最初的時候需要融合 C 和 C++。
        Java 誕生的時候, C 和 C++ 橫行,想要立足,必須要有調(diào)用 C 和 C++的程序。
        它在內(nèi)存區(qū)域中專門開辟了一塊標(biāo)記區(qū)域: Native Method Stack ,登記 native 方法,
        在最終執(zhí)行的時候,加載本地方法庫中的方法通過 JNI。
比如: Java程序驅(qū)動打印機(jī),管理系統(tǒng)。這部分掌握即可,在企業(yè)級應(yīng)用中較為少見。

現(xiàn)在調(diào)用第三方語言接口的方式很多,比如:Socket、WebService、HTTP等。

6、PC 寄存器

程序計(jì)數(shù)器:Program Counter Register

每個線程都有一個程序計(jì)數(shù)器,是線程私有的,就是一個指針,指向方法區(qū)中的方法字節(jié)碼(用來存儲指向一條指令的地址,也即將要執(zhí)行的指令代碼),在執(zhí)行引擎下讀取下一條指令,是一個非常小的內(nèi)存空間,幾乎可以忽略不計(jì)。

7、方法區(qū)

Method Area 方法區(qū)

方法區(qū)是被所有線程共享的,所有字段和方法字節(jié)碼,以及一些特殊方法,如構(gòu)造函數(shù),接口代碼也在此定義,簡單說,所有定義的方法的信息都保存在該區(qū)域,此區(qū)域?qū)儆诠蚕韰^(qū)間。

**==靜態(tài)變量、常量、類信息(構(gòu)造方法、接口定義)、運(yùn)行時的常量池存在方法區(qū)中,但是實(shí)例變量存在堆內(nèi)存中,和方法區(qū)無關(guān)== ** 。

static、final、Class、常量池。

8、棧

棧是一種數(shù)據(jù)結(jié)構(gòu),可以形象地理解為一個水桶或水杯,其特點(diǎn)是先進(jìn)后出。 比如,依次將乒乓球放入杯子中,先放進(jìn)去的球,最后才能拿出來。

棧中存放 8 大基本數(shù)據(jù)類型 + 對象引用 + 實(shí)例的方法。

棧內(nèi)存主管程序的運(yùn)行,生命周期和線程同步。Java 中執(zhí)行方法的過程就是調(diào)用棧的過程。為什么 main() 方法,最先執(zhí)行,最后結(jié)束呢?因?yàn)閙ain() 方法是程序的入口,執(zhí)行時 main() 會最先被壓到棧底, 在 main() 方法中調(diào)用其他方法時,依次將其他方法壓入棧中。

線程結(jié)束,棧內(nèi)存就釋放了,對于棧來說,不存在垃圾回收問題。

棧的運(yùn)行原理:

棧的運(yùn)行原理

棧 + 堆 + 方法區(qū)的交互關(guān)系

棧堆方法區(qū)的交互關(guān)系

畫一個對象實(shí)例化的過程在內(nèi)存中:百度、看視頻。

JVM的內(nèi)存區(qū)域劃分,對象實(shí)例化分析

JVM系列分析- 內(nèi)存模型

9、三種 JVM

  • Sun 公司 HotSpotJava HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
  • BEA:JRockit
  • IBM:J9VM

我們學(xué)習(xí)的都是HotSpot

10、堆

Heap,一個 JVM 只有一個堆內(nèi)存,堆內(nèi)存的大小是可以調(diào)節(jié)的。

類加載器讀取類文件后,一般會把什么東西放入堆中?類、方法、常量、變量,保存所有引用類型的真實(shí)對象。

堆內(nèi)存中還要細(xì)分為三個區(qū)域:

  • 新生區(qū)(伊甸園區(qū) Eden):Young/New
  • 養(yǎng)老區(qū):old
  • 永久區(qū):Perm
堆內(nèi)存的細(xì)分

新生區(qū)中沒有被垃圾收集器干掉的對象會進(jìn)入幸存區(qū)0區(qū),幸存區(qū)0區(qū)中沒有被干掉的對象會進(jìn)入幸存區(qū)1區(qū)幸存區(qū)0區(qū)幸存區(qū)1區(qū)會不停的交換位置。經(jīng)過一定次數(shù)后的垃圾回收后還沒有被干掉的對象會進(jìn)入養(yǎng)老區(qū),這個區(qū)域的對象一般不會被干掉,但不是絕對的。假設(shè)養(yǎng)老區(qū)滿了,對象會進(jìn)入永久存儲區(qū)。

針對新生區(qū)的垃圾回收稱為輕量級的垃圾收集,也稱輕 GC。針對養(yǎng)老區(qū)的垃圾回收稱為重量級的垃圾收集,也稱重 GC。

GC 垃圾回收,主要是在伊甸園區(qū)和養(yǎng)老區(qū)。

假設(shè)內(nèi)存滿了,會報錯 OOM,OutOfMemeroy,對內(nèi)存不夠!

public static void main(String[] args) {
    String str = "hello world!";

    while (true) {
        str += str + new Random().nextInt(88888888) + new Random().nextInt(99999999);
    }
    //        Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    //        at java.util.Arrays.copyOf(Arrays.java:3332)
    //        at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
    //        at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
    //        at java.lang.StringBuilder.append(StringBuilder.java:208)
    //        at Hello.main(Hello.java:8)
}

在 JDK8 以后,永久存儲區(qū)改名為元空間。

11、新生區(qū)、老年區(qū)

新生區(qū):

  • 類:誕生和成長的地方,甚至死亡;
  • 伊甸園,所有對象都是在伊甸園區(qū) new 出來的;
  • 幸存者區(qū):0 和 1 區(qū)。
  • 經(jīng)過研究,99%的對象都是臨時對象,所以進(jìn)入老年區(qū)的對象很少。

12、永久區(qū)

這個區(qū)域常駐內(nèi)存的。用來存放 JDK 自身攜帶的 Class 對象。Interface 元數(shù)據(jù),存儲的是 Java 運(yùn)行時的一些環(huán)境或類信息,這個區(qū)域不存在垃圾回收!關(guān)閉 VM 虛擬機(jī)就會釋放這個區(qū)域的內(nèi)存。

一個啟動類,加載了大量的第三方 jar 包。Tomcat 部署了太多的應(yīng)用,大量動態(tài)生成的反射類。不斷的被加載,知道內(nèi)存滿,就會出現(xiàn) OOM。

  • JDK1.6:永久代,常量池存放在方法區(qū);
  • JDK1.7:永久代,但是慢慢的退化了,常量池在堆中;
  • JDK1.8:無永久代,常量池在元空間。
堆空間內(nèi)存模型

13、堆內(nèi)存調(diào)優(yōu)

public static void main(String[] args) {
    // 返回 JVM 試圖使用的最大內(nèi)存
    long max = Runtime.getRuntime().maxMemory(); // 字節(jié)

    // 返回 JVM 的初始化總內(nèi)存
    long total = Runtime.getRuntime().totalMemory();

    System.out.println("max=" + max + "字節(jié)\t" + (max / (double)1024/1024) + "MB");
    // max=2831679488字節(jié) 2700.5MB
    System.out.println("total=" + max + "字節(jié)\t" + (total / (double)1024/1024) + "MB");
    // total=2831679488字節(jié)   182.5MB

    // 默認(rèn)情況下:分配的從內(nèi)存是電腦內(nèi)存的 1/4, 而初始化內(nèi)存是電腦內(nèi)存的 1/64。
}

元空間在邏輯上存在,物理上不存在。

OOM 解決方案:

  1. 嘗試擴(kuò)大堆內(nèi)存看結(jié)果;
  2. 分析內(nèi)存,看一下哪個地方出現(xiàn)了問題(專業(yè)工具)。
堆內(nèi)存調(diào)優(yōu)
image

14、使用 JProfier 工具分析 OOM 原因

在一個項(xiàng)目中,突然出現(xiàn)了 OOM 故障,那么該如何排除,研究為什么出錯?

  • 能夠看到代碼第幾行出錯:內(nèi)存快照分析工具,MAT(Eclipse)、JProfiler
  • Debugger,一行行分析代碼。

MAT 、JProfiler的作用:

  • 分析 Dump 內(nèi)存文件,快讀定位內(nèi)存泄漏;
  • 獲得堆中的數(shù)據(jù);
  • 獲得大的對象;
  • 。。。。。。

JProfiler 插件和Windows客戶端安裝百度。

配置JProfiler

編寫一個 OOM 的程序測試

public class Demo03 {
    byte[] array = new byte[1 * 1024 * 1024]; // 1MB

    public static void main(String[] args) {
        ArrayList<Demo03> list = new ArrayList<>();
        int count = 0;

        try {
            while (true) {
                list.add(new Demo03());
                count ++;
            }
        } catch (Exception e) { // 錯誤寫法
            e.printStackTrace();
        }
    }
//    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
//    at Demo03.<init>(Demo03.java:4)
//    at Demo03.main(Demo03.java:12)
}

這個程序出現(xiàn)了 OOM,但是從報錯信息無法看出哪里的問題。

此時需要添加一些配置,打印一些信息。

配置Dump參數(shù)
Dump文件

通過 JProfiler 工具打開文件:

線程Dump
大對象
總結(jié):
    // -Xms 設(shè)置初始化內(nèi)存分配的大小  默認(rèn)1/64
    // -Xmx 設(shè)置最大分配內(nèi)存    默認(rèn) 1/4
    // -XX:+PrintDCDetails  // 打印GC垃圾回收信息
    // -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError    OOM  Dump(轉(zhuǎn)儲文件)

15、GC : 常用算法

GC 的作用區(qū)域:

GC的作用區(qū)域

JVM 在進(jìn)行 GC 時,并不是對這三個區(qū)域統(tǒng)一回收,大部分時候,回收都是新生區(qū)。

  • 新生區(qū)
  • 幸存區(qū):from to
  • 老年區(qū)

GC 的分類:

輕 GC(普通的 GC):主要針對新生區(qū),偶爾對幸存區(qū)進(jìn)行 GC。

重 GC(全局 GC):把上面所有的區(qū)域都進(jìn)行 GC,也就是釋放內(nèi)存。

堆內(nèi)存中的幸存區(qū)是可以交換位置的。

堆內(nèi)存中幸存區(qū)

GC 的題目:

  • JVM 的內(nèi)存模型和分區(qū),詳細(xì)到每個區(qū)放什么;
  • 堆里面的分區(qū)有哪些? Eden、from、to、Old,說說他們的特點(diǎn);
  • GC 的算法有哪些?標(biāo)記清除法、標(biāo)記壓縮、復(fù)制算法、引用計(jì)數(shù)器,怎么用的?
  • 輕 GC 和重 GC 分別在什么時候發(fā)生?

引用計(jì)數(shù)器:(用得少)

引用計(jì)數(shù)法

復(fù)制算法:幸存區(qū)的復(fù)制

GC復(fù)制算法
GC復(fù)制算法2

GC 復(fù)制算法:

好處:沒有內(nèi)存碎片;

壞處:浪費(fèi)了空間內(nèi)存,多了一半空間(to)永遠(yuǎn)都是空的。極端情況下,比如對象 100% 存活,這個缺點(diǎn)就很明顯。

復(fù)制算法最佳使用場景:對象存活度較低,比如新生區(qū)。

標(biāo)記清除算法

優(yōu)點(diǎn):不需要額外的空間!

缺點(diǎn):兩次掃描,嚴(yán)重浪費(fèi)時間,會產(chǎn)生內(nèi)存碎片。

標(biāo)記清除算法

標(biāo)記壓縮

對標(biāo)清除進(jìn)行再優(yōu)化。

標(biāo)記壓縮算法

標(biāo)記清除壓縮

再次優(yōu)化上述算法,可以多次進(jìn)行標(biāo)記清除,進(jìn)行一次標(biāo)記壓縮。

16、總結(jié):

  • 內(nèi)存效率:復(fù)制算法 > 標(biāo)記清除算法 > 標(biāo)記壓縮算法(時間復(fù)雜度)
  • 內(nèi)存整齊度:復(fù)制算法 == 標(biāo)記壓縮算法 > 標(biāo)記清除算法
  • 內(nèi)存利用率:標(biāo)記壓縮算法 == 標(biāo)記清除算法 > 復(fù)制算法

思考一個問題:難道沒有一個最優(yōu)的算法嗎?

答案:沒有,沒有最好的算法,只有最合適的算法---> GC:分代收集算法

年輕代:

  • 存活率低
  • 復(fù)制算法

老年代:

  • 區(qū)域大,存活率低
  • 標(biāo)記清除(內(nèi)存碎片不是太多)+ 標(biāo)記壓縮混合實(shí)現(xiàn)。

參考書籍:《深入理解 JVM》

17、JMM(高頻) 和 快速學(xué)習(xí)方法

  1. 什么是 JMM?

JMM:Java Memory Model 的縮寫。

  1. 它是干嘛的?

作用:緩存一致性協(xié)議,用于定義數(shù)據(jù)讀寫的規(guī)則。

JMM 定義了線程工作內(nèi)存和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(Local Memory)。

解決共享對象可見性這個問題: volilate

  1. 它該如何學(xué)習(xí)?

官方、其他人的博客、對應(yīng)的視頻。。。

Java內(nèi)存模型(JMM)總結(jié)

關(guān)于內(nèi)存圖:可以去思維導(dǎo)圖 : processon 網(wǎng)站搜索 JVM 可以看到別人畫的相關(guān)思維導(dǎo)圖。


參考文章

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

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

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