JVM常見面試題


1. 內存模型以及分區,需要詳細到每個區放什么。

棧區:

棧分為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的區塊。

 

2. 堆里面的分區:Eden,survivalfrom to,老年代,各自的特點。

1.JVM中堆空間可以分成三個大區,新生代、老年代、永久代

2.新生代可以划分為三個區,Eden區,兩個幸存區。

在JVM運行時,可以通過配置以下參數改變整個JVM堆的配置比例

1.JVM運行時堆的大小

-Xms堆的最小值
-Xmx堆空間的最大值

2.新生代堆空間大小調整

  -XX:NewSize新生代的最小值
  -XX:MaxNewSize新生代的最大值
  -XX:NewRatio設置新生代與老年代在堆空間的大小
  -XX:SurvivorRatio新生代中Eden所占區域的大小

3.永久代大小調整

 -XX:MaxPermSize

4.其他

-XX:MaxTenuringThreshold,設置將新生代對象轉到老年代時需要經過多少次垃圾回收,但是仍然沒有被回收

 

復制(Copying)算法

將內存平均分成A、B兩塊,算法過程:

1. 新生對象被分配到A塊中未使用的內存當中。當A塊的內存用完了, 把A塊的存活對象對象復制到B塊。

2. 清理A塊所有對象。

3. 新生對象被分配的B塊中未使用的內存當中。當B塊的內存用完了, 把B塊的存活對象對象復制到A塊。

4. 清理B塊所有對象。

5. goto 1。

優點:簡單高效。

缺點:內存代價高,有效內存為占用內存的一半。

 

對復制算法進一步優化:使用Eden/S0/S1三個分區

 

平均分成A/B塊太浪費內存,采用Eden/S0/S1三個區更合理,空間比例為Eden:S0:S1==8:1:1,有效內存(即可分配新生對象的內存)是總內存的9/10。 

算法過程:

1. Eden+S0可分配新生對象;

2. 對Eden+S0進行垃圾收集,存活對象復制到S1。清理Eden+S0。一次新生代GC結束。

3. Eden+S1可分配新生對象;

4. 對Eden+S1進行垃圾收集,存活對象復制到S0。清理Eden+S1。二次新生代GC結束。

5. goto 1。

 

默認Eden:S0:S1=8:1:1,因此,新生代中可以使用的內存空間大小占用新生代的9/10,那么有人就會問,為什么不直接分成兩個區,一個區占9/10,另一個區占1/10,

這樣做的原因大概有以下幾種

1.S0與S1的區間明顯較小,有效新生代空間為Eden+S0/S1,因此有效空間就大,增加了內存使用率

2.有利於對象代的計算,當一個對象在S0/S1中達到設置的XX:MaxTenuringThreshold值后,會將其分到老年代中,設想一下,如果沒有S0/S1,直接分成兩個區,該如何計算對象經過了多少次GC還沒被釋放,你可能會說,在對象里加一個計數器記錄經過的GC次數,或者存在一張映射表記錄對象和GC次數的關系,是的,可以,但是這樣的話,會掃描整個新生代中的對象, 有了S0/S1我們就可以只掃描S0/S1區了~~~

3. 對象創建方法,對象的內存分配,對象的訪問定位。

創建:

1. 類加載檢查

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

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

2. 對象分配內存

對象所需內存的大小在類加載完成后便完全確定(對象內存布局),為對象分配空間的任務等同於把一塊確定大小的內存從Java堆中划分出來。

根據Java堆中是否規整有兩種內存的分配方式:(Java堆是否規整由所采用的垃圾收集器是否帶有壓縮整理功能決定)。 

指針碰撞(Bump the pointer)

Java堆中的內存是規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放着一個指針作為分界點的指示器,分配內存也就是把指針向空閑空間那邊移動一段與內存大小相等的距離。例如:Serial、ParNew等收集器。

空閑列表(Free List)

Java堆中的內存不是規整的,已使用的內存和空閑的內存相互交錯,就沒有辦法簡單的進行指針碰撞了。虛擬機必須維護一張列表,記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新列表上的記錄。例如:CMS這種基於Mark-Sweep算法的收集器。

3. 並發處理

對象創建在虛擬機中時非常頻繁的行為,即使是僅僅修改一個指針指向的位置,在並發情況下也並不是線程安全的,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。

 

同步

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

本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)

