2021最新 Java虛擬機(JVM)面試題精選(附刷題小程序)

推薦使用小程序閱讀

爲了能讓您更加方便的閱讀
本文全部的面試題目均已整理至小程序《面試手冊》
能夠經過微信掃描(或長按)下圖的二維碼享受更好的閱讀體驗!java

ah_xct



目錄android



1. JVM 基礎

1.1 JVM 內存分哪幾個區,每一個區的做用是什麼?

方法區

1. 有時候也成爲永久代,在該區內不多發生垃圾回收,可是並不表明不發生 GC,在這裏進行的 GC 主要是對方法區裏的常量池和對類型的卸載

2. 方法區主要用來存儲已被虛擬機加載的類的信息、常量、靜態變量和即時編譯器編譯後的代碼等數據。

3. 該區域是被線程共享的。

4. 方法區裏有一個運行時常量池,用於存放靜態編譯產生的字面量和符號引用。該常量池具備動態性,也就是說常量並不必定是編譯時肯定,運行時生成的常量也會存在這個常量池中。

虛擬機棧:

  1. 虛擬機棧也就是咱們日常所稱的棧內存,它爲 java 方法服務,每一個方法在執行的時候都會建立一個棧幀,用於存儲局部變量表、操做數棧、動態連接和方法出口等信息。程序員

  2. 虛擬機棧是線程私有的,它的生命週期與線程相同。面試

  3. 局部變量表裏存儲的是基本數據類型、returnAddress 類型(指向一條字節碼指令的地址)和對象引用,這個對象引用有多是指向對象起始地址的一個指針,也有多是表明對象的句柄或者與對象相關聯的位置。局部變量所需的內存空間在編譯器間肯定算法

  4. 操做數棧的做用主要用來存儲運算結果以及運算的操做數,它不一樣於局部變量表經過索引來訪問,而是壓棧和出棧的方式編程

  5. 每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接.動態連接就是將常量池中的符號引用在運行期轉化爲直接引用。小程序

本地方法棧

本地方法棧和虛擬機棧相似,只不過本地方法棧爲 Native 方法服務。數組

java 堆是全部線程所共享的一塊內存,在虛擬機啓動時建立,幾乎全部的對象實例都在這裏建立,所以該區域常常發生垃圾回收操做。瀏覽器

程序計數器

內存空間小,字節碼解釋器工做時經過改變這個計數值能夠選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理和線程恢復等功能都須要依賴這個計數器完成。該內存區域是惟一一個 java 虛擬機規範沒有規定任何 OOM 狀況的區域。緩存

1.2 對象的訪問定位有幾種方式?

Java程序須要經過 JVM 棧上的引用訪問堆中的具體對象。對象的訪問方式取決於 JVM 虛擬機的實現。目前主流的訪問方式有 句柄 和 直接指針 兩種方式。

  • 句柄:
    能夠理解爲指向指針的指針,維護着對象的指針。句柄不直接指向對象,而是指向對象的指針(句柄不發生變化,指向固定內存地址),再由對象的指針指向對象的真實內存地址。
  • 直接指針:
    指向對象,表明一個對象在內存中的起始地址。

句柄訪問

Java堆中劃分出一塊內存來做爲句柄池,引用中存儲對象的句柄地址,而句柄中包含了對象實例數據與對象類型數據各自的具體地址信息,具體構造以下圖所示:
句柄訪問對象.png
優點:引用中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而引用自己不須要修改。

直接指針訪問

若是使用直接指針訪問,引用 中存儲的直接就是對象地址,那麼Java堆對象內部的佈局中就必須考慮如何放置訪問類型數據的相關信息。
指針訪問對象.png
優點:速度更快,節省了一次指針定位的時間開銷。因爲對象的訪問在Java中很是頻繁,所以這類開銷聚沙成塔後也是很是可觀的執行成本。HotSpot 中採用的就是這種方式。

1.3 JVM內存模型是什麼?

java 內存模型(JMM)是線程間通訊的控制機制.JMM 定義了主內存和線程之間抽象關係。線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是 JMM 的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。Java 內存模型的抽象示意圖以下:

