文章目錄
- 一、概述
- 二、GC Root物件
- 三、使用場景
- 1、虛擬機堆疊中區域變數表參考的物件
- 2、方法區中類靜態屬性參考的物件
- 3、在方法區中常量參考的物件
- 4、所有被同步鎖(synchronized關鍵字)持有的物件
一、概述
可達性分析演算法,是Java虛擬機判定物件是否可回收的常用演算法,它的思路是通過一系列稱為“GC Roots”的根物件作為起始節點集,從這些節點開始,根據參考關系向下搜索,搜索程序所走過的路徑稱為“參考鏈”(Reference Chain),如果某個物件到GC Roots間沒有任何參考鏈相連,或者用圖論的話來說就是從GC Roots到這個物件不可達時,則證明此物件是不可能再被使用的,如圖所示:

圖中藍色,代表以GC Root為根節點,路徑是可達的,所以在判定時,認為藍色是不可回收物件,圖中橙色,雖然物件之間有參考,但是沒有GC Roots集合中的物件關聯,所以在判定時會認為橙色為可回收物件,
二、GC Root物件
可作為GC Roots的物件包括以下幾種:
1、在虛擬機堆疊(堆疊幀中的本地變數表)中參考的物件,譬如各個執行緒被呼叫的方法堆疊中使用到的引數、區域變數、臨時變數等,
2、在方法區中類靜態屬性參考的物件,譬如Java類的參考型別靜態變數,
3、在方法區中常量參考的物件,譬如字串常量池(String Table)里的參考,
4、所有被同步鎖(synchronized關鍵字)持有的物件,
5、在本地方法堆疊中JNI(即通常所說的Native方法)參考的物件,
6、Java虛擬機內部的參考,如基本資料型別對應的Class物件,一些常駐的例外物件(比如NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器,
7、反映Java虛擬機內部情況的JMXBean、JVMTI中注冊的回呼、本地代碼快取等,
在代碼層面,我們主要關心1、2、3、4點,下面我們來看看具體場景,
三、使用場景
1、虛擬機堆疊中區域變數表參考的物件
方法的執行實際上是一個堆疊幀在虛擬機堆疊中一次進堆疊和出堆疊的程序,堆疊幀的其中一個結構就是區域變數表,
區域變數表是一組變數值存盤空間,用于存放方法引數和方法內部定義的區域變數,在Java程式被編譯成Class檔案時,就在方法的Code屬性的max_locals資料項中確定了該方法所需要分配的最大區域變數表的容量,區域變數表的容量以變數槽(Slot)為最小單位,32位虛擬機中一個Slot可以存放一個32位以內的資料型別(boolean、byte、char、short、int、float、reference和returnAddress八種),注意:區域變數表并不是根據方法中定義的變數的數量來決定其最大容量的,它允許變數槽重用,例如以下代碼區域變數表的最大容量是1,而不是2.
public static void test() {
try {
int i = 0;
} catch (Exception e) {
e.printStackTrace();
}
int j = 1;
}
OK,在大致了解了區域變數表的作用后,我們來看一段具體代碼,首先貼出在運行程式時,需要設定的虛擬機引數:
-Xmx20m -Xms20m -XX:+PrintGCDetails
該引數的含義為設定堆的最大、最小容量,同時輸出GC的詳細資訊,
場景一:
public class Test02 {
public static void main(String[] args) {
byte[] b1 = new byte[1024*1024*5];
//提醒虛擬機需要進行一次垃圾回收,注意,只是提醒,并不是立刻強制執行
System.gc();
}
}
得到的垃圾回收日志如下:

本次創建的物件大小為5M,按照分配策略,大物件是分配到老年代的,此時從圖中可以看出老年代中還有約5M大小的記憶體未被釋放,我們結合位元組碼來講述這一現象,位元組碼如圖所示:

newarray指令是在隊中創建一個大小為5242880大小的陣列,然后astore_1是將該陣列的參考保存在main方法堆疊幀的區域變數表1號變數槽的位置,所以呀,此時區域變數表中包含了該陣列物件的參考,以至于在進行垃圾回收時,會判定該物件時不可被回收物件,
場景二,代碼如下:
public class Test02 {
public static void main(String[] args) {
try{
byte[] b1 = new byte[1024*1024*5];
}catch (Exception e){
}
int i = 0;
System.gc();
}
}
得到的GC日志如下:

從圖中可以發現,在老年代中已經將陣列所占用空間回收了,這是因為b1的作用域僅僅只是在try陳述句塊中,上述我們說過,區域變數表槽會重用,所以呀,b1的變數槽,在執行到int i = 0這一行代碼時,已經被i變數用了,區域變數表中自然也不再包含該物件的參考,自然該陣列物件就被回收了呀,如圖所示:

針對場景二,再來看看場景三,
場景三,代碼如下:
public class Test02 {
public static void main(String[] args) {
try{
byte[] b1 = new byte[1024*1024*5];
}catch (Exception e){
}
System.gc();
}
}
得到的GC日志如圖所示:

從圖中可以看到,該陣列并沒有被回收,這也印證了咱們對場景二的分析,因為在定義該變數之后,就沒有再對區域變數表進行存取了,該陣列的參考也一直保存在區域變數表中,以至于GC時不能將該陣列所占孔吉安回收掉,所以呀,我們在程式開發程序中,一定要注意,不要定義無用的變數,這樣會白白耗費記憶體資源,怎么樣,有趣吧,
2、方法區中類靜態屬性參考的物件
public class Test02 {
public static void main(String[] args) {
try{
byte[] b1 = new byte[1024*1024*5];
Person.bytes = b1;
}catch (Exception e){
}
int i = 0;
System.gc();
}
}
class Person{
public static byte[] bytes;
}
GC日志如圖所示:

按照場景二的描述,該陣列所占空間應該被回收,但是我們將其賦值給Person類的靜態屬性,以至于在GC時,它不會被回收,
寫到此處,筆記本沒電了,換了一臺電腦繼續寫,下文使用eclipse,
3、在方法區中常量參考的物件
代碼如下:
public class Test03 {
private static final byte[] b1 = new byte[5*1024*1024];
public static void main(String[] args) {
Test03 test03 = new Test03();
test03 = null;
System.gc();
}
}
得到的GC日志如圖所示:

此處,博主也有一個一個,我一直嘗試去證實這一點:“譬如字串常量池(String Table)里的參考”,但是沒法復現,如果哪位小伙伴寫出了代碼,可評論區留言哦,
4、所有被同步鎖(synchronized關鍵字)持有的物件
代碼如下:
public class Test03 {
public static void main(String[] args) {
try {
byte[] bytes = new byte[5 * 1024 * 1024];
Target target = new Target(bytes);
new Thread(target).start();
} catch (Exception e) {
}
int i = 0;
//此處是為了讓主執行緒等待Target開始執行,等待將b1置為null操作,
try {
Thread.sleep(500);
} catch (Exception e) {
}
System.gc();
}
}
class Target implements Runnable {
private byte[] b1;
public Target(byte[] b) {
this.b1 = b;
}
@Override
public void run() {
synchronized (b1) {
b1 = null;
try {
//此處休眠是為了讓Target任務停留在synchronized代碼塊中
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
得到的GC日志如下:

我們可以看到,在同步代碼塊內,我們將b1置為null,但是該陣列所占空間并沒有被回收掉,這是因為該物件被synchronized持有,為了印證該結論,我們將代碼簡單變化,如下:
public class Test03 {
public static void main(String[] args) {
try {
byte[] bytes = new byte[5 * 1024 * 1024];
Target target = new Target(bytes);
new Thread(target).start();
} catch (Exception e) {
}
int i = 0;
try {
Thread.sleep(500);
} catch (Exception e) {
}
System.gc();
}
}
class Target implements Runnable {
private byte[] b1;
public Target(byte[] b) {
this.b1 = b;
}
@Override
public void run() {
b1 = null;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
得到的GC日志如下:

可以看到,該陣列物件所占空間已經被回收了,
從代碼層面,大致的場景就是這些,在實際開發中定位到記憶體問題,大家可以嘗試從這些方面去尋找原因,好啦,本文就講到這兒,如果這篇文章幫助到您,請點贊支持,謝謝大家,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/253470.html
標籤:java