把內存分配的動作按照線程划分為在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存(TLAB)。哪個線程要分配內存,就在哪個線程的TLAB上分配。只有TLAB用完並分配新的TLAB時,才需要同步鎖定。

4. 內存空間初始化

虛擬機將分配到的內存空間都初始化為零值(不包括對象頭),如果使用了TLAB,這一工作過程也可以提前至TLAB分配時進行。

內存空間初始化保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。 

注意:類的成員變量可以不顯示地初始化(Java虛擬機都會先自動給它初始化為默認值)。方法中的局部變量如果只負責接收一個表達式的值,可以不初始化,但是參與運算和直接輸出等其它情況的局部變量需要初始化。

5. 對象設置

虛擬機對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭之中。

6. 執行init()

在上面的工作都完成之后,從虛擬機的角度看,一個新的對象已經產生了。但是從Java程序的角度看,對象的創建才剛剛開始init()方法還沒有執行,所有的字段都還是零。

所以,一般來說(由字節碼中是否跟隨invokespecial指令所決定),執行new指令之后會接着執行init()方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象才算產生出來。

訪問定位:句柄或者直接指針。

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

1.在JDK1.2之前,使用的是引用計數器算法,即當這個類被加載到內存之后,就會產生方法區,堆棧、程序計數

器等一系列信息,當創建對象的時候,為這個對象在堆棧空間中分配對象,同時會產生一個引用計數器,同時引

用計數器+1,當有新的引用時,引用計數器繼續+1,而當其中一個引用銷毀時,引用計數器-1,當引用計數器減

為0的時候,標志着這個對象已經沒有引用了,可以回收了!但是這樣會有一個問題:

當我們的代碼出現這樣的情況時:

a)ObjA.obj=ObjB

b)ObjB.obj=ObjA

這樣的代碼會產生如下引用情形objA指向objB,而ObjB又指向objA,這樣當其他所有的引用都消失了之后,objA

和objB還有一個相互的引用,也就是說兩個對象的引用計數器各為1,而實際上這兩個對象都已經沒有額外的引用,已經是垃圾了。

2.根搜索算法:

根搜索算法是從離散數學中的圖論引入的,程序把所有的引用關系看做一張圖,從一個節點GC Root開始,尋找對

應的引用節點,找到這個節點之后,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之后,剩余的節

點則被認為是沒有被飲用到的節點,即無用的節點。

目前Java中可作為GC Root的對象有:

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

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

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

4.本地方法棧中引用的對象(Native對象)。

java中存在的四種引用

(1)強引用:

只要引用存在,垃圾回收器永遠不會回收。

(2)軟引用

非必須引用,內存溢出之前進行回收,可以通過以下代碼實現

Object obj=new Object();

SoftReference<Object> sf=newSoftRerence<Object>(obj);

obj=null;

sf.get();//有時會返回null

這時候sf是對obj的一個軟引用,通過sf.get()方法可以取到這個對象,當然這個對象被標記為需要回收的對象時,

則返回null;

軟引用主要用於用戶實現類似緩存的功能,在內存不足的情況下直接通過軟引用取值,無需從繁忙的真實來源查

詢數據,提升速度;當內存不足時,自動刪除這部分緩存數據,從真實的來源查詢這些數據。

(3)弱引用

第二次垃圾回收時回收,可以通過如下代碼實現

Object obj=new Object();

WeakReference<Object> wf=newWeakReference<Object>(obj);

obj=null;

wf.get();//有時會返回null

wf.isEnQueued();//返回是否被垃圾回收器標記為即將回收的垃圾

弱引用是在第二次垃圾回收時回收,短時間內通過弱引用取對應的數據,可以取到,當執行過第二次垃圾回收時,

將返回null。弱引用主要用於監控對象是否已經被標記為即將回收的垃圾,可以通過弱引用的isEnQueues方法

返回對象是否被垃圾回收器標記。

(4)虛引用

垃圾回收時回收,無法通過引用取到對象值,可以通過如下代碼實現

Object obj=new Object();

PhantomReference<Object> pf=newPhantomReference<Object>(obj);

obj=null;

pf.get();//永遠是返回null

pf.isEnQueued();//返回從內從中已經刪除。

虛引用是每次垃圾回收的時候都會被回收,通過虛引用的get方法永遠獲取到的數據為null。

 

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

垃圾回收算法

標記-清除算法(Mark-Sweep)

從根節點開始標記所有可達對象,其余沒有標記的即為垃圾對象,執行清除。但回收后的空間是不連續的。

