首頁 > 軟體

10道餓了麼JVM面試真題(兩輪面試親身經歷)

2021-05-20 12:30:58

前言

JVM系列知識,非常的重要,面試基本是必問的。大家面試的時候這方面的知識一定要提前做好儲備。另外小編這裡也整理了100多道的併發程式設計面試專題,想實戰的朋友也可以拿去做一做!

併發程式設計面試題獲取:關注+轉發 私信【併發程式設計】獲取面試真題!

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

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

「什麼是逃逸分析?」逃逸分析(Escape Analysis),是一種可以有效減少Java 程式中同步負載和記憶體堆分配壓力的跨函數全局資料流分析演算法。通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的物件的引用的使用範圍,從而決定是否要將這個物件分配到堆上。

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

「一個逃逸分析的例子」/**

* @author 撿田螺的小男孩

*/

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)。換句話說,就是方法區還是在的,只是實現變了,從永久代變為元空間了。

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

永久代的方法區,和堆使用的實體記憶體是連續的。

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

-XX:PremSize:設定永久代的初始大小-XX:MaxPermSize: 設定永久代的最大值,預設是64M對於「永久代」,如果動態生成很多class的話,就很可能出現「java.lang.OutOfMemoryError: PermGen space錯誤」,因為永久代空間配置有限嘛。最典型的場景是,在web開發比較多jsp頁面的時候。

JDK8之後,方法區存在於元空間(Metaspace)。實體記憶體不再與堆連續,而是直接存在於本地記憶體中,理論上機器「記憶體有多大,元空間就有多大」

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

-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,記錄下棧上和寄存器裡哪些位置是引用。

這些特定的位置主要在:

1.迴圈的末尾(非 counted 迴圈)2.方法臨返回前 / 呼叫方法的call指令後3.可能拋異常的位置這些位置就叫作「安全點(safepoint)。」 使用者程式執行時並非在程式碼指令流的任意位置都能夠在停頓下來開始垃圾收集,而是必須是執行到安全點才能夠暫停。

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

JVM包含兩個子系統和兩個元件,分別為

Class loader(類裝載子系統)Execution engine(執行引擎子系統);Runtime data area(運行時資料區元件)Native Interface(本地介面元件)。

「Class loader(類裝載):」 根據給定的全限定名類名(如:java.lang.Object)來裝載class檔案到運行時資料區的方法區中。「Execution engine(執行引擎)」:執行class的指令。「Native Interface(本地介面):」 與native lib互動,是其它程式語言互動的介面。「Runtime data area(運行時資料區域)」:即我們常說的JVM的記憶體。 首先通過編譯器把 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「工作原理」回答這兩點:

WeakHashMap具有弱引用的特點:隨時被回收物件。 發生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種。

語法糖一、switch 支援 String 與列舉語法糖二、 泛型語法糖三、 自動裝箱與拆箱語法糖四 、 方法變長參數語法糖五 、 列舉語法糖六 、 內部類語法糖七 、條件編譯語法糖八 、 斷言語法糖九 、 數值字面量語法糖十 、 for-each語法糖十一 、 try-with-resource語法糖十二、Lambda表示式感興趣的朋友,可以看下這篇文章哈:不瞭解這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收集器的區別:」

CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;G1收集器收集範圍是老年代和新生代,不需要結合其他收集器使用;CMS收集器以最小的停頓時間為目標的收集器;G1收集器可預測垃圾回收的停頓時間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空間的使用情況


IT145.com E-mail:sddin#qq.com