1. 程式人生 > >JVM常見面試題以及解答彙總

JVM常見面試題以及解答彙總

一. 記憶體模型以及分割槽,需要詳細到每個區放什麼。

Java虛擬機器在程式執行過程會把jvm的記憶體分為若干個不同的資料區域來管理,這些區域有自己的用途,以及建立和銷燬時間。 
jvm管理的記憶體區域包括以下幾個區域: 


棧區: 
棧分為java虛擬機器棧和本地方法棧

重點是Java虛擬機器棧,它是執行緒私有的,生命週期與執行緒相同。
每個方法執行都會建立一個棧幀,用於存放區域性變量表,操作棧,動態連結,方法出口等。每個方法從被呼叫,直到被執行完。對應著一個棧幀在虛擬機器中從入棧到出棧的過程。
通常說的棧就是指區域性變量表部分,存放編譯期間可知的8種基本資料型別,及物件引用和指令地址。區域性變量表是在編譯期間完成分配,當進入一個方法時,這個棧中的區域性變數分配記憶體大小是確定的。
會有兩種異常StackOverFlowError和 OutOfMemoneyError。當執行緒請求棧深度大於虛擬機器所允許的深度就會丟擲StackOverFlowError錯誤;虛擬機器棧動態擴充套件,當擴充套件無法申請到足夠的記憶體空間時候,丟擲OutOfMemoneyError。
本地方法棧 為虛擬機器使用到本地方法服務(native)
堆區:

堆被所有執行緒共享區域,在虛擬機器啟動時建立,唯一目的存放物件例項。
堆區是gc的主要區域,通常情況下分為兩個區塊年輕代和年老代。更細一點年輕代又分為Eden區最要放新建立物件,From survivor 和 To survivor 儲存gc後倖存下的物件,預設情況下各自佔比 8:1:1。 
不過很多文章介紹分為3個區塊,把方法區算著為永久代。這大概是基於Hotspot虛擬機器劃分, 然後比如IBM j9就不存在永久代概論。不管怎麼分割槽,都是存放物件例項。
會有異常OutOfMemoneyError
方法區:

被所有執行緒共享區域,用於存放已被虛擬機器載入的類資訊,常量,靜態變數等資料。被Java虛擬機器描述為堆的一個邏輯部分。習慣是也叫它永久代(permanment generation)
垃圾回收很少光顧這個區域,不過也是需要回收的,主要針對常量池回收,型別解除安裝。
常量池用於存放編譯期生成的各種位元組碼和符號引用,常量池具有一定的動態性,裡面可以存放編譯期生成的常量;執行期間的常量也可以新增進入常量池中,比如string的intern()方法。
程式計數器:

當前執行緒所執行的行號指示器。通過改變計數器的值來確定下一條指令,比如迴圈,分支,跳轉,異常處理,執行緒恢復等都是依賴計數器來完成。
Java虛擬機器多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式實現的。為了執行緒切換能恢復到正確的位置,每條執行緒都需要一個獨立的程式計數器,所以它是執行緒私有的。
唯一一塊Java虛擬機器沒有規定任何OutofMemoryError的區塊
jvm分割槽大致就這個塊,具體裡面還有很多細節,及其各個模組工作的演算法都很複雜,這裡只是對分割槽進行簡單介紹,掌握一些基本的知識點。
 

二. 堆裡面的分割槽:Eden,survival from to,老年代,各自的特點。

在垃圾回收演算法中,有一個演算法稱之為複製演算法。其基本思想是把記憶體分為大小相等的2塊,每次只在其中一塊區域內進行記憶體分配,當發生GC時,將這塊區域記憶體活的物件複製到另外一個區域內,然後對這塊區域進行Full GC,這樣可以保證記憶體的連續性,減少了空間碎片的產生.然後這種方式犧牲了一半的記憶體使用空間。Eden區與Survior區的概念也由此得來。

