Java的內(nèi)存管理機制詳解
在Java中,內(nèi)存管理機制是自動且相對復(fù)雜的,它主要由Java虛擬機(JVM)來負責。
這個機制確保了內(nèi)存的有效分配和釋放,從而幫助開發(fā)者避免了許多常見的內(nèi)存管理問題,如內(nèi)存泄漏和懸掛指針。
Java內(nèi)存區(qū)域
Java的內(nèi)存主要分為幾個區(qū)域:
方法區(qū)(Method Area)
- 功能:存儲每個類的結(jié)構(gòu)信息,包括運行時常量池、字段和方法數(shù)據(jù)、構(gòu)造函數(shù)和普通方法的字節(jié)碼內(nèi)容等。
- 共享性:這部分區(qū)域是全局共享的,所有線程都可以訪問。
- 生命周期:它在JVM啟動時創(chuàng)建,并在JVM被銷毀時銷毀。
堆
- 功能:Java中最大的內(nèi)存區(qū)域,用于存放所有的對象實例和數(shù)組。
- 垃圾收集:堆內(nèi)存是垃圾收集器管理的主要區(qū)域,因此也被稱為GC堆。當對象不再被引用時,垃圾收集器會回收這些對象的內(nèi)存。
- 線程共享:堆內(nèi)存是所有線程共享的,因此需要同步機制來管理對堆內(nèi)存的訪問。
堆內(nèi)存的結(jié)構(gòu)
Java堆內(nèi)存通常被劃分為幾個區(qū)域,如新生代(Young Generation)、老年代(Old Generation)以及永久代(在Java 8及以后版本中,永久代被元空間Metaspace取代)。這些區(qū)域各自承擔著不同的角色和職責。
- 新生代:用于存放新生成的對象。由于大多數(shù)對象都是朝生夕滅的,所以新生代區(qū)域被設(shè)計為相對較小,并且經(jīng)常進行垃圾回收。
- 老年代:存放經(jīng)過多次垃圾回收后仍然存活的對象。
- 元空間(Java 8及以后):用于存儲類的元數(shù)據(jù),取代了永久代。
動態(tài)分配過程
- 分配內(nèi)存:當Java程序創(chuàng)建一個新的對象時,JVM會檢查堆內(nèi)存中的空閑空間是否足夠。如果足夠,JVM會為該對象分配一塊連續(xù)的內(nèi)存空間。
- 初始化內(nèi)存:分配的內(nèi)存空間會被初始化為零值(對于對象引用類型,默認值為null)。
- 設(shè)置對象頭:在分配的內(nèi)存空間中,JVM會設(shè)置對象頭信息,包括對象的哈希碼、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時間戳等。
- 返回引用:最后,JVM會將分配的內(nèi)存空間的地址(即對象的引用)返回給變量,這樣程序就可以通過這個引用來訪問對象了。
堆內(nèi)存溢出
- 如果Java堆內(nèi)存中的對象過多,以至于堆內(nèi)存無法容納更多的對象,那么JVM會拋出OutOfMemoryError: Java heap space錯誤。這通常是因為程序中存在內(nèi)存泄漏或者堆內(nèi)存設(shè)置得太小。
堆結(jié)構(gòu)的特點
完全二叉樹:除了最后一層外,每一層都被完全填滿,且所有節(jié)點都盡可能地向左對齊。
首先,是TreeNode類的定義:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}接下來,是一個簡單的遞歸函數(shù)來構(gòu)建完全二叉樹(這里為了簡化,我們假設(shè)完全二叉樹是通過層序遍歷的序列來構(gòu)建的,但通常完全二叉樹不會這樣直接構(gòu)建,因為很多位置是空的,這里只是為了展示):
// 注意:這個方法僅用于演示,實際上完全二叉樹不會這樣直接通過數(shù)組構(gòu)建節(jié)點
// 因為完全二叉樹中有很多空節(jié)點位置,這里我們假設(shè)所有位置都填滿了有效值
TreeNode buildCompleteBinaryTreeFromLevelOrder(int[] nums) {
if (nums == null || nums.length == 0) {
return null;
}
Queue<TreeNode> queue = new LinkedList<>();
TreeNode root = new TreeNode(nums[0]);
queue.offer(root);
int index = 1;
while (index < nums.length) {
TreeNode currentNode = queue.poll();
if (index < nums.length) {
currentNode.left = new TreeNode(nums[index++]);
queue.offer(currentNode.left);
}
if (index < nums.length) {
currentNode.right = new TreeNode(nums[index++]);
queue.offer(currentNode.right);
}
}
return root;
}
// 注意:上面的方法其實構(gòu)建的是一個滿二叉樹,而非一般的完全二叉樹
// 在完全二叉樹中,很多位置可能是空的,這里為了簡化沒有處理空節(jié)點的情況然而,正如注釋中所說,上面的方法實際上構(gòu)建的是一個滿二叉樹,而不是一個通用的完全二叉樹。在完全二叉樹中,許多節(jié)點可能是空的。由于直接通過數(shù)組構(gòu)建這樣的樹在Java中并不直觀(因為你需要處理空節(jié)點),我們通常會使用其他數(shù)據(jù)結(jié)構(gòu)(如隊列)來輔助構(gòu)建,或者簡單地通過遞歸調(diào)用構(gòu)建特定形狀的二叉樹。
如果你只是想看看完全二叉樹在數(shù)組中的表示,那么你可以理解為一個完全二叉樹(非滿)可以“填充”到一個數(shù)組中,其中數(shù)組的索引反映了樹中的位置(根節(jié)點在索引0,左子節(jié)點在2i+1,右子節(jié)點在2i+2,其中i是父節(jié)點的索引),但數(shù)組中可能包含空值(或某種表示空節(jié)點的值)來表示樹中的空位置。
堆屬性:1.最大堆:每個父節(jié)點的值都大于或等于其任何子節(jié)點的值。2.最小堆:每個父節(jié)點的值都小于或等于其任何子節(jié)點的值。
堆結(jié)構(gòu)的應(yīng)用
優(yōu)先隊列:堆結(jié)構(gòu)是實現(xiàn)優(yōu)先隊列(Priority Queue)的理想選擇。優(yōu)先隊列是一種特殊的隊列,其中每個元素都有一個優(yōu)先級,元素的出隊順序基于它們的優(yōu)先級,而不是它們被加入隊列的順序。最大堆和最小堆可以分別用來實現(xiàn)最大優(yōu)先隊列和最小優(yōu)先隊列。
使用 PriorityQueue 來存儲整數(shù),并根據(jù)整數(shù)的自然順序(即從小到大)進行排序:
import java.util.PriorityQueue;
public class PriorityQueueExample {
public static void main(String[] args) {
// 創(chuàng)建一個默認的PriorityQueue,它將根據(jù)元素的自然順序進行排序
PriorityQueue<Integer> pq = new PriorityQueue<>();
// 向優(yōu)先隊列中添加元素
pq.add(3);
pq.add(1);
pq.add(4);
pq.add(1);
pq.add(5);
// 遍歷并移除(彈出)優(yōu)先隊列中的所有元素
while (!pq.isEmpty()) {
System.out.println(pq.poll()); // poll() 方法會移除并返回隊列頭部的元素
}
// 輸出結(jié)果將按從小到大的順序顯示:1, 1, 3, 4, 5
}
}在這個例子中,我們創(chuàng)建了一個 PriorityQueue<Integer> 類型的隊列,并向其中添加了幾個整數(shù)。由于我們沒有為 PriorityQueue 提供 Comparator,因此它將使用整數(shù)的自然順序進行排序。然后,我們使用一個 while 循環(huán)和 poll() 方法來遍歷并移除隊列中的所有元素。poll() 方法會移除并返回隊列頭部的元素(即優(yōu)先級最高的元素),在這個例子中,就是數(shù)值最小的元素。
堆排序:堆排序是一種基于比較的排序算法,它利用堆結(jié)構(gòu)進行排序。首先,將待排序的序列構(gòu)造成一個最大堆(或最小堆),此時,整個序列的最大值(或最小值)就是堆頂?shù)母?jié)點。將它移走(其實就是將其與堆數(shù)組的末尾元素交換,此時末尾元素就是最大值),然后將剩余的n-1個序列重新構(gòu)造成一個堆,這樣就會得到n個元素的次小值。如此反復(fù)執(zhí)行,便能得到一個有序序列了。
使用了最大堆的性質(zhì)來進行排序,使得數(shù)組的第一個元素是最大值,然后將其與數(shù)組的最后一個元素交換,之后減小堆的大小(排除已排序的最大元素),再次將剩余的元素調(diào)整為最大堆,并重復(fù)上述過程,直到整個數(shù)組排序完成。
public class HeapSort {
// 用于構(gòu)建最大堆的輔助函數(shù)
private static void buildMaxHeap(int arr[], int n) {
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
}
// 調(diào)整給定索引處的元素,使其符合最大堆的性質(zhì)
private static void heapify(int arr[], int n, int i) {
int largest = i; // 初始化最大為根
int l = 2 * i + 1; // 左子節(jié)點
int r = 2 * i + 2; // 右子節(jié)點
// 如果左子節(jié)點大于根節(jié)點
if (l < n && arr[l] > arr[largest])
largest = l;
// 如果右子節(jié)點大于當前的最大值
if (r < n && arr[r] > arr[largest])
largest = r;
// 如果最大值不是根節(jié)點,則交換
if (largest != i) {
int swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
// 遞歸地調(diào)整受影響的子樹
heapify(arr, n, largest);
}
}
// 主要的堆排序函數(shù)
public static void sort(int arr[]) {
int n = arr.length;
// 構(gòu)建最大堆
buildMaxHeap(arr, n);
// 一個個從堆頂取出元素
for (int i = n - 1; i > 0; i--) {
// 移動當前根到末尾
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 調(diào)用max heapify在減少的堆上
heapify(arr, i, 0);
}
}
// 驅(qū)動代碼
public static void main(String args[]) {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = arr.length;
HeapSort ob = new HeapSort();
ob.sort(arr);
System.out.println("Sorted array is");
for (int i = 0; i < n; ++i)
System.out.print(arr[i] + " ");
System.out.println();
}
}- 圖算法:在圖的算法中,堆結(jié)構(gòu)可以用來實現(xiàn)Dijkstra算法等最短路徑算法中的優(yōu)先隊列,以優(yōu)化算法的效率。
- 內(nèi)存管理:雖然這與堆數(shù)據(jù)結(jié)構(gòu)不直接相關(guān),但堆內(nèi)存(Heap Memory)的管理算法(如垃圾收集器中的某些算法)有時會借鑒堆數(shù)據(jù)結(jié)構(gòu)的思想,特別是在處理內(nèi)存塊的分配和回收時。
- 數(shù)據(jù)流處理:在處理數(shù)據(jù)流或?qū)崟r數(shù)據(jù)時,堆結(jié)構(gòu)可以用來維護一組數(shù)據(jù)中的最大值或最小值,以便進行實時分析或決策。
棧
- 功能:每個線程在創(chuàng)建時都會創(chuàng)建一個虛擬機棧,用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。
- 線程隔離:每個線程的棧是隔離的,確保了線程之間數(shù)據(jù)的獨立性。
- 執(zhí)行過程:方法調(diào)用和返回的執(zhí)行過程,對應(yīng)著棧幀在虛擬機棧中入棧和出棧的過程。
棧結(jié)構(gòu)的特點
- 后進先出:棧中的元素按照后進先出的順序進行存取。即最后進入棧的元素會最先被移除,而最先進入棧的元素只有在其他元素都被移除后才能被移除。
- 棧頂操作:棧的所有操作都只能在棧頂進行,包括添加元素(入棧/壓棧)和刪除元素(出棧/退棧)。
- 棧底固定:棧的另一端,即棧底,是固定的,不允許進行插入或刪除操作。
棧結(jié)構(gòu)的組成
- 從數(shù)據(jù)存儲結(jié)構(gòu)的角度來看,??梢苑譃閮煞N類型:
- 順序棧:使用一組地址連續(xù)的內(nèi)存單元依次保存棧中的數(shù)據(jù)。在Java中,可以通過定義一個固定大小的數(shù)組來模擬順序棧,其中數(shù)組的第一個元素通常被視為棧底,數(shù)組的末尾(或特定索引位置)作為棧頂。當棧滿時,無法繼續(xù)添加新元素;當??諘r,無法進行刪除操作。
- 鏈棧:使用鏈表來實現(xiàn)棧結(jié)構(gòu)。鏈棧的每個節(jié)點包含數(shù)據(jù)和指向下一個節(jié)點的指針。鏈棧的棧頂是鏈表的頭節(jié)點,通過修改頭節(jié)點的指針來實現(xiàn)元素的入棧和出棧操作。鏈棧在動態(tài)擴展方面比順序棧更加靈活,因為它不需要在添加元素時檢查棧的容量。
Java中的棧實現(xiàn)
- Java標準庫提供了Stack類來實現(xiàn)棧結(jié)構(gòu),但需要注意的是,Stack類已經(jīng)被標記為過時(deprecated),因為它繼承自Vector類,而Vector類本身就是一個同步的、動態(tài)數(shù)組的實現(xiàn),這導(dǎo)致了Stack類在性能上并不是最優(yōu)的。因此,在Java中,推薦使用Deque接口的實現(xiàn)類(如ArrayDeque)來作為棧的替代品。
棧結(jié)構(gòu)的應(yīng)用
棧結(jié)構(gòu)在編程中有廣泛的應(yīng)用,例如:
函數(shù)調(diào)用:在函數(shù)調(diào)用時,會將返回地址、參數(shù)等信息壓入棧中,函數(shù)返回時再從棧中彈出這些信息。
代碼示例:
public class FunctionCallExample {
public static void main(String[] args) {
int result = add(5, 3);
System.out.println("The sum is: " + result);
}
public static int add(int a, int b) {
return a + b;
}
}在這個例子中,main 方法調(diào)用了 add 方法,并傳遞了兩個整數(shù)參數(shù) 5 和 3。當 add 方法被調(diào)用時,JVM會執(zhí)行以下操作(在概念層面上):
- 參數(shù)傳遞:5 和 3 作為參數(shù)被傳遞給 add 方法。在Java中,基本數(shù)據(jù)類型(如int)是按值傳遞的,這意味著它們的值被復(fù)制到 add 方法的參數(shù)變量中。
- 棧幀創(chuàng)建:JVM為每個方法調(diào)用創(chuàng)建一個新的棧幀(Stack Frame),并將其推入調(diào)用棧(Call Stack)中。棧幀包含了方法的局部變量、操作數(shù)棧(用于執(zhí)行方法中的操作)、動態(tài)鏈接(指向當前方法的運行時常量池的方法引用)以及返回地址(方法執(zhí)行完成后返回到調(diào)用者的地址)。
- 執(zhí)行方法:add 方法在其棧幀中執(zhí)行,使用傳遞的參數(shù)進行計算,并將結(jié)果存儲在局部變量中(在這個例子中,結(jié)果直接通過返回語句返回,沒有存儲在局部變量中)。
- 返回結(jié)果:add 方法執(zhí)行完畢后,其棧幀中的返回地址被用來將控制權(quán)返回給調(diào)用者(即 main 方法)。返回值(在這個例子中是 8)被放置在調(diào)用者的操作數(shù)棧上,以便進一步使用。
- 棧幀銷毀:隨著 add 方法執(zhí)行完畢并返回,其棧幀會從調(diào)用棧中彈出并銷毀。
需要注意的是,雖然這個過程涉及到了棧的使用,但Java程序員通常不需要直接管理棧的操作。JVM會自動處理這些底層細節(jié)
遞歸調(diào)用:遞歸調(diào)用過程中,每次調(diào)用都會將當前的狀態(tài)壓入棧中,直到找到基本情況,然后逐層返回,從棧中彈出狀態(tài)。
代碼示例:
這個示例將展示遞歸調(diào)用過程中如何將狀態(tài)壓入棧(盡管在Java中,這個棧是由JVM隱式管理的,我們通常不直接操作它),并逐層返回。我們將通過計算階乘的遞歸函數(shù)來演示這一點。
public class RecursionExample {
// 計算n的階乘
public static int factorial(int n) {
// 基本情況:如果n是0或1,返回1
if (n == 0 || n == 1) {
return 1;
}
// 遞歸調(diào)用:將n-1的狀態(tài)壓入“隱式棧”中(實際上是由JVM的調(diào)用棧管理)
// 然后計算n * (n-1)!
else {
return n * factorial(n - 1);
}
}
public static void main(String[] args) {
int number = 5;
System.out.println("The factorial of " + number + " is " + factorial(number));
}
}在這個例子中,factorial 方法是一個遞歸函數(shù),它計算并返回給定整數(shù)的階乘。每次遞歸調(diào)用都會將當前的 n 值(或說,是當前的狀態(tài))壓入到JVM的調(diào)用棧中。當 n 達到基本情況(即 n == 0 或 n == 1)時,遞歸開始逐層返回,并從棧中彈出之前的狀態(tài)(雖然這個彈出過程是自動的,我們看不到),直到返回到最初的調(diào)用點。
注意,雖然我們說“將狀態(tài)壓入棧中”,但實際上在Java中,這是由JVM的調(diào)用棧自動管理的,我們不需要(也無法)直接操作它。這個示例僅僅是為了說明遞歸調(diào)用的工作機制。
表達式求值:在解析和計算數(shù)學(xué)表達式時,可以使用棧來保存中間結(jié)果和操作符。
代碼示例
使用棧來解析和計算數(shù)學(xué)表達式(特別是中綴表達式)是一個經(jīng)典的編程問題 該示例使用兩個棧:一個用于保存數(shù)字(稱為數(shù)字棧),另一個用于保存操作符(稱為操作符棧)。
注意,為了簡化,這個示例將只處理加法(+)和乘法(*),并且假設(shè)輸入是一個有效的、格式良好的表達式,不包含括號。
import java.util.Stack;
public class ExpressionEvaluator {
public static int evaluate(String expression) {
Stack<Integer> numbers = new Stack<>();
Stack<Character> operators = new Stack<>();
int i = 0;
while (i < expression.length()) {
char ch = expression.charAt(i);
if (Character.isDigit(ch)) {
// 假設(shè)單個數(shù)字不會超過一位,為了簡化處理
int num = Character.getNumericValue(ch);
numbers.push(num);
} else if (ch == '+' || ch == '*') {
// 遇到操作符時,可能需要處理之前的操作符和數(shù)字
while (!operators.isEmpty() && hasPrecedence(operators.peek(), ch)) {
applyOp(numbers, operators.pop());
}
operators.push(ch);
}
i++;
}
// 處理所有剩余的操作符
while (!operators.isEmpty()) {
applyOp(numbers, operators.pop());
}
// 最終數(shù)字棧中應(yīng)只剩下一個結(jié)果
return numbers.pop();
}
private static boolean hasPrecedence(char op1, char op2) {
// 乘法優(yōu)先級高于加法
if ((op1 == '*' && op2 == '+') || (op2 == '*')) {
return true;
}
return false;
}
private static void applyOp(Stack<Integer> numbers, char op) {
int val2 = numbers.pop();
int val1 = numbers.pop();
switch (op) {
case '+':
numbers.push(val1 + val2);
break;
case '*':
numbers.push(val1 * val2);
break;
}
}
public static void main(String[] args) {
String expression = "3+4*2";
System.out.println("Result: " + evaluate(expression)); // 應(yīng)輸出 11
}
}這個代碼示例展示了如何使用棧來解析和計算一個只包含加法和乘法的數(shù)學(xué)表達式。注意,這個實現(xiàn)為了簡化而做了一些假設(shè)(比如數(shù)字都是單個的),并且沒有處理括號。在實際應(yīng)用中,你可能需要擴展這個實現(xiàn)以處理更復(fù)雜的表達式,包括多位數(shù)、括號和更多的運算符。
頁面訪問歷史:在瀏覽器中,用戶訪問的頁面歷史可以用棧來保存,實現(xiàn)“前進”和“后退”功能。
程序計數(shù)器
- 功能:一塊較小的內(nèi)存空間,可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。
- 線程隔離:每個線程都有一個獨立的程序計數(shù)器,互不干擾。
- 指令執(zhí)行:字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。
本地方法棧
- 功能:與虛擬機棧的作用非常相似,但它是為Native方法服務(wù)的。
- 實現(xiàn)自由:在JVM規(guī)范中,并沒有對本地方法棧的具體實現(xiàn)方法以及數(shù)據(jù)結(jié)構(gòu)作強制規(guī)定,虛擬機可以自由實現(xiàn)它。
- 用途:用于執(zhí)行Java中的本地方法,這些方法是使用非Java語言(如C或C++)編寫的,并通過JNI(Java Native Interface)與Java代碼交互。
Java中堆內(nèi)存和棧內(nèi)存的主要區(qū)別
堆內(nèi)存
- 存儲內(nèi)容:堆內(nèi)存主要用于存儲對象實例(即對象的實際數(shù)據(jù))和數(shù)組。每當使用new關(guān)鍵字創(chuàng)建一個對象時,這個對象就會被分配在堆內(nèi)存上。
- 生命周期:堆內(nèi)存中的對象生命周期相對較長,因為它們的創(chuàng)建和銷毀不是由編譯器自動管理的,而是由垃圾回收器(Garbage Collector, GC)根據(jù)對象的可達性來自動回收。
- 大小管理:堆內(nèi)存的大小可以在JVM(Java虛擬機)啟動時設(shè)置,并且可以在運行時通過JVM的參數(shù)進行調(diào)整。堆內(nèi)存的大小對應(yīng)用程序的性能有很大影響,過小的堆內(nèi)存可能導(dǎo)致頻繁的垃圾回收,而過大的堆內(nèi)存則可能增加垃圾回收的耗時。
- 線程共享:堆內(nèi)存是線程共享的,這意味著多個線程可以訪問堆內(nèi)存中的對象。因此,在訪問堆內(nèi)存中的對象時,需要適當?shù)耐綑C制來避免并發(fā)問題。
棧內(nèi)存
- 存儲內(nèi)容:棧內(nèi)存主要用于存儲局部變量和方法調(diào)用的上下文信息(如方法調(diào)用時的參數(shù)、返回值地址、局部變量表等)。每當線程執(zhí)行一個方法時,就會在這個線程的棧內(nèi)存中創(chuàng)建一個棧幀(Stack Frame),用于存儲該方法調(diào)用的相關(guān)信息。
- 生命周期:棧內(nèi)存中的棧幀隨著方法的執(zhí)行而創(chuàng)建,隨著方法的結(jié)束而銷毀。棧幀的生命周期通常與方法的執(zhí)行周期相同,因此棧內(nèi)存中的變量和方法調(diào)用的上下文信息也是短暫的。
- 大小管理:棧內(nèi)存的大小在JVM啟動時就已經(jīng)確定,并且通常不允許在運行時動態(tài)調(diào)整。棧內(nèi)存的大小限制了對遞歸調(diào)用的深度,因為過深的遞歸調(diào)用會耗盡棧內(nèi)存空間,導(dǎo)致StackOverflowError錯誤。
- 線程隔離:棧內(nèi)存是線程隔離的,每個線程都有自己獨立的棧內(nèi)存空間。這意味著線程之間不會相互影響,各自維護自己的局部變量和方法調(diào)用的上下文信息。
總結(jié)來說,堆內(nèi)存和棧內(nèi)存是Java內(nèi)存管理中兩個重要的部分,它們各自承擔著不同的角色和用途。堆內(nèi)存主要用于存儲對象實例和數(shù)組,是線程共享的;而棧內(nèi)存則主要用于存儲局部變量和方法調(diào)用的上下文信息,是線程隔離的。
垃圾收集機制
1. 垃圾收集的對象
- JVM中的垃圾收集主要針對堆(Heap)內(nèi)存中的對象。
- 堆內(nèi)存是JVM所管理的最大一塊內(nèi)存區(qū)域,它用于存放對象實例。
- 當對象不再被任何引用所指向時,這些對象就變成了垃圾收集的目標。
2. 垃圾收集算法
JVM中常用的垃圾收集算法包括以下幾種:
- 標記-清除(Mark-Sweep):首先標記出所有需要回收的對象,然后統(tǒng)一回收這些被標記的對象。該算法的缺點是效率不高,且標記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片。
- 復(fù)制(Copying):將內(nèi)存分為大小相等的兩塊,每次只使用其中一塊。當這塊內(nèi)存快滿時,就將還存活的對象復(fù)制到另一塊上,然后清理掉已使用的內(nèi)存空間。這種算法適用于對象存活率不高的場景,但會浪費一半的內(nèi)存空間。
- 標記-整理(Mark-Compact):標記出所有需要回收的對象,然后將存活的對象都向內(nèi)存的一端移動,最后清理掉邊界以外的內(nèi)存。該算法可以有效避免內(nèi)存碎片的產(chǎn)生。
- 分代收集(Generational Collection):根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊,如新生代和老年代。新生代主要采用復(fù)制算法,因為新生代中對象的存活率較低;老年代則主要采用標記-整理算法,因為老年代中對象的存活率較高。
3. 垃圾收集器
JVM提供了多種垃圾收集器,每種收集器都有其特點和適用場景。常見的垃圾收集器包括:
- Serial GC:單線程執(zhí)行垃圾收集,適用于單核CPU和小型應(yīng)用。
- Parallel GC:多線程執(zhí)行垃圾收集,適用于多核CPU和需要高吞吐量的應(yīng)用。
- CMS(Concurrent Mark Sweep)GC:以獲取最短回收停頓時間為目標的收集器,適用于對停頓時間要求較高的應(yīng)用。
- G1(Garbage-First)GC:面向服務(wù)端應(yīng)用的垃圾收集器,基于“標記-整理”算法實現(xiàn),能夠預(yù)測停頓時間,適用于堆內(nèi)存較大的應(yīng)用。
常見的垃圾收集器及其特點:
1. Serial 垃圾收集器
特點:Serial收集器是一個單線程的收集器,使用復(fù)制算法。它在進行垃圾收集時,會暫停其他所有的工作線程(即“Stop The World”)。盡管這會導(dǎo)致應(yīng)用程序的短暫停頓,但在單核處理器或小型應(yīng)用中,由于其簡單性,它可能是一個高效的選擇。
適用場景:主要適用于Client模式下的虛擬機,或單核服務(wù)器環(huán)境。
2. ParNew 垃圾收集器
特點:ParNew收集器是Serial收集器的多線程版本,使用多線程來執(zhí)行垃圾收集,其余行為(包括控制參數(shù)、收集算法等)與Serial收集器相同。在多核CPU上,ParNew的回收效率通常高于Serial收集器。
適用場景:許多運行在Server模式下的虛擬機首選ParNew作為新生代收集器,特別是當需要與CMS收集器配合使用時。
3. Parallel Scavenge 垃圾收集器
特點:Parallel Scavenge收集器是一個注重吞吐量的新生代收集器,使用復(fù)制算法和并行多線程收集。它提供了兩個參數(shù)用于精確控制吞吐量:-XX:MaxGCPauseMillis(控制最大垃圾收集停頓時間)和-XX:GCTimeRatio(設(shè)置吞吐量大?。4送?,它還支持自適應(yīng)的GC調(diào)節(jié)策略。
適用場景:適用于后臺運算而不需要太多交互的任務(wù),追求高吞吐量和高效利用CPU資源。
4. Serial Old 垃圾收集器
特點:Serial Old是Serial收集器的老年代版本,同樣使用單線程和“標記-整理”算法進行垃圾收集。它是運行在Client模式下的Java虛擬機默認的老年代垃圾收集器。
適用場景:主要用于Client模式或單核服務(wù)器環(huán)境,以及作為CMS收集器失敗時的后備方案。
5. Parallel Old 垃圾收集器
特點:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。它可以在老年代提供與新生代相同的吞吐量優(yōu)先的垃圾收集。
適用場景:適用于對吞吐量要求較高的系統(tǒng),特別是當新生代使用Parallel Scavenge收集器時,可以與之搭配使用以保證整體的吞吐量。
6. CMS(Concurrent Mark Sweep)垃圾收集器
特點:CMS收集器是一種以獲取最短回收停頓時間為目標的并發(fā)收集器,使用多線程和“標記-清除”算法。它的收集過程分為初始標記、并發(fā)標記、重新標記和并發(fā)清除四個階段,其中并發(fā)標記和并發(fā)清除階段可以與用戶線程并發(fā)執(zhí)行。
適用場景:適用于對停頓時間要求較高的應(yīng)用場景,如Web服務(wù)器和B/S架構(gòu)的應(yīng)用。但需要注意的是,CMS對CPU資源敏感,且無法處理浮動垃圾,可能會出現(xiàn)“Concurrent Mode Failure”而導(dǎo)致Full GC。
7. G1(Garbage-First)垃圾收集器
特點:G1收集器在JDK 1.9之后成為默認的垃圾收集器。它兼顧響應(yīng)時間和吞吐量,將堆內(nèi)存劃分為多個區(qū)域,每個區(qū)域都可以充當新生代、老年代或存放大對象。G1收集器采用標記復(fù)制算法進行垃圾收集,并支持并發(fā)標記和混合收集。
適用場景:適用于大多數(shù)應(yīng)用場景,特別是需要同時兼顧響應(yīng)時間和吞吐量的場合。
4. 觸發(fā)條件
垃圾收集的觸發(fā)條件通常包括:
- 內(nèi)存不足:當堆內(nèi)存中的可用空間不足以滿足新對象的分配時,會觸發(fā)垃圾收集。
- 顯式調(diào)用:雖然Java不鼓勵程序員手動釋放內(nèi)存,但可以通過System.gc()方法建議JVM執(zhí)行垃圾收集,但JVM是否執(zhí)行并不保證。
5. 注意事項
- 垃圾收集并不能保證立即回收所有不再使用的對象,因為JVM的垃圾收集器是按需運行的。
- 過度依賴垃圾收集器可能會掩蓋內(nèi)存泄露等問題,因此程序員仍需注意代碼的內(nèi)存使用情況。
Java垃圾收集器主要工作原理
- 標記-清除算法:從根對象開始,遞歸地訪問所有引用對象并標記為“存活”。之后,遍歷堆內(nèi)存,回收未被標記的對象。
- 分代收集:根據(jù)對象生命周期,將堆內(nèi)存劃分為不同區(qū)域(代),如年輕代和老年代,并采用不同策略進行垃圾回收,以提高效率。
- 復(fù)制算法:將內(nèi)存分為兩個等大區(qū)域,每次只使用一個。當內(nèi)存滿時,將存活對象復(fù)制到另一區(qū)域,并清空當前區(qū)域。此算法常用于年輕代。
- 垃圾回收器 : 如Serial垃圾收集器, 使用單線程進行垃圾回收, 回收時會暫停所有應(yīng)用線程。
內(nèi)存碎片是什么?
內(nèi)存碎片指的是在堆(Heap)內(nèi)存中被分配和回收對象后,留下的不連續(xù)、無法被有效利用的內(nèi)存空間。
這些碎片空間雖然存在,但由于其大小或位置的原因,無法被用來存儲新的對象,從而導(dǎo)致內(nèi)存的浪費。
內(nèi)存碎片的分類
內(nèi)存碎片主要分為兩種:
內(nèi)部碎片(Internal Fragmentation):
- 當一個對象被分配的內(nèi)存空間大于其實際需要時,多余的空間即為內(nèi)部碎片。
- 在Java中,由于JVM通常使用某種形式的對象對齊(Object Alignment),以簡化內(nèi)存訪問和提高性能,這可能導(dǎo)致對象占用的內(nèi)存空間略大于其實際所需。
外部碎片(External Fragmentation):
- 外部碎片是指堆內(nèi)存中已經(jīng)被分配出去但當前未被使用的內(nèi)存塊之間的空隙。
- 這些空隙可能是由于多次的分配和回收操作造成的,雖然它們總的大小可能足夠用來存儲新的對象,但由于它們是不連續(xù)的,因此無法被有效利用。
垃圾收集與內(nèi)存碎片
在垃圾收集過程中,如果JVM不采取適當?shù)拇胧﹣砉芾韮?nèi)存碎片,那么隨著時間的推移,堆內(nèi)存中的碎片可能會越來越多,導(dǎo)致可用的連續(xù)內(nèi)存空間減少,從而影響新對象的分配效率。
為了解決這個問題,一些JVM實現(xiàn)采用了壓縮(Compacting)技術(shù)。在壓縮過程中,JVM會將所有的活動對象(即仍然被引用的對象)移動到堆的一端,從而消除外部碎片,并在堆的另一端形成一個連續(xù)的空閑內(nèi)存區(qū)。這樣,新的對象就可以被快速且連續(xù)地分配到這個空閑區(qū)域中,從而提高內(nèi)存的使用效率和分配速度。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于MyBatis通用Mapper@Table注解使用的注意點
這篇文章主要介紹了關(guān)于MyBatis通用Mapper@Table注解使用的注意點,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
idea項目結(jié)構(gòu)中不顯示out文件夾的解決
本文通過圖片的方式詳細解釋操作步驟,使讀者能夠更直觀更方便地理解和執(zhí)行操作,同時,文章末尾祝福讀者步步高升,一帆風順,展現(xiàn)了作者的人情味和親和力,整體來說,這是一篇簡單易懂、實用性強的操作指南2024-10-10
Java中你絕對沒用過的一個關(guān)鍵字Record的使用
這篇文章主要給大家介紹一個?Java?中的一個關(guān)鍵字?Record,那?Record?關(guān)鍵字跟不可變類有什么關(guān)系呢?看完今天的文章你就知道了,快跟隨小編一起學(xué)習一下吧2022-11-11