線程 A 與線程 B 之間如要通訊的話,必需要經歷下面 2 個步驟:

  1. 首先,線程 A 把本地內存 A 中更新過的共享變量刷新到主內存中去。
  2. 而後,線程 B 到主內存中去讀取線程 A 以前已更新過的共享變量。

1.4 finalize()方法何時被調用?析構函數(finalization)的目的是什麼?

垃圾回收器(garbage colector)決定回收某對象時,就會運行該對象的finalize()方法;
finalize是Object類的一個方法,該方法在Object類中的聲明protected void finalize() throws Throwable { }
在垃圾回收器執行時會調用被回收對象的finalize()方法,能夠覆蓋此方法來實現對其資源的回收。注意:一旦垃圾回收器準備釋放對象佔用的內存,將首先調用該對象的finalize()方法,而且下一次垃圾回收動做發生時,才真正回收對象佔用的內存空間

GC原本就是內存回收了,應用還須要在finalization作什麼呢? 答案是大部分時候,什麼都不用作(也就是不須要重載)。只有在某些很特殊的狀況下,好比你調用了一些native的方法(通常是C寫的),能夠要在finaliztion裏去調用C的釋放函數。

1.5 什麼是深拷貝和淺拷貝?什麼是深複製和淺複製?

淺拷貝(shallowCopy)只是增長了一個指針指向已存在的內存地址,

深拷貝(deepCopy)是增長了一個指針而且申請了一個新的內存,使這個增長的指針指向這個新的內存,

使用深拷貝的狀況下,釋放內存的時候不會由於出現淺拷貝時釋放同一個內存的錯誤。

淺複製:僅僅是指向被複制的內存地址,若是原地址發生改變,那麼淺複製出來的對象也會相應的改變。

深複製:在計算機中開闢一塊新的內存地址用於存放複製的對象。

1.6 說一下堆棧的區別?

  • 物理地址

    • 堆的物理地址分配對對象是不連續的。所以性能慢些。在GC的時候也要考慮到不連續的分配,因此有各類算法。好比,標記-消除,複製,標記-壓縮,分代(即新生代使用複製算法,老年代使用標記——壓縮)

    • 棧使用的是數據結構中的棧,先進後出的原則,物理地址分配是連續的。因此性能快。

  • 內存分別

    • 堆由於是不連續的,因此分配的內存是在運行期確認的,所以大小不固定。通常堆大小遠遠大於棧。
    • 棧是連續的,因此分配的內存大小要在編譯期就確認,大小是固定的。
  • 存放的內容

    • 堆存放的是對象的實例和數組。所以該區更關注的是數據的存儲
    • 棧存放:局部變量,操做數棧,返回結果。該區更關注的是程序方法的執行。
      PS:
  1. 靜態變量放在方法區
  2. 靜態的對象仍是放在堆。

程序的可見度

  • 堆對於整個應用程序都是共享、可見的。
  • 棧只對於線程是可見的。因此也是線程私有。他的生命週期和線程相同。

1.7 隊列和棧是什麼?有什麼區別?

隊列和棧都是被用來預存儲數據的。

  • 操做的名稱不一樣。隊列的插入稱爲入隊,隊列的刪除稱爲出隊。棧的插入稱爲進棧,棧的刪除稱爲出棧。
  • 可操做的方式不一樣。隊列是在隊尾入隊,隊頭出隊,即兩邊均可操做。而棧的進棧和出棧都是在棧頂進行的,沒法對棧底直接進行操做。
  • 操做的方法不一樣。隊列是先進先出(FIFO),即隊列的修改是依先進先出的原則進行的。新來的成員老是加入隊尾(不能從中間插入),每次離開的成員老是隊列頭上(不容許中途離隊)。而棧爲後進先出(LIFO),即每次刪除(出棧)的老是當前棧中最新的元素,即最後插入(進棧)的元素,而最早插入的被放在棧的底部,要到最後才能刪除。

1.8 Java會存在內存泄漏嗎?請簡單描述

內存泄漏是指再也不被使用的對象或者變量一直被佔據在內存中。理論上來講,Java是有GC垃圾回收機制的,也就是說,再也不被使用的對象,會被GC自動回收掉,自動從內存中清除。

