java面試(五)JVM

目錄

JVM的組成(類加載器、運行數據區、解釋器、本地接口各自的含義與作用)

java程序在JVM中的執行過程

加載

鏈接

初始化

類加載的時機

類加載器的種類(Bootstrap 、Extension 、App 、自定義四個類加載器的工作責任)

類加載器的工作過程(雙親委派機制)

類加載的詳細過程(加載-連接-初始化-使用-卸載 五個步驟的含義)

加載

鏈接

初始化

卸載

JVM內存模型(程序計數器、堆、虛擬機棧、本地方法棧、方法區)

GC(垃圾回收機制)

判斷對象存活的方法(引用計數法和可達性分析的區別)

1、引用計數算法

 2、可達性分析算法:

可達性分析中可以作爲GC Roots的對象

垃圾回收算法(標記/清除算法、複製算法、標記/整理算法的區別以及適用環境)

GC時間

什麼情況下出現內存溢出,內存泄漏?

GC回收器

JVM內存爲什麼要分成新生代,老年代,持久代。新生代中爲什麼要分爲Eden和Survivor。​

JVM中一次完整的GC流程是怎樣的,對象如何晉升到老年代



JVM的組成(類加載器、運行數據區、解釋器、本地接口各自的含義與作用)

JVM 整體組成可分爲以下四個部分:

  1. 類加載器(ClassLoader)

  2. 運行時數據區(Runtime Data Area)

  3. 執行引擎(Execution Engine)

  4. 本地庫接口(Native Interface)

各個組成部分的用途:

程序在執行之前先要把java代碼轉換成字節碼(class文件),jvm首先需要把字節碼通過一定的方式 類加載器(ClassLoader) 把文件加載到內存中 運行時數據區(Runtime Data Area) ,而字節碼文件是jvm的一套指令集規範,並不能直接交個底層操作系統去執行,因此需要特定的命令解析器 執行引擎(Execution Engine) 將字節碼翻譯成底層系統指令再交由CPU去執行,而這個過程中需要調用其他語言的接口 本地庫接口(Native Interface) 來實現整個程序的功能,這就是這4個主要組成部分的職責與功能。

java程序在JVM中的執行過程

加載

JVM將java類的二進制形式加載到內存中,並將他緩存在內存中,以便後面使用,如果沒有找到指定的類就會拋出異常classNotFound,進程在這裏結束。沒有錯誤就繼續在Java堆中生成一個代表這個類的java.lang.Class對象,作爲方法區域數據的訪問入口。

鏈接

這個階段做三件事:驗證、準備和解析(可選)。

驗證是JVM根據java語言和JVM的語義要求檢查這個二進制形式。確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

虛擬機規範:如果驗證到輸入的字節流不符合Class文件的存儲格式,就拋出一個java.lang.VerifyError異常或其子類異常

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

public static int num = 8;

那麼在這個階段,num的值爲0,而不是8。 final修飾的類變量將會賦值成真實的值。

解析是檢查指定的類是否引用了其他的類/接口,是否能找到和加載其他的類/接口。這些檢查將針對被引用的類/接口遞歸進行,JVM的實施也可以在後面階段執行解析,即正在執行的代碼真正要使用被引用的類/接口的時候。

初始化

在這最後一步中,JVM用賦值或者缺省值將靜態變量初始化,初始化發生在執行main方法之前。在指定的類初始化前,會先初始化它的父類,此外,在初始化父類時,父類的父類也要這樣初始化。這個過程是遞歸進行的。

簡而言之,整個流程是將類存進內存中,檢查類的對應調用的類和接口是否可正常使用,再對類進行初始化的過程。

類加載的時機

 

  • 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節碼指令時,如果類還沒有進行過初始化,則需要先觸發其初始化。生成這 4 條指令的最常見的 java 代碼場景是:使用 new 關鍵字實例化對象的時候讀取或設置一個類的靜態字段(被 final 修飾 以及 已在編譯期把結果放入常量池的靜態字段除外)的時候以及調用一個類的靜態方法的時候
  • 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬器會先初始化這個主類。
  • 當使用 JDK 1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果爲 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要出發其初始化。
  •  

類加載器的種類(Bootstrap 、Extension 、App 、自定義四個類加載器的工作責任)