1、Eden區

    Edeb區位於JVM中的新生代,是新物件分配記憶體的地方,由於堆是所有執行緒共享的,所以在堆上分配記憶體需要加鎖。

而Sun JDK為了提升效率,會為每個新建的執行緒分配一個獨立的記憶體區域,這塊區域稱之為TLAB(Thread Location Allocation

Buffer).在TLAB上分配記憶體是不需要進行加鎖的,所以Eden區域的物件記憶體分配會優先在TLAB上進行.若是物件過大或者是TLAB的記憶體空間使用完,則物件的記憶體分配會在堆上進行。如果Eden區記憶體耗盡,則會觸發Minor GC(Young GC)。

2、From Survivor和To Survivor區

針對新生代物件"朝夕生死"的特點,將新生代劃分為3塊區域u,分別為Eden、From Survior、ToSurvior,比例為8:1:1。

From和To是相對的,每次Eden和From發生Minor GC時,會將存活的物件複製到To區域,並清除記憶體。To區域內的物件每存活一次,它的"age"就會+1,當達到某個閾值(預設為15)時,ToSurvior區域內的物件就會被轉移到老年代。

  可以通過設定引數-XX:MaxTenuringThreshold來設定晉升的年齡。

虛擬機器提供了一個引數:-XX  PertenureSizeThreshold 使得大於這個引數的物件直接在老年代中分配記憶體,這樣就避免了在Eden區域以及Survior區域進行大量的記憶體複製。

3、老年代

    老年代中是存活時間久的大物件(很長的字串或者是陣列),因此老年代使用標記-整理演算法。當老年代容量滿的時候,會觸發一次MajorGC (FullGC)
 

三. 物件建立方法,物件的記憶體分配,物件的訪問定位。

物件建立方法:

  JVM遇到一條new指令時,首先檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、連線和初始化過。

如果沒有,那必須先執行相應的類的載入過程。

 

物件的記憶體分配:

  物件所需記憶體的大小在類載入完成後便完全確定(物件記憶體佈局),為物件分配空間的任務等同於把一塊確定大小的記憶體從Java堆中劃分出來。

  根據Java堆中是否規整有兩種記憶體的分配方式:

 

  指標碰撞:所有用過的記憶體在一邊,空閒記憶體在另一邊,中間放著一個指標作為分界點的指示器,

分配記憶體就是把指標往空閒記憶體那邊挪一段與物件大小相等的距離。在使用Serial,ParNew等收集器,

(也就是用複製演算法,標記-整理演算法的收集器),分配演算法通常採用指標碰撞。

  空閒列表:虛擬機器維護一個列表,記錄哪些記憶體是可用的,分配的時候從列表中找到一塊足夠大的空間劃分給物件,並更新列表。

使用CMS這種基於標記-清除演算法的收集器,通常用空閒列表。

 

  物件建立在虛擬機器中時非常頻繁的行為,即使是僅僅修改一個指標指向的位置,在併發情況下也並不是執行緒安全的,可能出現正在給物件A分配記憶體,指標還沒來得及修改,物件B又同時使用了原來的指標來分配記憶體的情況。

同步

  虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性

本地執行緒分配緩衝(Thread Local Allocation Buffer, TLAB)

  把記憶體分配的動作按照執行緒劃分為在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體(TLAB)。

  哪個執行緒要分配記憶體,就在哪個執行緒的TLAB上分配。只有TLAB用完並分配新的TLAB時,才需要同步鎖定。

 

  記憶體分配完之後,虛擬機器要將分配到的記憶體空間都初始化為零值(不包括物件頭),保證了物件的例項欄位在Java程式碼中可以不賦初始值就直接使用。

 

物件的記憶體佈局:

  物件在記憶體中可分為3個部分,物件頭,例項資料,對齊填充。

  物件頭的第一部分用於儲存物件自身的執行時資料,如物件的雜湊碼,GC分代年齡,鎖狀態標誌,執行緒持有的鎖等。

  另一部分是型別指標,即物件指向它的類元資料的指標,通過這個來確定這個物件是哪個類的例項。

  例項資料是物件真正儲存的有效資訊。

 