可是,即便這樣,Java也仍是存在着內存泄漏的狀況,java致使內存泄露的緣由很明確:長生命週期的對象持有短生命週期對象的引用就極可能發生內存泄露,儘管短生命週期對象已經再也不須要,可是由於長生命週期對象持有它的引用而致使不能被回收,這就是java中內存泄露的發生場景。

1.9 Java對象結構是什麼?

Java對象由三個部分組成:對象頭、實例數據、對齊填充。

  • 對象頭由兩部分組成

    • 哈希碼;
    • GC分代年齡;
    • 鎖標識狀態;
    • 線程持有的鎖;
    • 偏向線程ID(通常佔32/64 bit)。
    • 第一部分存儲對象自身的運行時數據:
    • 第二部分是指針類型,指向對象的類元數據類型(即對象表明哪一個類)。若是是數組對象,則對象頭中還有一部分用來記錄數組長度。
  • 實例數據用來存儲對象真正的有效信息(包括父類繼承下來的和本身定義的)

  • 對齊填充:JVM要求對象起始地址必須是8字節的整數倍(8字節對齊)

1.10 引用的分類有幾種?

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

2. JVM 垃圾回收

2.1 如和判斷一個對象是否存活?(或者 GC 對象的斷定方法)

判斷一個對象是否存活有兩種方法:

引用計數法

所謂引用計數法就是給每個對象設置一個引用計數器,每當有一個地方引用這個對象時,就將計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器爲零時,說明此對象沒有被引用,也就是「死對象」,將會被垃圾回收.

引用計數法有一個缺陷就是沒法解決循環引用問題,也就是說當對象 A 引用對象 B,對象 B 又引用者對象 A,那麼此時 A,B 對象的引用計數器都不爲零,也就形成沒法完成垃圾回收,因此主流的虛擬機都沒有采用這種算法。

可達性算法(引用鏈法)

該算法的思想是:從一個被稱爲 GC Roots 的對象開始向下搜索,若是一個對象到 GC Roots 沒有任何引用鏈相連時,則說明此對象不可用。在 java 中能夠做爲 GC Roots 的對象有如下幾種:

  • 虛擬機棧中引用的對象
  • 方法區類靜態屬性引用的對象
  • 方法區常量池引用的對象
  • 本地方法棧 JNI 引用的對象

雖然這些算法能夠斷定一個對象是否能被回收,可是當知足上述條件時,一個對象不必定會被回收。當一個對象不可達 GC Root 時,這個對象並不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收須要經歷兩次標記

若是對象在可達性分析中沒有與 GC Root 的引用鏈,那麼此時就會被第一次標記而且進行一次篩選,篩選的條件是是否有必要執行 finalize()方法。當對象沒有覆蓋 finalize()方法或者已被虛擬機調用過,那麼就認爲是不必的。

若是該對象有必要執行 finalize()方法,那麼這個對象將會放在一個稱爲 F-Queue 的對隊列中,虛擬機會觸發一個 Finalize()線程去執行,此線程是低優先級的,而且虛擬機不會承諾一直等待它運行完,這是由於若是 finalize()執行緩慢或者發生了死鎖,那麼就會形成 F-Queue 隊列一直等待,形成了內存回收系統的崩潰。GC 對處於 F-Queue 中的對象進行第二次被標記,這時,該對象將被移除」即將回收」集合,等待回收。

2.2 簡述 java 垃圾回收機制?

在 java 中,程序員是不須要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在JVM 中,有一個垃圾回收線程,它是低優先級的,在正常狀況下是不會執行的,只有在虛擬機空閒或者當前堆內存不足時,纔會觸發執行,掃面那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。

2.3 垃圾回收有什麼目的?何時進行垃圾回收?

垃圾回收是在內存中存在沒有引用的對象或超過做用域的對象時進行的。

垃圾回收的目的是識別而且丟棄應用再也不使用的對象來釋放和重用資源。

2.4 若是對象的引用被置爲null,垃圾收集器是否會當即釋放對象佔用的內存?

不會,在下一個垃圾回調週期中,這個對象將是被可回收的。

也就是說並不會當即被垃圾收集器馬上回收,而是在下一次垃圾回收時纔會釋放其佔用的內存。

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

