????垃圾回收(Garbage Collection, 簡(jiǎn)寫為GC) , Java開發(fā)者不需要手動(dòng)釋放對(duì)象的內(nèi)存,JVM中的垃圾回收器(Garbage Collection)會(huì)自動(dòng)回收。
????代價(jià): 這種自動(dòng)化機(jī)制一旦出錯(cuò),開發(fā)者就不得不去深入理解GC回收機(jī)制,甚至需要對(duì)這些“自動(dòng)化”的技術(shù)實(shí)施必要的監(jiān)控和調(diào)節(jié)。
????Java運(yùn)行時(shí)區(qū)域中的:程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧 這3個(gè)區(qū)域與線程生命周期同步,棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作,這幾個(gè)區(qū)域不需要過多考慮回收的問題。
????而堆和方法區(qū)則不一樣,一個(gè)接口中的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運(yùn)行期間才能知道會(huì)創(chuàng)建那些對(duì)象,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,垃圾收集器所關(guān)注的就是這部分內(nèi)存。
什么是垃圾
????所謂垃圾就是內(nèi)存中已經(jīng)沒有用的對(duì)象。Java虛擬機(jī)中使用一個(gè)叫作“可達(dá)性分析”的算法來決定對(duì)象是否可以被回收。
可達(dá)性分析
????JVM把內(nèi)存中所有對(duì)象之間的引用關(guān)系看作一張圖,通過一組名為“GC Root”的對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈,最后通過判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá)來決定對(duì)象是否可以被回收。

