1. 程式人生 > >Java虛擬機器面試知識點歸納【JAVA核心】

Java虛擬機器面試知識點歸納【JAVA核心】

1.JAVA記憶體結構

Java虛擬機器管理的記憶體包括幾個執行時資料記憶體:方法區、虛擬機器棧、堆、本地方法棧、程式計數器,其中方法區和堆是由執行緒共享的資料區,其他幾個是執行緒隔離的資料區。 

1.1 程式計數器

每個執行緒擁有一個PC暫存器

線上程建立時建立

指向下一條指令的地址

執行本地方法時,PC的值為undefined

1.2 方法區

儲存裝載的類資訊

  型別的常量池

  欄位,方法資訊

  方法位元組碼

通常和永久區(Perm)關聯在一起

1.3 堆記憶體

和程式開發密切相關

應用系統物件都儲存在Java堆中

所有執行緒共享Java堆

對分代GC來說,堆也是分代的

GC管理的主要區域

1.4 Java虛擬機器棧 

核心:一個執行緒一個棧,一個方法一個棧幀

虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每個方法從呼叫直至完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。 

棧記憶體就是虛擬機器棧,或者說是虛擬機器棧中區域性變量表的部分。區域性變量表存放了編輯期可知的各種基本資料型別(boolean、byte、char、short、int、long、double、float)、物件引用(reference)型別和returnAddress型別(指向了一條位元組碼指令的地址) 

其中64位長度的long和double型別的資料會佔用兩個區域性變數空間,其餘的資料型別只佔用1個。 

Java虛擬機器規範對這個區域規定了兩種異常狀況:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常。如果虛擬機器擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。 

1.5 本地方法棧 

本地方法棧和虛擬機器棧發揮的作用是非常類似的,他們的區別是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。 

本地方法棧區域也會丟擲StackOverflowError和OutOfMemoryError異常。 

1.6 執行時常量池 

它是方法區的一部分。class檔案中除了有關的版本、欄位、方法、介面等描述資訊外、還有一項資訊是常量池,用於存放編輯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。 

Java語言並不要求常量一定只有編輯期才能產生,也就是可能將新的常量放入池中,這種特性被開發人員利用得比較多是便是String類的intern()方法。 

當常量池無法再申請到記憶體時會丟擲OutOfMemoryError異常。

2.堆記憶體的構成

現在的GC基本都採用分代收集演算法,如果是分代的,那麼堆也是分代的。如果堆是分代的,那堆空間應該是下面這個樣子:

新生代:當一個物件被建立的時候,特別大的物件放在老年代,普通物件分配在年輕代,大部分物件建立以後都不再使用,物件很快變得不可達,就是物件無用,由於垃圾是被年輕代清理掉的,所以被叫做Minor GC或者Young GC。

過程:不大的新生物件new出來,放在Eden區,第一次GC後所有幸存物件放在survivor 區1,然後又有物件在Eden區new出來,第二次GC後,Eden和survivor區1中的倖存物件全都複製到survivor區2中,也就是survivor from和survivor to。經過多次GC仍然沒有被回收的倖存物件轉入老年代。

老年代:物件如果在建立時就非常大,或者在新生代存活了足夠長的時間而沒有被清理掉(即在幾次Young GC後存活了下來),則會被複制到年老代,年老代的空間一般比年輕代大,能存放更多的物件,在年老代上發生的GC次數也比年輕代少。當年老代記憶體不足時,將執行Major GC,也叫 Full GC。

3.垃圾收集

3.1 什麼是垃圾?

建議畫圖理解,沒有任何引用指向的物件叫垃圾(不完全對,比如互相引用造成的垃圾、環形引用的垃圾)

String ss = new String("laji");

ss = null;

這樣ss就是垃圾了。

3.2 什麼是引用

強引用就是在程式程式碼中普遍存在的,類似Object obj=new Object()這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。

軟引用是用來描述一些還有用但並非必須的元素。對於它在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二回收,如果這次回收還沒有足夠的記憶體才會丟擲記憶體溢位異常。 