2.6 垃圾回收的優勢有那些?

  • java語言最顯著的特色就是引入了垃圾回收機制,它使java程序員在編寫程序時再也不考慮內存管理的問題。
  • 因爲有這個垃圾回收機制,java中的對象再也不有「做用域」的概念,只有引用的對象纔有「做用域」。
  • 垃圾回收機制有效的防止了內存泄露,能夠有效的使用可以使用的內存。
  • 垃圾回收器一般做爲一個單獨的低級別的線程運行,在不可預知的狀況下對內存堆中已經死亡的或很長時間沒有用過的對象進行清除和回收。

2.7 垃圾回收器的基本原理是什麼?

對於GC來講,當程序員建立對象時,GC就開始監控這個對象的地址、大小以及使用狀況。

2.8 垃圾回收器能夠立刻回收內存嗎?

不會;一般,GC採用有向圖的方式記錄和管理堆(heap)中的全部對象。經過這種方式肯定哪些對象是"可達的",哪些對象是"不可達的"。當GC肯定一些對象爲"不可達"時,GC就有責任回收這些內存空間。而這個回收操做時達到必定閾值或者條件以後纔會觸發回收;並非實時的。

2.9 有什麼辦法主動通知虛擬機進行垃圾回收?

能夠。程序員能夠手動執行System.gc(),通知GC運行,可是Java語言規範並不保證GC必定會執行。

2.10 Java 中都有哪些引用類型?

  • 強引用:發生 gc 的時候不會被回收。
  • 軟引用:有用但不是必須的對象,在發生內存溢出以前會被回收。
  • 弱引用:有用但不是必須的對象,在下一次GC時會被回收。
  • 虛引用(幽靈引用/幻影引用):沒法經過虛引用得到對象,用 PhantomReference 實現虛引用,虛引用的用途是在 gc 時返回一個通知。

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

垃圾回收不會發生在永久代,若是永久代滿了或者是超過了臨界值,會觸發徹底垃圾回收(Full GC)。若是你仔細查看垃圾收集器的輸出信息,就會發現永久代也是被回收的。這就是爲何正確的永久代大小對避免Full GC是很是重要的緣由。

java 8中,永久代被移除,取而代之的爲元空間。

2.12 JVM 有哪些垃圾回收算法?

  1. 標記-清除:
    這是垃圾收集算法中最基礎的,根據名字就能夠知道,它的思想就是標記哪些要被回收的對象,而後統一回收。這種方法很簡單,可是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,致使之後程序在分配較大的對象時,因爲沒有充足的連續內存而提早觸發一次 GC 動做。

  2. 複製算法:
    爲了解決效率問題,複製算法將可用內存按容量劃分爲相等的兩部分,而後每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象複製到第二塊內存上,而後一次性清楚完第一塊內存,再將第二塊上的對象複製到第一塊。可是這種方式,內存的代價過高,每次基本上都要浪費通常的內存。
    因而將該算法進行了改進,內存區域再也不是按照 1:1 去劃分,而是將內存劃分爲 8:1:1 三部分,較大那分內存交 Eden 區,其他是兩塊較小的內存區叫 Survior 區。每次都會優先使用 Eden 區,若 Eden 區滿,就將對象複製到第二塊內存區上,而後清除 Eden 區,若是此時存活的對象太多,以致於 Survivor 不夠時,會將這些對象經過分配擔保機制複製到老年代中。(java 堆又分爲新生代和老年代)

  3. 標記-整理
    該算法主要是爲了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了複製算法的效率問題。它的不一樣之處就是在清除對象的時候現將可回收對象移動到一端,而後清除掉端邊界之外的對象,這樣就不會產生內存碎片了。

  4. 分代收集
    如今的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲新生代和老年代。在新生代中,因爲對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔保,因此可使用標記-整理 或者 標記-清除。

2.13 簡述一下標記-清除算法

標記無用對象,而後進行清除回收。

標記-清除算法(Mark-Sweep)是一種常見的基礎垃圾收集算法,它將垃圾收集分爲兩個階段:

  • 標記階段:標記出能夠回收的對象。
  • 清除階段:回收被標記的對象所佔用的空間。

