1. 程式人生 > >阿里Jvm必問面試題及答案

阿里Jvm必問面試題及答案

什麼是Java虛擬機器?為什麼Java被稱作是“平臺無關的程式語言”?

Java虛擬機器是一個可以執行Java位元組碼的虛擬機器程序。Java原始檔被編譯成能被Java虛擬機器執行的位元組碼檔案。 Java被設計成允許應用程式可以執行在任意的平臺,而不需要程式設計師為每一個平臺單獨重寫或者是重新編譯。Java虛擬機器讓這個變為可能,因為它知道底層硬體平臺的指令長度和其他特性。

Java記憶體結構?

《阿里Jvm必問面試題及答案》

方法區和對是所有執行緒共享的記憶體區域;而java棧、本地方法棧和程式設計師計數器是執行是執行緒私有的記憶體區域。

  • Java堆(Heap),是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。
  • 方法區(Method Area),方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
  • 程式計數器(Program Counter Register),程式計數器(Program Counter Register)是一塊較小的記憶體空間,它的作用可以看做是當前執行緒所執行的位元組碼的行號指示器。
  • JVM棧(JVM Stacks),與程式計數器一樣,Java虛擬機器棧(Java Virtual Machine Stacks)也是執行緒私有的,它的生命週期與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存區域性變量表、操作棧、動態連結、方法出口等資訊。每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。
  • 本地方法棧(Native Method Stacks),本地方法棧(Native Method Stacks)與虛擬機器棧所發揮的作用是非常相似的,其區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的Native方法服務。

解釋記憶體中的棧(stack)、堆(heap)和方法區(method area)的用法

通常我們定義一個基本資料型別的變數,一個物件的引用,還有就是函式呼叫的現場儲存都使用JVM中的棧空間;而通過new關鍵字和構造器建立的物件則放在堆空間,堆是垃圾收集器管理的主要區域,由於現在的垃圾收集器都採用分代收集演算法,所以堆空間還可以細分為新生代和老生代,再具體一點可以分為Eden、Survivor(又可分為From Survivor和To Survivor)、Tenured;方法區和堆都是各個執行緒共享的記憶體區域,用於儲存已經被JVM載入的類資訊、常量、靜態變數、JIT編譯器編譯後的程式碼等資料;程式中的字面量(literal)如直接書寫的100、”hello”和常量都是放在常量池中,常量池是方法區的一部分,。棧空間操作起來最快但是棧很小,通常大量的物件都是放在堆空間,棧和堆的大小都可以通過JVM的啟動引數來進行調整,棧空間用光了會引發StackOverflowError,而堆和常量池空間不足則會引發OutOfMemoryError。

String str = new String("hello");

上面的語句中變數str放在棧上,用new創建出來的字串物件放在堆上,而”hello”這個字面量是放在方法區的。

補充1:較新版本的Java(從Java 6的某個更新開始)中,由於JIT編譯器的發展和”逃逸分析”技術的逐漸成熟,棧上分配、標量替換等優化技術使得物件一定分配在堆上這件事情已經變得不那麼絕對了。

補充2:執行時常量池相當於Class檔案常量池具有動態性,Java語言並不要求常量一定只有編譯期間才能產生,執行期間也可以將新的常量放入池中,String類的intern()方法就是這樣的。 看看下面程式碼的執行結果是什麼並且比較一下Java 7以前和以後的執行結果是否一致。

String s1 = new StringBuilder("go")
    .append("od").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja")
    .append("va").toString();
System.out.println(s2.intern() == s2);

物件分配規則

  • 物件優先分配在Eden區,如果Eden區沒有足夠的空間時,虛擬機器執行一次Minor GC。
  • 大物件直接進入老年代(大物件是指需要大量連續記憶體空間的物件)。這樣做的目的是避免在Eden區和兩個Survivor區之間發生大量的記憶體拷貝(新生代採用複製演算法收集記憶體)。
  • 長期存活的物件進入老年代。虛擬機器為每個物件定義了一個年齡計數器,如果物件經過了1次Minor GC那麼物件會進入Survivor區,之後每經過一次Minor GC那麼物件的年齡加1,知道達到閥值物件進入老年區。
  • 動態判斷物件的年齡。如果Survivor區中相同年齡的所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件可以直接進入老年代。
  • 空間分配擔保。每次進行Minor GC時,JVM會計算Survivor區移至老年區的物件的平均大小,如果這個值大於老年區的剩餘值大小則進行一次Full GC,如果小於檢查HandlePromotionFailure設定,如果true則只進行Monitor GC,如果false則進行Full GC。

