JVM組成
JVM
JVM 就是 Java 程序的運行環境,它通過 類加載、字節碼執行、內存管理、GC、線程調度 等機制,讓 Java 實現了 跨平臺、自動內存管理和高效執行。
它是一個抽象的計算機,能執行以 字節碼(.class 文件) 為單位的指令。
好處:
-
一次編寫,到處運行
-
自動內存管理,垃圾回收機制
JVM由那些部分組成,運行流程是什么?
JVM 主要由 類加載器、運行時數據區、執行引擎、垃圾回收器、本地接口 組成。
運行流程是:類加載器加載字節碼 → 執行引擎解釋/編譯執行 → 內存區域分配對象與方法棧幀 → GC 自動回收。
從圖中可以看出 JVM 的主要組成部分
-
ClassLoader(類加載器)
-
Runtime Data Area(運行時數據區,內存分區)
-
Execution Engine(執行引擎)
-
Native Method Library(本地庫接口)
運行流程:
(1)類加載器(ClassLoader)把Java代碼轉換為字節碼
(2)運行時數據區(Runtime Data Area)把字節碼加載到內存中,而字節碼文件只是JVM的一套指令集規范,并不能直接交給底層系統去執行,而是有執行引擎運行
(3)執行引擎(Execution Engine)將字節碼翻譯為底層系統指令,再交由CPU執行去執行,此時需要調用其他語言的本地庫接口(Native Method Library)來實現整個程序的功能。
更具體:
JVM 的運行流程
一個 Java 程序從
.java
文件到運行的完整流程:
編譯階段:
.java
→ 編譯器(javac) →.class
字節碼文件。類加載階段:JVM 的類加載器加載
.class
文件到內存,放入 方法區。執行階段:節碼文件只是JVM的一套指令集規范,并不能直接交給底層系統去執行,而是有執行引擎運行
JVM 創建主線程,分配 虛擬機棧 和 PC 寄存器。
執行引擎從 方法區 取字節碼指令,解釋或 JIT 編譯成機器碼執行。
運行時內存分配
程序運行過程中需要創建對象,存入 堆內存。
方法調用時創建 棧幀,保存局部變量和操作數棧。
垃圾回收
GC 檢測堆內存中的對象,回收不可達對象。
JVM內存分配
JVM 內存分配分為 線程私有區(隨線程銷毀而釋放) 和 線程共享區(需 GC 管理)。
-
對象實例分配在堆,
-
引用存放在棧;
-
類元信息在方法區;
-
線程運行狀態保存在計數器和棧。
-
GC 主要負責堆和方法區的垃圾回收。
什么是程序計數器?
程序計數器(PC 寄存器)
-
保存當前線程執行的字節碼行號,線程切換時能恢復執行位置。
-
每個線程獨享(私有),不存在內存回收問題。
javap -verbose xx.class 打印堆棧大小,局部變量的數量和方法的參數。
程序計數器的作用
JVM對于多線程是通過線程輪流切換并且分配線程執行時間。
在任何的一個時間點上,一個處理器只會處理執行一個線程,如果當前被執行的這個線程它所分配的執行時間用完了,就會掛起。處理器會切換到另外的一個線程上來進行執行。并且這個線程的執行時間用完了,接著處理器就會又來執行被掛起的這個線程。
那么現在有一個問題就是,當前處理器如何能夠知道,對于這個被掛起的線程,它上一次執行到了哪里?
那么這時就需要從程序計數器中來回去到當前的這個線程他上一次執行的行號,然后接著繼續向下執行。
程序計數器是JVM規范中唯一一個不會出現內存溢出(OOM)的內存空間,所以這個空間也不會進行GC。
虛擬機棧(VM Stack)
每個線程運行時所需要的內存,稱為虛擬機棧,先進后出
虛擬機棧包括 局部變量表、操作數棧、方法出口 等。方法調用時分配,方法結束后回收。
若棧深度過大可能拋 StackOverflowError。
-
每個線程運行時所需要的內存,稱為虛擬機棧,先進后出
-
每個棧由多個棧幀(frame)組成,對應著每次方法調用時所占用的內存
-
每個線程只能有一個活動棧幀,對應著當前正在執行的那個方法
常見問題:
(1)垃圾回收是否涉及棧內存?
-
垃圾回收主要指就是堆內存,當棧幀彈棧以后,內存就會釋放
(2)棧內存分配越大越好嗎?
-
未必,默認的棧內存通常為1024k
-
棧幀過大會導致線程數變少,例如,機器總內存為512m,目前能活動的線程數則為512個,如果把棧內存改為2048k,那么能活動的棧幀就會減半
(4)方法內的局部變量是否線程安全?
-
如果方法內局部變量沒有逃離方法的作用范圍,它是線程安全的
-
如果是局部變量引用了對象,并逃離方法的作用范圍,需要考慮線程安全
-
比如以下代碼:
棧內存溢出情況
-
棧幀過多導致棧內存溢出,典型問題:遞歸調用
-
棧幀過大導致棧內存溢出
你能給我詳細的介紹Java堆嗎?
堆(Heap)存放 對象實例和數組,是 GC 管理的主要區域。
-
堆進一步劃分為:
-
新生代(Young Generation):Eden + Survivor0 + Survivor1,用于存放新對象。
-
老年代(Old Generation):存放生命周期長的對象。
-
-
JDK8 之后,堆外新增 Metaspace(元空間)。
堆屬于線程共享的區域:主要用來保存對象實例,數組等
-
當堆中沒有內存空間可分配給實例,也無法再擴展時,則拋出OutOfMemoryError異常。
-
年輕代被劃分為三部分,Eden區和兩個大小嚴格相同的Survivor區,根據JVM的策略,在經過幾次垃圾收集后,任然存活于Survivor的對象將被移動到老年代區間。
-
老年代主要保存生命周期長的對象,一般是一些老的對象
-
元空間保存的類信息、靜態變量、常量、編譯后的代碼
為了避免方法區出現OOM,所以在java8中將堆上的方法區【永久代】給移動到了本地內存上,重新開辟了一塊空間,叫做元空間。那么現在就可以避免掉OOM的出現了。
元空間(MetaSpace)介紹
元空間的本質和永久代(方法區在 JDK7 之前叫 永久代(PermGen))類似,都是對 JVM 規范中方法區的實現。
不過元空間與永久代之間最大的區別在于:
-
元空間并不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。
在 HotSpot JVM 中,永久代( ≈ 方法區)中用于存放類和方法的元數據以及常量池,比如Class 和 Method。每當一個類初次被加載的時候,它的元數據都會放到永久代中。
永久代是有大小限制的,因此如果加載的類太多,很有可能導致永久代內存溢出,即OutOfMemoryError,為此不得不對虛擬機做調優。
那么,Java 8 中 PermGen 為什么被移出 HotSpot JVM 了?
官網給出了解釋:http://openjdk.java.net/jeps/122
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
移除永久代是為融合HotSpot JVM與 JRockit VM而做出的努力,因為JRockit沒有永久代,不需要配置永久代。
1)由于 PermGen 內存經常會溢出,引發OutOfMemoryError,因此 JVM 的開發者希望這一塊內存可以更靈活地被管理,不要再經常出現這樣的 OOM。
2)移除 PermGen 可以促進 HotSpot JVM 與 JRockit VM 的融合,因為 JRockit 沒有永久代。
準確來說,Perm 區中的字符串常量池被移到了堆內存中是在 Java7 之后,Java 8 時,PermGen 被元空間代替,其他內容比如類元信息、字段、靜態屬性、方法、常量等都移動到元空間區。比如 java/lang/Object 類元信息、靜態屬性 System.out、整型常量等。
能不能解釋一下方法區?
概述
方法區(Method Area)是各個線程共享的內存區域,主要存儲類的信息、運行時常量池
虛擬機啟動的時候創建,關閉虛擬機時釋放
如果方法區域中的內存無法滿足分配請求,則會拋出OutOfMemoryError: Metaspace
常量池
可以看作是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等信息
可以通過查看字節碼結構(類的基本信息、常量池、方法定義)javap -v xx.class
理解
下圖,左側是main方法的指令信息,右側constant pool 是常量池
main方法按照指令執行的時候,需要到常量池中查表翻譯找到具體的類和方法地址去執行
運行時常量池
常量池是 *.class 文件中的,當該類被加載,它的常量池信息就會放入運行時常量池,并把里面的符號地址變為真實地址
你聽過直接內存嗎?
不受 JVM 內存回收管理,是虛擬機的系統內存,常見于 NIO 操作時,用于數據緩沖區,分配回收成本較高,但讀寫性能高,不受 JVM 內存回收管理
NIO 操作:Java NIO 中的
ByteBuffer.allocateDirect(int capacity)
方法會分配直接內存,用于高效的 IO 操作(如FileChannel
讀寫文件、SocketChannel
網絡通信)。
舉例:
需求,在本地電腦中的一個較大的文件(超過100m)從一個磁盤挪到另外一個磁盤
使用傳統的IO的時間要比NIO操作的時間長了很多了,也就說NIO的讀性能更好。
這個是跟我們的JVM的直接內存是有一定關系,如下圖,是傳統阻塞IO的數據傳輸流程
下圖是NIO傳輸數據的流程,在這個里面主要使用到了一個直接內存,不需要在堆中開辟空間進行數據的拷貝,jvm可以直接操作直接內存,從而使數據讀寫傳輸更快。
堆棧的區別是什么?
1、棧內存一般會用來存儲局部變量和方法調用,但堆內存是用來存儲Java對象和數組的。
2、堆會GC垃圾回收,而棧不會。
3、棧內存是線程私有的,而堆內存是線程共有的。
4,、兩者異常錯誤不同,但如果棧內存或者堆內存不足都會拋出異常。
棧空間不足:java.lang.StackOverFlowError。
堆空間不足:java.lang.OutOfMemoryError。
類加載器
什么是類加載器,類加載器有哪些?(高頻)
類加載器的作用:負載將的class文件加載到java虛擬機中,并為之創建一個Class對象
類加載器根據各自加載范圍的不同,劃分為四種類加載器:
-
啟動類加載器(BootStrap ClassLoader):
-
該類并不繼承ClassLoader類,其是由C++編寫實現。用于加載JAVA_HOME/jre/lib目錄下的類庫。
-
-
擴展類加載器(ExtClassLoader):
-
該類是ClassLoader的子類,主要加載JAVA_HOME/jre/lib/ext目錄中的類庫。
-
-
應用類加載器(AppClassLoader):
-
該類是ClassLoader的子類,主要用于加載classPath下的類,也就是加載開發者自己編寫的Java類。
-
-
自定義類加載器:
-
開發者自定義類繼承ClassLoader,實現自定義類加載規則。
-
Java的雙親委托機制是什么?(高頻)
我們的應用程序都是由這三種類加載器互相配合進行加載的,如果有必要,還可以加入自定義的類加載器。這些類加 載器之間的層次關系一般會如下圖所示:
上圖所展示的類加載器之間的這種層次關系,就稱之為類加載器的雙親委派模型。
雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應該有自己的父類加載器(邏輯繼承)。
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己嘗試加載這個類,而是把這請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳說到頂層的啟動類加載器中,只有當父類加載器返回自己無法完成這個加載請求(它的搜索 返回中沒有找到所需的類)時,子類加載器才會嘗試自己去加載
說一下類裝載的執行過程?
JVM 類加載過程分為 加載 → 鏈接(驗證、準備、解析) → 初始化。加載階段讀取字節碼并生成 Class 對象,鏈接階段完成驗證安全性、分配靜態變量內存和符號引用解析,最后初始化階段執行 <clinit>()
給靜態變量賦值并運行靜態代碼塊。
類加載的執行過程
加載(Loading)
通過類加載器讀取
.class
文件(可能來自磁盤、網絡等)。把字節碼加載到 JVM 內存,并在 方法區(元空間 Metaspace) 生成類的運行時數據結構。
在堆中生成一個 Class 對象,作為訪問方法區中類元數據的入口。
鏈接(Linking)
鏈接分為三個小步驟:
驗證(Verification):確保字節碼符合 JVM 規范,保證不會危害虛擬機安全。
比如校驗文件格式、元數據、字節碼指令合法性。
準備(Preparation):為類的 靜態變量分配內存 并設置 默認值(不是初始值)。
例如:
public static int a = 10;
在這一步只會分配空間并賦 0,真正賦值 10 在初始化階段完成。解析(Resolution):把常量池中的 符號引用(Symbolic Reference) 轉換為 直接引用(Direct Reference)。
例如方法、字段的引用地址解析成真正的內存地址。
初始化(Initialization)
執行類構造器
<clinit>()
方法,完成靜態變量的賦初始值 & 執行靜態代碼塊。多線程下,JVM 會保證類初始化的線程安全
垃圾收回
簡述Java垃圾回收機制?(GC是什么?為什么要GC)
GC 是 JVM 的自動內存管理機制,通過 可達性分析 判斷對象是否存活。
常用的回收算法有 標記-清除、復制、標記-整理、分代收集。它的作用是自動釋放無用對象內存,避免內存泄漏,提高系統穩定性和開發效率。
有了垃圾回收機制后,程序員只需要關心內存的申請即可,內存的釋放由系統自動識別完成。
在進行垃圾回收時,不同的對象引用類型,GC會采用不同的回收時機。
當然,除了Java語言,C#、Python等語言也都有自動的垃圾回收機制。
如何判斷一個對象是否為垃圾?(高頻)
簡單一句就是:如果一個或多個對象沒有任何的引用指向它了,那么這個對象現在就是垃圾,如果定位了垃圾,則有可能會被垃圾回收器回收。
如果要定位什么是垃圾,有兩種方式來確定,第一個是引用計數法,第二個是可達性分析算法
引用計數法
堆中每個對象實例都有一個引用計數。
一個對象被引用了一次,在當前的對象頭上遞增一次引用次數,如果這個對象的引用次數為0,代表這個對象可回收
但是當對象間出現了循環引用的話,則引用計數法就會失效
優點:
-
實時性較高,無需等到內存不夠的時候,才開始回收,運行時根據對象的計數器是否為0,就可以直接回收。
-
在垃圾回收過程中,應用無需掛起。如果申請內存時,內存不足,則立刻報OOM錯誤。
-
區域性,更新對象的計數器時,只是影響到該對象,不會掃描全部對象。
缺點:
-
浪費CPU資源,即使內存夠用,仍然在運行時進行計數器的統計。
-
無法解決循環引用問題,會引發內存泄露。(最大的缺點)
可達性分析算法
現在的虛擬機采用的都是通過可達性分析算法來確定哪些內容是垃圾。
會存在一個根節點【GC Roots】,引出它下面指向的下一個節點,再以下一個節點節點開始找出它下面的節點,依次往下類推。直到所有的節點全部遍歷完畢。
根對象是那些肯定不能當做垃圾回收的對象,就可以當做根對象:局部變量,靜態方法,靜態變量,類信息
核心是:判斷某對象是否與根對象有直接或間接的引用,如果沒有被引用,則可以當做垃圾回收
-
以 GC Roots 為起點,向下搜索引用鏈(Reference Chain)。
-
如果一個對象能通過引用鏈與 GC Roots 相連,則判定為“可達”。
-
如果對象不可達,第一次會被標記為“可回收對象”。
-
JVM 可能會執行一次
finalize()
方法給予“自救”機會。-
如果在
finalize()
里重新建立引用鏈,對象會“復活”。 -
否則,下一次 GC 會真正清理。
-
X,Y這兩個節點是可回收的,但是并不會馬上的被回收!! 對象中存在一個方法【finalize】。當對象被標記為可回收后,當發生GC時,
首先會判斷這個對象是否執行了finalize方法,
如果這個方法還沒有被執行的話,那么就會先來執行這個方法,接著在這個方法執行中,可以設置當前這個對象與GC ROOTS產生關聯,
那么這個方法執行完成之后,GC會再次判斷對象是否可達,如果仍然不可達,則會進行回收,如果可達了,則不會進行回收。
finalize方法對于每一個對象來說,只會執行一次。如果第一次執行這個方法的時候,設置了當前對象與RC ROOTS關聯,那么這一次不會進行回收。 那么等到這個對象第二次被標記為可回收時,那么該對象的finalize方法就不會再次執行了。
JVM 垃圾回收算法有哪些?
JVM 常見的垃圾回收算法有:標記-清除、復制、標記-整理、分代收集。
新生代常用復制算法(因為大部分對象很快死亡),
老年代常用標記-清除/整理(避免內存碎片),
而實際 JVM 垃圾回收器普遍采用 分代收集 策略。
標記-清除算法
標記清除算法,是將垃圾回收分為2個階段,分別是標記和清除。
1.根據可達性分析算法得出的垃圾進行標記
2.對這些標記為可回收的內容進行垃圾回收
可以看到,標記清除算法解決了引用計數算法中的循環引用的問題,沒有從root節點引用的對象都會被回收。
優點:實現簡單;速度快。
同樣,標記清除算法也是有缺點的:
-
效率較低,標記和清除兩個動作都需要遍歷所有的對象,并且在GC時,需要停止應用程序,對于交互性要求比較高的應用而言這個體驗是非常差的。
-
(重要)通過標記清除算法清理出來的內存,碎片化較為嚴重,因為被回收的對象可能存在于內存的各個角落,所以清理出來的內存是不連貫的。
標記-整理算法
標記壓縮算法是在標記清除算法的基礎之上,做了優化改進的算法。和標記清除算法一樣,也是從根節點開始,對對象的引用進行標記,在清理階段,并不是簡單的直接清理可回收對象,而是將存活對象都向內存另一端移動,然后清理邊界以外的垃圾,從而解決了碎片化的問題。
-
流程:
-
標記存活對象。
-
將存活對象壓縮到一端,清理邊界以外的空間。
-
-
優點:避免了內存碎片。
-
缺點:移動對象成本高,效率低于復制算法。
-
應用場景:老年代(存活對象多,不適合復制算法)。
復制算法
將有用的復制走,將剩下的全部刪掉
復制算法的核心就是,將原有的內存空間一分為二(From 區 和 To 區),每次只用其中的一塊,在垃圾回收時,將正在使用的對象復制到另一個內存空間中,然后將該內存空間清空,交換兩個內存的角色,完成垃圾的回收。
如果內存中的垃圾對象較多,需要復制的對象就較少,這種情況下適合使用該方式并且效率比較高,反之,則不適合。
-
優點:沒有內存碎片,分配效率高(指針碰撞)。在垃圾對象多的情況下,效率較高
-
缺點:浪費一半內存。
-
應用場景:新生代(大部分對象很快死亡,適合復制算法)
分代收集—— 實際應用最廣
-
思想:根據對象生命周期長短劃分區域,采用不同算法:
-
新生代:對象存活率低 → 采用 復制算法。
-
老年代:對象存活率高 → 采用 標記-清除 或 標記-整理。
-
-
優點:結合多種算法優勢,提高整體效率。
-
現代 JVM(HotSpot) 默認采用分代收集。
分代回收器有兩個分區:老生代和新生代
-
新生代默認的空間占比總空間的 1/3,
-
老生代的默認占比是2/3。
新生代使用的是復制算法,新生代里有3個分區:Eden、To Survivor、From Survivor,它們的默認占比是 8:1:1,
它的執行流程如下:
當年輕代中的Eden區分配滿的時候,就會觸發年輕代的GC(Minor GC)。具體過程如下:
1、在Eden區執行了第一次GC之后,存活的對象會被移動到其中一個Survivor分區(以下簡稱to)
2、From區中的對象根據對象的年齡值決定去向,達到閾值15移動到老年代,沒有達到復制到to區域(復制算 法)
3、在把Eden和to區中的對象清空掉
JVM的永久代中會發生垃圾回收么?
永久代會觸發垃圾回收的,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。 注:
Java 8 中已經移除了永久代,新加了一個叫做元數據區(Metaspace)的內存區。
說一下 JVM 有哪些垃圾回收器?
在JVM中,實現了多種垃圾收集器,包括:
-
串行垃圾收集器
-
并行垃圾收集器
-
CMS(并發)垃圾收集器
-
G1垃圾收集器
🔹 新生代收集器
Serial 收集器
單線程,回收和用戶線程互斥(STW,Stop The World),適合單 CPU、小內存環境。
ParNew 收集器
Serial 的多線程版本。
常和 CMS(老年代收集器)搭配使用。
Parallel Scavenge 收集器(吞吐量優先)
多線程,追求高吞吐量(用戶代碼運行時間 / 總時間)。
適合后臺計算類應用。
🔹 老年代收集器
Serial Old 收集器
Serial 的老年代版本,單線程,標記-整理算法。
主要作為 CMS 的備用方案。
Parallel Old 收集器
Parallel Scavenge 的老年代版本,多線程,標記-整理算法。
適合高吞吐量場景。
CMS(Concurrent Mark-Sweep)收集器
以最短停頓時間為目標。
并發標記、并發清除,減少 STW 時間。
缺點:會產生內存碎片,回收過程耗 CPU。
串行垃圾收集器
Serial和Serial Old串行垃圾收集器,是指使用單線程進行垃圾回收,堆內存較小,適合個人電腦
-
Serial 作用于新生代,采用復制算法
-
Serial Old 作用于老年代,采用標記-整理算法
垃圾回收時,只有一個線程在工作,并且java應用中的所有線程都要暫停(STW),等待垃圾回收的完成。
并行垃圾收集器
Parallel New和Parallel Old是一個并行垃圾回收器,JDK8默認使用此垃圾回收器
-
Parallel New作用于新生代,采用復制算法
-
Parallel Old作用于老年代,采用標記-整理算法
垃圾回收時,多個線程在工作,并且java應用中的所有線程都要暫停(STW),等待垃圾回收的完成。
CMS(并發)垃圾收集器
CMS全稱 Concurrent Mark Sweep),是一款并發的、使用標記-清除算法的垃圾回收器,該回收器是針對老年代垃圾回收的,是一款以獲取最短回收停頓時間為目標的收集器,停頓時間短,用戶體驗就好。其最大特點是在進行垃圾回收時,應用仍然能正常運行。
詳細聊一下G1垃圾回收器
G1 GC 是 JDK9 之后的默認垃圾收集器,把堆劃分為多個 Region,優先回收垃圾最多的 Region,通過 并發標記 + 混合回收 + 復制壓縮 來實現 高吞吐 + 可預測低延遲,非常適合大堆和低停頓場景。
概述: G1是一個分代的,并行與并發的"標記-整理"垃圾回收器。
它的設計目標是為了適應現在不斷擴大的內存和不斷增加的處理器數量,進一步降低暫停時間(pause time),同時兼顧良好的吞吐量。
相比于CMS:
1. G1垃圾回收器使用的是"標記-整理",因此其回收得到的空間是連續的。
2. G1回收器的內存與CMS回收器要求的內存模型有極大的不同。G1將內存劃分一個個固定大小的region,每個 region可以是年輕代、老年代的一個。內存的回收是以region作為基本單位的;
1.G1 的設計目標
-
面向 大堆內存(幾十 GB) 的場景,追求:
-
高吞吐量(充分利用多核 CPU 并行回收)。
-
低延遲(可預測的停頓時間)。
-
2. 內存結構(Region 化)
-
傳統分代(新生代 / 老年代) → G1 把整個堆劃分為 若干個等大小的 Region(分區)。
-
每個 Region 可以動態扮演:
-
Eden 區
-
Survivor 區
-
Old 區
-
-
特殊:有 Humongous Region 專門存放大對象(超過一個 Region 一半大小)。
好處:避免了傳統分代帶來的固定劃分,更靈活。
3. G1 的核心機制
-
Mixed GC(混合收集):
-
不再是單純的新生代 / 老年代 GC,而是根據需要選擇部分 Region 回收。
-
-
記憶集(Remembered Set, RSet):
-
記錄其他 Region 對本 Region 的引用,避免全堆掃描。
-
-
回收優先級:
-
G1 會根據 Region 的垃圾比例排序,優先回收垃圾最多的 Region,這也是名字 Garbage First 的由來。
-
4. G1 的工作流程
-
初始標記(Initial Mark)
-
標記 GC Roots 直接可達的對象。
-
停頓(STW)。
-
-
并發標記(Concurrent Mark)
-
在整個堆里做可達性分析,標記存活對象。
-
與用戶線程并發執行。
-
-
最終標記(Remark)
-
修正并發標記階段用戶線程繼續運行導致的標記變動。
-
停頓(STW)。
-
-
篩選回收(Cleanup/Copying)
-
計算每個 Region 的回收價值(垃圾比例)。
-
按優先級選擇部分 Region 進行復制回收(存活對象復制到空 Region,原 Region 清空)。
-
5. G1 的優缺點
優點:
-
可預測的停頓時間:用戶可指定期望的最大停頓時間(如 200ms)。
-
大內存場景表現優異(幾十 GB 以上)。
-
避免內存碎片:采用復制算法回收,天然整理內存。
缺點:
-
實現復雜,對 JVM 內部消耗較大。
-
在小堆或低配置機器上性能不如 Parallel GC。
Java中都有哪些引用類型?(高頻)
Java 有 強、軟、弱、虛 四種引用:
-
強引用:最普通,GC 不回收。
-
軟引用:內存不足時才回收,常用于緩存。
-
弱引用:只要 GC 就回收,常用于 ThreadLocal 等。
-
虛引用:對象回收時得到通知,用于堆外內存管理
強引用
Java中默認聲明的就是強引用
最常見的引用方式,比如:
Object obj = new Object();
特點:
-
只要存在強引用,GC 永遠不會回收對象。
-
就算內存不足,寧可拋
OutOfMemoryError
也不會回收。
適用場景:日常開發中的普通對象。
軟引用
僅有軟引用引用該對象時,在垃圾回收后,內存仍不足時會再次出發垃圾回收
使用 SoftReference
類創建:
SoftReference<Object> softRef = new SoftReference<>(new Object());
特點:
-
在 內存不足時,GC 才會回收軟引用的對象。
-
適合實現 內存敏感的緩存(如圖片緩存)。
適用場景:緩存系統(Ehcache 就用軟引用實現)。
弱引用
使用 WeakReference
類創建:
WeakReference<Object> weakRef = new WeakReference<>(new Object());
特點:
-
只要發生 GC,就會回收弱引用的對象(不管內存是否足夠)。
-
回收后
weakRef.get()
返回null
。
適用場景:
-
ThreadLocal 的
ThreadLocalMap
key 就是弱引用,防止內存泄漏。
虛引用
虛引用是最弱的一種引用關系,如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,它隨時可能會被回收,
在 JDK1.2 之后,用 PhantomReference 類來表示,通過查看這個類的源碼,發現它只有一個構造函數和一個 get() 方法,而且它的 get() 方法僅僅是返回一個null,也就是 說將永遠無法通過虛引用來獲取對象,虛引用必須要和 ReferenceQueue 引用隊列一起使用。
必須配合引用隊列使用,被引用對象回收時,會將虛引用入隊,由 Reference Handler 線程調用虛引用相關方法釋放直接內存
使用 PhantomReference
類創建,需要配合 ReferenceQueue
:
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
特點:
-
不能通過
get()
獲取對象。 -
主要作用是 在對象被回收時收到系統通知,用于資源回收(如堆外內存)。
適用場景:
-
管理堆外內存(Direct Memory)。
-
更靈活地監控 GC 行為。
JVM調優
JVM調優
JVM 調優的核心目標是 減少 GC 對業務的影響、避免內存泄漏、提升性能。
常見手段包括:調整內存參數、選擇合適 GC 收集器、分析 GC 日志、使用監控工具、優化代碼邏輯。
例如在大堆低延遲場景可以用 G1 GC,在吞吐量優先場景可以用 Parallel GC。
調優命令有哪些?
JVM 調優常用命令有:
-
jps(查看進程),
-
jstat(GC/內存監控),
-
jmap(導出堆快照),
-
jstack(線程堆棧分析),
-
jconsole / jvisualvm(可視化監控)。
它們配合使用能快速定位 內存泄漏、GC 頻繁、死鎖 等問題。
你知道哪些JVM性能調優參數?(高頻)
對于JVM調優,主要就是調整年輕代、年老代、元空間的內存空間大小及使用的垃圾回收器類型。
JVM 調優常用參數分為三類:
-
內存參數:
-Xms
,-Xmx
,-Xmn
,-Xss
-
-Xms:設置堆的初始化大小
-
-Xmx:設置堆的最大大小
-
-
GC 參數:
-XX:+UseG1GC
,-XX:NewRatio
,-XX:SurvivorRatio
,-XX:MaxTenuringThreshold
-
-XX:NewSize:新生代大小
-
-XX:NewRatio 新生代和老生代占比
-
-XX:NewSize:新生代大小
-
--XX:SurvivorRatio:伊甸園空間和幸存者空間的占比
-
-
日志參數:
-XX:+PrintGCDetails
,-Xloggc:gc.log
根據業務場景(高吞吐 / 低延遲 / 大堆內存)選擇合適的組合,才能達到最佳性能。
JVM 調優的參數可以在哪里設置參數值?
tomcat的設置vm參數
修改TOMCAT_HOME/bin/catalina.sh文件,如下圖
JAVA_OPTS="-Xms512m -Xmx1024m"
springboot項目jar文件啟動
通常在linux系統下直接加參數啟動springboot項目
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &
nohup : 用于在系統后臺不掛斷地運行命令,退出終端不會影響程序的運行
參數 & :讓命令在后臺執行,終端退出后命令仍舊執行。
你用過哪些性能調優工具?(高頻)
1、jdk自帶監控工具:
-
jconsole,Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制 臺,用于對JVM中內存,線程和類等的監控
-
-
jvisualvm,jdk自帶全能工具,可以分析內存快照、線程快照;監控內存變化、GC變化等。
2、第三方 :
MAT,Memory Analyzer Tool,一個基于Eclipse的內存分析工具,是一個快速、功能豐富的Java heap分析工 具,它可以幫助我們查找內存泄漏和 減少內存消耗 GChisto,一款專業分析gc日志的工具
你都有哪些手段用來排查內存溢出?(高頻)
內存溢出包含很多種情況,我在平常工作中遇到最多的就是堆溢出。
有一次線上遇到故障,重新啟動后,使用jstat 命令,發現Old區在一直增長。
我使用 jmap命令,導出了一份線上堆棧,然后使用MAT進行分析。通過對GC Roots的分析,我發現了一個非常大的 HashMap對象,這個原本是有位同學做緩存用的,但是一個無界緩存,造成了堆內存占用一直上升。后來,將這個緩存改成 Guava Cache,并設置了弱引用,故障就消失了。
Java 內存泄漏的排查思路是:監控發現問題 → 導出堆快照 → 使用 MAT/VisualVM 分析對象引用鏈 → 定位可疑代碼(靜態集合、ThreadLocal、緩存、未關閉資源等) → 修復并驗證。