類加載器 就是根據指定全限定名稱將class文件加載到JVM內存,轉爲Class對象。

啓動類加載器(Bootstrap ClassLoader):由C++語言實現(針對HotSpot),負責將存放在<JAVA_HOME>\lib目錄或-Xbootclasspath參數指定的路徑中的類庫加載到內存中。
其他類加載器:由Java語言實現,繼承自抽象類ClassLoader。如:
擴展類加載器(Extension ClassLoader):負責加載<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變量指定的路徑中的所有類庫。
應用程序類加載器(Application ClassLoader)。負責加載用戶類路徑(classpath)上的指定類庫,我們可以直接使用這個類加載器。一般情況,如果我們沒有自定義類加載器默認就是用這個加載器。

類加載器的工作過程(雙親委派機制)

如果一個類加載器收到類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器完成。每個類加載器都是如此,只有當父加載器在自己的搜索範圍內找不到指定的類時(即ClassNotFoundException),子加載器纔會嘗試自己去加載。

爲什麼需要雙親委派模型?

在這裏,先想一下,如果沒有雙親委派,那麼用戶是不是可以自己定義一個java.lang.Object的同名類,java.lang.String的同名類,並把它放到ClassPath中,那麼類之間的比較結果及類的唯一性將無法保證,因此,爲什麼需要雙親委派模型?防止內存中出現多份同樣的字節碼

怎麼打破雙親委派模型?

打破雙親委派機制則不僅要繼承ClassLoader類,還要重寫loadClass和findClass方法。

 

類加載的詳細過程(加載-連接-初始化-使用-卸載 五個步驟的含義)

加載

JVM將java類的二進制形式加載到內存中,並將他緩存在內存中,以便後面使用,如果沒有找到指定的類就會拋出異常classNotFound,進程在這裏結束。沒有錯誤就繼續在Java堆中生成一個代表這個類的java.lang.Class對象,作爲方法區域數據的訪問入口。

鏈接

這個階段做三件事:驗證、準備和解析(可選)。

驗證是JVM根據java語言和JVM的語義要求檢查這個二進制形式。確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

虛擬機規範:如果驗證到輸入的字節流不符合Class文件的存儲格式,就拋出一個java.lang.VerifyError異常或其子類異常

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

public static int num = 8;

那麼在這個階段,num的值爲0,而不是8。 final修飾的類變量將會賦值成真實的值。

解析是虛擬機將 常量池中的符號引用 轉化爲 直接引用 的過程,在 Class 文件中它以 CONSTANT_Class_info、CONSTANT_FIeldref_info、CONSTANT_Methodref_info 等類型的常量出現。

初始化

在這最後一步中,JVM用賦值或者缺省值將靜態變量初始化,初始化發生在執行main方法之前。在指定的類初始化前,會先初始化它的父類,此外,在初始化父類時,父類的父類也要這樣初始化。這個過程是遞歸進行的。

簡而言之,整個流程是將類存進內存中,檢查類的對應調用的類和接口是否可正常使用,再對類進行初始化的過程。

卸載

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

JVM內存模型(程序計數器、堆、虛擬機棧、本地方法棧、方法區)

程序計數器:當前線程所執行的字節碼的行號指示器,用於記錄正在執行的虛擬機字節指令地址,線程私有。

Java虛擬棧:存放基本數據類型、對象的引用、方法出口等,線程私有。

Native方法棧:和虛擬棧相似,只不過它服務於Native方法,線程私有。

Java堆:java內存最大的一塊,所有對象實例、數組都存放在java堆,GC回收的地方,線程共享。

方法區:存放已被加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼數據等。(即永久帶),回收目標主要是常量池的回收和類型的卸載,各線程共享

GC(垃圾回收機制)

Java內存

堆是Java虛擬機進行垃圾回收的主要場所,其次要場所是方法區。

GC的主要任務:
1.分配內存
2.確保被引用對象的內存不被錯誤的回收
3.回收不再被引用的對象的內存空間

垃圾回收機制的主要解決問題
1.哪些內存需要回收?
2.什麼時候回收?
3.如何回收?

 

判斷對象存活的方法(引用計數法和可達性分析的區別)

1、引用計數算法


每當一個地方引用它時,計數器+1;引用失效時,計數器-1;計數值=0——不可能再被引用。