標記-清除算法之因此是基礎的,是由於後面講到的垃圾收集算法都是在此算法的基礎上進行改進的。

優勢:實現簡單,不須要對象進行移動。

缺點:標記、清除過程效率低,產生大量不連續的內存碎片,提升了垃圾回收的頻率。

標記-清除算法的執行的過程以下圖所示
標記清除.png

2.14 簡述一下複製算法

爲了解決標記-清除算法的效率不高的問題,產生了複製算法。它把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾收集時,遍歷當前使用的區域,把存活對象複製到另一個區域中,最後將當前使用的區域的可回收的對象進行回收。

  • 優勢:
    按順序分配內存便可,實現簡單、運行高效,不用考慮內存碎片。

  • 缺點:
    可用的內存大小縮小爲原來的一半,對象存活率高時會頻繁進行復制。

複製算法的執行過程以下圖所示
複製算法.jpg

2.15 簡述一下標記-整理算法

在新生代中可使用複製算法,可是在老年代就不能選擇複製算法了,由於老年代的對象存活率會較高,這樣會有較多的複製操做,致使效率變低。標記-清除算法能夠應用在老年代中,可是它效率不高,在內存回收後容易產生大量內存碎片。所以就出現了一種標記-整理算法(Mark-Compact)算法,與標記-整理算法不一樣的是,在標記可回收的對象後將全部存活的對象壓縮到內存的一端,使他們緊湊的排列在一塊兒,而後對端邊界之外的內存進行回收。回收後,已用和未用的內存都各自一邊。

  • 優勢:
    解決了標記-清理算法存在的內存碎片問題。

  • 缺點:
    仍須要進行局部對象移動,必定程度上下降了效率。

標記-整理算法的執行過程以下圖所示
標記清除壓縮算法.jpg

2.16 簡述一下分代收集算法

當前商業虛擬機都採用分代收集的垃圾收集算法。分代收集算法,顧名思義是根據對象的存活週期將內存劃分爲幾塊。通常包括年輕代、老年代 和 永久代

如圖所示:
分代回收算法.jpg

2.17 JVM 有哪些垃圾回收器?

若是說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。下圖展現了7種做用於不一樣分代的收集器,其中用於回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,還有用於回收整個Java堆的G1收集器。不一樣收集器之間的連線表示它們能夠搭配使用。
垃圾回收期.jpg

  • Serial收集器(複製算法):
    新生代單線程收集器,標記和清理都是單線程,優勢是簡單高效;
  • ParNew收集器 (複製算法):
    新生代收並行集器,其實是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現;
  • Parallel Scavenge收集器 (複製算法):
    新生代並行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用戶線程時間/(用戶線程時間+GC線程時間),高吞吐量能夠高效率的利用CPU時間,儘快完成程序的運算任務,適合後臺應用等對交互相應要求不高的場景;
  • Serial Old收集器 (標記-整理算法):
    老年代單線程收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (標記-整理算法):
    老年代並行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(標記-清除算法):
    老年代並行收集器,以獲取最短回收停頓時間爲目標的收集器,具備高併發、低停頓的特色,追求最短GC回收停頓時間。
  • G1(Garbage First)收集器 (標記-整理算法):
    Java堆並行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基於「標記-整理」算法實現,也就是說不會產生內存碎片。此外,G1收集器不一樣於以前的收集器的一個重要特色是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限於新生代或老年代。

2.18 詳細介紹一下 CMS 垃圾回收器?

CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量爲代價來得到最短回收停頓時間的垃圾回收器。對於要求服務器響應速度的應用上,這種垃圾回收器很是適合。在啓動 JVM 的參數加上「-XX:+UseConcMarkSweepGC」來指定使用 CMS 垃圾回收器。

CMS 使用的是標記-清除的算法實現的,因此在 gc 的時候回產生大量的內存碎片,當剩餘內存不能知足程序運行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會採用 Serial Old 回收器進行垃圾清除,此時的性能將會被下降。

2.19 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什麼區別?

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

新生代垃圾回收器通常採用的是複製算法,複製算法的優勢是效率高,缺點是內存利用率低;老年代回收器通常採用的是標記-整理的算法進行垃圾回收。

