那天我和小姐姐扯了半天的JVM~

前言

就在昨天,小碼仔和同事蛋哥面試了一個前來求職的小姐姐。回顧整個面試過程,小姐姐的表現能夠說是可圈可點。因此小碼仔忙裏偷閒把對小姐姐的面試過程整理出來,分享給你們。java

事情的整個過程是這樣子的,就在昨天陽光明媚的午後,我和蛋哥一如從前同樣處理着社畜的平常工做,被hr小姐姐通知進行Java候選人面試。當我和蛋哥抱着吃飯的小本本進了面試接待室。啊,一個眉清目秀的小姐姐。程序員

我老臉一紅,不不不,同是女孩紙,我要矜持。我和蛋哥禮貌的衝妹子笑了笑說「很差意思,讓你久等了」,而後我示意妹紙坐下,說:「咱們開始吧,我看你的簡歷有作過JVM調優,那咱們今天就來討論下JVM……」。web

正文

什麼是jvm?

JVM簡單來講它是一個虛構出來的計算機,是經過在實際的計算機上仿真模擬各類計算機功能來實現的。面試

JVM屏蔽了與具體操做系統平臺相關的信息,使Java程序只需生成在Java虛擬機上運行的目標代碼也就是字節碼,就能夠在多種平臺上不加修改地運行。JVM在執行字節碼時,實際上最終仍是把字節碼解釋成具體平臺上的機器指令執行。算法

jvm的運行時區域

Java虛擬機包括一套字節碼指令集、一組寄存器、一個棧、一個垃圾回收堆和一個存儲方法域。數組

它們的做用分別是什麼?

  1. PC寄存器

PC寄存器是用於存儲每一個線程下一步將執行的JVM指令,如該方法爲native的,則PC寄存器中不存儲任何信息。安全

  1. JVM棧

JVM棧是線程私有的,每一個線程建立的同時都會建立JVM棧,JVM棧中存放的爲當前線程中局部基本類型的變量(java中定義的八種基本類型:boolean、char、byte、short、int、long、float、double)、部分的返回結果以及Stack Frame,非基本類型的對象在JVM棧上僅存放一個指向堆上的地址。併發

  1. 堆(Heap)

它是JVM用來存儲對象實例以及數組值的區域,能夠認爲Java中全部經過new建立的對象的內存都在此分配,Heap中的對象的內存須要等待GC進行回收。jvm

  1. 方法區域(Method Area)

(1)在Sun JDK中這塊區域對應的爲PermanetGeneration,又稱爲持久代。編輯器

(2)方法區域存放了所加載的類的信息(名稱、修飾符等)、類中的靜態變量、類中定義爲final類型的常量、類中的Field信息、類中的方法信息,當開發人員在程序中經過Class對象中的getName、isInterface等方法來獲取信息時,這些數據都來源於方法區域,同時方法區域也是全局共享的,在必定的條件下它也會被GC,當方法區域須要使用的內存超過其容許的大小時,會拋出OutOfMemory的錯誤信息。

  1. 運行時常量池(Runtime Constant Pool)

存放的爲類中的固定的常量信息、方法和Field的引用信息等,其空間從方法區域中分配。

  1. 本地方法堆棧(Native Method Stacks)

JVM採用本地方法堆棧來支持native方法的執行,此區域用於存儲每一個native方法調用的狀態。

簡要介紹虛擬機類加載機制

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、裝換解析和初始化,最終造成能夠被虛擬機直接使用的 Java 類型。

對象建立過程

  1. 類加載檢查

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數可否在常量池中定位到一個類的符號引用,並檢查這個符號引用表明的類是否已經被加載、解析和初始化過,若是沒有,那麼必須先執行相應的類加載過程。

  1. 分配內存

在類加載檢查經過後,接下來虛擬機將會爲新生的對象分配內存。對象所須要的內存大小在類加載完成後即可徹底肯定,爲對象分配空間等同於把一塊肯定大小的內存從java堆中劃分出來。

  1. 初始零值

內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操做保證了對象的實例字段在 Java 代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。若是使用TLAB,這一工做過程也能夠提早到TLAB分配時進行。

  1. 設置對象頭

接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪一個類的實例,如何才能找到類的元數據信息,對象的哈希嗎,對象的GC分代年齡等信息,這些信息存放在對象的對象頭中。根據虛擬機當前的運行狀態的不一樣,對象頭會有不一樣的設置方式。

  1. 執行init方法

在上面工做都完成以後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象建立纔剛開始, 方法尚未執行,全部的字段都還爲零。因此通常來講,執行 new 指令以後會接着執行 方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。

在建立對象的時候,虛擬機是如何來保證線程安全的?

在建立對象的時候有一個很重要的問題,就是線程安全,由於在實際開發過程當中,建立對象是很頻繁的事情,例如正在給A對象分配內存,可是指針還沒修改,這時候對象B可能使用原來的指針來分配內存的狀況。做爲虛擬機來講,必需要保證線程是安全的,一般來說,虛擬機採用兩種方式來保證線程安全:

  1. CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。虛擬機採用 CAS 配上失敗重試的方式保證更新操做的原子性。

  2. TLAB: 爲每個線程預先在 Eden 區分配一塊內存。JVM 在給線程中的對象分配內存時,首先在各個線程的TLAB 分配,當對象大於TLAB 中的剩餘內存或 TLAB 的內存已用盡時,再採用上述的 CAS 進行內存分配。虛擬機是否啓用TLAB,能夠經過-XX:+/-UseTLAB參數來設定。

你前面有提到垃圾回收,何爲垃圾?

簡而言之就是已經再也不存活的對象即爲垃圾。

有哪些算法能夠斷定對象已再也不存活?

斷定對象是否存活的算法有兩種:

  1. 引用計數算法:給對象中添加一個引用計數器,每當一個地方應用了對象,計數器加1;當引用失效,計數器減1;當計數器爲0表示該對象已死、可回收。可是它很難解決兩個對象之間相互循環引用的狀況。

  2. 可達性分析算法:經過一系列稱爲「GC Roots」的對象做爲起點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(即對象到GC Roots不可達),則證實此對象已死、可回收。Java中能夠做爲GC Roots的對象包括:虛擬機棧中引用的對象、本地方法棧中Native方法引用的對象、方法區靜態屬性引用的對象、方法區常量引用的對象。

因爲引用計數算法很難解決兩個對象之間相互循環引用的狀況,所以在咱們的平常開發過程當中,一般經過可達性分析算法來斷定對象是否存活的。

哪些狀況下對象變成垃圾,並舉例說明

使對象變爲垃圾主要有如下狀況:

  1. 對非線程的對象來講,全部的活動線程都不能訪問該對象,那麼該對象就會變爲垃圾。
  2. 對線程對象來講,知足上面的條件,且線程未啓動或者已中止。

例如:

(1)改變對象的引用,如置爲null或者指向其餘對象。 
Object x=new Object();//object1
Object y=new Object();//object2
x=y;//object1 變爲垃圾
x=y=null;//object2 變爲垃圾

(2)超出做用域
if(i==0){
Object x=new Object();//object1
}//括號結束後object1將沒法被引用,變爲垃圾
(3)類嵌套致使未徹底釋放
class A{
A a;
}
A x= new A();//分配一個空間
x.a= new A();//又分配了一個空間
x=null;//將會產生兩個垃圾
(4)線程中的垃圾
class A implements Runnable{
void run(){
//....
}
}
//main
A x=new A();//object1
x.start();
x=null;//等線程執行完後object1才被認定爲垃圾
複製代碼

熟悉的JVM垃圾回收算法有哪些?

  1. 標記-清除算法

最基礎的算法,分標記和清除兩個階段:首先標記處所須要回收的對象,在標記完成後統一回收全部被標記的對象。

它有兩點不足:一個效率問題,標記和清除過程都效率不高;一個是空間問題,標記清除以後會產生大量不連續的內存碎片(相似於咱們電腦的磁盤碎片),空間碎片太多致使須要分配大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾回收動做。

  1. 複製算法

爲了解決效率問題,出現了「複製」算法,他將可用內存按容量劃分爲大小相等的兩塊,每次只須要使用其中一塊。當一塊內存用完了,將還存活的對象複製到另外一塊上面,而後再把剛剛用完的內存空間一次清理掉。這樣就解決了內存碎片問題,可是代價就是能夠用內容就縮小爲原來的一半。

  1. 標記-整理算法

複製算法在對象存活率較高時就會進行頻繁的複製操做,效率將下降。所以又有了標記-整理算法,標記過程同標記-清除算法,可是在後續步驟不是直接對對象進行清理,而是讓全部存活的對象都向一側移動,而後直接清理掉端邊界之外的內存。

  1. 分代收集算法

當前商業虛擬機的GC都是採用分代收集算法,這種算法並無什麼新的思想,而是根據對象存活週期的不一樣將堆分爲:新生代和老年代,方法區稱爲永久代(在新的版本中已經將永久代廢棄,引入了元空間的概念,永久代使用的是JVM內存而元空間直接使用物理內存)。

這樣就能夠根據各個年代的特色採用不一樣的收集算法。

新生代中的對象「朝生夕死」,每次GC時都會有大量對象死去,少許存活,使用複製算法。新生代又分爲Eden區和Survivor區(Survivor from、Survivor to),大小比例默認爲8:1:1。

老年代中的對象由於對象存活率高、沒有額外空間進行分配擔保,就使用標記-清除或標記-整理算法。

新產生的對象優先進去Eden區,當Eden區滿了以後再使用Survivor from,當Survivor from 也滿了以後就進行Minor GC(新生代GC),將Eden和Survivor from中存活的對象copy進入Survivor to,而後清空Eden和Survivor from,這個時候原來的Survivor from成了新的Survivor to,原來的Survivor to成了新的Survivor from。複製的時候,若是Survivor to 沒法容納所有存活的對象,則根據老年代的分配擔保將對象copy進去老年代,若是老年代也沒法容納,則進行Full GC(老年代GC)。

大對象直接進入老年代:JVM中有個參數配置-XX:PretenureSizeThreshold,令大於這個設置值的對象直接進入老年代,目的是爲了不在Eden和Survivor區之間發生大量的內存複製。

長期存活的對象進入老年代:JVM給每一個對象定義一個對象年齡計數器,若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納,將被移入Survivor而且年齡設定爲1。沒熬過一次Minor GC,年齡就加1,當他的年齡到必定程度(默認爲15歲,能夠經過XX:MaxTenuringThreshold來設定),就會移入老年代。可是JVM並非永遠要求年齡必須達到最大年齡纔會晉升老年代,若是Survivor 空間中相同年齡(如年齡爲x)全部對象大小的總和大於Survivor的一半,年齡大於等於x的全部對象直接進入老年代,無需等到最大年齡要求。

簡要介紹一種你熟悉的垃圾收集器

CMS收集器是一種以獲取最短回收停頓時間爲目標的收集器,停頓時間短,用戶體驗就好。

基於「標記清除」算法,併發收集、低停頓,運做過程複雜,分4步:

1)初始標記:僅僅標記GC Roots能直接關聯到的對象,速度快,可是須要「Stop The World」

2)併發標記:就是進行追蹤引用鏈的過程,讓垃圾回收器和用戶線程同時運行,併發工做。

3)從新標記:修正併發標記階段因用戶線程繼續運行而致使標記發生變化的那部分對象的標記記錄,比初始標記時間長但遠比並發標記時間短,須要「Stop The World」

4)併發清除:清除標記爲能夠回收對象,能夠和用戶線程併發執行

因爲整個過程耗時最長的併發標記和併發清除均可以和用戶線程一塊兒工做,因此整體上來看,CMS收集器的內存回收過程和用戶線程是併發執行的。

可是CMS收集器有3個缺點:

1)對CPU資源很是敏感

併發收集雖然不會暫停用戶線程,但由於佔用一部分CPU資源,仍是會致使應用程序變慢,總吞吐量下降。

CMS的默認收集線程數量是=(CPU數量+3)/4;當CPU數量多於4個,收集線程佔用的CPU資源多於25%,對用戶程序影響可能較大;不足4個時,影響更大,可能沒法接受。

2)併發清理階段用戶線程還在運行,這段時間就可能產生新的垃圾,新的垃圾在這次GC沒法清除,只能等到下次清理。

併發清除時須要預留必定的內存空間,不能像其餘收集器在老年代幾乎填滿再進行收集;若是CMS預留內存空間沒法知足程序須要,就會出現一次"Concurrent Mode Failure"失敗;這時JVM啓用後備預案:臨時啓用Serail Old收集器,而致使另外一次Full GC的產生;

3)產生大量內存碎片:CMS基於"標記-清除"算法,清除後不進行壓縮操做產生大量不連續的內存碎片,這樣會致使分配大內存對象時,沒法找到足夠的連續內存,從而須要提早觸發另外一次Full GC動做。

併發清除除了會產生浮動垃圾,還會出現什麼問題呢?

還會形成「對象消失」。

舉個例子,咱們先看一下一次正常的標記過程:

藍色對象是存活的對象,白色對象是消亡了,能夠回收的對象。同時須要注意下面的圖片的箭頭方向,表明的是有向的,好比其中的一條引用鏈是:根節點->5->6->7->8->11->10

再來看一下這個,如圖對象7和對象10原本就是原引用鏈(根節點->5->6->7->8->11->10)的一部分。修改後的引用鏈變成了(根節點->5->6->7->10)。

因爲藍色對象不會從新掃描,這將致使掃描結束後對象10和對象11都會回收了。他們都是被修改以前的原來的引用鏈的一部分。

如何解決「對象消失」的問題?

當且僅當如下兩個條件同時知足時,會產生"對象消失"的問題,原來應該是藍色的對象被誤標爲了白色:

條件一:賦值器插入了一條或者多條從藍色對象到白色對象的新引用。

條件二:賦值器刪除了所有從藍色對象到該白色對象的直接或間接引用。

咱們結合前面形成「對象消失」的圖能夠看到:

藍色對象7到白色對象10之間的引用是新建的,對應條件一。

藍色對象8到白色對象11之間的引用被刪除了,對應條件二。

因爲兩個條件之間是當且僅當的關係。因此,咱們要解決併發標記時對象消失的問題,只須要破壞兩個條件中的任意一個就行。

因而產生了兩種解決方案:增量更新和原始快照。

什麼是增量更新?

增量更新要破壞的是第一個條件(賦值器插入了一條或者多條從藍色對象到白色對象的新引用),當藍色對象插入新的指向白色對象的引用關係時,就將這個新插入的引用記錄下來,等併發掃描結束以後,再將這些記錄過的引用關係中的藍色對象爲根,從新掃描一次。

這樣對象9又被掃描成爲了藍色。也就不會被回收,因此不會出現對象消失的狀況。

什麼是原始快照?

原始快照要破壞的是第二個條件,即賦值器刪除了所有從藍色對象到該白色對象的直接或間接引用,當藍色對象要刪除指向白色對象的引用關係時,就將這個要刪除的引用記錄下來,在併發掃描結束以後,再將這些記錄過的引用關係中的藍色對象爲根,從新掃描一次。

這個能夠簡化理解爲:不管引用關係刪除與否,都會按照剛剛開始掃描那一刻的對象圖快照開進行搜索。

最後

哇~你竟然看到了這裏!好了各位小可愛,以上就是這篇文章的所有內容了,也是JVM最多見的一些面試題,能看到這裏的人呀,都是最胖的!哦不,都是最棒噠~

最近忙裏偷閒更了一篇JVM相關的文章,很是感謝小可愛們能看到這裏,若是以爲這個文章寫得還不錯, 求點贊👍 求關注❤️ 求分享👥 沒錯,本少女就是這麼的虛榮!嘻嘻~

若是本篇文章有任何錯誤,請批評指教,不勝感激 !

對啦~最後蛋哥問小姐姐,「是什麼讓你如此優秀」???

小姐姐微微一笑拿出手機,「由於我一直在關注【小碼仔】呀~」

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息