什麼是類的載入

類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的Class物件,Class物件封裝了類在方法區內的資料結構,並且向Java程式設計師提供了訪問方法區內的資料結構的介面。

類載入器

《阿里Jvm必問面試題及答案》

  • 啟動類載入器:Bootstrap ClassLoader,負責載入存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫
  • 擴充套件類載入器:Extension ClassLoader,該載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入DK\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴充套件類載入器。
  • 應用程式類載入器:Application ClassLoader,該類載入器由sun.misc.Launcher$AppClassLoader來實現,它負責載入使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器

描述一下JVM載入class檔案的原理機制?

答:JVM中類的裝載是由類載入器(ClassLoader)和它的子類來實現的,Java中的類載入器是一個重要的Java執行時系統元件,它負責在執行時查詢和裝入類檔案中的類。 由於Java的跨平臺性,經過編譯的Java源程式並不是一個可執行程式,而是一個或多個類檔案。當Java程式需要使用某個類時,JVM會確保這個類已經被載入、連線(驗證、準備和解析)和初始化。類的載入是指把類的.class檔案中的資料讀入到記憶體中,通常是建立一個位元組陣列讀入.class檔案,然後產生與所載入類對應的Class物件。載入完成後,Class物件還不完整,所以此時的類還不可用。當類被載入後就進入連線階段,這一階段包括驗證、準備(為靜態變數分配記憶體並設定預設的初始值)和解析(將符號引用替換為直接引用)三個步驟。最後JVM對類進行初始化,包括:1)如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;2)如果類中存在初始化語句,就依次執行這些初始化語句。 類的載入是由類載入器完成的,類載入器包括:根載入器(BootStrap)、擴充套件載入器(Extension)、系統載入器(System)和使用者自定義類載入器(java.lang.ClassLoader的子類)。從Java 2(JDK 1.2)開始,類載入過程採取了父親委託機制(PDM)。PDM更好的保證了Java平臺的安全性,在該機制中,JVM自帶的Bootstrap是根載入器,其他的載入器都有且僅有一個父類載入器。類的載入首先請求父類載入器載入,父類載入器無能為力時才由其子類載入器自行載入。JVM不會向Java程式提供對Bootstrap的引用。下面是關於幾個類載入器的說明:

  • Bootstrap:一般用原生代碼實現,負責載入JVM基礎核心類庫(rt.jar);
  • Extension:從java.ext.dirs系統屬性所指定的目錄中載入類庫,它的父載入器是Bootstrap;
  • System:又叫應用類載入器,其父類是Extension。它是應用最廣泛的類載入器。它從環境變數classpath或者系統屬性java.class.path所指定的目錄中記載類,是使用者自定義載入器的預設父載入器。

描述一下JVM載入class檔案的原理機制?

JVM中類的裝載是由類載入器(ClassLoader)和它的子類來實現的,Java中的類載入器是一個重要的Java執行時系統元件,它負責在執行時查詢和裝入類檔案中的類。

由於Java的跨平臺性,經過編譯的Java源程式並不是一個可執行程式,而是一個或多個類檔案。當Java程式需要使用某個類時,JVM會確保這個類已經被載入、連線(驗證、準備和解析)和初始化。類的載入是指把類的.class檔案中的資料讀入到記憶體中,通常是建立一個位元組陣列讀入.class檔案,然後產生與所載入類對應的Class物件。載入完成後,Class物件還不完整,所以此時的類還不可用。當類被載入後就進入連線階段,這一階段包括驗證、準備(為靜態變數分配記憶體並設定預設的初始值)和解析(將符號引用替換為直接引用)三個步驟。最後JVM對類進行初始化,包括:

  • 1)如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;
  • 2)如果類中存在初始化語句,就依次執行這些初始化語句。