標記-清除算法采用從根集合進行掃描,對存活的對象標記,標記完畢后,在掃描整個空間中未被標記的對象,進

行回收。

標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活對象比較多的情況下極為高效,

但由於標記-清除算法直接回收不存活的對象,因此會造成內存碎片。

復制算法

復制算法采用從根集合掃描,並將存活對象復制到一塊新的,沒有使用過的空間中,這種算法當控件存活的對象

比較少時,極為高效,但是帶來的成本是需要一塊內存交換空間進行對象的移動。也就是s0,s1等空間。

標記-整理法

標記-整理算法采用標記-清除算法一樣的方式進行對象的標記,但在清除時,在回收不存活的對象占用的空間后,

會將所有的存活對象網左端空閑空間移動,並更新相應的指針。標記-整理算法是在標記-清除算法的基礎上,

又進行了對象的移動,因此成本更高,但是卻解決了內存碎片的問題。

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

串行垃圾回收器(Serial Garbage Collector)

並行垃圾回收器(Parallel Garbage Collector)

並發標記掃描垃圾回收器(CMS Garbage Collector)

G1垃圾回收器(G1 GarbageCollector)

 

並發標記垃圾回收使用多線程掃描堆內存,標記需要清理的實例並且清理被標記過的實例。並發標記垃圾回收器只會在下面兩種情況持有應用程序所有線程。

 

當標記的引用對象在tenured區域;

在進行垃圾回收的時候,堆內存的數據被並發的改變。

相比並行垃圾回收器,並發標記掃描垃圾回收器使用更多的CPU來確保程序的吞吐量。如果我們可以為了更好的程序性能分配更多的CPU,那么並發標記上掃描垃圾回收器是更好的選擇相比並發垃圾回收器。

 

通過JVM參數 XX:+USeParNewGC 打開並發標記掃描垃圾回收器

 

G1垃圾回收器適用於堆內存很大的情況,他將堆內存分割成不同的區域,並且並發的對其進行垃圾回收。G1也可以在回收內存之后對剩余的堆內存空間進行壓縮。並發掃描標記垃圾回收器在STW情況下壓縮內存。G1垃圾回收會優先選擇第一塊垃圾最多的區域

 

通過JVM參數–XX:+UseG1GC 使用G1垃圾回收器

 

7. Minor GC與Full GC分別在什么時候發生?

從年輕代空間(包括 Eden 和 Survivor 區域)回收內存被稱為 Minor GC

這一定義既清晰又易於理解。但是,當發生Minor GC事件的時候,有一些有趣的地方需要注意到:

當 JVM 無法為一個新的對象分配空間時會觸發 Minor GC,比如當 Eden 區滿了。所以分配率越高,越頻繁執行 Minor GC。

內存池被填滿的時候,其中的內容全部會被復制,指針會從0開始跟蹤空閑內存。Eden 和 Survivor 區進行了標記和復制操作,取代了經典的標記、掃描、壓縮、清理操作。所以 Eden 和 Survivor 區不存在內存碎片。寫指針總是停留在所使用內存池的頂部。

執行 Minor GC 操作時,不會影響到永久代。從永久代到年輕代的引用被當成 GC roots,從年輕代到永久代的引用在標記階段被直接忽略掉。

質疑常規的認知,所有的 Minor GC 都會觸發“全世界的暫停(stop-the-world)”,停止應用程序的線程。對於大部分應用程序,停頓導致的延遲都是可以忽略不計的。

其中的真相就是,大部分 Eden 區中的對象都能被認為是垃圾,永遠也不會被復制到 Survivor 區或者老年代空間。

如果正好相反,Eden 區大部分新生對象不符合 GC 條件,Minor GC 執行時暫停的時間將會長很多。

所以 Minor GC 的情況就相當清楚了——每次 Minor GC 會清理年輕代的內存。

 

大家應該注意到,目前,這些術語無論是在 JVM 規范還是在垃圾收集研究論文中都沒有正式的定義。但是我們一看就知道這些在我們已經知道的基礎之上做出的定義是正確的,

Minor GC 清理年輕帶內存應該被設計得簡單:

 

Major GC 是清理永久代。

Full GC 是清理整個堆空間—包括年輕代和永久代

很不幸,實際上它還有點復雜且令人困惑。首先,許多 Major GC 是由 Minor GC 觸發的,所以很多情況下將這兩種 GC 分離是不太可能的。另一方面,許多現代垃圾收集機制會清理部分永久代空間,所以使用“cleaning”一詞只是部分正確。