物件的訪問定位:

  程式要通過棧上的reference資料來操作堆上的具體物件。物件的訪問方式有使用控制代碼直接指標

  使用控制代碼:java堆會劃分一塊記憶體作為控制代碼池,reference中存的是物件的控制代碼地址,而控制代碼中包含了物件的例項資料的地址和型別資料的地址(在方法區)

優點:物件被移動,reference不用修改,只會改變控制代碼中儲存的地址。

  使用直接指標:reference中存的是物件的地址,物件中分一小塊記憶體儲存型別資料的地址。優點:速度快。

 

四. GC的兩種判定方法:引用計數與引用鏈。

 

物件是否死亡的2中判定方法:引用計數和可達性分析(又稱引用鏈)

1.引用計數

        物件再被建立時,物件頭裡會儲存引用計數器,物件被引用,計數器+1;引用失效,計數器 -1;GC時會回收計數器為0的物件。但是JVM沒有用這種方式,因為無法判定相互迴圈引用(A引用B,B引用A)的情況,無法解決物件互相迴圈引用。

 

2.引用鏈

        程式把所有的引用看作圖(類似樹結構的圖),選定一個物件作為GC Root根節點,從該節點開始尋找對應的引用節點並標記,找到這個節點之後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之後,剩餘的節點認為是不可達的無用節點,會被回收。

        可以作為GC Root根節點的物件有:

            a.虛擬機器棧中的引用物件(本地變量表)

            b,方法區類靜態屬性的引用物件

            c,方法區常量引用的物件

             d,本地方法棧中的引用物件

 

Java中存在的4種引用型別:

   a 強引用

是指建立一個物件並把這個物件賦給一個引用變數 類似 string s="hello",只要引用存在,GC永遠不會回收

  b 軟引用

非必需引用,記憶體不足時回收。軟引用主要用於使用者實現類似快取的功能,在沒有被回收前可以直接通過軟引用取值,無需從繁忙的真實來源查詢資料,提升速度;當記憶體不足時,自動刪除這部分快取資料,從真實的來源查詢這些資料。

c 弱引用

描述非必需物件。被弱引用關聯的物件只能生存到下一次垃圾回收之前,垃圾收集器工作之後,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。弱引用主要用於監控物件是否已經被標記為即將回收的垃圾,可以通過弱引用的isEnQueues方法返回物件是否被垃圾回收器標記。

d 虛引用

虛引用是每次垃圾回收的時候都會被回收,唯一作用當物件被回收時,可以收到通知。

 

五. GC的三種收集方法:標記清除、標記整理、複製演算法的原理與特點,分別用在什麼地方,如果讓你優化收集方法,有什麼思路?

 

第一種:標記清除 
它是最基礎的收集演算法。 
原理:分為標記和清除兩個階段:首先標記出所有的需要回收的物件,在標記完成以後統一回收所有被標記的物件。 
特點:(1)效率問題,標記和清除的效率都不高;(2)空間的問題,標記清除以後會產生大量不連續的空間碎片,空間碎片太多可能會導致程式執行過程需要分配較大的物件時候,無法找到足夠連續記憶體而不得不提前觸發一次垃圾收集。 
地方 :適合在老年代進行垃圾回收,比如CMS收集器就是採用該演算法進行回收的。

第二種:標記整理 
原理:分為標記和整理兩個階段:首先標記出所有需要回收的物件,讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。 
特點:不會產生空間碎片,但是整理會花一定的時間。 
地方:適合老年代進行垃圾收集,parallel Old(針對parallel scanvange gc的) gc和Serial old收集器就是採用該演算法進行回收的。