類的載入是由類載入器完成的,類載入器包括:根載入器(BootStrap)、擴充套件載入器(Extension)、系統載入器(System)和使用者自定義類載入器(java.lang.ClassLoader的子類)。

從Java 2(JDK 1.2)開始,類載入過程採取了父親委託機制(PDM)。PDM更好的保證了Java平臺的安全性,在該機制中,JVM自帶的Bootstrap是根載入器,其他的載入器都有且僅有一個父類載入器。類的載入首先請求父類載入器載入,父類載入器無能為力時才由其子類載入器自行載入。JVM不會向Java程式提供對Bootstrap的引用。下面是關於幾個類載入器的說明:

  • Bootstrap:一般用原生代碼實現,負責載入JVM基礎核心類庫(rt.jar);
  • Extension:從java.ext.dirs系統屬性所指定的目錄中載入類庫,它的父載入器是Bootstrap;
  • System:又叫應用類載入器,其父類是Extension。它是應用最廣泛的類載入器。它從環境變數classpath或者系統屬性java.class.path所指定的目錄中記載類,是使用者自定義載入器的預設父載入器。

Java物件建立過程

1.JVM遇到一條新建物件的指令時首先去檢查這個指令的引數是否能在常量池中定義到一個類的符號引用。然後載入這個類(類載入過程在後邊講)

2.為物件分配記憶體。一種辦法“指標碰撞”、一種辦法“空閒列表”,最終常用的辦法“本地執行緒緩衝分配(TLAB)”

3.將除物件頭外的物件記憶體空間初始化為0

4.對物件頭進行必要設定

類的生命週期

類的生命週期包括這幾個部分,載入、連線、初始化、使用和解除安裝,其中前三部是類的載入的過程,如下圖; 《阿里Jvm必問面試題及答案》

  • 載入,查詢並載入類的二進位制資料,在Java堆中也建立一個java.lang.Class類的物件
  • 連線,連線又包含三塊內容:驗證、準備、初始化。 1)驗證,檔案格式、元資料、位元組碼、符號引用驗證; 2)準備,為類的靜態變數分配記憶體,並將其初始化為預設值; 3)解析,把類中的符號引用轉換為直接引用
  • 初始化,為類的靜態變數賦予正確的初始值
  • 使用,new出物件程式中使用
  • 解除安裝,執行垃圾回收

Java物件結構

Java物件由三個部分組成:物件頭、例項資料、對齊填充。

物件頭由兩部分組成,第一部分儲存物件自身的執行時資料:雜湊碼、GC分代年齡、鎖標識狀態、執行緒持有的鎖、偏向執行緒ID(一般佔32/64 bit)。第二部分是指標型別,指向物件的類元資料型別(即物件代表哪個類)。如果是陣列物件,則物件頭中還有一部分用來記錄陣列長度。

例項資料用來儲存物件真正的有效資訊(包括父類繼承下來的和自己定義的)

對齊填充:JVM要求物件起始地址必須是8位元組的整數倍(8位元組對齊)

Java物件的定位方式

控制代碼池、直接指標。

如何判斷物件可以被回收?

判斷物件是否存活一般有兩種方式:

  • 引用計數:每個物件有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收。此方法簡單,無法解決物件相互迴圈引用的問題。
  • 可達性分析(Reachability Analysis):從GC Roots開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的,不可達物件。

JVM的永久代中會發生垃圾回收麼?

垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細檢視垃圾收集器的輸出資訊,就會發現永久代也是被回收的。這就是為什麼正確的永久代大小對避免Full GC是非常重要的原因。請參考下Java8:從永久代到元資料區 (注:Java8中已經移除了永久代,新加了一個叫做元資料區的native記憶體區)

引用的分類

  • 強引用:GC時不會被回收
  • 軟引用:描述有用但不是必須的物件,在發生記憶體溢位異常之前被回收
  • 弱引用:描述有用但不是必須的物件,在下一次GC時被回收
  • 虛引用(幽靈引用/幻影引用):無法通過虛引用獲得物件,用PhantomReference實現虛引用,虛引用用來在GC時返回一個通知。