弱引用是用來描述非必須物件的,但是它的強度比軟引用更弱一些,被引用關聯的物件只能生存到下一次垃圾收集發生之前,當垃圾收集器工作時,無論當前記憶體是否足夠都會回收掉只被弱引用關聯的物件 

虛引用的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。

3.3 如何確定垃圾

3.3.1 引用計數器法 

給物件新增一個引用計數器,每當由一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的。 不過會有迴圈引用的問題,兩個垃圾惺惺相惜。。。

3.3.2 可達性分析演算法 

順藤摸瓜,能摸到的瓜都是好瓜。通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時(用圖論的話來說就是從GC Roots到這個物件不可達),則證明此物件是不可用的。 

 Java語言中GC Roots的物件包括下面幾種: 
 1.虛擬機器棧(棧幀中的本地變量表)中引用的物件 
 2.方法區中類靜態屬性引用的物件 
 3.方法區中常量引用的物件 
 4.本地方法棧JNI(Native方法)引用的物件

3.4 垃圾收集演算法

3.4.1 標記-清除演算法

演算法分為標記和清除兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。 

不足:一個是效率問題,標記和清除兩個過程的效率都不高,另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後再程式執行過程中需要分配較大的物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。 清除不是擦除,內容不會被馬上清空,直到有新的的內容寫入才會覆蓋。

3.4.2 複製演算法 

它將可用記憶體按照容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這塊的記憶體用完了,就將還活著的物件複製到另一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可。 

不足:記憶體浪費。 

實際中我們並不需要按照1:1比例來劃分記憶體空間,如堆記憶體的新生代,將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor,當另一個Survivor空間沒有足夠空間存放上一次新生代收集下來的存活物件時,這些物件將直接通過分配擔保機制進入老年代。 

3.4.3 標記壓縮演算法 

讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。 主要用在老年代中

3.4.4 分代收集演算法 

只是根據物件存活週期的不同將記憶體劃分為幾塊。一般把Java堆分成新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用標記清理或者標記壓縮演算法來進行回收。

3.5 垃圾收集器

a)Serial收集器: 【序列化的】

【資料量小於100M,單處理器】這個收集器是一個單執行緒的收集器,但它的單執行緒的意義不僅僅說明它會只使用一個CPU或一條收集執行緒去完成收集工作,更重要的是它在進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。 

b)ParNew收集器: 【並行的】

Serial收集器的多執行緒版本,除了使用了多執行緒進行收集以外,其餘行為和Serial收集器一樣 

並行:指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍然處於等待狀態

併發:指使用者執行緒與垃圾收集執行緒同時執行(不一定是並行的,可能會交替執行),使用者程式在繼續執行,而垃圾收集程式運行於另一個CPU上。 

c)Parallel Scanvenge  【並行的】

【峰值表現最重要,對於停頓可接受】該收集器是一個新生代收集器,它是使用複製演算法的收集器,又是並行的多執行緒收集器。 併發量大,不過每次GC時,JVM需要停頓。

吞吐量:就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值。即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間) 

d)Serial old收集器: 是Serial收集器的老年代版本,是一個單執行緒收集器,使用標記整理演算法。 

e)Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多執行緒和標記整理演算法。 

f)CMS收集器: 【併發的】

【對響應時間要求高】CMS收集器是基於標記清除演算法實現的,整個過程分為4個步驟:1.初始標記 2.併發標記 3.重新標記 4.併發清除 
優點:併發收集、停頓時間短 

缺點:

  • CMS收集器對CPU資源非常敏感,CMS預設啟動的回收執行緒是(CPU數量+3)/4; 
  • CMS收集器無法處理浮動垃圾,可能出現Failure失敗而導致一次Full G場地產生。 
  • CMS是基於標記清除演算法實現的。 

g)G1收集器: 【併發的】