判定效率很高。不會完全準確,因爲如果出現兩個對象相互引用的問題就不行了。

//舉例:
        Test test1 = new Test();
        Test test2 = new Test();
        test1.obj = test2;
        test2.obj = test1;
        //test1 ,test12能否被回收?
        System.gc();

 2、可達性分析算法:

通過一系列的GC Roots的對象作爲起始點,從這些根節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。

舉例:一顆樹有很多丫枝,其中一個分支斷了,跟樹上沒有任何聯繫,那就說明這個分支沒有用了,就可以當垃圾回收去燒了。

可達性分析中可以作爲GC Roots的對象

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象;
  2. 方法區中類靜態屬性引用的對象;
  3. 方法區中常量引用的對象;
  4. 本地方法棧中JNI(就是native方法)引用的對象

垃圾回收算法(標記/清除算法、複製算法、標記/整理算法的區別以及適用環境)

標記清除法:標記所有的可達對象,則未標記的對象就是不存在引用的垃圾對象,GC時清除所有未標記的對象;

【先標記再清除】

  • 優點:只標記正常引用的對象,不標記循環引用這樣的垃圾對象以及沒有引用的對象,解決了循環引用的問題;
  • 缺點:可能會產生空間碎片(不連續的內存空間),不連續的內存空間在內存分配時的工作效率低於連續的內存空間,尤其時堆大對象的內存分配;

複製算法:將內存空間分爲兩塊相同的存儲空間,每次只使用一塊,GC時將正在使用的內存中的存活對象複製到另一塊存儲空間中,然後清除正在使用的空間的所有對象;【先複製再清除】

  • 優點:存活對象相對少時,效率很高(需要複製的對象少),存活對象複製到另一空間時,解決了空間碎片的問題;
  • 缺點:系統內存只能使用一半的內存空間,而且如果存活對象過多時,比較耗時;
  • 應用場景:java新生代串行垃圾回收器(優點:保證了內存空間的連續性,又避免了大量的空間浪費)

標記壓縮法(標記清除壓縮法):同標記清除算法一樣首先標記存活的對象,但是再標記之後還要將所有標記的對象壓縮到內存空間的一端後再清理邊界外的所有空間;【標記+壓縮+清除】

  • 優點:解決了標記清除法帶來的空間碎片問題,也不需要折損可使用空間;
  • 缺點:壓縮過程的處理提升了垃圾回收過程的複雜度
  • 應用場景:老年代的回收算法

分代算法:根據對象的特點堆內存空間進行劃分,選擇合適的垃圾回收算法。

  • 優點:提交了垃圾回收的效率;
  • 缺點:不同區域是使用不同的垃圾回收算法提升了垃圾回收過程的複雜度?
  • 應用場景:虛擬機的垃圾回收器

分區算法:將整個堆空間劃分爲連續的不同小區間,每一個小區間都獨立使用、獨立回收

  • 優點:可以控制一次回收多少個小區間,從而減少1GC的停頓時間;
     
  1. GC時間

       ①在程序空閒的時候。這個回答無力吐槽

  ②程序不可預知的時候/手動調用system.gc()。關於手動調用不推薦

  ③Java堆內存不足時,GC會被調用。當應用線程在運行,並在運行過程中創建新對象,若這時內存空間不足,JVM就會強制地調用GC線程,以便回收內存用於新的分配。若GC一次之後仍不能滿足內存分配的要求,JVM會再進行兩次GC作進一步的嘗試,若仍無法滿足要求,則 JVM將報“out of memory”的錯誤,Java應用將停止。就是

  這時候如果你們講出新生代和老年代的話或許會更細的瞭解一下Minor GC、Full GC、OOM什麼時候觸發!

  創建對象是新生代的Eden空間調用Minor GC;當升到老年代的對象大於老年代剩餘空間Full GC;GC與非GC時間耗時超過了GCTimeRatio的限制引發OOM。

 

什麼情況下出現內存溢出,內存泄漏?

內存泄漏:

概念:由於java的JVM引入了垃圾回收機制,垃圾回收器會自動回收不再使用的對象;JVM是使用引用計數法和可達性分析算法來判斷對象是否是不再使用的對象,本質都是判斷一個對象是否還被引用。那麼對於這種情況下,由於代碼的實現不同就會出現很多種內存泄漏問題(讓JVM誤以爲此對象還在引用中,無法回收,造成內存泄漏)。

 

造成內存泄漏的方式:

1:靜態集合類

2:各種連接沒有關閉:數據庫,io

3:變量不合理的作用域:變量定義的範圍大於其使用的範圍,如果沒有及時把變量置爲null,就容易內存泄露

4、內部類持有外部類,如果一個外部類的實例對象的方法返回了一個內部類的實例對象,這個內部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由於內部類持有外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成內存泄露。

5、改變哈希值,當一個對象被存儲進HashSet集合中以後,就不能修改這個對象中的那些參與計算哈希值的字段了,否則,對象修改後的哈希值與最初存儲進HashSet集合中時的哈希值就不同了,在這種情況下,即使在contains方法使用該對象的當前引用作爲的參數去HashSet集合中檢索對象,也將返回找不到對象的結果,這也會導致無法從HashSet集合中單獨刪除當前對象,造成內存泄露

 

GC回收器

1)幾種垃圾收集器:

  • Serial收集器: 單線程的收集器,收集垃圾時,必須stop the world,使用複製算法。
  • ParNew收集器: Serial收集器的多線程版本,也需要stop the world,複製算法。
  • Parallel Scavenge收集器: 新生代收集器,複製算法的收集器,併發的多線程收集器,目標是達到一個可控的吞吐量。如果虛擬機總共運行100分鐘,其中垃圾花掉1分鐘,吞吐量就是99%。
  • Serial Old收集器: 是Serial收集器的老年代版本,單線程收集器,使用標記整理算法。
  • Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多線程,標記-整理算法。
  • CMS(Concurrent Mark Sweep) 收集器: 是一種以獲得最短回收停頓時間爲目標的收集器,標記清除算法,運作過程:初始標記,併發標記,重新標記,併發清除,收集結束會產生大量空間碎片。
  • G1收集器: 標記整理算法實現,運作流程主要包括以下:初始標記,併發標記,最終標記,篩選標記。不會產生空間碎片,可以精確地控制停頓。

2)CMS收集器和G1收集器的區別:

  1. CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
  2. G1收集器收集範圍是老年代和新生代,不需要結合其他收集器使用;
  3. CMS收集器以最小的停頓時間爲目標的收集器;
  4. G1收集器可預測垃圾回收的停頓時間
  5. CMS收集器是使用“標記-清除”算法進行的垃圾回收,容易產生內存碎片
  6. G1收集器使用的是“標記-整理”算法,進行了空間整合,降低了內存空間碎片。

JVM內存爲什麼要分成新生代,老年代,持久代。新生代中爲什麼要分爲Eden和Survivor。

1)共享內存區劃分

  • 共享內存區 = 持久帶 + 堆
  • 持久帶 = 方法區 + 其他
  • Java堆 = 老年代 + 新生代
  • 新生代 = Eden + S0 + S1

2)一些參數的配置

  • 默認的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值爲 1:2 ,可以通過參數 –XX:NewRatio 配置。
  • 默認的,Edem : from : to = 8 : 1 : 1 ( 可以通過參數 –XX:SurvivorRatio 來設定)
  • Survivor區中的對象被複制次數爲15(對應虛擬機參數 -XX:+MaxTenuringThreshold)

3)爲什麼要分爲Eden和Survivor?爲什麼要設置兩個Survivor區?

  • 如果沒有Survivor,Eden區每進行一次Minor GC,存活的對象就會被送到老年代。老年代很快被填滿,觸發Major GC.老年代的內存空間遠大於新生代,進行一次Full GC消耗的時間比Minor GC長得多,所以需要分爲Eden和Survivor。
  • Survivor的存在意義,就是減少被送到老年代的對象,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷16次Minor GC還能在新生代中存活的對象,纔會被送到老年代。
  • 設置兩個Survivor區最大的好處就是解決了碎片化,剛剛新建的對象在Eden中,經歷一次Minor GC,Eden中的存活對象就會被移動到第一塊survivor space S0,Eden被清空;等Eden區再滿了,就再觸發一次Minor GC,Eden和S0中的存活對象又會被複制送入第二塊survivor space S1(這個過程非常重要,因爲這種複製算法保證了S1中來自S0和Eden兩部分的存活對象佔用連續的內存空間,避免了碎片化的發生)

 