###GC是什麼?為什麼要有GC? 答:GC是垃圾收集的意思,記憶體處理是程式設計人員容易出現問題的地方,忘記或者錯誤的記憶體回收會導致程式或系統的不穩定甚至崩潰,Java提供的GC功能可以自動監測物件是否超過作用域從而達到自動回收記憶體的目的,Java語言沒有提供釋放已分配記憶體的顯示操作方法。Java程式設計師不用擔心記憶體管理,因為垃圾收集器會自動進行管理。要請求垃圾收集,可以呼叫下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以遮蔽掉顯示的垃圾回收呼叫。 垃圾回收可以有效的防止記憶體洩露,有效的使用可以使用的記憶體。垃圾回收器通常是作為一個單獨的低優先順序的執行緒執行,不可預知的情況下對記憶體堆中已經死亡的或者長時間沒有使用的物件進行清除和回收,程式設計師不能實時的呼叫垃圾回收器對某個物件或所有物件進行垃圾回收。在Java誕生初期,垃圾回收是Java最大的亮點之一,因為伺服器端的程式設計需要有效的防止記憶體洩露問題,然而時過境遷,如今Java的垃圾回收機制已經成為被詬病的東西。移動智慧終端使用者通常覺得iOS的系統比Android系統有更好的使用者體驗,其中一個深層次的原因就在於android系統中垃圾回收的不可預知性。

補充:垃圾回收機制有很多種,包括:分代複製垃圾回收、標記垃圾回收、增量垃圾回收等方式。標準的Java程序既有棧又有堆。棧儲存了原始型區域性變數,堆儲存了要建立的物件。Java平臺對堆記憶體回收和再利用的基本演算法被稱為標記和清除,但是Java對其進行了改進,採用“分代式垃圾收集”。這種方法會跟Java物件的生命週期將堆記憶體劃分為不同的區域,在垃圾收集過程中,可能會將物件移動到不同區域:

  • 伊甸園(Eden):這是物件最初誕生的區域,並且對大多數物件來說,這裡是它們唯一存在過的區域。
  • 倖存者樂園(Survivor):從伊甸園倖存下來的物件會被挪到這裡。
  • 終身頤養園(Tenured):這是足夠老的倖存物件的歸宿。年輕代收集(Minor-GC)過程是不會觸及這個地方的。當年輕代收集不能把物件放進終身頤養園時,就會觸發一次完全收集(Major-GC),這裡可能還會牽扯到壓縮,以便為大物件騰出足夠的空間。 與垃圾回收相關的JVM引數:

-Xms / -Xmx — 堆的初始大小 / 堆的最大大小 -Xmn — 堆中年輕代的大小 -XX:-DisableExplicitGC — 讓System.gc()不產生任何作用 -XX:+PrintGCDetails — 列印GC的細節 -XX:+PrintGCDateStamps — 列印GC操作的時間戳 -XX:NewSize / XX:MaxNewSize — 設定新生代大小/新生代最大大小 -XX:NewRatio — 可以設定老生代和新生代的比例 -XX:PrintTenuringDistribution — 設定每次新生代GC後輸出倖存者樂園中物件年齡的分佈 -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:設定老年代閥值的初始值和最大值 -XX:TargetSurvivorRatio:設定倖存區的目標使用率

判斷一個物件應該被回收

1.該物件沒有與GC Roots相連

2.該物件沒有重寫finalize()方法或finalize()已經被執行過則直接回收(第一次標記)、否則將物件加入到F-Queue佇列中(優先順序很低的佇列)在這裡finalize()方法被執行,之後進行第二次標記,如果物件仍然應該被GC則GC,否則移除佇列。 (在finalize方法中,物件很可能和其他 GC Roots中的某一個物件建立了關聯,finalize方法只會被呼叫一次,且不推薦使用finalize方法)

回收方法區

方法區回收價值很低,主要回收廢棄的常量和無用的類。

如何判斷無用的類:

1.該類所有例項都被回收(Java堆中沒有該類的物件)