2.20 簡述分代垃圾回收器是怎麼工做的?

分代回收器有兩個分區:老生代和新生代,新生代默認的空間佔比總空間的 1/3,老生代的默認佔比是 2/3。

新生代使用的是複製算法,新生代裏有 3 個分區:Eden、To Survivor、From Survivor,它們的默認佔比是 8:1:1,它的執行流程以下:

  • 把 Eden + From Survivor 存活的對象放入 To Survivor 區;
  • 清空 Eden 和 From Survivor 分區;
  • From Survivor 和 To Survivor 分區交換,From Survivor 變 To Survivor,To Survivor 變 From Survivor。

每次在 From Survivor 到 To Survivor 移動時都存活的對象,年齡就 +1,當年齡到達 15(默認配置是 15)時,升級爲老生代。大對象也會直接進入老生代。

老生代當空間佔用到達某個值以後就會觸發全局垃圾收回,通常使用標記整理的執行算法。以上這些循環往復就構成了整個分代垃圾回收的總體執行流程。

3. JVM 類加載

3.1 JVM類加載的時機?

5種場景會觸發類加載:

  1. 遇到new,getstatic,putstatic或invokestatic這四條字節碼指令時,若是類沒有進行過初始化,則須要先觸發初始化

  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化,

  3. 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化

  4. 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main方法的類),虛擬機會先初始化這個類

  5. 當時用,JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例後的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄對應的類尚未進行過初始化,則須要先觸發其初始化

3.2 JVM類加載過程?

類從被加載到虛擬機內存開始,到卸載出內存爲止,整個生命週期包括:加載,驗證,準備,解析,初始化,使用和卸載

加載

加載是類加載過程的一個階段,在加載階段虛擬機須要完成三件事

  1. 經過一個類的全限定名來獲取定義此類的輔而進之字節流
  2. 將字節流所表明的的靜態存儲結構轉化爲方法區的運行時數據結構
  3. 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口

驗證

驗證就是確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全

  1. 文件格式驗證
    驗證字節流是否符合Class文件格式的規範
  2. 元數據驗證
    對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求
  3. 字節碼驗證
    經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的
  4. 符號引用驗證
    確保解析動做能正確執行。

準備

準備階段是正式爲類靜態變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都在方法區中進行分配

public static int value = 123 //在準備階段 value的值是 0 並非123
public static final int value = 123 // 準備階段value 的值爲123

若是屬性有Constant Value 屬性,那麼在準備階段變量就會被初始化爲所指定的值

這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中

這裏所設置的初始值一般狀況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值

解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程

  • 直接引用
    直接引用可使直接指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄

  • 符號引用
    符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義的定位到目標便可

主要包含:

  • 類或接口的解析

  • 字段解析

  • 類方法解析

  • 接口方法解析

初始化

初始化,爲類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:

  1. 聲明類變量是指定初始值
  2. 使用靜態代碼塊爲類變量指定初始值

JVM初始化步驟:

  1. 假如這個類尚未被加載和鏈接,則程序先加載並鏈接該類
  2. 假如該類的直接父類尚未被初始化,則先初始化其直接父類
  3. 假如類中有初始化語句,則系統依次執行這些初始化語句

類的初始化

  1. 建立類的實例,也就是new的方式
  2. 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
  3. 調用類的靜態方法
  4. 反射(如Class.forName(「com.shengsiyuan.Test」))
  5. 初始化某個類的子類,則其父類也會被初始化
  6. Java虛擬機啓動時被標明爲啓動類的類(Java Test),直接使用java.exe命令來運行某個主類

卸載

  1. 執行了 System.exit()方法
  2. 程序正常執行結束
  3. 程序在執行過程當中遇到了異常或錯誤而異常終止
  4. 因爲操做系統出現錯誤而致使Java虛擬機進程終止

3.3 JVM加載Class文件的原理機制是什麼?

Java中的全部類,都須要由類加載器裝載到JVM中才能運行。類加載器自己也是一個類,而它的工做就是把class文件從硬盤讀取到內存中。在寫程序的時候,咱們幾乎不須要關心類的加載,由於這些都是隱式裝載的,除非咱們有特殊的用法,像是反射,就須要顯式的加載所須要的類。

