面試題彙總四 JVM 篇

前言

題目彙總來源 史上最全各類面試題彙總,沒有之一,不接受反駁

 


目錄

前言

JVM

談談你對解析與分派的認識。

分派:靜態分派與動態分派。

你知道哪些或者你們線上使⽤什麼GC策略?它有什麼優勢,適⽤於什麼場景?

Java類加載器包括⼏種?它們之間的⽗⼦關係是怎麼樣的?雙親委派機制是什麼意思?有什麼好處?

如何⾃定義⼀個類加載器?你使⽤過哪些或者你在什麼場景下需要⼀個⾃定義的類加載器嗎?

雙親委派流程

雙親委派機制打破

堆內存設置的參數是什麼?

類加載過程

類加載時機

Perm Space中保存什麼數據?會引起OutOfMemory嗎?

垃圾回收算法

做GC時,⼀個對象在內存各個Space中被移動的順序是什麼?

吞吐量和暫停時間

垃圾收集器

你有沒有遇到過OutOfMemory問題?你是怎麼來處理這個問題的?處理 過程中有哪些收穫?

StackOverflow異常有沒有遇到過?⼀般你猜測會在什麼情況下被觸發?如何指定⼀個線程的堆棧⼤⼩?⼀般你們寫多少?

內存模型以及分區,需要詳細到每個區放什麼。

虛擬機在運行時有哪些優化策略

請解釋StackOverflowError和OutOfMemeryError的區別?

在JVM中,如何判斷一個對象是否死亡?


 

JVM

談談你對解析與分派的認識。

分派:靜態分派與動態分派。

多態方法調用的解析和分派

解析和分派屬於方法調用的內容。

方法調用階段唯一的任務就是確定被調用方法的版本(即調用哪一個方法),暫時還不涉及方法內部具體的運行過程。

解析

將一部分(編譯期可知,運行期不可變)的符號引用轉爲直接引用

調用方法的四種字節碼:

invokestatic 調用靜態方法

invokespacial 調用<init>、私有方法和父類方法

invokevirtual 調用所有虛方法

invokeinterface 調用接口方法,運行時確定實現此接口的方法

能被 invokestatic 和 invokespecial 調用的方法可以在解析時確定唯一版本調用。

Java中符合的有靜態方法、私有方法、實例構造器和父類方法,它們不可繼承或重寫,稱爲非虛方法。

另外,final方法也是非虛方法,雖然是 invokevirtual 指令調用,但無法被覆蓋,對多態來說選擇結果唯一。

分派

分派與面向對象的三個基本特徵之一:多態有關,如重載和重寫。

解析和分派不是對立關係,如重載靜態方法,既發生瞭解析,又發生了靜態分派。

靜態分派

依據靜態類型來定位方法執行版本的分派動作稱爲靜態分派。

和方法重載有關,重載方法的參數類型是依據靜態類型,而靜態類型編譯期已知。

動態分派

運行期根據實際類型確定方法執行版本的分派過程稱爲動態分派。

單分派和多分派

Java靜態多分派,動態單分派

靜態分派,根據靜態類型和參數類型兩個宗量進行選擇,多分派

動態分派,傳入參數已確定,只要判斷最終執行對象類型,單分派。

 

你知道哪些或者你們線上使⽤什麼GC策略?它有什麼優勢,適⽤於什麼場景?

觸發JVM進行Full GC的情況及應對策略

Minor GC:指發生在新生代的垃圾收集動作

MajorGC / Full GC:指發生在老年代的GC

對象優先分配在Eden區

eden區空間不足時Minor GC

大對象直接進入老年代

避免短命大對象

長期存活對象進入老年代

每個對象有一個對象年齡(Age)計數器

Eden區一個對象第一次Minor GC存活並能被Survivor區容納,則移至Survivor且對象年齡設爲1

在Survivor中每經過一次Minor GC對象年齡+1

到達一定年齡(默認15)移至老年代

動態對象年齡判斷

Survivor中相同年齡所有對象大小的總和大於Survivor空間的一半,大於等於該年齡的對象直接進入老年代

空間分配擔保

Minor GC時,若之前每次晉升入老年代的平均大小>老年代剩餘空間,則直接Full GC

否則若設置允許擔保失敗,則只Minor GC,否則Full GC

 

Java類加載器包括⼏種?它們之間的⽗⼦關係是怎麼樣的?雙親委派機制是什麼意思?有什麼好處?