如圖,對(duì)象A/B/C/DE與GC Root之間都存在一條直接或間接的引用鏈,這表明它們與GC Root之間是可達(dá)的,因此它們不能被GC回收。而對(duì)象M和J雖然被對(duì)象J引用,但并不存在一條引用鏈連與GC Root連接,所以當(dāng)GC進(jìn)行垃圾回收時(shí),只要遍歷到J/K/M這3個(gè)對(duì)象就會(huì)將它們回收。
????注意:上圖代表的是些對(duì)象在內(nèi)存中的引用。包括GC Root也是一組引用而并非對(duì)象。
GC Root對(duì)象
????在Java中,有以下幾種對(duì)象可以作為GC Root:
- Java虛擬機(jī)(局部變量表)中引用的對(duì)象
- 方法區(qū)中靜態(tài)引用指向的對(duì)象
- 仍處于存活狀態(tài)中的線程對(duì)象
- Native方法中JNI引用的對(duì)象
什么時(shí)候回收
不同的虛擬機(jī)有著不同的GC實(shí)現(xiàn)機(jī)制,但是一般情況下每一種GC實(shí)現(xiàn)都會(huì)在以下兩種情況下觸發(fā)垃圾回收。
- Allocation Failure: 在堆內(nèi)存中分配時(shí),如果因?yàn)榭捎檬S嗫臻g不足導(dǎo)致對(duì)象內(nèi)存分配失敗。
- System.gc(): 在應(yīng)用層,Java開發(fā)工程師可以主動(dòng)調(diào)用此API來請(qǐng)求一次GC。
代碼驗(yàn)證GC Root的幾種情況
Java命令參數(shù)
????-Xms 初始分配JVM運(yùn)行時(shí)內(nèi)存大小;默認(rèn)為物理內(nèi)存的1/64。
#分配200M內(nèi)存空間給JVM
java -Xms200m HelloWorld
驗(yàn)證虛擬機(jī)棧(局部變量)中引用的對(duì)象
public class GCRootLocalVariable{
private int _10MB = 10 * 1024 * 1024;
private byte[] memroy = new byte[8 * _10MB];
public static void main(String[] args) {
System.out.println("開始時(shí):");
printMemory();
method();
System.gc();
System.out.println("第二次GC完成");
printMemory();
}
public static void method() {
GCRootLocalVariable g = new GCRootLocalVariable();
System.gc();
System.out.println("第一次GC完成");
printMemory();
}
/**
* 打印出當(dāng)前JVM剩余空間和總的空間大小
*/
public static void printMemory() {
System.out.print("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + " M, ");
System.out.print("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + " M, ");
}
}
????Log:
開始時(shí)
free is 242 M, total is 245 M,
第一次GC完成
free is 163 M, total is 245 M,
第二次GC完成
free is 243 M, total is 245 M,
- 第一次GC時(shí),G作為局部變量,引用了new出的對(duì)象,且它被視為GC Roots,所以在GC后不會(huì)被回收
- 第二次GC時(shí),Method()方法執(zhí)行完后,局部變量g跟隨方法消失,不再有引用指向該對(duì)象,所以第二次GC后此對(duì)象也會(huì)被回收。
????注意:上面日志包括后面的實(shí)例中,因?yàn)橛兄虚g變量,所以會(huì)有1M左右的誤差。
靜態(tài)變量引用的對(duì)象
public class GCRootStaticVariable{
private static int _10MB = 10 * 1024 * 1024;
private byte[] memroy;
private static GCRootStaticVariable staticVariable;
public GCRootStaticVariable(int size) {
memory = new byte[size];
}
public static void main(String[] args) {
System.out.println("程序開始:");
printMemory();
GCRootStaticVariable g = new GCRootStaticVariable(4 * _10MB);
g.staticVariable = new GCRootStaticVariable(8 * _10MB);
g = null;
System.gc();
System.out.println("GC完成");
printMemory();
}
/**
* 打印出當(dāng)前JVM剩余空間和總的空間大小
*/
public static void printMemory() {
System.out.print("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + " M, ");
System.out.print("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + " M, ");
}
}
????Log:
程序開始:
free is 242 M, total is 245 M,
GC完成
free is 163 M, total is 245 M,
????當(dāng)調(diào)用GC時(shí),只有g對(duì)象的40M被GC回收掉,而靜態(tài)變量staticVariable作為GC Root,它引用的80M并不會(huì)被回收。
活躍線程作為GC Root
public class GCRootThread{
private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];
public static void main(String[] args) throw Exception {
System.out.println("開始前內(nèi)存情況:");
printMemory();
AsyncTasky at = new AsyncTasky(new GCRootThread());
Thread thread = new Thread(at);
thread.start();
System.gc();
System.out.println("main方法執(zhí)行完畢,完成GC");
printMemory();
thread.join();
at = null;
System.gc();
System.out.println("線程代碼執(zhí)行完畢,完成GC");
printMemory();
}
/**
* 打印出當(dāng)前JVM剩余空間和總的空間大小
*/
public static void printMemory() {
System.out.print("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + " M, ");
System.out.print("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + " M, ");
}
private static class AsyncTask implements Runnable {
private GCRootThread gcRootThread;
public AsyncTask(GCRootThread gcRootThread) {
this.gcRootThread = gcRootThread;
}
@Override
public void run() {
try {
Thread.sleep(500);
} catch(Exception e) {}
}
}
}
?????Log:
開始前內(nèi)存情況:
free is 242 M, total is 245 M,
main方法執(zhí)行完畢,完成GC
free is 163 M, total is 245 M,
線程代碼執(zhí)行完畢,完成GC
free is 243 M, total is 245 M,
????當(dāng)?shù)谝淮握{(diào)用GC時(shí),線程沒有結(jié)束它被作為GC Root,所以它所引用的對(duì)象gcRootThread不會(huì)被GC回收;當(dāng)?shù)诙螆?zhí)行GC時(shí),線程已執(zhí)行完畢并被置為null,這時(shí)線程已經(jīng)被銷毀,所以它引用的對(duì)象gcRootThread也會(huì)被GC回收。
全局變量不能作為GC Root
public class GCRootClassVariable {
private static int _10MB = 10 * 1024 * 1024;
private byte[] memory;
private GCRootClassVariable classVariable;
public GCRootClassVariable(int size) {
memory = new byte[size];
}
public static void main(String[] args) {
System.out.println("程序開始:");
printMemory();
GCRootClassVariable g = new GCRootClassVariable(4 * _10MB);
g.classVariable = new GCRootClassVariable(8 * _10MB);
g = null;
System.gc();
System.out.println("GC完成");
printMemory();
}
/**
* 打印出當(dāng)前JVM剩余空間和總的空間大小
*/
public static void printMemory() {
System.out.print("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + " M, ");
System.out.print("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + " M, ");
}
}
????log:
程序開始:
free is 242 M, total is 245 M,
GC完成
free is 243 M, total is 245 M,
????從Log中可以看出,當(dāng)g被置空后,全局變量classVariable也再被GC Root所引用。所以當(dāng)調(diào)用GC時(shí),對(duì)象g和對(duì)象classVariable都會(huì)被回收。因此全局變量與靜態(tài)變量不同,它不會(huì)被當(dāng)作GC Root。
如何回收垃圾
????幾種垃圾收集算法的思想以及優(yōu)缺點(diǎn)。
標(biāo)記清除算法(Mark and Sweep GC)
????從“GC Roots”集合開始,將內(nèi)存整個(gè)遍歷一次,保留所有可以被GC Roots直接或間接引用到的對(duì)象,而剩下的對(duì)象都當(dāng)作垃圾被回收。
- Mark標(biāo)記階段:找到內(nèi)存中所有GC Root對(duì)象,只要和GC Root對(duì)象直接或間接相連則標(biāo)記為灰色(也就是存活對(duì)象),否則標(biāo)記為黑(也就是垃圾對(duì)象)。
- Sweep清除標(biāo)記階段:將標(biāo)記為垃圾的對(duì)象直接清除。

- 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,不需要移動(dòng)對(duì)象;
- 缺點(diǎn):需要中斷進(jìn)程內(nèi)其他執(zhí)行的組件,并可能產(chǎn)生內(nèi)存碎片,回收頻率高
復(fù)制算法(Copying)
????將現(xiàn)有內(nèi)存空間分為兩塊,每次只使用其中一塊,在垃圾回收時(shí)將正在使用的內(nèi)存中的存活對(duì)象復(fù)制到未被使用的內(nèi)存塊中。之后,清除正在使用的內(nèi)存塊中的所有對(duì)象,交換兩個(gè)內(nèi)存的角色,完成垃圾回收。


- 優(yōu)點(diǎn): 按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單、運(yùn)行高效,不用考慮內(nèi)存碎片
- 缺點(diǎn):可用的內(nèi)存大小為原來的一半,對(duì)象存活率高時(shí)會(huì)頻繁進(jìn)行復(fù)制。
標(biāo)記-壓縮算法(Mark-Compact)
????需要先從根節(jié)點(diǎn)對(duì)所有可達(dá)對(duì)象做一次標(biāo)記,之后將所有存活對(duì)象壓縮到內(nèi)存一端。最后,清理邊界外所有的空間。
- Mark標(biāo)記階段:找到內(nèi)存中的所有GC Root對(duì)象,只要是可達(dá)對(duì)象則標(biāo)記為灰色,否則標(biāo)記為黑色。
- Compact壓縮階段:將剩余存活對(duì)象按順序壓縮到內(nèi)存某一端。

- 優(yōu)點(diǎn):這種方法既避免了碎片的產(chǎn)生,又不需要兩塊相同的內(nèi)存空間。性價(jià)比比較高。
- 缺點(diǎn):所謂壓縮操作,仍需要進(jìn)行局部對(duì)象移動(dòng),所以一定程度上還是降低了效率。
JVM分代回收策略
????Java虛擬機(jī)對(duì)象存活的周期不同,把堆內(nèi)存劃分為幾塊:新生代、老年代。這就是JVM的內(nèi)存分代策略。注意:在HosPot中除了新生代和老年代,還有永久代。
????中心思想:
對(duì)于新創(chuàng)建的對(duì)象會(huì)在新生代中分配內(nèi)存,此區(qū)域的對(duì)象生命周期一般比較短。如果經(jīng)過多次回收仍然存活下來,則將它轉(zhuǎn)移到老年代中。
新生代(Young Generation)
????新生成的對(duì)象優(yōu)先存放在新生代中,在新生代中常規(guī)應(yīng)用進(jìn)行一次垃圾回收一般可以回收70%~95%的內(nèi)存空間,回收效率高。因?yàn)橐M(jìn)行一些復(fù)制操作,一般采用GC回收算法的復(fù)制算法。
????新生代又細(xì)分為3部分:Eden、Survivor0(簡(jiǎn)稱S0)、Survivor1(簡(jiǎn)稱S1)。這3部分按照8:1:1比例來劃分新生代。

當(dāng)Eden區(qū)第一次滿時(shí)進(jìn)行垃圾回收:清除垃圾對(duì)象,并將存活對(duì)象復(fù)制到S0

Eden區(qū)再滿,再進(jìn)行垃圾回收:Eden和S0區(qū)中的所有垃圾被清除,復(fù)制S0存活對(duì)到S1

如此反復(fù)多次(默認(rèn)15次)之后,如果還有存活對(duì)象,說明這些對(duì)象生命周期較長(zhǎng),移至老年代。

老年代(Old Generation)
????老年代的內(nèi)存一般比新生代大,能存放更多對(duì)象。如果對(duì)象比較大,并且新生代的剩余空間不足,則這個(gè)大對(duì)象直接被分配到老年代上。
????-XX:PretenureSizeThreshold設(shè)置老年代內(nèi)存大小。老年代中的對(duì)象生命周期較長(zhǎng),不需要過多的復(fù)制操作,所以一般采用標(biāo)記壓縮算法
注意:老年代中的對(duì)象有時(shí)會(huì)引用到新生代中的對(duì)象。若執(zhí)行GC可能需要查詢整個(gè)老年代的對(duì)象引用情況,大大降低了效率。所以老年代中維護(hù)了一個(gè)
512byte的card table,所有引用新生代對(duì)象的信息記錄在這里。每次新生代發(fā)生GC時(shí),只需要檢查這個(gè)card table即可。大提高了性能。
GC Log分析
????新生代和老年代打印日志的區(qū)別:
- 新生代GC:稱Minor GC,非常頻繁,回收速度快
- 老年代GC:稱Major GC或Full GC,Major GC執(zhí)行時(shí)至少會(huì)執(zhí)行一次Minor GC。
注意:在有些虛擬機(jī)實(shí)現(xiàn)中,Major GC和Full GC存在著區(qū)別。Major GC只是代表回收老年代的內(nèi)存,而Full GC則代表回收整個(gè)堆中的內(nèi)存,即:新生代+老年代。
案例分析
Java命令參數(shù)

代碼演示:
/**
* VM args: -Xms20M -Xmx20M -Xmn10M -XX:+printGCDetails
* -XX:SurvivorRatio=8
*/
public class MinorGCTest{
private static final int _1MB = 1024 * 1024;
public static void testAllocation() {
byte[] a1, a2, a3, a4;
a1 = new byte[2 * _1MB];
a2 = new byte[2 * _1MB];
a3 = new byte[2 * _1MB];
a4 = new byte[1 * _1MB];
}
public static void main(String[] agrs) {
testAllocation();
}
}
執(zhí)行代碼打印日志如下:
Heap
PSYoungGen total9216K,used8003K[0x00000007bf600000,0x00000007c0000000,0x00000007c0000000)
eden space 8192K, 97% used [0x00000007bf600000,0x00000007bfdd0ed8,0x00000007bfe00000)
from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
ParOldGen total 10240K, used 0K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
object space 10240K, 0% used [0x00000007bec00000,0x00000007bec00000,0x00000007bf600000)
Metaspace used 2631K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
日志字段意義:

????從日志可得出:a1、a2、a3、a4四個(gè)對(duì)象都被分配在了新生代(Eden)區(qū)。
????將a4改為a4 = new byte[2 * _1MB]日志如下:
[GC(AllocationFailure)[PSYoungGen:6815K->480K(9216K)]6815K->6632K(19456K),0.0067344secs][Times: user=0.04 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 9216K, used2130K[0x00000007bf600000,0x00000007c0000000,0x00000007c0000000)
eden space 8192K, 26% used [0x00000007bf600000,0x00000007bf814930,0x00000007bfe00000)
from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
to space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
ParOldGen total 10240K, used 6420K [0x00000007bec00000,0x00000007bf600000,0x00000007bf600000)
object space 10240K, 62% used [0x00000007bec00000,0x00000007bf2450d0,0x00000007bf600000)
Metaspace used 2632K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
????在給a4分配內(nèi)存前,Eden區(qū)已經(jīng)被占6M內(nèi)存。沒有足夠的內(nèi)存來存儲(chǔ)2M的a4對(duì)象。所以執(zhí)行一次Minor GC,并嘗試將a1、a2、a3復(fù)制到S1,但S1只有1M空間,最終只能將a1、a2、a3保存到Eden區(qū)。所以Eden區(qū)戰(zhàn)勝2M(a4),老年代占用6M(a1、a2、a3)。
這個(gè)案例也間接驗(yàn)證了JVM的內(nèi)存分配和分代回收策略。
再談引用
????JVM中的引用關(guān)系強(qiáng)度由強(qiáng)到弱可以分成四種:強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、 弱引用(Weak Reference)、虛引用(Phantom Reference)。

軟件引用常規(guī)
????代碼如下:
public class SoftReferenceNormal{
static class SoftObject{
byte[] data = new byte[1024 * 1024 *1024];
}
public static void main(String[] args){
// 將緩存數(shù)據(jù)用軟引用持有
SoftReference<SoftObject> cacheRef = new SoftReference<>(new SoftObject());
System.out.println("第一次GC前 軟引用:" + cacheRef.get());
System.gc()
// 進(jìn)行一次GC后查看對(duì)象的回收情況
System.out.println("第一次GC后 軟引用: " + cacheRef.get());
// 再分配一個(gè)120M的對(duì)象,看看緩存對(duì)象的回收情況
SoftObject newSo = new SoftObject():
System.out.println("再次分配120M強(qiáng)引用對(duì)象之后 軟引用:" + cacheRef.get());
}
}
????執(zhí)行并查看日志:
java -Xmx200m SoftReferenceNormal
第一次GC前 軟件引用:SoftReferenceNormal$SoftObject@7852e922
第一次GC后 軟件引用:SoftReferenceNormal$SoftObject@7852e922
再次分配120M強(qiáng)引用對(duì)象后 軟引用: null
????從日志得出:當(dāng)?shù)谝淮蜧C時(shí),內(nèi)存中還有剩余可用內(nèi)存,所以軟引用關(guān)聯(lián)對(duì)象并不會(huì)被GC回收,但再次創(chuàng)建對(duì)象,JVM內(nèi)存已經(jīng)不夠時(shí)軟引用關(guān)聯(lián)被回收掉了。
軟件引用隱藏的問題
????注意: 軟件引用關(guān)聯(lián)的對(duì)象自動(dòng)會(huì)被GC回收,但軟件引用對(duì)象本身也是一個(gè)強(qiáng)引用對(duì)象,因此不會(huì)自動(dòng)被回收。如:
public class SoftReferenceTest{
public static class SoftObject{
byte[] data = new byte[1024];
}
public static int CACHE_INITIAL_CAPSCITY = 100 * 1024;
// 靜態(tài)集合保存軟引用,會(huì)導(dǎo)致這些引用對(duì)象無法被垃圾回收器回收
public static Set<SoftReference<SoftObject>> cache = new HashSet<>(CACHE_INITIAL_CAPSCITY);
public static void main(String args[]){
for(int i = 0; i < CACHE_INITIAL_APSCITY; i++){
SoftObject obj = new SoftObject();
cache.add(new SoftReference<>(obj));
if (i % 10000 == 0){
System.out.pringln("size of cache: " + cache.size());
}
}
System.out.pringln("end!");
}
}
??執(zhí)行并查看日志:
java --Xms4M -Xmx4M -Xmn2M SoftRefenceTest
size of cache: 1
size of cache: 10001
size of cache: 20001
size of cache: 30001
Exception int thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at SoftReferenceTest$SoftObject.<init>(SoftReferenceTest.java:9)
at SoftReferenceTest.main(SoftReferenceTest.java:18)
????分析:GC overhead異常是由于虛擬機(jī)不斷回收軟件引用,回收進(jìn)行的速度過快,點(diǎn)用的CPU過大,且每次回收掉的內(nèi)存過小,最終拋出了這個(gè)錯(cuò)誤
????優(yōu)化:注冊(cè)一個(gè)引用隊(duì)列,每次循環(huán)后將引用隊(duì)列中出現(xiàn)的軟引用對(duì)象從cache中移除
public class SoftReferenceTest{
public static class MyBigObject{
byte[] data = new byte[1024];
}
public static int removedSoftRefs = 0;
public static CACHE_INITIAL_CAPACITY = 100 * 1024;
// 靜態(tài)集合保存軟引用,會(huì)導(dǎo)致這些引用對(duì)象本身無法被垃圾回收器回收
public static Set<SoftReference<MyBigObject>> cache = new HashSet<>(CACHE_INITIAL_CAPACITY);
public static ReferenceQueue<MyBigObject> referenceQueue = new ReferenceQueue<>();
public static void main(String[] args) {
for (int i = 0; i < CACHE_INITIAL_CAPACITY; i++) {
MyBigObject obj = new MyBigObject();
cache.add(new SoftRefernece<>(obj, referenceQueue));
clearUselessReferences();
if (i % 10000 == 0){
System.out.pringln("size of cache: " + cache.size());
}
}
System.out.println("End, removed soft references = " + removedSoftRefs);
}
public static void clearUselessReferences() {
Reference<? extends MyBigObject> ref = referenceQueue.poll();
while (ref != null) {
if (cache.remove(ref)) {
removedSoftRefs ++;
}
ref = referenceQueue.poll();
}
}
}
????執(zhí)行并查看日志:
java -Xms4M -Xmx4M -Xmn2M SoftReferenceTest
size of cache: 1
size of cache: 1677
size of cache: 1262
size of cache: 847
size of cache: 432
size of cache: 1873
size of cache: 1685
size of cache: 1206
size of cache: 731
size of cache: 468
size of cache: 1924
End, removed soft references = 100718
????執(zhí)行過程中動(dòng)態(tài)將集合中的軟件引用刪除后,程序正常運(yùn)行。
總結(jié)
????虛擬機(jī)垃圾回收機(jī)制是影響系統(tǒng)性能,并發(fā)能力的主要因素之一。對(duì)Android開發(fā)來說,有時(shí)垃圾回收會(huì)很大程序上影響UI線程,并造成卡頓現(xiàn)象。