JVM 虛擬機篇
- 1. JVM組成
- 1.1 JVM由那些部分組成,運行流程是什么?
- 1.2 什么是程序計數器?
- 1.3 你能給我詳細的介紹Java堆嗎?
- 1.4 Java 虛擬機棧
- 1.4.1 Java Virtual machine Stacks (java 虛擬機棧)
- 1.4.2 棧和堆的區別
- 1.4.3 垃圾回收是否涉及棧內存?
- 1.4.4 棧內存分配越大越好嗎?
- 1.4.5 方法內的局部變量是否線程安全?
- 1.4.6 棧內存溢出情況
- 1.5 能不能解釋一下方法區?
- 1.5.1 概述
- 1.5.2 運行時常量池
- 1.5.3 方法區中的方法的執行過程?
- 1.6 你聽過直接內存嗎?
- 2. 類加載器
- 2.1 什么是類加載器
- 2.2 類加載器種類
- 2.3 什么是雙親委派模型?
- 2.4 JVM為什么采用雙親委派機制(作用)
- 2.5 說一下類裝載的執行過程
- 3. 垃圾回收
- 3.1 對象什么時候可以被垃圾器回收
- 3.2 VM 垃圾回收算法有哪些?
- 3.2.1 標記清除算法
- 3.2.2 標記整理算法
- 3.2.3 復制算法
- 3.3 分代回收算法
- MinorGC、 Mixed GC 、 FullGC的區別是什么
- 3.4 說一下 JVM 有哪些垃圾回收器?
- 3.4.1 串行垃圾收集器
- 3.4.2 并行垃圾收集器
- 3.4.3 CMS(并發)垃圾收集器
- 3.4.4 G1垃圾回收器
- 3.5 強引用、軟引用、弱引用、虛引用的區別?
- 4. JVM 實踐
- 4.1 JVM 調優的參數都有哪些?
- 4.2 java內存泄露的排查思路?
- 4.3 服務器CPU持續飆高,你的排查方案與思路?
1. JVM組成
1.1 JVM由那些部分組成,運行流程是什么?
難易程度:☆☆☆
出現頻率:☆☆☆☆
JVM是什么
Java Virtual Machine Java程序的運行環境(java二進制字節碼的運行環境)
- 一次編寫,到處運行
- 自動內存管理,垃圾回收機制
從圖中可以看出 JVM 的主要組成部分
- ClassLoader(類加載器)
- Runtime Data Area(運行時數據區,內存分區)
- Execution Engine(執行引擎)
- Native Method Library(本地庫接口)
運行流程:
- 類加載器(ClassLoader)把Java代碼轉換為字節碼
- 運行時數據區(Runtime Data Area)把字節碼加載到內存中,而字節碼文件只是JVM的一套指令集規范,并不能直接交給底層系統去執行,而是有執行引擎運行
- 執行引擎(Execution Engine)將字節碼翻譯為底層系統指令,再交由CPU執行去執行,此時需要調用其他語言的本地庫接口(Native Method Library)來實現整個程序的功能。
1.2 什么是程序計數器?
難易程度:☆☆☆
出現頻率:☆☆☆☆
程序計數器(PC Register):線程私有的,內部保存的字節碼的行號。用于記錄正在執行的字節碼指令的地址。
javap -verbose xx.class 打印堆棧大小,局部變量的數量和方法的參數。
1.3 你能給我詳細的介紹Java堆嗎?
難易程度:☆☆☆
出現頻率:☆☆☆☆
線程共享的區域:Java堆 (Heap) 是Java虛擬機中內存管理的一個重要區域,主要用于存放對象實例和數組。當堆中沒有內存空間可分配給實例,也無法再擴展時,則拋出OutOfMemoryError異常。
- 新生代(Young Generation):新生代分為Eden Space和Survivor Space。在Eden Space中,大多數新創建的對象首先存放在這里。Eden區相對較小,當Eden區滿時,會觸發一次 Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分為兩個相等大小的區域,稱為 S0(Survivor 0) 和 S1(Survivor1)。在每次Minor GC后,存活下來的對象會被移動到其中一個Sunvivor空間,以繼續它們的生命周期。
- 老年代(Old Generation/Tenured Generation):存放過一次或多次Minor GC仍存活的對象會被移動到老年代。老年代中的對象生命周期較長,因此Major Gc(也稱為Ful GC,涉及老年代的垃圾回收)發生的頻率相對較低,但其執行時間通常比Minor Gc長。老年代的空間通常比新生代大,以存儲更多的長期存活對象。
- 元空間保存的類信息、靜態變量、常量、編譯后的代碼。
為了避免方法區出現OOM,所以在java8中將堆上的方法區【永久代】給移動到了本地內存上,重新開辟了一塊空間,叫做元空間。那么現在就可以避免掉OOM的出現了。
1.4 Java 虛擬機棧
與程序計數器一樣,Java 虛擬機棧(后文簡稱棧)也是線程私有的,它的生命周期和線程相同,隨著線程的創建而創建,隨著線程的死亡而死亡。
1.4.1 Java Virtual machine Stacks (java 虛擬機棧)
- 每個線程運行時所需要的內存,稱為虛擬機棧,先進后出。
- 每個棧由多個棧幀(frame)組成,對應著每次方法調用時所占用的內存。每一次方法調用都會有一個對應的棧幀被壓入棧中,每一個方法調用結束后,都會有一個棧幀被彈出。
- 每個線程只能有一個活動棧幀,對應著當前正在執行的那個方法
1.4.2 棧和堆的區別
- 在JVM內存模型中,棧(Stack)主要用于管理線程的局部變量和方法調用的上下文,而堆(Heap)則是
用于存儲所有類的實例和數組。堆會GC垃圾回收,而棧不會。 - 棧內存是線程私有的,而堆內存是線程共有的。
- 兩者異常錯誤不同,但如果棧內存或者堆內存不足都會拋出異常
- 棧空間不足: java.lang.StackOverFlowError。
- 堆空間不足: java.lang.OutOfMemoryError。
1.4.3 垃圾回收是否涉及棧內存?
垃圾回收主要指就是堆內存,當棧幀彈棧以后,內存就會釋放。
1.4.4 棧內存分配越大越好嗎?
未必,默認的棧內存通常為1024k。棧幀過大會導致線程數變少,例如,機器總內存為512m,目前能活動的線程數則為512個,如果把棧內存改為2048k,那么能活動的棧幀就會減半。
1.4.5 方法內的局部變量是否線程安全?
- 如果方法內局部變量沒有逃離方法的作用范圍,它是線程安全的
- 如果是局部變量引用了對象,并逃離方法的作用范圍,需要考慮線程安全
1.4.6 棧內存溢出情況
- 棧幀過多導致棧內存溢出,典型問題:遞歸調用
- 棧幀過大導致棧內存溢出(不容易出現)
1.5 能不能解釋一下方法區?
難易程度:☆☆☆
出現頻率:☆☆☆
1.5.1 概述
- 方法區(Method Area)是各個線程共享的內存區域
- 主要存儲類的信息、運行時常量池
- 虛擬機啟動的時候創建,關閉虛擬機時釋放
- 如果方法區域中的內存無法滿足分配請求,則會拋出OutOfMemoryError: Metaspace
1.5.2 運行時常量池
- 常量池:可以看作是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等信息
- 常量池是 *.class 文件中的,當該類被加載,它的常量池信息就會放入運行時常量池,并把里面的符號地址變為真實地址
1.5.3 方法區中的方法的執行過程?
當程序中通過對象或類直接調用某個方法時,主要包括以下幾個步驟:
- 解析方法調用:JVM會根據方法的
符號引用找到實際的方法地址
- 棧幀創建:在調用一個方法前,JVM會在當前線程的
Java虛擬機棧中
為該方法分配一個新的棧幀,用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。
執行方法:執行方法內的字節碼指令,涉及的操作可能包括局部變量的讀寫、操作數棧的操作、跳轉控制、對象創建、方法調用等。
返回處理:方法執行完畢后,可能會返回一個結果給調用者,并清理當前棧幀
,恢復調用者的執行環境。
1.6 你聽過直接內存嗎?
難易程度:☆☆☆
出現頻率:☆☆☆
- 并不屬于JVM中的內存結構,不由JVM進行管理。是虛擬機的系統內存
- 常見于 NIO 操作時,用于數據緩沖區,分配回收成本較高,但讀寫性能高,不受 JVM 內存回收管理
2. 類加載器
難易程度:☆☆☆☆
出現頻率:☆☆☆
2.1 什么是類加載器
JVM只會運行二進制文件,而類加載器(ClassLoader)的主要作用就是將字節碼文件加載到JVM中,從而讓Java程序能夠啟動起來。
2.2 類加載器種類
類加載器根據各自加載范圍的不同,劃分為四種類加載器:
啟動類加載器(BootStrap ClassLoader)
:該類并不繼承ClassLoader類,其是由C++編寫實現。負責加載Java的核心庫(如JAVA_HOME/jre/lib目錄下的類庫)。擴展類加載器(ExtClassLoader)
:該類是ClassLoader的子類,主要加載JAVA_HOME/jre/lib/ext目錄中的類庫。應用類加載器(AppClassLoader)
:該類是ClassLoader的子類,主要用于加載classPath下的類,也就是加載開發者自己編寫的Java類。自定義類加載器
:開發者自定義類繼承ClassLoader,實現自定義類加載規則。
2.3 什么是雙親委派模型?
難易程度:☆☆☆☆
出現頻率:☆☆☆☆
如果一個類加載器在接到加載類的請求時,它首先不會自己嘗試去加載這個類,而是把這個請求任務委托給父類加載器去完成,依次遞歸,如果父類加載器可以完成類加載任務,就返回成功;只有父類加載器無法完成此加載任務時,才由下一級去加載。
2.4 JVM為什么采用雙親委派機制(作用)
難易程度:☆☆☆
出現頻率:☆☆☆
- 保證類的唯一性:通過雙親委派機制可以避免某一個類被重復加載,當父類已經加載后則無需重復加載,保證唯一性。
- 保證安全性:為了安全,保證類庫API不會被修改。例如,惡意代碼無法自定義一個Java.lang.System類并加載到IM中,因為這個請求會被委托給啟動類加載器,而啟動類加載器只會加載標準的Java庫中的類。
2.5 說一下類裝載的執行過程
- 加載:查找和導入class文件
- 通過類的全名,獲取類的二進制數據流。
- 解析類的二進制數據流為方法區內的數據結構(Java類模型)
- 創建java.lang.Class類的實例,表示該類型。作為方法區這個類的各種數據的訪問入口
- 驗證:保證加載類的準確性
- 格式檢查:文件格式是否錯誤、語法是否錯誤、字節碼是否合規
- 文件格式驗證
- 元數據驗證
- 字節碼驗證
- 符號引用驗證:Class文件在其常量池中通過字符串記錄自己將要使用的其它類或者方法,檢查它們是否存在
- 格式檢查:文件格式是否錯誤、語法是否錯誤、字節碼是否合規
- 準備:為類變量分配內存并設置類變量初始值
- static變量,分配空間在準備階段完成(設置默認值),賦值在初始化階段完成
- static變量是final的基本類型,以及字符串常量,值已確定,賦值在準備階段完成
- static變量是final的引用類型,那么賦值也會在初始化階段完成
- 解析:把類中的符號引用轉換為直接引用
- 比如:方法中調用了其他方法,方法名可以理解為符號引用,而直接引用就是使用指針直接指向方法。
- 初始化:對類的靜態變量,靜態代碼塊執行初始化操作
- 如果初始化一個類的時候,其父類尚未初始化,則優先初始化其父類。
- 如果同時包含多個靜態變量和靜態代碼塊,則按照自上而下的順序依次執行。
- 使用:JVM 從入口方法開始執行用戶的程序代碼
- 調用靜態類成員信息(比如:靜態字段、靜態方法)
- 使用new關鍵字為其創建對象實例
- 卸載:當用戶程序代碼執行完畢后,JVM 便開始銷毀創建的 Class 對象
- 最后負責運行的 JVM 也退出內存
3. 垃圾回收
垃圾回收(Garbage Collection,Gc)是自動管理內存的一種機制,它負責自動釋放不再被程序引用的對象所占用的內存,這種機制減少了內存泄漏和內存管理錯誤的可能性。
3.1 對象什么時候可以被垃圾器回收
如果一個或多個對象沒有任何的引用指向它了,那么這個對象現在就是垃圾,如果定位了垃圾,則有可能會被垃圾回收器回收。
如果要定位什么是垃圾,有兩種方式來確定,第一個是引用計數法,第二個是可達性分析算法。
- 引用計數
- 原理: 為每個對象分配一個引用計數器,每當有一個地方引用它時,計數器加1;當引用失效時,計數器減1。當計數器為0時,表示對象不再被任何變量引用,可以被回收。
- 缺點: 不能解決循環引用的問題,即兩個對象相互引用,但不再被其他任何對象引用,這時引用計數器不會為0,導致對象無法被回收。
- 可達性分析算法
- 原理: 從一組稱為GC Roots(垃圾收集根)的對象出發,向下追溯它們引用的對象,以及這些對象用的其他對象,以此類推。如果一個對象到GC Roots沒有任何引用鏈相連(即從GC Roots到這個對不可達),那么這個對象就被認為是不可達的,可以被回收。
- GC Roots對象包括: 虛擬機棧(棧幀的本地變量表)中引用的對象、方法區中類靜態屬性引用的對象、本地方法棧中JNI (Java Native Interface)引用的對象、活躍線程的引用等。
3.2 VM 垃圾回收算法有哪些?
難易程度:☆☆☆
出現頻率:☆☆☆☆
3.2.1 標記清除算法
標記清除算法,是將垃圾回收分為2個階段,分別是標記和清除。
- 根據可達性分析算法得出的垃圾進行標記
- 對這些標記為可回收的內容進行垃圾回收
標記清除算法也是有缺點的:通過標記清除算法清理出來的內存,碎片化較為嚴重,因為被回收的對象可能存在于內存的各個角落,所以清理出來的內存是不連貫的。
3.2.2 標記整理算法
標記壓縮算法是在標記清除算法的基礎之上,做了優化改進的算法。和標記清除算法一樣,也是從根節點開始,對對象的引用進行標記,在清理階段,并不是簡單的直接清理可回收對象,而是將存活對象都向內存另一端移動,然后清理邊界以外的垃圾,從而解決了碎片化的問題。
優缺點同標記清除算法,解決了標記清除算法的碎片化的問題,同時,標記壓縮算法多了一步,對象移動內存位置的步驟,其效率也有有一定的影響。
3.2.3 復制算法
將原有的內存空間一分為二,每次只用其中的一塊,在垃圾回收時,將正在使用的對象復制到另一個內存空間中,然后將該內存空間清空,交換兩個內存的角色,完成垃圾的回收。
如果內存中的垃圾對象較多,需要復制的對象就較少,這種情況下適合使用該方式并且效率比較高,反之,則不適合。 缺點:內存利用率不足。
3.3 分代回收算法
堆被分為了兩份:新生代和老年代【1:2】
分代回收是將內存劃分成了新生代和老年代。分配的依據是對象的生存周期,或者說經歷過的 GC 次數。對象創建時,一般在新生代申請內存,當經歷一次 GC之后如果對還存活,那么對象的年齡 +1。當年齡超過一定值(默認是 15)后,如果對象還存活,那么該對象會進入老年代。
MinorGC、 Mixed GC 、 FullGC的區別是什么
- MinorGC【young GC】
- 發生在新生代的垃圾回收
- 當 Eden 區空間不足時,JVM 觸發依次Minor GC,將Eden區和一個Survivor區中的存活對象移動到另一個Survivor區或老年區。
- 頻率高,暫停時間短(STW)
- Mixed GC
- 新生代 + 老年代部分區域的垃圾回收,G1 收集器特有
- FullGC
- 新生代 + 老年代完整垃圾回收
- 暫停時間長(STW),應盡力避免
STW(Stop-The-World)
:暫停所有應用程序線程,等待垃圾回收的完成
3.4 說一下 JVM 有哪些垃圾回收器?
難易程度:☆☆☆☆
出現頻率:☆☆☆☆
在jvm中,實現了多種垃圾收集器,包括:
- 串行垃圾收集器
- 并行垃圾收集器
- CMS(并發)垃圾收集器
- G1垃圾收集器
3.4.1 串行垃圾收集器
Serial
和 Serial Old
串行垃圾收集器,是指使用單線程進行垃圾回收,堆內存較小,適合個人電腦
- Serial 作用于新生代,采用復制算法
- Serial Old 作用于老年代,采用標記-整理算法
- 垃圾回收時,只有一個線程在工作,并且java應用中的所有線程都要暫停(STW),等待垃圾回收的完成。
- 優點是簡單高效
3.4.2 并行垃圾收集器
Parallel New
和 Parallel Old
是一個并行垃圾回收器,JDK8默認使用此垃圾回收器
- Parallel New作用于新生代,采用復制算法
- Parallel Old作用于老年代,采用標記-整理算法
- 垃圾回收時,多個線程在工作,并且java應用中的所有線程都要暫停(STW),等待垃圾回收的完成。
3.4.3 CMS(并發)垃圾收集器
CMS全稱 Concurrent Mark Sweep
,是一款并發的、使用標記-清除算法的垃圾回收器,該回收器是針對老年代垃圾回收的,是一款以獲取最短回收停頓時間為目標的收集器,停頓時間短,用戶體驗就好。其最大特點是在進行垃圾回收時,應用仍然能正常運行。
3.4.4 G1垃圾回收器
- 應用于新生代和老年代,在JDK9之后默認使用G1
- 劃分成多個區域(弱化了分代的概念),每個區域都可以充當 eden,survivor,old, humongous,其中 humongous 專為大對象準備
- 采用復制算法
- 響應時間與吞吐量兼顧
- 分成三個階段:新生代回收、并發標記、混合收集
- 如果并發失敗(即回收速度趕不上創建新對象速度),會觸發 Full GC
3.5 強引用、軟引用、弱引用、虛引用的區別?
難易程度:☆☆☆☆
出現頻率:☆☆☆
- 強引用: 只要所有 GC Roots 能找到,就不會被回收
- 軟引用: 需要配合SoftReference使用,當垃圾多次回收,內存依然不夠的時候會回收軟引用對象
- 弱引用: 需要配合WeakReference使用,只要進行了垃圾回收,就會把弱引用對象回收
- 虛引用: 必須配合引用隊列使用,被引用對象回收時,會將虛引用入隊,由 Reference Handler 線程調用虛引用相關方法釋放直接內存
4. JVM 實踐
4.1 JVM 調優的參數都有哪些?
通常在linux系統下直接加參數啟動springboot項目
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &
nohup : 用于在系統后臺不掛斷地運行命令,退出終端不會影響程序的運行
參數 & :讓命令在后臺執行,終端退出后命令仍舊執行。
1)設置堆的初始大小和最大大小,為了防止垃圾收集器在初始大小、最大大小之間收縮堆而產生額外的時間,通常把最大、初始大小設置為相同的值。
-Xms:設置堆的初始化大小
-Xmx:設置堆的最大大小
2)設置虛擬機棧的位置
3)年輕代中 Eden 區和兩個 Survivor 區的大小比例
4)設置垃圾回收器
4.2 java內存泄露的排查思路?
第一,可以通過jmap指定打印他的內存快照 dump文件,不過有的情況打印不了,我們會設置vm參數讓程序自動生成dump文件
第二,可以通過工具去分析 dump文件,jdk自帶的VisualVM就可以分析
第三,通過查看堆信息的情況,可以大概定位內存溢出是哪行代碼出了問題
第四,找到對應的代碼,通過閱讀上下文的情況,進行修復即可
4.3 服務器CPU持續飆高,你的排查方案與思路?
第一可以使用使用 top
命令查看占用cpu的情況
第二通過top命令查看后,可以查看是哪一個進程占用cpu較高,記錄這個 進程id
第三可以通過ps 查看當前進程中的 線程信息
,看看哪個線程的cpu占用較高
第四可以jstack命令打印進行的id,找到這個線程,就可以進一步定位問題代碼的行號