目錄
什么是JVM?
JVM 執(zhí)行流程
JVM 運行時數(shù)據(jù)區(qū)
堆(線程共享)
Java虛擬機棧(線程私有)
什么是線程私有?
程序計數(shù)器(線程私有)
方法區(qū)(線程共享)
JDK 1.8 元空間的變化
運行時常量池
內存布局中的異常問題
1.? Java堆溢出
2.??虛擬機棧和本地方法棧溢出
JVM 類加載
1. 類加載過程
加載
驗證
準備
解析
初始化
雙親委派模型
垃圾回收機制
死亡對象的判斷算法
引用計數(shù)算法
可達性分析算法
垃圾回收算法
標記-清除算法(Mark and Sweep):
復制算法(Copying Garbage Collection):
標記-整理算法(Mark and Compact):
?分代算法(Generational Garbage Collection):
?文章來源地址http://www.zghlxwxcb.cn/news/detail-707053.html
什么是JVM?
JVM(Java虛擬機)是Java編程語言的關鍵組成部分,它是一種虛擬計算機環(huán)境,用于執(zhí)行Java程序。JVM的主要作用是將Java源代碼編譯成與特定計算機硬件無關的字節(jié)碼,并在運行時將這些字節(jié)碼轉換為機器碼,以便在不同平臺上運行Java應用程序。
在JVM的運行環(huán)境中,Java程序能夠實現(xiàn)跨平臺的特性,因為它們不需要直接與底層操作系統(tǒng)進行交互,而是依賴JVM來處理與硬件的交互。這使得Java成為一種高度可移植和可擴展的編程語言。
JVM的關鍵功能包括:
- 類加載:JVM負責加載Java類的字節(jié)碼文件,通過類加載器實現(xiàn)這一任務。
- 內存管理:JVM自動管理內存分配和垃圾回收,以確保應用程序不會出現(xiàn)內存泄漏和溢出。
- 字節(jié)碼執(zhí)行:JVM解釋或編譯Java字節(jié)碼,將其轉換為本地機器碼以執(zhí)行應用程序。
- 多線程支持:JVM提供多線程支持,允許并發(fā)執(zhí)行Java應用程序的部分或全部代碼。
- 垃圾回收:JVM使用垃圾回收機制來自動釋放不再被引用的內存,以提高內存利用率。
JVM 是 Java 運行的基礎,也是實現(xiàn)一次編譯到處執(zhí)行的關鍵,那么 JVM 是如何執(zhí)行的呢?
JVM 執(zhí)行流程
程序在執(zhí)行之前先要把java代碼轉換成字節(jié)碼(class文件),JVM 首先需要把字節(jié)碼通過一定的方式 類加載器(ClassLoader) 把文件加載到內存中 運行時數(shù)據(jù)區(qū)(Runtime Data Area) ,而字節(jié)碼 文件是 JVM 的一套指令集規(guī)范,并不能直接交個底層操作系統(tǒng)去執(zhí)行,因此需要特定的命令解析器 執(zhí)行引擎(Execution Engine)將字節(jié)碼翻譯成底層系統(tǒng)指令再交由CPU去執(zhí)行,而這個過程中需要調 用其他語言的接口 本地庫接口(Native Interface) 來實現(xiàn)整個程序的功能,這就是這4個主要組成部分的職責與功能。
總結來看, JVM 主要通過分為以下 4 個部分,來執(zhí)行 Java 程序的,它們分別是:
1. 類加載器(ClassLoader)
2. 運行時數(shù)據(jù)區(qū)(Runtime Data Area)
3. 執(zhí)行引擎(Execution Engine)
4. 本地庫接口(Native Interface)?
JVM 運行時數(shù)據(jù)區(qū)
JVM 運行時數(shù)據(jù)區(qū)域也叫內存布局,但需要注意的是它和 Java 內存模型((Java Memory Model,簡稱JMM)完全不同,屬于完全不同的兩個概念,它由以下5大部分組成:
堆(線程共享)
堆的作用:程序中創(chuàng)建的所有對象都在保存在堆中。
我們常見的 JVM 參數(shù)設置 -Xms10m 最小啟動內存是針對堆的,-Xmx10m 最大運行內存也是針對堆的。
ms 是 memory start 簡稱,mx 是 memory max 的簡稱。
堆里面分為兩個區(qū)域:新生代和老生代,新生代放新建的對象,當經過一定 GC 次數(shù)之后還存活的對象會放入老生代。新生代還有 3 個區(qū)域:一個 Endn + 兩個 Survivor(S0/S1)。
垃圾回收的時候會將 Eden 中存活的對象放到一個未使用的 Survivor 中,并把當前的 Endn 和正在使用的 Survivor 清楚掉。?
Java虛擬機棧(線程私有)
Java 虛擬機棧的作用:Java 虛擬機棧的生命周期和線程相同,Java 虛擬機棧描述的是 Java 方法執(zhí)行的 內存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。咱們常說的堆內存、棧內存中,棧內存指的就是虛擬機棧。
Java 虛擬機棧中包含了以下 4 部分:
1. 局部變量表: 存放了編譯器可知的各種基本數(shù)據(jù)類型(8大基本數(shù)據(jù)類型)、對象引用。局部變量? ? ?表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局? ? ?部變量空間是完全確定的,在執(zhí)行期間不會改變局部變量表大小。簡單來說就是存放方法參數(shù)? ? ? ?和局部變量。
2. 操作棧:每個方法會生成一個先進后出的操作棧。
3. 動態(tài)鏈接:指向運行時常量池的方法引用。
4. 方法返回地址:即在方法執(zhí)行完成后將控制返回到調用方法的指令位置。方法出口通常用于支持方法調用的返回操作。
什么是線程私有?
由于JVM的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn),因此在任何一個確定的時刻,一個處理器 (多核處理器則指的是一個內核) 都只會執(zhí)行一條線程中的指令。因此為了切換線程后能恢復到正確的執(zhí)行位置,每條線程都需要獨立的程序計數(shù)器,各條線程之間計數(shù)器互不影響,獨立存儲。我們就把類似這類區(qū)域稱之為"線程私有"的內存。
程序計數(shù)器(線程私有)
程序計數(shù)器的作用:用來記錄當前線程執(zhí)行的行號的。
程序計數(shù)器是一塊比較小的內存空間,可以看做是當前線程所執(zhí)行的字節(jié)碼的行號指示器。
如果當前線程正在執(zhí)行的是一個Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;
如果正在執(zhí)行的是一個Native方法,這個計數(shù)器值為空。 程序計數(shù)器內存區(qū)域是唯一一個在JVM規(guī)范中沒有規(guī)定任何OOM情況的區(qū)域!
方法區(qū)(線程共享)
方法區(qū)的作用:用來存儲被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)的。
在《Java虛擬機規(guī)范中》把此區(qū)域稱之為“方法區(qū)”,而在 HotSpot 虛擬機的實現(xiàn)中,在 JDK 7 時此區(qū)域叫做永久代(PermGen),JDK 8 中叫做元空間(Metaspace)。
汽車(Java虛擬機規(guī)范):在這個比喻中,汽車代表了Java虛擬機規(guī)范,它定義了Java虛擬機的基本結構和功能,其中包括了方法區(qū)的概念。
動能提供裝置(方法區(qū)):這相當于汽車的一個重要部分,就像Java虛擬機中的方法區(qū)一樣。方法區(qū)是用于存儲類的結構信息、靜態(tài)變量、常量池、方法的字節(jié)碼等的內存區(qū)域。它提供了Java應用程序所需的關鍵信息。
發(fā)動機和電機(永久代和元空間):在不同類型的汽車中,動能提供裝置可以有不同的實現(xiàn)。對于燃油車,它使用發(fā)動機作為動能提供裝置,而電動汽車使用電機。同樣地,Java虛擬機可以使用永久代或元空間來實現(xiàn)方法區(qū)。
永久代(PermGen):就像燃油車使用汽油發(fā)動機一樣,一些早期版本的Java虛擬機使用永久代作為方法區(qū)的實現(xiàn)。永久代有一些限制,如固定大小,可能會導致內存溢出問題。
元空間(Metaspace):與之不同,元空間是Java虛擬機規(guī)范的一種新實現(xiàn)方式。它更靈活,不再受到永久代的限制,可以動態(tài)調整大小,避免了一些與永久代相關的問題。
總之,永久代和元空間都是Java虛擬機規(guī)范中對方法區(qū)的不同實現(xiàn)方式,就像汽油發(fā)動機和電動機都是動能提供裝置的不同實現(xiàn)一樣。選擇使用哪種實現(xiàn)方式取決于Java虛擬機的版本和配置,以及應用程序的需求。這個比喻很好地概括了它們之間的關系。
JDK 1.8 元空間的變化
1. 對于 HotSpot 來說,JDK 8 元空間的內存屬于本地內存,這樣元空間的大小就不在受 JVM 最大內存的參數(shù)影響了,而是與本地內存的大小有關。
2. JDK 8 中將字符串常量池移動到了堆中。
運行時常量池
運行時常量池是方法區(qū)的一部分,存放字面量與符號引用。
字面量 : 字符串(JDK 8 移動到堆中) 、final常量、基本數(shù)據(jù)類型的值。
符號引用 : 類和結構的完全限定名、字段的名稱和描述符、方法的名稱和描述符。
內存布局中的異常問題
1.? Java堆溢出
Java堆用于存儲對象實例,只要不斷的創(chuàng)建對象,并且保證GC Roots到對象之間有可達路徑來避免來 GC清除這些對象,那么在對象數(shù)量達到最大堆容量后就會產生內存溢出異常。
我們可以設置JVM參數(shù)-Xms:設置堆的最小值、-Xmx:設置堆最大值。下面我們來看一個 Java堆OOM的測試,測試以下代碼之前先設置 Idea 的啟動參數(shù),如下圖所示:
?JVM 參數(shù)為:-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
示例:
public class Main {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list =
new ArrayList<>();
while(true) {
list.add(new OOMObject());
}
}
}
以上程序的執(zhí)行結果如下:
Java堆內存的OOM異常是實際應用中最常見的內存溢出情況。當出現(xiàn)Java堆內存溢出時,異常堆棧信 息"java.lang.OutOfMemoryError"會進一步提示"Java heap space"。當出現(xiàn)"Java heap space"則很明 確的告知我們,OOM發(fā)生在堆上。?
此時要對Dump出來的文件進行分析,以MAT為例。分析問題的產生到底是出現(xiàn)了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)
內存泄漏 :?內存泄漏是指在程序中分配了一些內存(通常是用于存儲對象或數(shù)據(jù)),但在后續(xù)的程序執(zhí)行中,無法釋放或回收這些內存,導致內存消耗不斷增加,最終可能導致程序性能下降或崩潰。
內存溢出 : 內存溢出(Memory Overflow)是指在程序運行時,嘗試分配的內存超出了可用的物理內存或虛擬內存的范圍,導致程序無法繼續(xù)正常執(zhí)行的情況。內存溢出通常是由于程序內存管理不當或程序本身存在缺陷引起的。內存溢出會導致程序崩潰或異常終止。
2.??虛擬機棧和本地方法棧溢出
由于我們HotSpot虛擬機將虛擬機棧與本地方法棧合二為一,因此對于HotSpot來說,棧容量只需要由-Xss參數(shù)來設置。
關于虛擬機棧會產生的兩種異常:
1. 如果線程請求的棧深度大于虛擬機所允許的最大深度,會拋出StackOverFlow異常
2. 如果虛擬機在拓展棧時無法申請到足夠的內存空間,則會拋出OOM異常
示例:
觀察StackOverFlow異常(單線程環(huán)境下)
/**
* JVM參數(shù)為:-Xss128k
*/
public class Test {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
Test test = new Test();
try {
test.stackLeak();
} catch (Throwable e) {
System.out.println("Stack Length: " + test.stackLength);
throw e;
}
}
}
會出現(xiàn)如下運行結果:?
出現(xiàn)StackOverflowError異常時有錯誤堆??梢蚤喿x,比較好找到問題所在。如果使用虛擬機默認參數(shù),棧深度在多多數(shù)情況下達到1000-2000完全沒問題,對于正常的方法調用(包括遞歸),完全夠用。
如果是因為多線程導致的內存溢出問題,在不能減少線程數(shù)的情況下,只能減少最大堆和減少棧容量的 方式來換取更多線程。
/**
* JVM參數(shù)為:-Xss2M
*/
public class Test {
private void dontStop() {
while(true) {
}
}
public void stackLeakByThread() {
while(true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
Test test = new Test();
test.stackLeakByThread();
}
}
以上代碼運行電腦可能會崩,記得保存所有工作~~??
JVM 類加載
1. 類加載過程
對于一個類來說,它的生命周期是這樣的:
其中前 5 步是固定的順序并且也是類加載的過程,其中中間的 3 步我們都屬于連接,所以對于類加載來說總共分為以下幾個步驟:
1. 加載
2. 連接
????????1. 驗證
????????2. 準備
????????3. 解析
3. 初始化
下面我們來看看每個步驟的具體執(zhí)行內容~~??
加載
“加載”(Loading)階段是整個“類加載”(Class Loading)過程中的一個階段,它和類加載 Class Loading 是不同的,一個是加載 Loading 另一個是類加載 Class Loading,所以不要把二者搞混了。
在加載 Loading 階段,Java虛擬機需要完成以下三件事情:
1)通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
2)將這個字節(jié)流所代表的靜態(tài)存儲結構轉化為方法區(qū)的運行時數(shù)據(jù)結構。
3)在內存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
驗證
驗證是連接階段的第一步,這一階段的目的是確保Class文件的字節(jié) 流中包含的信息符合《Java虛擬機 規(guī)范》的全部約束要求,保證這些信息被當作代碼運行后不會危害虛擬機自身的安全。
驗證選項: 文件格式驗證,字節(jié)碼驗證,符號引用驗證...
準備
準備階段是正式為類中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內存并設置類變量初始值的階段。
比如此時有這樣一行代碼:
public static int value = 123;
它是初始化 value 的 int 值為 0,而非 123。
解析
解析階段是 Java 虛擬機將常量池內的符號引用替換為直接引用的過程,也就是初始化常量的過程。
初始化
初始化階段,Java 虛擬機真正開始執(zhí)行類中編寫的 Java 程序代碼,將主導權移交給應用程序。初始化 階段就是執(zhí)行類構造器方法的過程。
雙親委派模型
Java虛擬機(JVM)的雙親委派模型是一種類加載機制,用于確保Java類的加載和安全性。這個模型基于"委派"原則,它指定了JVM在加載類時首先嘗試委派給父類加載器,只有在父類加載器無法加載類時才會由子類加載器嘗試加載。這個模型有助于確保類的一致性、避免重復加載、并提高安全性。
下面是雙親委派模型的關鍵概念:
-
父類加載器(Parent Class Loader):在JVM中,有一個層次結構的類加載器鏈,根類加載器(Bootstrap Class Loader)位于最頂層,然后是擴展類加載器(Extension Class Loader),最后是應用程序類加載器(Application Class Loader)。父類加載器負責加載核心Java類庫(如java.lang包中的類),并向下委派加載請求。
-
子類加載器(Child Class Loader):自定義的類加載器通常是子類加載器,它們負責加載應用程序特定的類。當一個類加載器收到加載請求時,它首先將請求委派給其父類加載器,如果父類加載器無法找到類,子類加載器才會嘗試加載。
-
雙親委派規(guī)則:當類加載器收到加載類的請求時,它首先檢查是否已經加載過該類。如果沒有,它將委派給其父類加載器進行加載。這一過程遞歸繼續(xù),直到達到根類加載器。如果根類加載器仍然找不到類,那么會由底層的類加載器嘗試加載。這種層層委派的方式確保了類的唯一性和一致性。
雙親委派模型的優(yōu)點包括:
-
避免了類的重復加載:如果一個類已經被一個類加載器加載,那么它不會被另一個類加載器重復加載,這有助于節(jié)省內存和資源。
-
安全性:通過限制用戶自定義類加載器的能力,雙親委派模型可以確保核心Java類庫的安全性,防止惡意代碼替代標準類庫。
-
保持類的一致性:雙親委派模型可以確保不同類加載器加載的類是相同的,避免了類的混亂和不一致性。
總之,雙親委派模型是Java類加載的關鍵機制之一,它有助于保持類加載的安全性和一致性,同時提高了性能和資源利用率。在自定義類加載器時,通常建議遵循雙親委派模型,以確保類加載行為的一致性~~??
啟動類加載器:加載 JDK 中 lib 目錄中 Java 的核心類庫,即$JAVA_HOME/lib目錄。
擴展類加載 器。加載 lib/ext 目錄下的類。
應用程序類加載器:加載我們寫的應用程序。
自定義類加載器:根據(jù)自己的需求定制類加載器。
垃圾回收機制
垃圾回收機制(Garbage Collection,簡稱GC)是Java虛擬機(JVM)的一個關鍵特性,用于管理和回收不再被程序使用的內存,以確保內存的有效使用和防止內存泄漏。垃圾回收機制的主要目標是自動釋放不再引用的對象,以便釋放內存并減少內存泄漏的風險。
在 Java 中,所有的對象都是要存在內存中的(也可以說內存中存儲的是一個個對象),因此我們將內存回收,也可以叫做死亡對象的回收。
死亡對象的判斷算法
引用計數(shù)算法
每個對象都帶有一個引用計數(shù)器,每當有一個引用指向對象時,計數(shù)器加1,每當引用不再指向對象時,計數(shù)器減1。當對象的引用計數(shù)器減到零時,表示對象不再被引用,即可認為該對象是垃圾,可以被回收。
引用計數(shù)法的一些關鍵概念和示例:
引用計數(shù)器:每個對象都包含一個整數(shù)計數(shù)器,用于記錄對象被引用的次數(shù)。
引用操作:當新的引用指向對象時,引用計數(shù)器加1;當引用不再指向對象時,引用計數(shù)器減1。
垃圾對象:當一個對象的引用計數(shù)器降到零時,表示對象不再被引用,可以被認為是垃圾對象,即可被回收。
內存回收:引用計數(shù)法的內存回收是實時的,即當引用計數(shù)器降到零時,立即回收對象所占用的內存。
但是JVM中沒有選用引用計數(shù)法來管理內存,最主要的原因就是引用計數(shù)法無法解決對象的循環(huán)引用問題,下面是一個循環(huán)引用示例
class Node {
Node next;
Node() {
next = null;
}
void setNext(Node node) {
next = node;
}
}
public class ReferenceCycleExample {
public static void main(String[] args) {
Node node1 = new Node();
Node node2 = new Node();
// 創(chuàng)建循環(huán)引用
node1.setNext(node2);
node2.setNext(node1);
// 現(xiàn)在node1和node2互相引用,但引用計數(shù)法無法檢測到這一點
// 嘗試釋放引用
node1 = null;
node2 = null;
// 即使沒有引用指向這兩個對象,它們仍然相互引用,無法被回收,造成內存泄漏
}
}
在上述示例中,Node
類表示一個簡單的鏈表節(jié)點,其中的next
字段指向下一個節(jié)點。在main
方法中,我們創(chuàng)建了兩個節(jié)點node1
和node2
,并通過setNext
方法相互引用,形成了循環(huán)引用。盡管在后續(xù)代碼中將node1
和node2
設置為null
,但它們仍然相互引用,因此無法被引用計數(shù)法正確回收,導致內存泄漏。
這個示例突出了引用計數(shù)法的一個主要問題:它無法處理循環(huán)引用的情況。因為引用計數(shù)法只關注引用計數(shù),而不考慮對象之間的引用關系,因此循環(huán)引用可能導致對象的引用計數(shù)永遠不會降為零,即使對象已經不再被程序使用,也無法被正確回收。這就是為什么現(xiàn)代垃圾回收器通常使用基于可達性分析的算法,如標記-清除,以解決這個問題。
可達性分析算法
可達性分析算法是JVM用于標記和回收不再被程序引用的對象的核心機制。它通過以下步驟來確定哪些對象仍然是可達的,哪些是不可達的:
根集合(Root Set):JVM從一組根對象開始,這些根對象包括程序的主方法中的局部變量、靜態(tài)變量(類變量)中引用的對象以及已經在執(zhí)行過程中被分配的對象。
標記階段:在標記階段,JVM從根集合出發(fā),通過引用鏈遍歷對象,將它們標記為活動對象(可達)。引用鏈包括字段、數(shù)組元素、方法參數(shù)等。如果對象可以通過引用鏈訪問到,它被認為是可達的。
清除階段:在標記階段之后,未被標記為活動對象的對象被認為是不可達的垃圾,將被回收。這些對象占用的內存將被釋放,以供將來分配新對象。
內存區(qū)域
對于可達性分析算法,主要關注堆內存的分配和回收。
堆內存(Heap):堆是用于存儲對象的主要內存區(qū)域。在可達性分析算法中,堆被劃分為不同的代,通常包括新生代(Young Generation)、幸存區(qū)(Survivor Space)、老年代(Old Generation)。
新生代:新分配的對象通常位于新生代。在新生代中,有三個區(qū)域:一個Eden區(qū)和兩個幸存區(qū)(通常稱為S0和S1)。對象首先分配到Eden區(qū),然后在垃圾回收發(fā)生時,存活的對象會被移動到幸存區(qū)。幸存區(qū)之間也會發(fā)生對象的復制。
老年代:在對象經歷多次幸存區(qū)的復制后,它們最終會被晉升到老年代。老年代主要用于存儲生命周期較長的對象。
棧內存(Stack):棧內存用于存儲方法調用的局部變量、操作數(shù)棧和方法調用的返回地址。它的生命周期與方法的調用和返回密切相關,不用于存儲對象。
可達性分析與內存區(qū)域的關系
可達性分析算法主要影響堆內存的使用和回收。當可達性分析算法標記不再被引用的對象為垃圾時,這些對象所占用的堆內存將被釋放??蛇_性分析還幫助JVM確定哪些對象需要在不同代之間進行移動,以優(yōu)化內存使用。
新生代和老年代之間的對象移動與可達性分析密切相關,因為幸存區(qū)中的對象在不同代之間移動。幸存區(qū)的復制和清理操作是為了確保年輕代內存的高效利用。老年代的管理也依賴于可達性分析,以及在必要時執(zhí)行Full GC(Full Garbage Collection)來回收老年代的垃圾。
垃圾回收算法
垃圾回收算法是Java虛擬機(JVM)用于管理內存中對象的方法,以便回收不再被引用的對象,從而釋放內存資源。不同的垃圾回收算法適用于不同的情況和內存分配模式。以下是四種常見的垃圾回收算法的詳細講解:
標記-清除算法(Mark and Sweep):
-
標記階段:在此階段,垃圾回收器從一組根對象(通常包括程序的主方法中的局部變量、靜態(tài)變量等)出發(fā),通過引用鏈遍歷對象圖,并將可達對象標記為活動對象。這些對象不會被回收。
-
清除階段:在標記階段之后,所有未被標記為活動對象的對象被認為是垃圾,將被回收。此時,垃圾回收器會釋放垃圾對象占用的內存。
-
優(yōu)點:簡單明了,適用于任何內存分配模式。
-
缺點:可能會產生內存碎片,回收效率較低,會引發(fā)應用程序停頓。
?像這樣子我們把上面的黑色垃圾都打上標記,垃圾回收器就會釋放這些內存。
復制算法(Copying Garbage Collection):
-
新生代劃分:堆內存被劃分為兩個相等的區(qū)域,通常稱為Eden區(qū)和幸存區(qū)(S0和S1)。
-
標記和復制階段:新對象首先分配到Eden區(qū)。當Eden區(qū)滿時,執(zhí)行標記和復制階段。在這個階段,垃圾回收器標記所有活動對象,然后將它們復制到另一個幸存區(qū)。幸存區(qū)之間也會進行對象的復制。
-
清理階段:清理階段將不再被引用的對象所占用的區(qū)域標記為空閑,并將Eden區(qū)和一個幸存區(qū)的對象互換。
-
優(yōu)點:有效避免了內存碎片,回收效率高,適用于新生代的短生命周期對象。
-
缺點:需要額外的內存來進行復制操作,老年代對象無法直接受益。
如上示例,假如a和c對象被認為是垃圾,它會把剩下的對象(我們這個示例只剩下了b對象)先復制一份到s0上,然后把Eden一整個區(qū)域內存全部釋放,這樣有效避免了內存碎片,回收效率高,適用于新生代的短生命周期對象。
標記-整理算法(Mark and Compact):
-
標記階段:與標記-清除算法相同,標記所有可達對象。
-
整理階段:在清理階段,不會簡單地清除垃圾對象,而是將存活的對象向一端移動,然后清除掉未移動的垃圾對象。這樣可以減少內存碎片。
-
優(yōu)點:避免了內存碎片,回收效率高,適用于堆中包含較多長生命周期對象的情況。
-
缺點:需要對象移動操作,可能引發(fā)一定的停頓。
?分代算法(Generational Garbage Collection):
-
堆分代:堆內存被分為不同的代,通常包括新生代和老年代。新分配的對象通常位于新生代。
-
新生代回收:在新生代中,通常采用復制算法,因為新生代的對象生命周期短。垃圾回收頻繁發(fā)生,但效率高。
-
老年代回收:在老年代中,通常采用標記-清除或標記-整理算法,因為老年代的對象生命周期較長。垃圾回收不太頻繁,但可能導致較長的停頓。
-
優(yōu)點:根據(jù)對象的生命周期進行不同的回收,提高了效率和響應時間。
-
缺點:需要管理不同代的對象,引入了復雜性。
當前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,這個算法并沒有新思想,只是根據(jù)對象存活周期的不同將內存劃分為幾塊。一般是把Java堆分為新生代和老年代。在新生代中,每次垃圾回收都有大批對象死去,只有少量存活,因此我們采用復制算法;而老年代中對象存活率高、沒有額外空間對它進行分配擔保,就必須采用"標記-清理"或者"標記-整理"算法。
新生代(Young Generation):
特點:新生代是堆內存的一部分,通常用于存儲具有較短生命周期的對象。由于大多數(shù)對象在創(chuàng)建后很快就變得不可達,因此新生代采用了一種高效的垃圾回收策略。
回收策略:新生代通常采用復制算法(Copying Garbage Collection)作為主要的垃圾回收策略。這意味著新生代被劃分為三個區(qū)域:Eden區(qū)和兩個幸存區(qū)(S0和S1)。新對象首先分配到Eden區(qū)。當Eden區(qū)滿時,發(fā)生垃圾回收,將存活的對象復制到其中一個幸存區(qū),同時清理掉Eden區(qū)。隨著垃圾回收的進行,對象會在幸存區(qū)之間進行復制,最終存活的對象會晉升到老年代。
優(yōu)點:新生代的復制算法有效避免了內存碎片,回收效率高。
缺點:復制算法需要額外的內存來存儲復制后的對象,老年代對象無法直接受益,可能導致內存使用效率不高。
老年代(Old Generation):
特點:老年代用于存儲具有較長生命周期的對象,這些對象在多次垃圾回收后仍然存活。
回收策略:老年代通常采用標記-清除(Mark and Sweep)或標記-整理(Mark and Compact)算法作為主要的垃圾回收策略。這些算法涉及標記不再被引用的對象,然后清除或整理內存。
標記-清除算法(Mark and Sweep):標記不再被引用的對象,然后清除垃圾對象,但可能會導致內存碎片。
標記-整理算法(Mark and Compact):標記不再被引用的對象,然后將存活的對象向一端移動,清除未移動的垃圾對象,以減少內存碎片。
優(yōu)點:老年代的標記-清除或標記-整理算法能夠處理具有較長生命周期的對象,并在垃圾回收時避免內存碎片。
缺點:可能引發(fā)較長的垃圾回收停頓,影響應用程序的響應時間。
?寫在最后
通過本博客,我們深入探討了Java虛擬機(JVM)的工作原理和內部機制。我們從JVM的基本概念開始,了解了其執(zhí)行流程和運行時數(shù)據(jù)區(qū)域的重要性。我們探討了JVM內存管理的關鍵方面,包括堆內存、棧內存、方法區(qū)、運行時常量池等,以及Java堆溢出和虛擬機棧溢出等內存問題的解決方法。
我們還深入研究了JVM的類加載過程,了解了加載、驗證、準備、解析和初始化等步驟,以及雙親委派模型的重要性。此外,我們介紹了JDK 1.8中元空間的變化和垃圾回收機制的工作原理。
在垃圾回收方面,我們討論了死亡對象的判斷算法,引用計數(shù)算法和可達性分析算法等關鍵概念。我們還介紹了不同的垃圾回收算法,包括標記-清除、復制、標記-整理和分代算法,以及它們在不同內存區(qū)域的應用。
最后,我們希望這篇博客能夠幫助您更深入地理解Java虛擬機的內部工作原理,從而更好地編寫高性能和可靠的Java應用程序。JVM作為Java生態(tài)系統(tǒng)的核心組件,其深入理解對于Java開發(fā)人員來說是至關重要的。感謝您的閱讀!??文章來源:http://www.zghlxwxcb.cn/news/detail-707053.html
?
到了這里,關于深入探討Java虛擬機(JVM):執(zhí)行流程、內存管理和垃圾回收機制的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!