JVM中一次完整的GC流程是怎樣的,對象如何晉升到老年代
 

  1. 新建的對象,大部分存儲在Eden中
  2. 當Eden內存不夠,就進行Minor GC釋放掉不活躍對象;然後將部分活躍對象複製到Survivor中(如Survivor1),同時清空Eden區
  3. 當Eden區再次滿了,將Survivor1中不能清空的對象存放到另一個Survivor中(如Survivor2),同時將Eden區中的不能清空的對象,複製到Survivor1,同時清空Eden區
  4. 4重複多次(默認15次):Survivor中沒有被清理的對象就會複製到老年區(Old)
  5. 當Old達到一定比例,則會觸發Major GC釋放老年代
  6. 當Old區滿了,則觸發一個一次完整的垃圾回收(Full GC)
  7. 如果內存還是不夠,JVM會拋出內存不足,發生oom,內存泄漏。
     

Minor GC 和 Full GC的區別

       新生代GC(Minor GC):Minor GC指發生在新生代的GC,因爲新生代的Java對象大多都是朝生夕死,所以Minor GC非常頻繁,一般回收速度也比較快。當Eden空間不足以爲對象分配內存時,會觸發Minor GC。

       老年代GC(Full GC/Major GC):Full GC指發生在老年代的GC,出現了Full GC一般會伴隨着至少一次的Minor GC(老年代的對象大部分是Minor GC過程中從新生代進入老年代),比如:分配擔保失敗。Full GC的速度一般會比Minor GC慢10倍以上。當老年代內存不足或者顯式調用System.gc()方法時,會觸發Full GC。

 

什麼是OOM,請你說說OOM產生的原因?如何分析?

OOM:當JVM因爲沒有足夠的內存來爲對象分配空間並且垃圾回收器也已經沒有空間可回收時;
OOM產生的原因:內存泄露、內存溢出

  1. 分配的少了
  2. 應用用的太多

分析OOM:
1.java.lang.OutOfMemoryError: Java heap space

  • 產生原因:內存泄露或者堆的大小設置不當引起;
  • 解決辦法:對於內存泄露,需要通過內存監控軟件查找程序中的泄露代碼,而堆大小可以通過虛擬機參數-Xms,-Xmx等修改;

2.java.lang.OutOfMemoryError: PermGen space(即方法區溢出)

  • 產生原因:產生大量的Class信息存儲於方法區(大量Class或者jsp頁面,或者採用cglib等反射機制)或者過多的常量尤其是字符串也會導致方法區溢出;
  • 解決辦法:可以通過更改方法區的大小來解決,使用類似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改;

3.java.lang.StackOverflowError:不會拋OOM error

  • 產生原因:程序中存在死循環或者深度遞歸調用或者棧大小設置太小;
  • 解決辦法:通過虛擬機參數-Xss來設置棧的大小

 

JVM的常用調優參數有哪些?

 

  • -Xmx:指定java程序的最大堆內存, 使用java -Xmx5000M -version判斷當前系統能分配的最大堆內存;
  • -Xms指定最小堆內存, 通常設置成跟最大堆內存一樣,減少GC
  • -Xmn:設置年輕代大小。整個堆大小=年輕代大小 + 年老代大小。所以增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8。
  • -Xss:指定線程的最大棧空間, 此參數決定了java函數調用的深度, 值越大調用深度越深, 若值太小則容易出棧溢出錯誤(StackOverflowError)
  • -XX:PermSize:指定方法區(永久區)的初始值,默認是物理內存的1/64, 在Java8永久區移除, 代之的是元數據區, 由-XX:MetaspaceSize指定
  • -XX:MaxPermSize:指定方法區的最大值, 默認是物理內存的1/4, 在java8中由-XX:MaxMetaspaceSize指定元數據區的大小
  • -XX:NewRatio=n:年老代與年輕代的比值,-XX:NewRatio=2, 表示年老代與年輕代的比值爲2:1
  • -XX:SurvivorRatio=n:Eden區與Survivor區的大小比值,-XX:SurvivorRatio=8表示Eden區與Survivor區的大小比值是8:1:1,因爲Survivor區有兩個(from, to)


https://blog.csdn.net/qq_40655613/article/details/104737246

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章