1. 了解下 Java 中內存區(qū)域的劃分
Java 虛擬機在執(zhí)行 Java 程序的過程中,會把它所管理的內存劃分為若干個不同的數據區(qū)域。如圖所示:

-
程序計數器
程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看作是當前線程所執(zhí)行的字節(jié)碼的信號指示器。
每條線程都需要一個獨立的程序計數器,是為了線程切換后能恢復到正確的位置。
此內存區(qū)域是唯一一個在 Java 虛擬機規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。
-
Java 虛擬機棧
Java 虛擬機棧是線程私有的,生命周期與線程相同。虛擬棧描述的是 Java 方法執(zhí)行的內存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀,用于存儲局部變量表、操作數棧、動態(tài)鏈接、方法出口等信息。
每一個方法從調用直至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
局部變量表存放了編譯期可知的各種基本數據類型、對象引用和 returnAddress 類型。
局部變量表所需的內存空間在編譯期間完成分配,在方法運行期間不會改變局部變量表的的大小。
-
本地方法棧
本地方法棧為虛擬機使用到的 Native 方法服務。
-
Java 堆
Java 堆是被所有線程共享的一塊內存區(qū)域,在虛擬機啟動時創(chuàng)建。此內存區(qū)域的唯一目的就是存放對象實例。
Java 堆是垃圾收集器管理的主要區(qū)域。
-
方法區(qū)
方法區(qū)是各個線程共享的內存區(qū)域,用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數據。
內存回收目標主要是針對常量池的回收和對類型的卸載
-
運行時常量池
運行時常量池是方法區(qū)的一部分,用于存放編譯期生成的各種字面量和符號引用。
程序計數器、虛擬機棧、本地方法棧 3 個區(qū)域隨線程而生,隨線程而滅。
棧中的棧幀隨著方法的進入和退出而有條不紊的執(zhí)行著出棧和入棧操作。每一個棧幀中分配多少內存是在類結構確定下來時就已知的,這幾個區(qū)域的內存分配和回收多具備確定性。這幾個區(qū)域不需要過多考慮回收的問題,方法結束或者線程結束時。內存就自然跟隨著回收了。
Java 堆和方法區(qū),只有在程序運行期間才能知道會創(chuàng)建哪些對象,內存的分配和回收都是動態(tài)的。
2. JVM 在進行垃圾回收之前,需要判斷哪些對象是需要回收的
-
引用計數算法
給對象中添加一個的引用計數器, 每當有一個地方 引用它時, 計數器值就加 1; 當引用失效時, 計數器值就減 1; 任何時刻計數器為 0 的 對象 就是不可能再被使用的。
public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024*1024; private byte[] bigSize = new byte[2 * _1MB]; public static void main(String[] args) { testGc(); } private static void testGc() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objA.instance = objA; objA = null; objB = null; System.gc(); } }弊端:很難解決對象之間相互循環(huán)引用的問題。
-
可達性分析算法
通過一系列的稱為 GC Roots 的對象作為起始點,從這些節(jié)點開始向下搜索。搜索所有走過的路徑稱為引用鏈,當一個對象到 GC Roots 對象沒有任何引用鏈相連,則證明此對象是不可用的。
可達性分析算法判定對象是否可回收.png可作為 GC Roots 的對象包括下面幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象
方法區(qū)中類靜態(tài)屬性引用的對象
方法區(qū)中常量引用的對象
本地方法中 JNI 引用的對象
回收方法區(qū)
方法區(qū)(永久代)的垃圾收集主要回收兩部分:廢棄常量和無用的類。
無用的類判定條件:
該類所有的實例都已被回收。
加載該類的 ClassLoader 被回收。
該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
3. 通過垃圾收集算法進行垃圾回收
-
標記清除算法
首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象。
標記清除算法不足:
-
效率問題
標記和清除兩個過程的效率都不高。
-
空間問題
標記清除之后會產生大量不連續(xù)的內存碎片, 空間碎片太多可能會導致以后在程序運行 過程中 需要分配較大對象時,無法找到足夠的連續(xù)內存 而不得不提前觸發(fā)另一次垃圾收集動作。
標記-清除算法示意圖.png -
-
復制算法
將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還活著的對象復制到另一塊上面,然后再把已使用的內存空間一次清理掉。
復制算法示意圖.png -
標記整理算法
標記過程仍然與標記清除算法一樣,但是后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
標記整理算法示意圖.png -
分代收集算法
根據對象生活周期的不同將內存劃分為幾塊,一般是把 Java 堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的算法。
在新生代中,每次垃圾收集時都發(fā)現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。
在老年代中,因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記清理或標記整理算法來 實現。
參考資料「 深入理解Java虛擬機:JVM高級特性與最佳實踐(第2版)」