類裝載方式,有兩種 :

  1. 隱式裝載
    程序在運行過程當中當碰到經過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中,

  2. 顯式裝載,
    經過class.forname()等方法,顯式加載須要的類

Java類的加載是動態的,它並不會一次性將全部類所有加載後再運行,而是保證程序運行的基礎類(像是基類)徹底加載到jvm中,至於其餘類,則在須要的時候才加載。這固然就是爲了節省內存開銷。

3.4 什麼是類加載器,類加載器有哪些?

實現經過類的權限定名獲取該類的二進制字節流的代碼塊叫作類加載器。

主要有一下四種類加載器:

  1. 啓動類加載器(Bootstrap ClassLoader)用來加載java核心類庫,沒法被java程序直接引用。
  2. 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
  3. 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。通常來講,Java 應用的類都是由它來完成加載的。能夠經過 ClassLoader.getSystemClassLoader()來獲取它。
  4. 用戶自定義類加載器,經過繼承 java.lang.ClassLoader類的方式實現。

3.5 什麼是雙親委派模型?

在介紹雙親委派模型以前先說下類加載器。對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立在 JVM 中的惟一性,每個類加載器,都有一個獨立的類名稱空間。類加載器就是根據指定全限定名稱將 class 文件加載到 JVM 內存,而後再轉化爲 class 對象。
雙親委派.png

類加載器分類:

  • 啓動類加載器(Bootstrap ClassLoader),是虛擬機自身的一部分,用來加載Java_HOME/lib/目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中而且被虛擬機識別的類庫;
  • 其餘類加載器:
  • 擴展類加載器(Extension ClassLoader):負責加載\lib\ext目錄或Java. ext. dirs系統變量指定的路徑中的全部類庫;
  • 應用程序類加載器(Application ClassLoader)。負責加載用戶類路徑(classpath)上的指定類庫,咱們能夠直接使用這個類加載器。通常狀況,若是咱們沒有自定義類加載器默認就是用這個加載器。

雙親委派模型:若是一個類加載器收到了類加載的請求,它首先不會本身去加載這個類,而是把這個請求委派給父類加載器去完成,每一層的類加載器都是如此,這樣全部的加載請求都會被傳送到頂層的啓動類加載器中,只有當父加載沒法完成加載請求(它的搜索範圍中沒找到所需的類)時,子加載器纔會嘗試去加載類。

當一個類收到了類加載請求時,不會本身先去加載這個類,而是將其委派給父類,由父類去加載,若是此時父類不能加載,反饋給子類,由子類去完成類的加載。

4. JVM調優

4.1 用過那些JVM 調優的工具?

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

4.2 經常使用的 JVM 調優的參數都有哪些?

  • -Xms2g:
    初始化推大小爲 2g;
  • -Xmx2g:
    堆最大內存爲 2g;
  • -XX:NewRatio=4:
    設置年輕的和老年代的內存比例爲 1:4;
  • -XX:SurvivorRatio=8:
    設置新生代 Eden 和 Survivor 比例爲 8:2;
  • –XX:+UseParNewGC:
    指定使用 ParNew + Serial Old 垃圾回收器組合;
  • -XX:+UseParallelOldGC:
    指定使用 ParNew + ParNew Old 垃圾回收器組合;
  • -XX:+UseConcMarkSweepGC:
    指定使用 CMS + Serial Old 垃圾回收器組合;
  • -XX:+PrintGC:
    開啓打印 gc 信息;
  • -XX:+PrintGCDetails:
    打印 gc 詳細信息。

4.3 如何分析GC日誌?

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

20xx-0x-0xT10: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] 
20xx-0x-0xT10: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的次數

4.4 JVM調優命令有那些?

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

4.5 你知道哪些JVM性能調優方式有那些?

  • 設定堆內存大小
    -Xmx:堆內存最大限制。

  • 設定新生代大小。 新生代不宜過小,不然會有大量對象涌入老年代
    -XX:NewSize:新生代大小

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

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

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


感謝您的點贊、評論、關注;
您還能夠掃碼關注「公衆號」獲取粉絲福利。

相關文章
相關標籤/搜索