第三種:複製演算法 
原理:它先將可用的記憶體按容量劃分為大小相同的兩塊,每次只是用其中的一塊。當這塊記憶體用完了,就將還存活著的物件複製到另一塊上面,然後把已經使用過的記憶體空間一次清理掉。 
特點:沒有記憶體碎片,只要移動堆頂指標,按順序分配記憶體即可。代價是將記憶體縮小位原來的一半。 
地方:適合新生代區進行垃圾回收。serial new,parallel new和parallel scanvage 
收集器,就是採用該演算法進行回收的。 
複製演算法改進思路:由於新生代都是朝生夕死的,所以不需要1:1劃分記憶體空間,可以將記憶體劃分為一塊較大的Eden和兩塊較小的Suvivor空間。每次使用Eden和其中一塊Survivor。當回收的時候,將Eden和Survivor中還活著的物件一次性地複製到另一塊Survivor空間上,最後清理掉Eden和剛才使用過的Suevivor空間。其中Eden和Suevivor的大小比例是8:1。缺點是需要老年代進行分配擔保,如果第二塊的Survovor空間不夠的時候,需要對老年代進行垃圾回收,然後儲存新生代的物件,這些新生代當然會直接進入來老年代。

優化收集方法的思路 
分代收集演算法 
原理:根據物件存活的週期的不同將記憶體劃分為幾塊,然後再選擇合適的收集演算法。 
一般是把java堆分成新生代和老年代,這樣就可以根據各個年待的特點採用最適合的收集演算法。在新生代中,每次垃圾收集都會有大量的物件死去,只有少量存活,所以選用複製演算法。老年代因為物件存活率高,沒有額外空間對他進行分配擔保,所以一般採用標記整理或者標記清除演算法進行回收。
-

六. GC收集器有哪些?CMS收集器與G1收集器的特點。

1、CMS收集器

  CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。基於“標記-清除”演算法實現,它的運作過程如下:

1)初始標記

2)併發標記

3)重新標記

4)併發清除

  初始標記、從新標記這兩個步驟仍然需要“stop the world”,初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,熟讀很快,併發標記階段就是進行GC Roots Tracing,而重新標記階段則是為了修正併發標記期間因使用者程式繼續運作而導致標記產生表動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長點,但遠比並發標記的時間短。

 

  CMS是一款優秀的收集器,主要優點:併發收集、低停頓。

缺點:

1)CMS收集器對CPU資源非常敏感。在併發階段,它雖然不會導致使用者執行緒停頓,但是會因為佔用了一部分執行緒而導致應用程式變慢,總吞吐量會降低。

2)CMS收集器無法處理浮動垃圾,可能會出現“Concurrent Mode Failure(併發模式故障)”失敗而導致Full GC產生。

浮動垃圾:由於CMS併發清理階段使用者執行緒還在執行著,伴隨著程式執行自然就會有新的垃圾不斷產生,這部分垃圾出現的標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC中再清理。這些垃圾就是“浮動垃圾”。

3)CMS是一款“標記--清除”演算法實現的收集器,容易出現大量空間碎片。當空間碎片過多,將會給大物件分配帶來很大的麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次Full GC。

2、G1收集器

G1是一款面向服務端應用的垃圾收集器。G1具備如下特點:

1、並行於併發:G1能充分利用CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短stop-The-World停頓時間。部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以通過併發的方式讓java程式繼續執行。

2、分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間,熬過多次GC的舊物件以獲取更好的收集效果。

3、空間整合:與CMS的“標記--清理”演算法不同,G1從整體來看是基於“標記整理”演算法實現的收集器;從區域性上來看是基於“複製”演算法實現的。

4、可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,

5、G1運作步驟:

1、初始標記;2、併發標記;3、最終標記;4、篩選回收

上面幾個步驟的運作過程和CMS有很多相似之處。初始標記階段僅僅只是標記一下GC Roots能直接關聯到的物件,並且修改TAMS的值,讓下一個階段使用者程式併發執行時,能在正確可用的Region中建立新物件,這一階段需要停頓執行緒,但是耗時很短,併發標記階段是從GC Root開始對堆中物件進行可達性分析,找出存活的物件,這階段時耗時較長,但可與使用者程式併發執行。而最終標記階段則是為了修正在併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程Remenbered Set Logs裡面,最終標記階段需要把Remembered Set Logs的資料合併到Remembered Set Logs裡面,最終標記階段需要把Remembered Set Logs的資料合併到Remembered Set中,這一階段需要停頓執行緒,但是可並行執行。最後在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃。

 

注:一般堆記憶體4G以上的可用G1垃圾回收方式。4G以下用CMS。

 

.-XX:+UseConcMarkSweepGC

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

 

何時發生? 
(1)Minor GC發生:當jvm無法為新的物件分配空間的時候就會發生Minor gc,所以分配物件的頻率越高,也就越容易發生Minor gc。

(2)Full GC:發生GC有兩種情況,①當老年代無法分配記憶體的時候,會導致MinorGC,②當發生Minor GC的時候可能觸發Full GC,由於老年代要對年輕代進行擔保,由於進行一次垃圾回收之前是無法確定有多少物件存活,因此老年代並不能清除自己要擔保多少空間,因此採取採用動態估算的方法:也就是上一次回收傳送時晉升到老年代的物件容量的平均值作為經驗值,這樣就會有一個問題,當發生一次Minor GC以後,存活的物件劇增(假設小物件),此時老年代並沒有滿,但是此時平均值增加了,會造成發生Full GC
 

九 幾種常用的記憶體除錯工具:jmap、jstack、jconsole。

常用的記憶體除錯工具:jps、jmap、jhat、jstack、jconsole,jstat:

jps:檢視虛擬機器程序的狀況,如程序ID。

jmap: 用於生成堆轉儲快照檔案(某一時刻的)。

jhat:對生成的堆轉儲快照檔案進行分析。

jstack:用來生成執行緒快照(某一時刻的)。

生成執行緒快照的主要目的是定位執行緒長時停頓的原因(如死鎖,死迴圈,等待I/O 等),通過檢視各個執行緒的呼叫堆疊,就可以知道沒有響應的執行緒在後臺做了什麼或者等待什麼資源。
jstat:虛擬機器統計資訊監視工具。如顯示垃圾收集的情況,記憶體使用的情況。

jconsole:主要是記憶體監控和執行緒監控。

記憶體監控:可以顯示記憶體的使用情況。執行緒監控:遇到執行緒停頓時,可以使用這個功能。
 

十. 類載入的五個過程:載入、驗證、準備、解析、初始化。

https://blog.csdn.net/chenge_j/article/details/72677766

十一. 雙親委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。

https://blog.csdn.net/u014629433/article/details/51645271

 

十二. 分派:靜態分派與動態分派。

 

這裡所謂的分派指的是在Java中對方法的呼叫。Java中有三大特性:封裝、繼承和多型。分派是多型性的體現,Java虛擬機器底層提供了我們開發中“重寫”和“過載”的底層實現。其中過載屬於靜態分派,而重寫則是動態分派的過程。除了使用分派的方式對方法進行呼叫之外,還可以使用解析呼叫,解析呼叫是在編譯期間就已經確定了,在類裝載的解析階段就會把符號引用轉化為直接引用,不會延遲到執行期間再去完成。而分派呼叫則既可以是靜態的也可以是動態(就是這裡的靜態分派和動態分派)的。

1.靜態分派

靜態分派只會涉及過載,而過載是在編譯期間確定的,那麼靜態分派自然是一個靜態的過程(因為還沒有涉及到Java虛擬機器)。靜態分派的最直接的解釋是在過載的時候是通過引數的靜態型別而不是實際型別作為判斷依據的。比如建立一個類O,在O中建立了靜態類內部類A,O中又有兩個靜態類內部類B、C繼承了這個靜態內部類A,那麼實際上當編寫如下的程式碼:

public class O{
    static class A{}
    static class B extends A{}
    static class C extends A{}
    public void a(A a){
        System.out.println("A method");
    }
    public void a(B b){
        System.out.println("B method");
    }
    public void a(C c){
        System.out.println("C method");
    }
    public static void main(String[] args){
        O o = new O();
        A b = new B();
        A c = new C();
        o.a(b);
        o.a(c);
    }
}


執行的結果是打印出連個“A method”。原因在於靜態型別的變化僅僅在使用時發生,變數本省的型別不會發生變化。比如我們這裡中A b = new B();雖然在建立的時候是B的物件,但是當呼叫o.a(b)的時候才發現是A的物件,所以會輸出“A method”。也就是說在發生過載的時候,Java虛擬機器是通過引數的靜態型別而不是實際引數型別作為判斷依據的。因此,在編譯階段,Javac編譯器選擇了a(A a)這個過載方法。

雖然編譯器能夠在編譯階段確定方法的版本,但是很多情況下過載的版本不是唯一的,在這種模糊的情況下,編譯器會選擇一個更合適的版本。

2.動態分派

動態分派的一個最直接的例子是重寫。對於重寫,我們已經很熟悉了,那麼Java虛擬機器是如何在程式執行期間確定方法的執行版本的呢?

解釋這個現象,就不得不涉及Java虛擬機器的invokevirtual指令了,這個指令的解析過程有助於我們更深刻理解重寫的本質。該指令的具體解析過程如下:

找到運算元棧棧頂的第一個元素所指向的物件的實際型別,記為C
如果在型別C中找到與常量中描述符和簡單名稱都相符的方法,則進行訪問許可權的校驗,如果通過則返回這個方法的直接引用,查詢結束;如果不通過,則返回非法訪問異常
如果在型別C中沒有找到,則按照繼承關係從下到上依次對C的各個父類進行第2步的搜尋和驗證過程
如果始終沒有找到合適的方法,則丟擲抽象方法錯誤的異常

從這個過程可以發現,在第一步的時候就在執行期確定接收物件(執行方法的所有者程稱為接受者)的實際型別,所以當呼叫invokevirtual指令就會把執行時常量池中符號引用解析為不同的直接引用,這就是方法重寫的本質。

3.虛擬機器動態分派的實現

其實上面的敘述已經把虛擬機器重寫與過載的本質講清楚了,那麼Java虛擬機器是如何做到這點的呢?

由於動態分派是非常頻繁的操作,實際實現中不可能真正如此實現。Java虛擬機器是通過“穩定優化”的手段——在方法區中建立一個虛方法表(Virtual Method Table),通過使用方法表的索引來代替元資料查詢以提高效能。虛方法表中存放著各個方法的實際入口地址(由於Java虛擬機器自己建立並維護的方法表,所以沒有必要使用符號引用,那不是跟自己過不去嘛),如果子類沒有覆蓋父類的方法,那麼子類的虛方法表裡面的地址入口與父類是一致的;如果重寫父類的方法,那麼子類的方法表的地址將會替換為子類實現版本的地址。

方法表是在類載入的連線階段(驗證、準備、解析)進行初始化,準備了子類的初始化值後,虛擬機器會把該類的虛方法表也進行初始化。
 

參考地址:

https://blog.csdn.net/qq_35181209/article/details/77931494

http://blog.sina.com.cn/s/blog_61d758500102xijx.html

https://blog.csdn.net/FateRuler/article/details/81158510?utm_source=blogxgwz9

http://www.cnblogs.com/feicheninfo/p/9684736.html

https://www.cnblogs.com/mengchunchen/p/7859668.html

https://blog.csdn.net/weixin_30300689/article/details/79888642

https://blog.csdn.net/honjane/article/details/51542183