深入理解和探究Java類加載機制----

Java類加載器包括幾種?父子關係?

這裏的層次關係爲組合,即子加載器持有父加載器的引用,而非繼承關係。

Application ClassLoader 應該和 System ClassLoader 是一個意思

雙親委派機制

雙親委派模型如上圖所示。

工作過程:一個類加載器收到類加載請求,先把這個請求委託給父類加載器完成,因此所有加載請求都會傳送到頂層啓動加載器中,只有當父加載器反饋自己無法完成加載請求(其搜索範圍沒有找到所需類),子類才嘗試自己加載。

雙親委派機制好處

保證java核心庫的安全性(例如:如果用戶自己寫了一個java.lang.String類就會因爲雙親委派機制不能被加載,不會破壞原生的String類的加載)

 

如何⾃定義⼀個類加載器?你使⽤過哪些或者你在什麼場景下需要⼀個⾃定義的類加載器嗎?

  1. 加載特定路徑的class文件
  2. 加載一個加密的網絡class文件
  3. 熱部署加載class文件

 

雙親委派流程

深入理解Java類加載器(ClassLoader)

loadClass

找緩存 → 找父加載器 loadClass → 自己嘗試 findClass → 根據條件解析 class

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

findClass

JDK1.2後不建議覆蓋 loadClass,將自定義加載類的過程放在 findClass 裏。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

 

雙親委派機制打破

服務提供者接口SPI存在於rt.jar,由Bootstrap類加載器加載,但服務方提供的SPI實現類無法由Bootstrap類加載器加載,需由App類加載器加載。Thread類中的contextClassLoader默認保存App類加載器的引用,Bootstrap類加載器以此委託App類加載器加載SPI實現類。但這就打破了雙親委派模型。

 

堆內存設置的參數是什麼?


 

類加載過程

Java 類加載過程

加載:通過類的全限定名獲取二進制流,加載入方法區,生成一個代表這個類的java.lang.Class對象。

驗證:確保class字節流符合規定且安全。

準備:類變量賦初值(準確說是置零)。

解析:符號引用轉換爲直接引用。

初始化:調用類的<clinit>()方法,初始化類的資源和變量。

 

類加載時機

面試必問之JVM篇

JVM類生命週期概述:加載時機與加載過程

  • new新對象 / 讀取或設置static屬性 / 調用static方法;
  • java.lang.reflect對類反射;
  • 子類初始化時先初始化父類;
  • 虛擬機啓動時先初始化main方法所在的主類;
  • jdk1.7動態語言支持

 

Perm Space中保存什麼數據?會引起OutOfMemory嗎?

Java方法區和永久代

Java8內存模型—永久代(PermGen)和元空間(Metaspace)

JAVA 方法區與堆--java7前,java7,java8各不相同

方法區存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等

 

java7之前,方法區位於永久代(PermGen),永久代和堆相互隔離,永久代的大小在啓動JVM時可以設置一個固定值,不可變;

java7中,存儲在永久代的部分數據就已經轉移到Java Heap或者Native memory。但永久代仍存在於JDK 1.7中,並沒有完全移除,譬如符號引用(Symbols)轉移到了native memory;字符串常量池(interned strings)轉移到了Java heap;類的靜態變量(class statics)轉移到了Java heap。

java8中,取消永久代,方法存放於元空間(Metaspace),元空間仍然與堆不相連,但與堆共享物理內存,邏輯上可認爲在堆中

由於方法區主要存儲類的相關信息,所以對於動態生成類的情況比較容易出現永久代的內存溢出。最典型的場景就是,在 jsp 頁面比較多的情況,容易出現永久代內存溢出。

 

垃圾回收算法

垃圾回收算法

標記清除算法 Mark-Sweep

第一遍掃描內存標記出無用對象,第二遍掃描內存清除無用對象。

效率低且會造成內存碎片化。

複製算法 Copying

將存活對象複製到新內存空間中,清空舊內存空間。

適用於對象存活率低的情況。

JVM中,新生代的Survivor區用到該算法。

標記整理算法 Mark-Compact

在標記清除算法基礎上,把對象移向內存一端,留出連續的內存空間,解決碎片化問題。

適用於對象較大、存活率較高的情況。

JVM中,老年代使用該算法。

分代收集算法 Generational Collection