這使得我們不用去關心到底是叫 Major GC 還是 Full GC,大家應該關注當前的 GC 是否停止了所有應用程序的線程,還是能夠並發的處理而不用停掉應用程序的線程。

8. 幾種常用的內存調試工具:jmap、jstack、jconsole。

9. 類加載的五個過程:加載、驗證、准備、解析、初始化。

加載:

       在加載階段,虛擬機主要完成三件事:

1.通過一個類的全限定名來獲取定義此類的二進制字節流。

2.將這個字節流所代表的靜態存儲結構轉化為方法區域的運行時數據結構。

3.在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區域數據的訪問入口。

驗證:

       驗證階段作用是保證Class文件的字節流包含的信息符合JVM規范,不會給JVM造成危害。如果驗證失敗,就會拋出一個java.lang.VerifyError異常或其子類異常。驗證過程分為四個階段:

1.文件格式驗證:驗證字節流文件是否符合Class文件格式的規范,並且能被當前虛擬機正確的處理。

2.元數據驗證:是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言的規范。

3.字節碼驗證:主要是進行數據流和控制流的分析,保證被校驗類的方法在運行時不會危害虛擬機。

4.符號引用驗證:符號引用驗證發生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在解析階段中發生。

准備:

       准備階段為變量分配內存並設置類變量的初始化。在這個階段分配的僅為類的變量(static修飾的變量),而不包括類的實例變量。對已非final的變量,JVM會將其設置成“零值”,而不是其賦值語句的值:

pirvate static int size = 12;

       那么在這個階段,size的值為0,而不是12。 final修飾的類變量將會賦值成真實的值。

解析:

       解析過程是將常量池內的符號引用替換成直接引用。主要包括四種類型引用的解析。類或接口的解析、字段解析、方法解析、接口方法解析。

初始化:

       在准備階段,類變量已經經過一次初始化了,在這個階段,則是根據程序員通過程序制定的計划去初始化類的變量和其他資源。這些資源有static{}塊,構造函數,父類的初始化等。

       至於使用和卸載階段階段,這里不再過多說明,使用過程就是根據程序定義的行為執行,卸載由GC完成。

10. 雙親委派模型:Bootstrap ClassLoader、ExtensionClassLoader、ApplicationClassLoader。

類加載器按照層次,從頂層到底層,分為以下三種:

 (1)啟動類加載器(BootstrapClassLoader)

  這個類加載器負責將存放在JAVA_HOME/lib下的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的類庫加載到虛擬機內存中。啟動類加載器無法被Java程序直接引用。

 (2)擴展類加載器(ExtensionClassLoader)

  這個加載器負責加載JAVA_HOME/lib/ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器

 (3)應用程序類加載器(ApplicationClassLoader)

  這個加載器是ClassLoader中getSystemClassLoader()方法的返回值,所以一般也稱它為系統類加載器。它負責加載用戶類路徑(Classpath)上所指定的類庫,可直接使用這個加載器,如果應用程序沒有自定義自己的類加載器,一般情況下這個就是程序中默認的類加載

 類加載的雙親委派模型

  雙親委派模型要求除了頂層的啟動類加載器外,其他的類加載器都應當有自己的父類加載器。這里類加載器之間的父子關系一般不會以繼承關系來實現,而是都使用組合關系來復用父加載器的代碼

  工作過程:

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳遞到頂層的啟動類加載器中,

只有當父類加載器反饋自己無法完成這個請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載

好處:

Java類隨着它的類加載器一起具備了一種帶有優先級的層次關系。例如類Object,它放在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類

判斷兩個類是否相同是通過classloader.class這種方式進行的,所以哪怕是同一個class文件如果被兩個classloader加載,那么他們也是不同的類

 實現自己的加載器

  只需要繼承ClassLoader,並覆蓋findClass方法

  在調用loadClass方法時,會先根據委派模型在父加載器中加載,如果加載失敗,則會調用自己的findClass方法來完成加載

 

11. 分派:靜態分派與動態分派。

靜態分派

所有依賴靜態類型來定位方法執行版本的分派動作稱為靜態分派,其典型應用是方法重載(根據參數的靜態類型來定位目標方法)。

靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機執行的。

動態分派

在運行期根據實際類型確定方法執行版本。

 

摘自:https://www.cnblogs.com/alsf/p/9398951.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM