這 10 個的 JVM 面試題,牛逼!

小林整理了 10 個經典

又容易被疏忽的 JVM 面試題

1. 對象一定分配在堆中嗎?有沒有了解逃逸分析技術?

「對象一定分配在堆中嗎?」不一定的,JVM 通過「逃逸分析」,那些逃不出方法的對象會在棧上分配。

逃逸分析 (Escape Analysis),是一種可以有效減少 Java 程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。通過逃逸分析,Java Hotspot 編譯器能夠分析出一個新的對象的引用的使用範圍,從而決定是否要將這個對象分配到堆上。

逃逸分析是指分析指針動態範圍的方法,它同編譯器優化原理的指針分析和外形分析相關聯。當變量(或者對象)在方法中分配後,其指針有可能被返回或者被全局引用,這樣就會被其他方法或者線程所引用,這種現象稱作指針(或者引用)的逃逸 (Escape)。通俗點講,如果一個對象的指針被多個方法或者線程引用時,那麼我們就稱這個對象的指針發生了逃逸。

public class EscapeAnalysisTest {

    public static Object object;

    //StringBuilder可能被其他方法改變,逃逸到了方法外部。
    public StringBuilder  escape(String a, String b) {
        StringBuilder str = new StringBuilder();
        str.append(a);
        str.append(b);
        return str;
    }

    //不直接返回StringBuffer,不發生逃逸
    public String notEscape(String a, String b) {
        StringBuilder str = new StringBuilder();
        str.append(a);
        str.append(b);
        return str.toString();
    }

    //外部線程可見object,發生逃逸
    public void objectEscape(){
        object = new Object();
    }

    //僅方法內部可見,不發生逃逸
    public void objectNotEscape(){
        Object object = new Object();
    }
}

「逃逸分析的好處」

  • 棧上分配,可以降低垃圾收集器運行的頻率。

  • 同步消除,如果發現某個對象只能從一個線程可訪問,那麼在這個對象上的操作可以不需要同步。

  • 標量替換,把對象分解成一個個基本類型,並且內存分配不再是分配在堆上,而是分配在棧上。這樣的好處有,一、減少內存使用,因爲不用生成對象頭。二、程序內存回收效率高,並且 GC 頻率也會減少。

2. 虛擬機爲什麼使用元空間替換了永久代?

**「什麼是元空間?什麼是永久代?爲什麼用元空間代替永久代?」我們先回顧一下「方法區」**吧, 看看虛擬機運行時數據內存圖,如下:

方法區和堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據。

「什麼是永久代?它和方法區有什麼關係呢?」

如果在 HotSpot 虛擬機上開發、部署,很多程序員都把方法區稱作永久代。可以說方法區是規範,永久代是 Hotspot 針對該規範進行的實現。在 Java7 及以前的版本,方法區都是永久代實現的。

「什麼是元空間?它和方法區有什麼關係呢?」

對於 Java8,HotSpots 取消了永久代,取而代之的是元空間 (Metaspace)。換句話說,就是方法區還是在的,只是實現變了,從永久代變爲元空間了。

「爲什麼使用元空間替換了永久代?」

**「永久代」**是通過以下這兩個參數配置大小的~

對於**「永久代」**,如果動態生成很多 class 的話,就很可能出現**「java.lang.OutOfMemoryError: PermGen space 錯誤」**,因爲永久代空間配置有限嘛。最典型的場景是,在 web 開發比較多 jsp 頁面的時候。

可以通過以下的參數來設置元空間的大小:

  • -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時 GC 會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過 MaxMetaspaceSize 時,適當提高該值。

  • -XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。

  • -XX:MinMetaspaceFreeRatio,在 GC 之後,最小的 Metaspace 剩餘空間容量的百分比,減少爲分配空間所導致的垃圾收集

  • -XX:MaxMetaspaceFreeRatio,在 GC 之後,最大的 Metaspace 剩餘空間容量的百分比,減少爲釋放空間所導致的垃圾收集

「所以,爲什麼使用元空間替換永久代?」

表面上看是爲了避免 OOM 異常。因爲通常使用 PermSize 和 MaxPermSize 設置永久代的大小就決定了永久代的上限,但是不是總能知道應該設置爲多大合適, 如果使用默認值很容易遇到 OOM 錯誤。當使用元空間時,可以加載多少類的元數據就不再由 MaxPermSize 控制, 而由系統的實際可用空間來控制啦。

3. 什麼是 Stop The World ?  什麼是 OopMap?什麼是安全點?

進行垃圾回收的過程中,會涉及對象的移動。爲了保證對象引用更新的正確性,必須暫停所有的用戶線程,像這樣的停頓,虛擬機設計者形象描述爲**「Stop The World」**。