2.載入該類的ClassLoader已經被回收

3.該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方利用反射訪問該類

垃圾收集演算法

GC最基礎的演算法有三種: 標記 -清除演算法、複製演算法、標記-壓縮演算法,我們常用的垃圾回收器一般都採用分代收集演算法。

  • 標記 -清除演算法,“標記-清除”(Mark-Sweep)演算法,如它的名字一樣,演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收掉所有被標記的物件。
  • 複製演算法,“複製”(Copying)的收集演算法,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。
  • 標記-壓縮演算法,標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體
  • 分代收集演算法,“分代收集”(Generational Collection)演算法,把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。

垃圾回收器

  • Serial收集器,序列收集器是最古老,最穩定以及效率高的收集器,可能會產生較長的停頓,只使用一個執行緒去回收。
  • ParNew收集器,ParNew收集器其實就是Serial收集器的多執行緒版本。
  • Parallel收集器,Parallel Scavenge收集器類似ParNew收集器,Parallel收集器更關注系統的吞吐量。
  • Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法
  • CMS收集器,CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。
  • G1收集器,G1 (Garbage-First)是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量效能特徵

GC日誌分析

摘錄GC日誌一部分(前部分為年輕代gc回收;後部分為full gc回收):

2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs] 
2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]

通過上面日誌分析得出,PSYoungGen、ParOldGen、PSPermGen屬於Parallel收集器。其中PSYoungGen表示gc回收前後年輕代的記憶體變化;ParOldGen表示gc回收前後老年代的記憶體變化;PSPermGen表示gc回收前後永久區的記憶體變化。young gc 主要是針對年輕代進行記憶體回收比較頻繁,耗時短;full gc 會對整個堆記憶體進行回城,耗時長,因此一般儘量減少full gc的次數

調優命令

Sun JDK監控和故障處理命令有jps jstat jmap jhat jstack jinfo

  • jps,JVM Process Status Tool,顯示指定系統內所有的HotSpot虛擬機器程序。
  • jstat,JVM statistics Monitoring是用於監視虛擬機器執行時狀態資訊的命令,它可以顯示出虛擬機器程序中的類裝載、記憶體、垃圾收集、JIT編譯等執行資料。
  • jmap,JVM Memory Map命令用於生成heap dump檔案
  • jhat,JVM Heap Analysis Tool命令是與jmap搭配使用,用來分析jmap生成的dump,jhat內建了一個微型的HTTP/HTML伺服器,生成dump的分析結果後,可以在瀏覽器中檢視
  • jstack,用於生成java虛擬機器當前時刻的執行緒快照。
  • jinfo,JVM Configuration info 這個命令作用是實時檢視和調整虛擬機器執行引數。

調優工具

常用調優工具分為兩類,jdk自帶監控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。

  • jconsole,Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制檯,用於對JVM中記憶體,執行緒和類等的監控
  • jvisualvm,jdk自帶全能工具,可以分析記憶體快照、執行緒快照;監控記憶體變化、GC變化等。
  • MAT,Memory Analyzer Tool,一個基於Eclipse的記憶體分析工具,是一個快速、功能豐富的Java heap分析工具,它可以幫助我們查詢記憶體洩漏和減少記憶體消耗
  • GChisto,一款專業分析gc日誌的工具

Minor GC與Full GC分別在什麼時候發生?

新生代記憶體不夠用時候發生MGC也叫YGC,JVM記憶體不夠的時候發生FGC

你知道哪些JVM效能調優

  • 設定堆記憶體大小

-Xmx:堆記憶體最大限制。

  • 設定新生代大小。 新生代不宜太小,否則會有大量物件湧入老年代

-XX:NewSize:新生代大小

-XX:NewRatio  新生代和老生代佔比

-XX:SurvivorRatio:伊甸園空間和倖存者空間的佔比

  • 設定垃圾回收器 年輕代用  -XX:+UseParNewGC  年老代用-XX:+UseConcMarkSweepGC

 

原文:Java架構筆記

免費Java高階資料需要自己領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分散式等教程,一共30G。      
傳送門:       https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT