JVM方面-----面試彙總大全(未完待續)

面試題1. JVM內存模型

在這裏插入圖片描述
先說堆吧:
堆是所有對象分配的地方,也是GC垃圾回收的主要的地方,而且現在在年輕代中使用的都是分代收集算法,首先堆分爲新生代和老年代,但是在新生代中,又分爲Eden,From Survivor ,To Survivor 且爲8:1:1的一個分配,如果一個創建的新對象都會分配到Eden區(其實每一個對象都有一個分代年齡,當每一次GC如果存活下來一次之後年齡就會加一,到了15次左右就會移動到survivor中),但是如果Eden滿了之後就會進行一個Full GC。
可以通過 -Xms 和 -Xmx 兩個虛擬機參數來指定一個程序的堆內存大小,第一個參數設置初始值,第二個參數設置最大值,一般都是把這個堆內存的初始值設置爲何堆內存的最大值保持一致,能夠防止內存抖動

方法區:也被稱爲是永久區

  • 方法區中最爲重要的是類的類型信息、常量池、域信息、方法信息。
  • 類型信息包括類的完整名稱、父類的完整名稱、類型修飾符( pub Ii c/protected/pri va )和類型的直接接口類表。
  • 常量池包括類方法 域等信息所引用的常量信息。域信息包括域名稱、域類型和域修飾符。在str s = “你好”的時候這個字符串其實就加載到常量池中去了
  • 方法信息包括方法名稱、返回類型、方法參數、方法修飾符、方法字節碼、操作數技和方法梳幀的局部變量區大小以及異常表。

Java虛擬機棧:
其實就是每個 Java 方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、常量池引用等信息。從方法調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
可以通過 -Xss 這個虛擬機參數來指定每個線程的 Java 虛擬機棧內存大小:
在這裏插入圖片描述

面試題2. 爲什麼要有雙親委派模型

  • 爲了安全,也可以防止一些別人自己寫的 “病毒代碼” 代碼注入到JVM的內存中,使用雙親委派模型來組織類加載器間的關係,能夠使類的加載也具有層次關係,這樣能夠保證核心基礎的Java類會被根加載器加載,而不會去加載用戶自定義的和基礎類庫相同名字的類,從而保證系統的有序、安全。
  • 還一個原因就是避免多份字節碼的加載,而佔用過多的內存空間

面試題3. GC的過程

在這裏插入圖片描述
1. Minor GC過程

  • 首先如果當Eden區滿了之後,會先進行一次minorGC,然後把Eden區中的存放對象放到S1中
  • 但是隨着Eden區一直不斷的有新的對象產生,Eden區又滿了,此時需要再次進行一次minnorGC 這次GC的範圍不僅有Eden區還有上一步S1中存活的對象,然後把S1中存活的對象複製到S2中。(補充:在S1和S2中他們是相對的,如果說當前對象存活在S1區,就去清理S1然後把存活的對象放到S2區,反之如果存活的對象在S2區時也是一樣的)

2, 對象進入老年代的4種情況

  • 第一種是當假如進行Minor GC時發現,存活的對象在ToSpace區中存不下,那麼把存活的對象存入老年代
  • 第二種就是比如一個創建的比較大的新對象放Eden區的話,就可能放不下或者說是當一個新建的對象達到一個最大值之後(也就是PretenureSizeThreshold這個參數),這個時候就需要放到老年代
  • 第三種就是當一個對象的年齡超過了配置的這個參數-XX:MaxTenuringThreshold默認的是15,也就是新生代Eden區每次向S1或者S2區中複製一次年齡就增加1歲,當到達15歲之後就會被轉移到老年代中
  • 第四種就是動態對象年齡判定,如果在From空間中,相同年齡所有對象的大小總和大於From和To空間總和的一半,那麼年齡大於等於該年齡的對象就會被移動到老年代,而不用等到15歲(默認):

3. Full GC
由於一直的在往老年代添加新的對象,直到老年代的內存也開始不夠用的時候,這個時候就會發生一次Full GC來進行對整個堆內存進行清理,然後依次往復執行

4. 空間分配擔保
在發生Minor GC前,虛擬機會先進行去檢查老年代最大可以使用的連續內存是不是大於新生代所有對象的總空間,如果這個條件成立,代表執行Minor GC可以確保是安全的。如果不成立的話,就會去看HandlerPromotionFailure這個參數的值設置的是false還是true,也就是是否允許擔保失敗。代表虛擬機擔保說:你儘管GC出了了事我負責。

如果是HandlerPromotionFailure爲true的話,會繼續進行判斷老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於的話,將嘗試進行一次Minor GC,儘管這次Minor GC是有風險的,如果小於的話,那麼這次Minor GC將升級爲Full GC。

如果HandlerPromotionFailure爲false,那麼這次Minor GC也將升級爲Full GC

面試題4. GC涉及了什麼算法

1.標記-清除算法
在這裏插入圖片描述
缺點:

  • 效率不高
  • 空間問題,會產生大量不連續的的內存碎片

2.複製算法
在這裏插入圖片描述
缺點:

  • 雖然解決了標記清除算法的效率不足問題,但是又帶來了浪費內存空間的問題

3.標記整理算法
在這裏插入圖片描述

4.分代收集算法:

  • 分代收集算法在對堆的垃圾回收中用到,把堆內存分爲新生代和年老代,所以現代的虛擬機基本都是這種思想
  • 新生代使用:複製算法
  • 老年代使用:標記 - 清除 或者 標記 - 整理 算法

GC中的分帶收集算法的實現原理:

首先由於GC在分代收集算法中,是把年輕代和老年代進行分開收集的,但是有沒有考慮過一個問題,就是當在對年輕代進行回收的時候,由於是按照GCroots開始進行根不可達的算法檢索,那麼就會出現在年輕代中沒有任何引用,但是在老年代中有一個引用,分帶搜索的時候又不能連同老年代也進行一起GCroot,所以就會把年輕的對象誤回收的這種情況?

  1. 針對這種情況Java虛擬機用了一個叫做CardTable(卡表)的數據結構來標記老年代的某一塊內存區域中的對象是否持有新生代對象的引用,

  2. 這個卡表其實是一個比特位的集合,每一個位置的只能是1或者0,每個位置對應這個老年代的一塊內存區域(卡表的數量取決於老年代的大小和每張卡對應的內存大小),卡表位置的1 就表示該卡位置對應的老年代的內存區域含有老年代對新生代的引用,0 就表示沒有老年代對新生代的引用

  3. 當每次年老代對象中某個引用新生代的字段發生變化時,這個老年代中對應的卡表的位置的狀態就要隨之變化來維持卡表的準確性,每次變化Hotspot VM就必須將該卡所對應的卡表元素設置爲1還是0,從而將該引用字段所在的卡標記爲髒,這個過程中會執行一個寫屏障,在高併發情況下,頻繁的寫屏障很容易發生虛共享(false sharing),從而帶來性能開銷。
    在這裏插入圖片描述
    那麼爲什麼要多使用一個卡表來進行這麼做呢,每次變化都還要維護這個卡表?

  4. 首先他能從根部解決我上面提出的問題,不會再清理新生代的時候,不知道老年代有沒有引用新生代的對象,然後把新生代的對象給誤回收的問題解決了

  5. 還有一點就是在進行新生代的垃圾回收的時候,並不用在對老年代也進行GCroots遍歷老年代有沒有對新生代的引用,而是直接去掃描一下卡表中狀態爲1 的位置引用的老年代的內存塊(就是對應的4K大小的那個內存區域)
    在這裏插入圖片描述
    由於上面提到的每次維護卡表的時候,都會有一次寫屏障,

  6. 就是JDK 7中引入的解決方法,引入了一個新的JVM參數-XX:+UseCondCardMark,在執行寫屏障之前,先簡單的做一下判斷。如果卡頁已被標識過,則不再進行標識。


if (CARD_TABLE [this address >> 9] != 0)
  CARD_TABLE [this address >> 9] = 0;

面試題5. JVM中如何判斷一個對象是否被回收?

  • 其實採用的是:根搜索算法 是 JVM 虛擬機判斷一個對象是否存活的算法。它把內存中的每一個對象看成一個節點,並定義了一些對象作爲根節點(GC Roots)
  • 如果一個對象中有對另一個對象中的引用,那麼就認爲第一個對象有一條指向第二個對象的邊。JVM會起一個線程從所有的GC Roots開始往下遍歷,當遍歷完之後如果發現有一些對象不可到達,那麼就認爲這些對象已經沒有用了,需要被回收。

面試題6. 哪些對象是GCRoots

1.虛擬機棧(棧幀中的本地變量表)中引用的對象;

2.方法區中的類靜態屬性引用的對象;

3.方法區中常量引用的對象;

4.本地方法棧中JNI(即一般說的Native方法)中引用的對象

面試題7. 哪些算法是運用在年輕代,哪些算法那是運用在老年代的,以及年輕代和年老代都有哪些垃圾收集器?

年輕代運用的算法:

  • 複製算法

新年代運用的垃圾收集器:

  • Serial
  • ParNew
  • Paralle Scavenge

老年代運用的算法:

  • 標記-清除算法

老年代運用的垃圾收集器:

  • Serial Old
  • Paralle Old
  • CMS(當前用的最多的)
  • G1(當前用的最多的)

收集器的特點對比:

  • Serial 和 SerialOld 單線程,簡單高效。進行GC的時候會把所有工作線程停掉,是早期的回收器。
  • ParNew是Serial的併發版本,都是新生代收集器
  • Paralle Scavenge(採用複製算法,吞吐量優先收集器) 和 Paralle Scavenge Old(標記-整理) 組合追求吞吐量,可以高效的利用CPU時間,儘快完成程序的運算任務,適合在後臺運算而不需要太多交互的程序
  • CMS 是一種以獲取最短回收停頓時間爲目標的收集器。對於尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗的系統很適合。採用標記清除算法,所以缺點就是會有大量的空間碎片。

面試題8. CMS工作原理,會stop the world嗎?CMS在哪個階段會停頓,哪個階段停頓的時間最長?

會。因爲CMS分爲四個階段,且在併發標記和標記清除兩個步驟的用時會相對較長一些

  • 初始標記(需要進行Stop The World停頓) (只是簡單的標記一下GC Roots能關聯到的對象,速度很快)
  • 併發標記 (併發標記其實就是進行GCroots Tracing 的過程)
  • 再次標記(也需要進行Stop The World且時間比初始標記要長一些)(是爲了修正併發標記期間因用戶程序繼續運行導致標記產生變動的那一部分對象的記錄)
  • 標記清除
    CMS收集器的優點:
  • 低停頓
  • 併發收集垃圾
    CMS收集器的缺點:
  • CMS對CPU的資源非常敏感
  • CMS無法清除浮動垃圾(就是在並行執行期間應用程序不斷產生新的垃圾,CMS也在不斷收集,肯定有一部分不能收集到,這個部分就是浮動垃圾,迴流到下次GC的時候進行清理)
  • 以爲CMS是基於標記-清除的的算法來實現的,所以會產生大量的空間碎片

說一下G1收集器:

  1. 首先G1是把整個Java堆中分爲很多個大小相等的Region,雖然還保留着分代垃圾回收的概念,但是新生代和老年代之間並不是進行物理隔離的了,但是每個對象又不可能只是在一個Region中,所以這就衍生出了 卡表 也就是RemembSet的出現了
  2. G1的特點就是並行和併發,充分利用當代多核多CPU的硬件特點來縮短STW的時間
  3. 然後是分代收集 這裏就用到了卡表來進行實現的
  4. 然後不會產生內存碎片,是因爲G1從整體宏觀來看其實他主要採用的是“標記-整理“”算法思想實現的垃圾收集器,但是從局部來看其實他是兩個Region之間基於複製算法實現的垃圾收集器,,因此這兩種算法都不會產生內存空間碎片的問題,主要是採用化整爲0 的思想。

面試題9、講一下Java中的四種引用?

  1. 第一種是強引用,也就是我們經常用到的User user = new User()這種模式
  2. 第二種是軟引用,也就是說如果你要是定義且是用了這種軟引用的話,那麼如果JVM中的內存空間足夠,垃圾回收器就不會回收它【可以用來做緩存一種比較大的對象,當內存不夠用的時候會自動回收對象】
SoftReference aSoftRef=new SoftReference(這裏傳進來的是一個對象);  
  1. 第三種是弱引用,這種就是容易引起內存泄漏的一種引用,當JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示。比如在ThreadLocal中就有用到弱引用(需要謹慎使用)
 WeakReference<People>reference=new WeakReference<People>(這裏傳進來的也是一個對象);  
  1. 第四種是虛引用,就是說只要你用到了虛引用,如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。

在這裏插入圖片描述
補充幾種利用這個四種引用可能會使用到的場景:

  • 一般在使用軟引用的時候主要是爲了解決一些當加載一些大的文件或者資源的時候可能會導致內存溢出,用來做緩存,當加載的資源太大的話防止報錯OOM
    在這裏插入圖片描述
  • 弱引用是隻要進行垃圾回收就會被清理,主要用處也是爲了降低OOM的概率,一般可以和WeakHashMap進行連用
  • 而虛引用一般使用的時候都是跟一個ReferenceQueue隊列進行連用,因爲由於虛引用有沒有GC他都會被回收,他的主要作用就是可以進行對一個對象進行監控,如果被回收了就放到ReferenceQueue隊列中,比如Spring中的AOP的後置通知實現就是這樣的原理實現

面試題10. 如何判斷一個對象存活,如果要讓對象不被回收該怎麼辦?

在這裏插入圖片描述
首先判斷一個對象是不是存活着,那麼關鍵主要是看在JVM中通過GC roots引用的對象是如何判斷的?(就是能否在方法區或者堆棧中找到一個該對象的引用,如果有這個對象就是不可回收的,如果沒有JVM就會回收該對象)

  1. 第一個就是虛擬機棧中引用的對象(其實就是上面的每一個棧幀中,這個每個棧幀對應java中的每一個方法,說白了就是每一個方法中的成員變量)
  2. 方法區中的類靜態屬性引用的對象
  3. 方法區中常量引用的對象。
  4. 本地方法棧中JNI的引用對象

然後是不讓對象回收,其實就是知道JVM中回收類的條件:

  1. Java堆中不存在該類的所有實例;
  2. 加載該類的ClassLoader已經被回收
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,無法再任何地方通過反射訪問該類
  4. JVM對類的回收要求比較嚴格,即使同時滿足上面的所有條件也只是能夠有資格回收類,但是並不能夠保證一定會回收

面試題11. gc的原理,有哪些垃圾收集器,優缺點,有哪些垃圾收集算法,優缺點

面試題12. gc內存管理

面試題13. 類加載機制,一個類加載到虛擬機中一共有幾個步驟,類加載過程中實例變量會被初始化幾次,這些步驟的順序哪些是固定的,哪些是不固定的,爲什麼不固定?

要想先回答這個一連串問題,那麼就需要把整個類加載的機制都給搞明白,裏面的問題也就自然而然的出來了。

(1)加載階段

  • 首先加載的階段的事情都是由ClassLoader來進行完成的功能,主要的功能就是下面幾個
  • 通過一個類的全限名來獲取定義此類的二進制字節流
  • 把這個二進制字節流所代表的的靜態存儲結構給轉換爲JVM中運行時數據區的數據結構,其實就是把什麼對象啊,變量啊,方法啊之類的都放到如:堆中,方法區中,虛擬機方法棧中等各自的位置上
  • 在內存中生成一個代表該類的Class對象,作爲方法區這個類的各種數據的入口
  • 補充:其實也就是因爲類在加載這一步是以二進制字節流的形式存在,才得以讓我們在此基礎上做一些很多的“”小動作“”,比如:我們可以對這些二進制字節流來使用動態代理技術、

(2)鏈接(驗證—>準備—>解析)

  • 鏈接裏面又分了三小步,
  • 第一小步驗證其實就是爲了保證JVM虛擬機的安全,來阻止一些不安全的因素和保證class文件格式的正確性
  • 第二小步就是進行第一次賦值 的時候,會先把一些我們定義的各種類型的變量先進行一個默認的初始化賦值,如:int i;就會先把i =0,Long id 會先把id = 0L,如果是String s 會默認初始化爲 null ,Boolean 的話默認爲 false
  • 第三小步的目的其實就是虛擬機把常量池內的符號引用替換爲直接引用的過程(這個過程不一定是必須在鏈接這個過程中的,他也有可能是在初始化之後來進行完成的,所以這個步驟的順序不是完全固定的

(3)初始化

  • 這一步其實就是二次初始化,也就是會去執行我們自己的定義的初始化類的代碼

所以上面的問題也就都知道了

面試題14. 哪些情況下會觸發java的類加載

  • 第一種就是在用new關鍵字實例化對象的時候,讀取或者設置一個類的靜態變量且被final修飾的時候,調用一個類的靜態方法的時候都會觸發類的加載
  • 使用java.lang.reflect包下的方法對類進行反射的時候,如果對類沒有初始化過,就會觸發其初始化
  • 當需要初始化一個類的時候,發現其父類還沒有被初始化,會先去初始化父類
  • 當虛擬機啓動的時候,用戶需要去執行一個主類(包含main方法的那個類)

面試題15. 說一下JVM的內存結構,哪些是共享的 ,哪些是線程私有的,java虛擬機棧裏面存放的是什麼?

  • 其實一共就分爲5大主要的區域分爲堆、方法區(常量池)、本地方法棧、虛擬機棧、程序計數器、其實還有常量池,期中堆內存和方法區是線程共享的
  • 而本地方法棧,虛擬機棧,程序計數器都是屬於線程私有的,都存放在方法棧貞中

java虛擬機棧中存放的其實有以下幾部分:

  • 棧幀:當線程去執行方法的時候,其實每個方法的執行都會創建一個棧幀,當方法開始執行就是入棧,方法調用結束然後出棧。
  • 局部變量表:方法中會有各種基本類型的數據類型、引用類型。其實在進入一個方法的棧貞的時候,這個棧幀的大小就已經被固定下來了,在運行期間就不會改變的。所以局部變量表的內存大小其實都是在編譯期間就已經完成了內存大小的分配。

面試題16. JVM的垃圾回收策略

  • 第一種:引用計數法進行決定是否需要垃圾回收
  • 第二種:可達性分析來決定一個類是否需要被回收

面試題17:JVM(內存模型、GC垃圾回收,包括分代,GC算法,收集器、類加載和雙親委派、JVM調優,內存泄漏和內存溢出)

面試題18:G1和CMS的區別(JDK1.8)?

G1的描述:

  • G1將Java堆分成多個連續的相等大小的內存區域(Region),跟老的垃圾回收一樣,它在邏輯上也分爲不同的年齡段(Eden,Survivor,Old),但是不同的是,這些區域並不是固定大小

區別一:堆分配的空間不同

  • CMS 將堆邏輯上分成Eden,Survivor(S0,S1),Old,並且他們是固定大小JVM啓動的時候就已經設定不能改變,並且是連續的內存塊
  • G1 將堆分成多個大小相同的Region(區域),默認2048個,在1Mb到32Mb之間大小,邏輯上分成Eden,Survivor,Old,巨型,空閒,他們不是固定大小,會根據每次GC的信息做出調整

區別二:G1可以 預測停頓的時間,但是CMS就不能預測停頓的時間

  • 相比CMS,G1可以設定每次GC的時間,從而讓GC在規定時間內回收效益最大的內存

區別三:壓縮策略不同

  • 如果當CMS不啓用壓縮的時候會產生很多的垃圾碎片,當產生很多內存碎片的時候,找不到空間來分配剩餘的對象,或者設定參數,使它合併相鄰的的空閒內存,當合並超過一定次數後觸發Full GC,進行壓縮
  • G1中每次回收過程中,將多個Region拷貝到空閒Region的時候都會進行壓縮

面試題19:jvm調優參數

面試題20. 講講 GC 機制,知道擔保機制嗎

擔保機制就是:在發生MinorGC的時候虛擬機會先進行檢測每次晉升到老年代的平均大小是不是大於老年代的剩餘大小的空間容量大小,如果大於的話,就可以直接進行一次(在老年代發生)full gc ,如果小於的話,則會去查看HandlePromotionFailure設置是不是是允許擔保失敗,如果允許,那就只會進行MinorGC,如果不允許的話,還是會進行一次full GC

面試題21. 垃圾回收算法,新生代和年老代用的什麼算法,爲什麼用這個算法?

新生代使用的是複製算法,主要有以下幾點原因:

  1. 首先使用複製算法的特點就是:兩塊內存,先把B進行預留出來,然後把A中的存活對象進行標記,然後在清除回收的對象,把存活的對象複製到B區,這樣不會有內存碎片產生,讓內存可以連續性。
  2. 第一點就是存活的對象複製後產生的是一組連續的內存空間
  3. 第二點就是由於在新生代中垃圾對象多於存活的對象,這樣的場景複製算法那更加的高效。

老生代使用的是標記-整理算法,主要有以下幾點原因:

  1. 首先因爲老年代的存活對象多於垃圾對象,而且老生代區域中的對象生命週期較長,採用複製算法將會產生頻繁的複製操作,對應的開銷較大,而標記整理算法可以很好的避免這個問題,提升垃圾回收的效率。

面試題22. 知道爲什麼loadClass和class.forName()之間的區別和好處嗎?

(1)你得先能說出JVM中loadClass()的類裝載的過程(分三大步):

  1. 第一大步是加載:先通過ClassLoader對class字節碼文件進行加載,然後加載到JVM中並將這些靜態數據轉化爲運行時的數據區
  2. 第二大步是鏈接:其中又包含了三小步:
    1. 分別是效驗:檢查加載過來的Class文件的安全性和正確性
    2. 然後是準備:爲類變量分配存儲空間,然後並設置類中的變量設置初始值比如(int 就是默認值爲0,boolea 就是false,都是在這個過程完成的)
    3. 最後是解析:JVM將長量池中的符號引用轉換爲直接引用,這個是可選的
  3. 第三大步是:類的初始化就是執行類變量的真實賦值(這裏的賦值就是你定義的int i = 10 ,就把10付給變量i),和執行靜態代碼塊的內容

(2)他們之間的區別和各個地方的用的時候的好處?

  1. 首先我們使用class.forName(“com.mysql.cj.jdbc.Driver”)一般都是這麼用,他的好處是使用forName加載之後的類都是經過初始化的,比如我們一般加載數據庫驅動的時候是這樣的,就是爲了進行初始化Driver驅動對象,這個相當於完成了三大步整個類的裝載和初始化,
    在這裏插入圖片描述
    上面的這個forName加載就需要把這個類進行初始化
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
使用了class.forName的時候下面這個靜態代碼塊也會進行加載
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

  1. 但是我們在使用loadclass的時候其實就只是完成了上面所說的類的裝載的第一大步而已,並沒有全部進行加載完成,比如說在Spring中,這麼做的目的就是爲了在啓動Spring容器的時候加快加載classpath下面的類的時候使用的就是loadclass()的懶加載機制,不用把類進行初始化節省了時間,下圖中到那個resolve 爲false
    在這裏插入圖片描述

測試:

  1. G1內部是如何分區的(region)
  2. GC 可達性分析中哪些算是GC ROOT
  3. 一個類在什麼情況下會被加載到虛擬機中
  4. 雙親委派模型,怎麼打破雙親委派
  5. CMS哪個階段是併發的哪個階段是串行的?(其實都是併發的,只不過初始標記和重新標記是垃圾回收線程的併發,而併發標記和併發回收是用戶線程和回收線程的併發)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章