在 HotSpot 中,有個數據結構(映射表)稱爲**「OopMap」**。一旦類加載動作完成的時候,HotSpot 就會把對象內什麼偏移量上是什麼類型的數據計算出來,記錄到 OopMap。在即時編譯過程中,也會在**「特定的位置」**生成 OopMap,記錄下棧上和寄存器裏哪些位置是引用。

這些特定的位置主要在:

這些位置就叫作**「安全點 (safepoint)。」**用戶程序執行時並非在代碼指令流的任意位置都能夠在停頓下來開始垃圾收集,而是必須是執行到安全點才能夠暫停。

4. 說一下 JVM 的主要組成部分及其作用?

JVM 包含兩個子系統和兩個組件,分別爲

  • Class loader(類裝載子系統)

  • Execution engine(執行引擎子系統);

  • Runtime data area(運行時數據區組件)

  • Native Interface(本地接口組件)。

首先通過編譯器把 Java 源代碼轉換成字節碼,Class loader(類裝載) 再把字節碼加載到內存中,將其放在運行時數據區的方法區內,而字節碼文件只是 JVM 的一套指令集規範,並不能直接交給底層操作系統去執行,因此需要特定的命令解析器執行引擎(Execution Engine),將字節碼翻譯成底層系統指令,再交由 CPU 去執行,而這個過程中需要調用其他語言的本地庫接口(Native Interface)來實現整個程序的功能。

5. 守護線程是什麼?守護線程和非守護線程的區別是?守護線程的作用是?

「守護線程」是區別於用戶線程哈,「用戶線程」即我們手動創建的線程,而守護線程是程序運行的時候在後臺提供一種「通用服務的線程」。垃圾回收線程就是典型的守護線程。

**「守護線程和非守護線程的區別是?」**我們通過例子來看吧~

    public static void main(String\[\] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
                while (true) {
                    try {
                        Thread.sleep(1000);
                        System.out.println("我是子線程(用戶線程.I am running");
                    } catch (Exception e) {
                    }
                }
        });
        //標記爲守護線程
        t1.setDaemon(true);
        //啓動線程
        t1.start();

        Thread.sleep(3000);
        System.out.println("主線程執行完畢...");
    }

運行結果:

可以發現標記爲守護線程後,「主線程銷燬停止,守護線程一起銷燬」。我們再看下,去掉 t1.setDaemon(true) 守護標記的效果:

    public static void main(String\[\] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
                while (true) {
                    try {
                        Thread.sleep(1000);
                        System.out.println("我是子線程(用戶線程.I am running");
                    } catch (Exception e) {
                    }
                }
        });
        //啓動線程
        t1.start();

        Thread.sleep(3000);
        System.out.println("主線程執行完畢...");
    }

所以,當主線程退出時,JVM 也跟着退出運行,守護線程同時也會被回收,即使是死循環。如果是用戶線程,它會一直停在死循環跑。這就是**「守護線程和非守護線程的區別」**啦。

守護線程擁有**「自動結束自己生命週期的特性」**,非守護線程卻沒有。如果垃圾回收線程是非守護線程,當 JVM 要退出時,由於垃圾回收線程還在運行着,導致程序無法退出,這就很尷尬。這就是**「爲什麼垃圾回收線程需要是守護線程啦」**。

6.WeakHashMap 瞭解過嘛?它是怎麼工作的?

**「WeakHashMap」類似 HashMap ,不同點在 WeakHashMap 的 key 是「弱引用」**的 key。

談到**「弱引用」**,在這裏回顧下四種引用吧

  • 強引用:Object obj=new Object() 這種,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象。

  • 軟引用: 一般情況不會回收,如果內存不夠要溢出時纔會進行回收

  • 弱引用:當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

  • 虛引用:爲一個對象設置虛引用的唯一目的只是爲了能在這個對象被回收時收到一個系統的通知。

正是因爲 WeakHashMap 使用的是弱引用,「它的對象可能隨時被回收」。WeakHashMap 類的行爲部分**「取決於垃圾回收器的動作」**, 調用兩次 size() 方法返回不同值,調用兩次 isEmpty(),一次返回 true,一次返回 false 都是**「可能的」**。

WeakHashMap**「工作原理」**回答這兩點:

  1. WeakHashMap 具有弱引用的特點:隨時被回收對象。

  2. 發生 GC 時,WeakHashMap 是如何將 Entry 移除的呢?

WeakHashMap 內部的 Entry 繼承了 WeakReference,即弱引用,所以就具有了弱引用的特點,「隨時可能被回收」。看下源碼哈:

    private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;

        /\*\*
         \* Creates new entry.
         \*/
        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
        ......