JVM中,新生代使用Copying,老年代使用Mark-Compact。

 

做GC時,⼀個對象在內存各個Space中被移動的順序是什麼?

參照上面GC策略的過程。

普通對象生於Eden區,然後在兩個Survivor區中往返,最後進入老年代。中途死於各種GC。

大對象或通過動態對象年齡判定的對象直接進入老年代。死於Major GC。

 

吞吐量和暫停時間

JVM實用參數(六) 吞吐量收集器

吞吐量:應用程序線程用時佔程序總用時的比例。GC線程會導致應用程序線程暫停,吞吐量越高,程序執行越快。

暫停時間:GC線程導致應用程序線程暫停的時長。過長的暫停時間會影響用戶體驗。

 

垃圾收集器

Jvm垃圾回收器(終結篇)

 

你有沒有遇到過OutOfMemory問題?你是怎麼來處理這個問題的?處理 過程中有哪些收穫?

常見的原因如下:

1)內存加載的數據量太大:一次性從數據庫取太多數據;

2)集合類中有對對象的引用,使用後未清空,GC不能進行回收;

3)代碼中存在循環產生過多的重複對象;

4)啓動參數堆內存值小。

 

StackOverflow異常有沒有遇到過?⼀般你猜測會在什麼情況下被觸發?如何指定⼀個線程的堆棧⼤⼩?⼀般你們寫多少?

觸發情況:

棧內存溢出,一般由棧內存的局部變量過爆了,導致內存溢出。出現在遞歸方法,參數個數過多,遞歸過深,遞歸沒有出口。

棧大小設置:

JVM優化系列之一(-Xss調整Stack Space的大小)

-Xss:如-Xss128k

JDK5.0以後每個線程堆 棧大小爲1M,以前每個線程堆棧大小爲256K。根據應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。

線程棧的大小是個雙刃劍,如果設置過小,可能會出現棧溢出,特別是在該線程內有遞歸、大的循環時出現溢出的可能性更大,如果該值設置過大,就有影響到創建棧的數量,如果是多線程的應用,就會出現內存溢出的錯誤。

 

內存模型以及分區,需要詳細到每個區放什麼。

內存模型以及分區,需要詳細到每個區放什麼。

程序計數器

較小的內存空間,當前線程所執行的字節碼的行號指示器。

線程私有。

Java虛擬機棧

線程私有。

描述java方法執行的內存模型。

每個方法執行時創建一個棧幀用於存儲局部變量表,操作棧,動態鏈接,方法出口等。

局部變量表:基本數據類型+reference,long和double兩個slot,其餘1個。

局部變量表的大小在編譯期確定。

異常情況:棧幀過大 StackOverflowError;虛擬機棧擴展無法申請足夠內存 OutOfMemoryError。

本地方法棧

與虛擬機棧類似,不過是執行Native方法服務。

java堆

線程共享。

對象實例和數組在此分配。

垃圾收集器管理的主要區域。

可以劃分線程私有的分配緩衝區(TLAB)。

方法區

線程共享。

存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等。

運行時常量池

方法區的一部分。

存放編譯期生成的各種字面量和符號引用。

直接內存

不屬於Java虛擬機內存。

某些io操作直接操控native堆。

 

虛擬機在運行時有哪些優化策略

JVM(1)---虛擬機在運行期的優化策略

 

請解釋StackOverflowError和OutOfMemeryError的區別?

根據《深入理解Java虛擬機》裏的說法,StackOverflowError出現在虛擬機棧和本地方法棧中,對虛擬機棧來說,StackOverflowError在線程請求的棧深度大於虛擬機所允許的深度時拋出;OutOfMemoryError出現在除程序計數器之外的其他區域,基本都是因爲無法滿足內存分配且無法擴展而產生的。

 

在JVM中,如何判斷一個對象是否死亡?

首先使用根搜索算法判定對象是否存活。

若根搜索算法不可達,還要經歷兩次標記:

  1. 判定對象是否有必要執行 finalize() 方法。若對象沒有覆蓋 finalize() 或已被虛擬機執行過則會被判定“不必要執行”。
  2. 若被判定要執行 finalize() 方法,對象會被移進 F-Queue等待方法執行,若在方法中對象被引用鏈上的任意對象引用,在第二次標記中會被移出“即將回收”集合。

對象被回收後,判定真正死亡。

 

 

 

 

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