2019年JVM最新面試題

1、JVN內存結構

 

 

方法區和對是所有線程共享的內存區域;而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方法服務。

2、對象分配規則

  • 對象優先分配在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。

3、解釋內存中的棧(stack)、堆(heap)和靜態區(static area)的用法

通常我們定義一個基本數據類型的變量,一個對象的引用,還有就是函數調用的現場保存都使用內存中的棧空間;而通過new關鍵字和構造器創建的對象放在堆空間;程序中的字面量(literal)如直接書寫的100、”hello”和常量都是放在靜態區中。棧空間操作起來最快但是棧很小,通常大量的對象都是放在堆空間,理論上整個內存沒有被其他進程使用的空間甚至硬盤上的虛擬內存都可以被當成堆空間來使用。

String str = new String("hello");

上面的語句中變量str放在棧上,用new創建出來的字符串對象放在堆上,而”hello”這個字面量放在靜態區。

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

Perm Space中保存的是加載class文件。

會引起,出現異常可以設置 -XX:PermSize 的大小。JDK 1.8後,字符串常量不存放在永久帶,而是在堆內存中,JDK8以後沒有永久代概念,而是用元空間替代,元空間不存在虛擬機中,二是使用本地內存。

5、什麼是類的加載

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

類加載器

 

 

  • 啓動類加載器: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)所指定的類,開發者可以直接使用該類加載器

雙親委派機制:類加載器收到類加載請求,自己不加載,向上委託給父類加載,父類加載不了,再自己加載。優勢就是避免Java核心API篡改。

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

自定義類加載的意義:

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

7、描述一下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的引用。

下面是關於幾個類加載器的說明:

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

8、Java對象創建過程

  1. JVM遇到一條新建對象的指令時首先去檢查這個指令的參數是否能在常量池中定義到一個類的符號引用。然後加載這個類(類加載過程在後邊講)
  2. 爲對象分配內存。一種辦法“指針碰撞”、一種辦法“空閒列表”,最終常用的辦法“本地線程緩衝分配(TLAB)”
  3. 將除對象頭外的對象內存空間初始化爲0
  4. 對對象頭進行必要設置

9、類的生命週期

類的生命週期包括這幾個部分,加載、連接、初始化、使用和卸載,其中前三部是類的加載的過程,如下圖:

 

 

  • 加載,查找並加載類的二進制數據,在Java堆中也創建一個java.lang.Class類的對象
  • 連接,連接又包含三塊內容:驗證、準備、初始化。 1)驗證,文件格式、元數據、字節碼、符號引用驗證; 2)準備,爲類的靜態變量分配內存,並將其初始化爲默認值; 3)解析,把類中的符號引用轉換爲直接引用
  • 初始化,爲類的靜態變量賦予正確的初始值
  • 使用,new出對象程序中使用
  • 卸載,執行垃圾回收

10、Java 中會存在內存泄漏嗎,請簡單描述。

理論上Java因爲有垃圾回收機制(GC)不會存在內存泄露問題(這也是Java被廣泛使用於服務器端編程的一個重要原因);然而在實際開發中,可能會存在無用但可達的對象,這些對象不能被GC回收,因此也會導致內存泄露的發生。例如hibernate的Session(一級緩存)中的對象屬於持久態,垃圾回收器是不會回收這些對象的,然而這些對象中可能存在無用的垃圾對象,如果不及時關閉(close)或清空(flush)一級緩存就可能導致內存泄露。下面例子中的代碼也會導致內存泄露。

import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
    private T[] elements;
    private int size = 0;
    private static final int INIT_CAPACITY = 16;
    public MyStack() {
        elements = (T[]) new Object[INIT_CAPACITY];
    }
    public void push(T elem) {
        ensureCapacity();
        elements[size++] = elem;
    }
    public T pop() {
        if(size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }
    private void ensureCapacity() {
        if(elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

上面的代碼實現了一個棧(先進後出(FILO))結構,乍看之下似乎沒有什麼明顯的問題,它甚至可以通過你編寫的各種單元測試。 然而其中的pop方法卻存在內存泄露的問題,當我們用pop方法彈出棧中的對象時,該對象不會被當作垃圾回收,即使使用棧的程序不再引用這些對象,因爲棧內部維護着對這些對象的過期引用(obsolete reference)。在支持垃圾回收的語言中,內存泄露是很隱蔽的,這種內存泄露其實就是無意識的對象保持。 如果一個對象引用被無意識的保留起來了,那麼垃圾回收器不會處理這個對象,也不會處理該對象引用的其他對象,即使這樣的對象只有少數幾個,也可能會導致很多的對象被排除在垃圾回收之外,從而對性能造成重大影響,極端情況下會引發Disk Paging(物理內存與硬盤的虛擬內存交換數據),甚至造成OutOfMemoryError。

11、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:設置倖存區的目標使用率

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

標記清除法,複製算法,標記整理、分代算法。

新生代一般採用複製算法 GC,老年代使用標記整理算法。

垃圾收集器:串行新生代收集器、串行老生代收集器、並行新生代收集器、並行老年代收集器。

CMS(Current Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,它是一種併發收集器,採用的是Mark-Sweep算法。

13、你知道哪些垃圾回收算法?

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

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

14、垃圾回收器

  • 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停頓時間要求的同時,還具備高吞吐量性能特徵

15、如何判斷一個對象是否應該被回收

判斷對象是否存活一般有兩種方式:

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

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

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

17、引用的分類

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

18、調優命令

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 這個命令作用是實時查看和調整虛擬機運行參數。

19、調優工具

常用調優工具分爲兩類,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日誌的工具

20、jstack 是⼲什麼的? jstat 呢?如果線上程序週期性地出現卡頓,你懷疑可 能是 GC 導致的,你會怎麼來排查這個問題?線程⽇志⼀般你會看其中的什麼 部分?

jstack 用來查詢 Java 進程的堆棧信息。

jvisualvm 監控內存泄露,跟蹤垃圾回收、執行時內存、cpu分析、線程分析。

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

新生代內存不夠用時候發生MGC也叫YGC,JVM內存不夠的時候發生FGC

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

permgen space、heap space 錯誤。

常見的原因

  • 內存加載的數據量太大:一次性從數據庫取太多數據;
  • 集合類中有對對象的引用,使用後未清空,GC不能進行回收;
  • 代碼中存在循環產生過多的重複對象;
  • 啓動參數堆內存值小。

23、JDK 1.8之後Perm Space有哪些變動? MetaSpace⼤⼩默認是⽆限的麼? 還是你們會通過什麼⽅式來指定⼤⼩?

JDK 1.8後用元空間替代了 Perm Space;字符串常量存放到堆內存中。

MetaSpace大小默認沒有限制,一般根據系統內存的大小。JVM會動態改變此值。

-XX:MetaspaceSize:分配給類元數據空間(以字節計)的初始大小(Oracle邏輯存儲上的初始高水位,the initial high-water-mark)。此值爲估計值,MetaspaceSize的值設置的過大會延長垃圾回收時間。垃圾回收過後,引起下一次垃圾回收的類元數據空間的大小可能會變大。

-XX:MaxMetaspaceSize:分配給類元數據空間的最大值,超過此值就會觸發Full GC,此值默認沒有限制,但應取決於系統內存的大小。JVM會動態地改變此值。

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

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

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