它是一款面向伺服器應用的垃圾收集器 ,不僅停頓短,同時併發量大

  • 並行與併發:利用CPU縮短STOP-The-World停頓的時間 
  • 分代收集 
  • 空間整合:不會產生記憶體碎片 
  • 可預測的停頓 

執行方式:初始標記,併發標記,最終標記,篩選回收 

3.6 JVM引數

——————————————————————————————————————————————————

———————————————————————————————————————————

4. JAVA物件的分配與VM調優

4.1 分配原則

物件分配先在棧上分配,如果分配不下再在TLAB分配,如果過大判斷是否需要分配在老年代,最終分配在Eden區

4.1.1 棧上分配

  • 執行緒私有小物件
  • 無逃逸
  • 支援標量替換
  • 無需調整(使用虛擬機器預設設定)

小物件(一般幾十個bytes),在沒有逃逸的情況下,可以直接分配在棧上,可以自動回收,減輕GC壓力;大物件或者逃逸物件無法棧上分配。

4.1.2 執行緒本地分配TLAB (thread local allocation buffer)

  • 佔用Eden,預設1%,執行緒專屬。這樣就不需要加鎖
  • 多執行緒的時候不用競爭Eden就可以申請空間,提高效率
  • 小物件
  • 無需調整(使用虛擬機器預設設定)

4.1.3 老年代

大物件(可以引數設定)

4.1.4 Eden空間

一般物件就在該區域建立

4.2 程式碼測試

程式碼:迴圈new 10000000個物件

4.2.1 測試1

配置:Run Configuration——>Arguments——>VM arguments輸入

-XX: -DoEscapeAnalysis -XX:-EliminateAllocations  -XX:-UseTLAB -XX:+PrintGC

解讀:

-XX: -DoEscapeAnalysis  (不)做逃逸分析(物件也就不能分配到棧上了);

-XX:-EliminateAllocations (不)做標量分析,-號表示取反,否定的意思;

-XX:-UseTLAB (不)使用執行緒本地快取

-XX:+PrintGC 列印GC資訊

-XX:+PrintGCDetails 列印GC詳細資訊(未使用)

也就是往Eden區分析

結果:

發生GC 775次,第一次GC,Eden區從6M下降到600KB

4.2.2 測試2

配置:使用執行緒本地快取

-XX: -DoEscapeAnalysis -XX:-EliminateAllocations  -XX:+UseTLAB -XX:+PrintGC

結果:

發生GC 下降到501次

4.2.3 測試3

配置:在棧上分配

-XX: +DoEscapeAnalysis -XX:+EliminateAllocations -XX:+UseTLAB -XX:+PrintGC

結果:

發生GC 下降到428次

4.2.4 測試4

使用JAVA方法 

Runtime.getRuntime().totalMemory()

Runtime.getRuntime().freeMemory()

兩者相減就是當前佔用的空間大小

4.2.5 測試5

程式碼:

// -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=c:\tmp\jvm.dump -XX:+PrintGCDetails -Xms10M -Xmx10M
//-Xms10M程式起始分配記憶體 -Xmx10M程式最大分配記憶體

public static void main(String[] args) {
    List<Object> lists = new ArrayList<>();
    for(int i=0; i<100000000; i++) lists.add(new byte[1024*1024]);
}

說明:

將堆疊溢位資訊列印到c:\tmp\jvm.dump資料夾中,使用visualVM軟體可以檢視

4.2.6 測試6

程式碼:

static int count = 0;
static void r() {
    count++;
    r();
}
public static void main(String[] args) {
  try{
    r();
  }catch(Throwable t){
    System.out.println(count);
    t.printStackTrace();
  }
}

說明:測試執行緒棧的大小

結果1:

程式異常退出,輸出1102

優化:虛擬機器VM arguments,配置堆stack的起始值(預設128k)

-Xss512k       

結果2:

程式異常退出,輸出9203

說明:

執行緒棧大小 -Xss 設定的小,執行緒的併發數量可以更多;設定的Xss大,執行緒遞迴呼叫更深,所以一個是“胖”,一個是“高”