「WeakHashMap 是如何將 Entry 移除的?」 GC 每次清理掉一個對象之後,引用對象會放到 ReferenceQueue 的,接着呢遍歷 queue 進行刪除。WeakHashMap 的增刪改查操作,就是直接 / 間接調用 expungeStaleEntries() 方法,達到及時清除過期 entry 的目的。可以看下 expungeStaleEntries 源碼哈:

  /\*\*
     \* Expunges stale entries from the table.
     \*/
    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table\[i\];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table\[i\] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

7. 是否瞭解 Java 語法糖嘛?說下 12 種 Java 中常用的語法糖?

語法糖(Syntactic Sugar),也稱糖衣語法,讓程序更加簡潔,有更高的可讀性。Java 中最常用的語法糖主要有泛型、變長參數、條件編譯、自動拆裝箱、內部類等 12 種。

感興趣的朋友,可以看下這篇文章哈:不瞭解這 12 個語法糖,別說你會 Java!

8. 什麼是指針碰撞?什麼是空閒列表?什麼是 TLAB?

一般情況下,JVM 的對象都放在堆內存中(發生逃逸分析除外)。當類加載檢查通過後,Java 虛擬機開始爲新生對象分配內存。如果 Java 堆中內存是絕對規整的,所有被使用過的的內存都被放到一邊,空閒的內存放到另外一邊,中間放着一個指針作爲分界點的指示器,所分配內存僅僅是把那個指針向空閒空間方向挪動一段與對象大小相等的實例,這種分配方式就是 “「指針碰撞」”。

如果 Java 堆內存中的內存並不是規整的,已被使用的內存和空閒的內存相互交錯在一起,不可以進行指針碰撞啦,虛擬機必須維護一個列表,記錄哪些內存是可用的,在分配的時候從列表找到一塊大的空間分配給對象實例,並更新列表上的記錄,這種分配方式就是 “「空閒列表」

對象創建在虛擬機中是非常頻繁的行爲,可能存在線性安全問題。如果一個線程正在給 A 對象分配內存,指針還沒有來的及修改,同時另一個爲 B 對象分配內存的線程,仍引用這之前的指針指向,這就出**「問題」**了。

可以把內存分配的動作按照線程劃分在不同的空間之中進行,每個線程在 Java 堆中預先分配一小塊內存, 這就是**「TLAB(Thread Local Allocation Buffer,本地線程分配緩存)」**。虛擬機通過 - XX:UseTLAB 設定它的。

9.CMS 垃圾回收器的工作過程,CMS 收集器和 G1 收集器的區別。

CMS(Concurrent Mark Sweep) 收集器:是一種以獲得最短回收停頓時間爲目標的收集器,標記清除算法,運作過程:「初始標記,併發標記,重新標記,併發清除」,收集結束會產生大量空間碎片。如圖(下圖來源互聯網):

「CMS 收集器和 G1 收集器的區別:」

10.JVM 調優

JVM 調優其實就是通過調節 JVM 參數,即對垃圾收集器和內存分配的調優,以達到更高的吞吐和性能。JVM 調優主要調節以下參數

「堆棧內存相關」

  • -Xms 設置初始堆的大小

  • -Xmx 設置最大堆的大小

  • -Xmn 設置年輕代大小,相當於同時配置 - XX:NewSize 和 - XX:MaxNewSize 爲一樣的值

  • -Xss  每個線程的堆棧大小

  • -XX:NewSize 設置年輕代大小 (for 1.3/1.4)

  • -XX:MaxNewSize 年輕代最大值 (for 1.3/1.4)

  • -XX:NewRatio 年輕代與年老代的比值 (除去持久代)

  • -XX:SurvivorRatio Eden 區與 Survivor 區的的比值

  • -XX:PretenureSizeThreshold 當創建的對象超過指定大小時,直接把對象分配在老年代。

  • -XX:MaxTenuringThreshold 設定對象在 Survivor 複製的最大年齡閾值,超過閾值轉移到老年代

「垃圾收集器相關」

  • -XX:+UseParallelGC:選擇垃圾收集器爲並行收集器。

  • -XX:ParallelGCThreads=20:配置並行收集器的線程數

  • -XX:+UseConcMarkSweepGC:設置年老代爲併發收集。

  • -XX:CMSFullGCsBeforeCompaction=5 由於併發收集器不對內存空間進行壓縮、整理,所以運行一段時間以後會產生 “碎片”,使得運行效率降低。此值設置運行 5 次 GC 以後對內存空間進行壓縮、整理。

  • -XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,但是可以消除碎片

「輔助信息相關」

  • -XX:+PrintGCDetails 打印 GC 詳細信息

  • -XX:+HeapDumpOnOutOfMemoryError 讓 JVM 在發生內存溢出的時候自動生成內存快照, 排查問題用

  • -XX:+DisableExplicitGC 禁止系統 System.gc(),防止手動誤觸發 FGC 造成問題.

  • -XX:+PrintTLAB 查看 TLAB 空間的使用情況

參考與感謝

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。