- 轉(zhuǎn)載請(qǐng)注明出處:http://www.itdecent.cn/p/9e6841a895b4
注意:
- 垃圾回收算法周陽老師講的有錯(cuò)誤,具體在p19,四大垃圾回收算法為復(fù)制算法、標(biāo)記-整理算法、標(biāo)記-清除算法、分代
收集算法(不是引用計(jì)數(shù)算法)。這里感謝@9c0bd0ceebfa指出。- 關(guān)于FUll GC:Full GC為老年代的GC(周志明先生在《深入理解Java虛擬機(jī)》中也是這樣說的,第三版已改正)說法是不準(zhǔn)確的。Full GC 指的是針對(duì)新生代、老年代、永久代的全體內(nèi)存空間的垃圾回收。就是對(duì) JVM 進(jìn)行一次整體的垃圾回收,把各個(gè)內(nèi)存區(qū)域的垃圾都回收掉。老年代的GC特指Old GC,但因?yàn)镺ld GC一般伴隨著Young GC,也可以看做觸發(fā)了Full GC。


-
注意:我們平時(shí)說的棧是指的Java棧,native method stack 里面裝的都是native方法。見下文
注意:
- 方法區(qū)并不是存放方法的區(qū)域,其是存放類的描述信息(模板)的地方
- Class loader只是負(fù)責(zé)class文件的加載,相當(dāng)于快遞員,這個(gè)“快遞員”并不是只有一家,Class loader有多種
- 加載之前是“小class”,加載之后就變成了“大Class”,這是安裝java.lang.Class模板生成了一個(gè)實(shí)例?!按驝lass”就裝載在方法區(qū),模板實(shí)例化之后就得到n個(gè)相同的對(duì)象
- JVM并不是通過檢查文件后綴是不是
.class來判斷是否需要加載的,而是通過文件開頭的特定文件標(biāo)志
文件開頭的特殊標(biāo)識(shí)

注意:
- Class loader有多種,可以說三個(gè),也可以說是四個(gè)(第四個(gè)為自己定義的加載器,繼承 ClassLoader),系統(tǒng)自帶的三個(gè)分別為:
- 啟動(dòng)類加載器(Bootstrap) ,C++所寫
- 擴(kuò)展類加載器(Extension) ,Java所寫
- 應(yīng)用程序類加載器(AppClassLoader)。
我們自己new的時(shí)候創(chuàng)建的是應(yīng)用程序類加載器(AppClassLoader)。
import com.gmail.fxding2019.T;
public class Test{
//Test:查看類加載器
public static void main(String[] args) {
Object object = new Object();
//查看是那個(gè)“ClassLoader”(快遞員把Object加載進(jìn)來的)
System.out.println(object.getClass().getClassLoader());
//查看Object的加載器的上一層
// error Exception in thread "main" java.lang.NullPointerException(已經(jīng)是祖先了)
//System.out.println(object.getClass().getClassLoader().getParent());
System.out.println();
Test t = new Test();
System.out.println(t.getClass().getClassLoader().getParent().getParent());
System.out.println(t.getClass().getClassLoader().getParent());
System.out.println(t.getClass().getClassLoader());
}
}
/*
*output:
* null
*
* null
* sun.misc.Launcher$ExtClassLoader@4554617c
* sun.misc.Launcher$AppClassLoader@18b4aac2
* */
注意:
- 如果是JDK自帶的類(Object、String、ArrayList等),其使用的加載器是Bootstrap加載器;如果自己寫的類,使用的是AppClassLoader加載器;Extension加載器是負(fù)責(zé)將把java更新的程序包的類加載進(jìn)行
- 輸出中,sun.misc.Launcher是JVM相關(guān)調(diào)用的入口程序
- Java加載器個(gè)數(shù)為3+1。前三個(gè)是系統(tǒng)自帶的,用戶可以定制類的加載方式,通過繼承Java. lang. ClassLoader

注意:
- 雙親委派機(jī)制:“我爸是李剛,有事找我爹”。
例如:需要用一個(gè)A.java這個(gè)類,首先去頂部Bootstrap根加載器去找,找得到你就用,找不到再下降一層,去Extension加載器去找,找得到就用,找不到再將一層,去AppClassLoader加載器去找,找得到就用,找不到就會(huì)報(bào)"CLASS NOT FOUND EXCEPTION"。
//測(cè)試加載器的加載順序
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("hello world!");
}
}
/*
* output:
* 錯(cuò)誤: 在類 java.lang.String 中找不到 main 方法
* */
上面代碼是為了測(cè)試加載器的順序:首先加載的是Bootstrap加載器,由于JVM中有java.lang.String這個(gè)類,所以會(huì)首先加載這個(gè)類,而不是自己寫的類,而這個(gè)類中并無main方法,所以會(huì)報(bào)“在類 java.lang.String 中找不到 main 方法”。
這個(gè)問題就涉及到,如果有兩個(gè)相同的類,那么java到底會(huì)用哪一個(gè)?如果使用用戶自己定義的java.lang.String,那么別使用這個(gè)類的程序會(huì)去全部出錯(cuò),所以,為了保證用戶寫的源代碼不污染java出廠自帶的源代碼,而提供了一種“雙親委派”機(jī)制,保證“沙箱安全”。即先找到先使用。


Thread類的start方法如下:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
Thread類中竟然有一個(gè)只有聲明沒有實(shí)現(xiàn)的方法,并使用native關(guān)鍵字。用native表示,也此方法是系統(tǒng)級(jí)(底層操作系統(tǒng)或第三方C語言)的,而不是語言級(jí)的,java并不能對(duì)其進(jìn)行操作。native方法裝載在native method stack中。

- 注意:native方法不歸java管,所以計(jì)數(shù)器是空的

上面圖中是亮色的地方有兩個(gè)特點(diǎn):
- 所有線程共享(灰色是線程私有)
- 亮色地方存在垃圾回收

注意:
- 方法區(qū):絕對(duì)不是放方法的地方,他是存儲(chǔ)的每一個(gè)類的結(jié)構(gòu)信息(比如static)
- 永久代和元空間的解釋:
方法區(qū)是一種規(guī)范,類似于接口定義的規(guī)范:List list = new ArrayList();
把這種比喻用到方法區(qū)則有:
- java 7中:
方法區(qū) f = new 永久代();- java 8中:
方法去 f = new 元空間();

注意:
- 棧管運(yùn)行,堆管存儲(chǔ)
- 棧是線程私有,不存在垃圾回收
- 棧幀的概念:java中的方法被扔進(jìn)虛擬機(jī)的??臻g之后就成為“棧幀”,比如main方法,是程序的入口,被壓棧之后就成為棧幀。



public class Test{
public static void m(){
m();
}
public static void main(String[] args) {
System.out.println("111");
//Exception in thread "main" java.lang.StackOverflowError
m();
System.out.println("222");
}
}
/*
*output:
* 111
* Exception in thread "main" java.lang.StackOverflowError
* */
注意:
- StackOverflowError是一個(gè)“”錯(cuò)誤,而不是“異常”。

注意:
HotSpot:如果沒有明確指明,JDK的名字就叫HotSpot
元數(shù)據(jù):描述數(shù)據(jù)的數(shù)據(jù)(即模板,也就是“大Class”)
上面的關(guān)系圖的一個(gè)實(shí)例為下圖:


注意:
- Java 7之前和圖上一模一樣,Java 8把永久區(qū)換成了元空間
- 堆邏輯上由”新生+養(yǎng)老+元空間“三個(gè)部分組成,物理上由”新生+養(yǎng)老“兩個(gè)部分組成
- 當(dāng)執(zhí)行
new Person();時(shí),其實(shí)是new在新生區(qū)的伊甸園區(qū),然后往下走,走到養(yǎng)老區(qū),但是并未到元空間。

注意:
- GC發(fā)生在伊甸園區(qū),當(dāng)對(duì)象快占滿新生代時(shí),就會(huì)發(fā)生YGC(Young GC,輕量級(jí)GC)操作,伊甸園區(qū)基本全部清空
- 幸存者0區(qū)(S0),別名“from區(qū)”。伊甸園區(qū)沒有被YGC清空的對(duì)象將移至幸存者0區(qū),幸存者1區(qū)別名“to 區(qū)”
- 每次進(jìn)行YGC操作,幸存的對(duì)象就會(huì)從伊甸園區(qū)移到幸存者0區(qū),如果幸存者0區(qū)滿了,就會(huì)繼續(xù)往下移,如果經(jīng)歷數(shù)次YGC操作對(duì)象還沒有消亡,最終會(huì)來到養(yǎng)老區(qū)
- 如果到最后,養(yǎng)老區(qū)也滿了,那么就對(duì)養(yǎng)老區(qū)進(jìn)行FGC(Full GC,重GC),對(duì)養(yǎng)老區(qū)進(jìn)行清洗
- 如果進(jìn)行了多次FGC之后,還是無法騰出養(yǎng)老區(qū)的空間,就會(huì)報(bào)OOM(out of Memory)異常
- from區(qū)和to區(qū)位置和名分不是固定的,每次GC過后都會(huì)交換,GC交換后,誰空誰是to區(qū)

注意:
- 整個(gè)堆分為新生區(qū)和養(yǎng)老區(qū),新生區(qū)占整個(gè)堆的1/3,養(yǎng)老區(qū)占2/3。新生區(qū)又分為3份:伊甸園區(qū):幸存者0區(qū)(from區(qū)):幸存者1區(qū)(to區(qū)) = 8:1:1
- 每次從伊甸園區(qū)經(jīng)過GC幸存的對(duì)象,年齡(代數(shù))會(huì)+1


注意:
- 臨時(shí)對(duì)象就是說明,其在伊甸園區(qū)生,也在伊甸園區(qū)死。
- 堆邏輯上由”新生+養(yǎng)老+元空間“三個(gè)部分組成,物理上由”新生+養(yǎng)老“兩個(gè)部分組成,元空間也叫方法區(qū)
- 永久代(方法區(qū))幾乎沒有垃圾回收,里面存放的都是加載的rt.jar等,讓你隨時(shí)可用




注意
- 上面的圖展示的是物理上的堆,分為兩塊,新生區(qū)和養(yǎng)老區(qū)。
- 堆的參數(shù)主要有兩個(gè):
-Xms,Xmx:
-Xms堆的初始化的大小Xmx堆的最大化- Young Gen(新生代)有一個(gè)參數(shù)
-Xmn,這個(gè)參數(shù)可以調(diào)新生區(qū)和養(yǎng)老區(qū)的比例。但是,這個(gè)參數(shù)一般不調(diào)。- 永久代也有兩個(gè)參數(shù):
-XX:PermSize,-XX:MaxPermSize,可以分別調(diào)永久帶的初始值和最大值。Java 8 后沒有這兩個(gè)參數(shù)啦,因?yàn)镴ava 8后元空間不在虛擬機(jī)內(nèi)啦,而是在本機(jī)物理內(nèi)存中


//查看自己機(jī)器上的默認(rèn)堆內(nèi)存和最大堆內(nèi)存
public class Test{
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
//返回 Java虛擬機(jī)試圖使用的最大內(nèi)存量。物理內(nèi)存的1/4(-Xmx)
long maxMemory = Runtime.getRuntime().maxMemory() ;
//返回 Java虛擬機(jī)中的內(nèi)存總量(初始值)。物理內(nèi)存的1/64(-Xms)
long totalMemory = Runtime.getRuntime().totalMemory() ;
System.out.println("MAX_MEMORY =" + maxMemory +"(字節(jié))、" + (maxMemory / (double)1024 / 1024) + "MB");
System.out.println("DEFALUT_MEMORY = " + totalMemory + " (字節(jié))、" + (totalMemory / (double)1024 / 1024) + "MB");
}
}
/*
* 8
MAX_MEMORY =1868038144(字節(jié))、1781.5MB
TOTAL_MEMORY = 126877696 (字節(jié))、121.0MB
* */
- 注意:JVM參數(shù)調(diào)優(yōu),平時(shí)可以隨便挑初始大小和最大大小,但是實(shí)際工作中,初始大小和最大大小應(yīng)該是一致的,原因是避免內(nèi)存忽高忽低產(chǎn)生停頓
- IDEA 的JVM內(nèi)存配置
點(diǎn)擊Run列表下的Edit Configuration
- 在VM Options中輸入以下參數(shù):
-Xms1024m -Xmx1024m -XX:+PrintGCDetails。
運(yùn)行程序查看結(jié)果
把堆內(nèi)存調(diào)成10M后,再一直new對(duì)象,導(dǎo)致Full GC也無法處理,直至撐爆堆內(nèi)存,查看堆溢出錯(cuò)誤(OOM),程序及結(jié)果如下:
GC收集日志信息詳解
第一次進(jìn)行YGC相關(guān)參數(shù):
[PSYoungGen: 2008K->482K(2560K)] 2008K->782K(9728K), 0.0011440 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
最后一次進(jìn)行FGC相關(guān)參數(shù):
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4025K->4005K(7168K)] 4025K->4005K(9216K), [Metaspace: 3289K->3289K(1056768K)], 0.0082055 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
面試題:GC是什么(分代收集算法)
- 次數(shù)上頻繁收集Young區(qū)
- 次數(shù)上較少收集Old區(qū)
- 基本不動(dòng)元空間
面試題:GC的四大算法(后有詳解)
- 1.復(fù)制算法(Copying)
- 標(biāo)記清除(Mark-Sweep)
- 標(biāo)記壓縮(Mark-Compact)
- 分代收集算法
面試題:下面程序中,有幾個(gè)線程在運(yùn)行
Answer:有兩個(gè)線程,一個(gè)是main線程,一個(gè)是后臺(tái)的gc線程。

知識(shí)點(diǎn):
- JVM在進(jìn)行GC時(shí),并非每次都對(duì)上面三個(gè)內(nèi)存區(qū)域一起回收的,大部分時(shí)候回收的都是指新生代。因此GC按照回收的區(qū)域又分了兩種類型,一種是普通GC(minor GC or Young GC),一種是全局GC(major GC or Full GC)
- Minor GC和Full GC的區(qū)別
普通GC(minor GC):只針對(duì)新生代區(qū)域的GC,指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)榇蠖鄶?shù)Java對(duì)象存活率都不高,所以Minor GC非常頻繁,一般回收速度也比較快。
全局GC(major GC or Full GC):指發(fā)生在老年代的垃圾收集動(dòng)作,出現(xiàn)了Major GC,經(jīng)常會(huì)伴隨至少一次的Minor GC(但并不是絕對(duì)的)。Major GC的速度一般要比Minor GC慢上10倍以上 (因?yàn)轲B(yǎng)老區(qū)比較大,占堆的2/3)
GC四大算法詳解:
首先看一下判斷Java中對(duì)象存活的算法:
-
1.引用計(jì)數(shù)算法:引用計(jì)數(shù)器算法是給每個(gè)對(duì)象設(shè)置一個(gè)計(jì)數(shù)器,當(dāng)有地方引用這個(gè)對(duì)象的時(shí)候,計(jì)數(shù)器+1,當(dāng)引用失效的時(shí)候,計(jì)數(shù)器-1,當(dāng)計(jì)數(shù)器為0的時(shí)候,JVM就認(rèn)為對(duì)象不再被使用,是“垃圾”了。
引用計(jì)數(shù)器實(shí)現(xiàn)簡單,效率高;但是不能解決循環(huán)引用問問題(A對(duì)象引用B對(duì)象,B對(duì)象又引用A對(duì)象,但是A,B對(duì)象已不被任何其他對(duì)象引用),同時(shí)每次計(jì)數(shù)器的增加和減少都帶來了很多額外的開銷,所以在JDK1.1之后,這個(gè)算法已經(jīng)不再使用了。 -
2.根搜索方法:根搜索方法是通過一些“GCRoots”對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開始往下搜索,搜索通過的路徑成為引用鏈(ReferenceChain),當(dāng)一個(gè)對(duì)象沒有被GCRoots的引用鏈連接的時(shí)候,說明這個(gè)對(duì)象是不可用的。
GCRoots對(duì)象包括:
1. 虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對(duì)象。
2. 方法區(qū)域中的類靜態(tài)屬性引用的對(duì)象。
3. 方法區(qū)域中常量引用的對(duì)象。
4. 方法棧中JNI(Native方法)的引用的對(duì)象。
1. 復(fù)制算法(Copying)
年輕代中使用的是Minor GC(YGC),這種GC算法采用的是復(fù)制算法(Copying)。
Minor GC會(huì)把Eden中的所有活的對(duì)象都移到Survivor區(qū)域中,如果Survivor區(qū)中放不下,那么剩下的活的對(duì)象就被移到Old generation中,也即一旦收集后,Eden是就變成空的了。
當(dāng)對(duì)象在 Eden ( 包括一個(gè) Survivor 區(qū)域,這里假設(shè)是 from 區(qū)域 ) 出生后,在經(jīng)過一次 Minor GC 后,如果對(duì)象還存活,并且能夠被另外一塊 Survivor 區(qū)域所容納( 上面已經(jīng)假設(shè)為 from 區(qū)域,這里應(yīng)為 to 區(qū)域,即 to 區(qū)域有足夠的內(nèi)存空間來存儲(chǔ) Eden 和 from 區(qū)域中存活的對(duì)象 ),則使用復(fù)制算法將這些仍然還存活的對(duì)象復(fù)制到另外一塊 Survivor 區(qū)域 ( 即 to 區(qū)域 ) 中,然后清理所使用過的 Eden 以及 Survivor 區(qū)域 ( 即 from 區(qū)域 ),并且將這些對(duì)象的年齡設(shè)置為1,以后對(duì)象在 Survivor 區(qū)每熬過一次 Minor GC,就將對(duì)象的年齡 + 1,當(dāng)對(duì)象的年齡達(dá)到某個(gè)值時(shí) ( 默認(rèn)是 15 歲,通過-XX:MaxTenuringThreshold 來設(shè)定參數(shù)),這些對(duì)象就會(huì)成為老年代。
-XX:MaxTenuringThreshold — 設(shè)置對(duì)象在新生代中存活的次數(shù)

年輕代中的GC,主要是復(fù)制算法(Copying)。 HotSpot JVM把年輕代分為了三部分:1個(gè)Eden區(qū)和2個(gè)Survivor區(qū)(分別叫from和to)。默認(rèn)比例為8:1:1,一般情況下,新創(chuàng)建的對(duì)象都會(huì)被分配到Eden區(qū)(一些大對(duì)象特殊處理),這些對(duì)象經(jīng)過第一次Minor GC后,如果仍然存活,將會(huì)被移到Survivor區(qū)。對(duì)象在Survivor區(qū)中每熬過一次Minor GC,年齡就會(huì)增加1歲,當(dāng)它的年齡增加到一定程度時(shí),就會(huì)被移動(dòng)到年老代中。因?yàn)槟贻p代中的對(duì)象基本都是朝生夕死的(90%以上),所以在年輕代的垃圾回收算法使用的是復(fù)制算法,復(fù)制算法的基本思想就是將內(nèi)存分為兩塊,每次只用其中一塊(from),當(dāng)這一塊內(nèi)存用完,就將還活著的對(duì)象復(fù)制到另外一塊上面。復(fù)制算法的優(yōu)點(diǎn)是不會(huì)產(chǎn)生內(nèi)存碎片,缺點(diǎn)是耗費(fèi)空間。
在GC開始的時(shí)候,對(duì)象只會(huì)存在于Eden區(qū)和名為“From”的Survivor區(qū),Survivor區(qū)“To”是空的。緊接著進(jìn)行GC,Eden區(qū)中所有存活的對(duì)象都會(huì)被復(fù)制到“To”,而在“From”區(qū)中,仍存活的對(duì)象會(huì)根據(jù)他們的年齡值來決定去向。年齡達(dá)到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設(shè)置)的對(duì)象會(huì)被移動(dòng)到年老代中,沒有達(dá)到閾值的對(duì)象會(huì)被復(fù)制到“To”區(qū)域。經(jīng)過這次GC后,Eden區(qū)和From區(qū)已經(jīng)被清空。這個(gè)時(shí)候,“From”和“To”會(huì)交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會(huì)保證名為To的Survivor區(qū)域是空的。Minor GC會(huì)一直重復(fù)這樣的過程,直到“To”區(qū)被填滿,“To”區(qū)被填滿之后,會(huì)將所有對(duì)象移動(dòng)到年老代中。

因?yàn)镋den區(qū)對(duì)象一般存活率較低,一般的,使用兩塊10%的內(nèi)存作為空閑和活動(dòng)區(qū)間,而另外80%的內(nèi)存,則是用來給新建對(duì)象分配內(nèi)存的。一旦發(fā)生GC,將10%的from活動(dòng)區(qū)間與另外80%中存活的eden對(duì)象轉(zhuǎn)移到10%的to空閑區(qū)間,接下來,將之前90%的內(nèi)存全部釋放,以此類推。

上面動(dòng)畫中,Area空閑代表to,Area激活代表from,綠色代表不被回收的,紅色代表被回收的。
復(fù)制算法它的缺點(diǎn)也是相當(dāng)明顯的:
- 它浪費(fèi)了一半的內(nèi)存,這太要命了。
- 如果對(duì)象的存活率很高,我們可以極端一點(diǎn),假設(shè)是100%存活,那么我們需要將所有對(duì)象都復(fù)制一遍,并將所有引用地址重置一遍。復(fù)制這一工作所花費(fèi)的時(shí)間,在對(duì)象存活率達(dá)到一定程度時(shí),將會(huì)變的不可忽視。 所以從以上描述不難看出,復(fù)制算法要想使用,最起碼對(duì)象的存活率要非常低才行,而且最重要的是,我們必須要克服50%內(nèi)存的浪費(fèi)。
2 .標(biāo)記清除(Mark-Sweep)
復(fù)制算法的缺點(diǎn)就是費(fèi)空間,其是用在年輕代的,老年代一般是由標(biāo)記清除或者是標(biāo)記清除與標(biāo)記整理的混合實(shí)現(xiàn)。


用通俗的話解釋一下標(biāo)記清除算法,就是當(dāng)程序運(yùn)行期間,若可以使用的內(nèi)存被耗盡的時(shí)候,GC線程就會(huì)被觸發(fā)并將程序暫停,隨后將要回收的對(duì)象標(biāo)記一遍,最終統(tǒng)一回收這些對(duì)象,完成標(biāo)記清理工作接下來便讓應(yīng)用程序恢復(fù)運(yùn)行。
主要進(jìn)行兩項(xiàng)工作,第一項(xiàng)則是標(biāo)記,第二項(xiàng)則是清除。
- 標(biāo)記:從引用根節(jié)點(diǎn)開始標(biāo)記遍歷所有的GC Roots, 先標(biāo)記出要回收的對(duì)象。
- 清除:遍歷整個(gè)堆,把標(biāo)記的對(duì)象清除。
缺點(diǎn):此算法需要暫停整個(gè)應(yīng)用,會(huì)產(chǎn)生內(nèi)存碎片

標(biāo)記清除算法小結(jié):
- 1、首先,它的缺點(diǎn)就是效率比較低(遞歸與全堆對(duì)象遍歷),而且在進(jìn)行GC的時(shí)候,需要停止應(yīng)用程序,這會(huì)導(dǎo)致用戶體驗(yàn)非常差勁
- 2、其次,主要的缺點(diǎn)則是這種方式清理出來的空閑內(nèi)存是不連續(xù)的,這點(diǎn)不難理解,我們的死亡對(duì)象都是隨即的出現(xiàn)在內(nèi)存的各個(gè)角落的,現(xiàn)在把它們清除之后,內(nèi)存的布局自然會(huì)亂七八糟。而為了應(yīng)付這一點(diǎn),JVM就不得不維持一個(gè)內(nèi)存的空閑列表,這又是一種開銷。而且在分配數(shù)組對(duì)象的時(shí)候,尋找連續(xù)的內(nèi)存空間會(huì)不太好找。
3. 標(biāo)記壓縮(Mark-Compact)
標(biāo)記壓縮(Mark-Compact)又叫標(biāo)記清除壓縮(Mark-Sweep-Compact),或者標(biāo)記清除整理算法。老年代一般是由標(biāo)記清除或者是標(biāo)記清除與標(biāo)記整理的混合實(shí)現(xiàn)




4. 分代收集算法
當(dāng)前商業(yè)虛擬機(jī)都是采用分代收集算法,它根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊,一般是把Java堆分為新生代和老年代,然后根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴?,在新生代中,每次垃圾收集都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,就選用復(fù)制算法,而老年代因?yàn)閷?duì)象存活率高,沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記清理”或者“標(biāo)記整理”算法來進(jìn)行回收。

圖的左半部分是未回收前的內(nèi)存區(qū)域,右半部分是回收后的內(nèi)存區(qū)域。
面試題:四種算法那個(gè)好
Answer:沒有那個(gè)算法是能一次性解決所有問題的,因?yàn)镴VM垃圾回收使用的是分代收集算法,沒有最好的算法,只有根據(jù)每一代他的垃圾回收的特性用對(duì)應(yīng)的算法。新生代使用復(fù)制算法,老年代使用標(biāo)記清除和標(biāo)記整理算法。沒有最好的垃圾回收機(jī)制,只有最合適的。
面試題:請(qǐng)說出各個(gè)垃圾回收算法的優(yōu)缺點(diǎn)
- 內(nèi)存效率:復(fù)制算法>標(biāo)記清除算法>標(biāo)記整理算法(此處的效率只是簡單的對(duì)比時(shí)間復(fù)雜度,實(shí)際情況不一定如此)。
- 內(nèi)存整齊度:復(fù)制算法=標(biāo)記整理算法>標(biāo)記清除算法。
- 內(nèi)存利用率:標(biāo)記整理算法=標(biāo)記清除算法>復(fù)制算法。
可以看出,效率上來說,復(fù)制算法是當(dāng)之無愧的老大,但是卻浪費(fèi)了太多內(nèi)存,而為了盡量兼顧上面所提到的三個(gè)指標(biāo),標(biāo)記/整理算法相對(duì)來說更平滑一些,但效率上依然不盡如人意,它比復(fù)制算法多了一個(gè)標(biāo)記的階段,又比標(biāo)記/清除多了一個(gè)整理內(nèi)存的過程
難道就沒有一種最優(yōu)算法嗎?Java 9 之后出現(xiàn)了G1垃圾回收器(使用分代收集),能夠解決以上問題,有興趣參考這篇文章。
總結(jié):
-
年輕代(Young Gen)
年輕代特點(diǎn)是區(qū)域相對(duì)老年代較小,對(duì)像存活率低。
這種情況復(fù)制算法的回收整理,速度是最快的。復(fù)制算法的效率只和當(dāng)前存活對(duì)像大小有關(guān),因而很適用于年輕代的回收。而復(fù)制算法內(nèi)存利用率不高的問題,通過hotspot中的兩個(gè)survivor的設(shè)計(jì)得到緩解。
-
老年代(Tenure Gen)
老年代的特點(diǎn)是區(qū)域較大,對(duì)像存活率高。
這種情況,存在大量存活率高的對(duì)像,復(fù)制算法明顯變得不合適。一般是由標(biāo)記清除或者是標(biāo)記清除與標(biāo)記整理的混合實(shí)現(xiàn)。
Mark階段的開銷與存活對(duì)像的數(shù)量成正比,這點(diǎn)上說來,對(duì)于老年代,標(biāo)記清除或者標(biāo)記整理有一些不符,但可以通過多核/線程利用,對(duì)并發(fā)、并行的形式提標(biāo)記效率。
Sweep階段的開銷與所管理區(qū)域的大小形正相關(guān),但Sweep“就地處決”的特點(diǎn),回收的過程沒有對(duì)像的移動(dòng)。使其相對(duì)其它有對(duì)像移動(dòng)步驟的回收算法,仍然是效率最好的。但是需要解決內(nèi)存碎片問題。
Compact階段的開銷與存活對(duì)像的數(shù)據(jù)成開比,如上一條所描述,對(duì)于大量對(duì)像的移動(dòng)是很大開銷的,做為老年代的第一選擇并不合適。
基于上面的考慮,老年代一般是由標(biāo)記清除或者是標(biāo)記清除與標(biāo)記整理的混合實(shí)現(xiàn)。以hotspot中的CMS回收器為例,CMS是基于Mark-Sweep實(shí)現(xiàn)的,對(duì)于對(duì)像的回收效率很高,而對(duì)于碎片問題,CMS采用基于Mark-Compact算法的Serial Old回收器做為補(bǔ)償措施:當(dāng)內(nèi)存回收不佳(碎片導(dǎo)致的Concurrent Mode Failure時(shí)),將采用Serial Old執(zhí)行Full GC以達(dá)到對(duì)老年代內(nèi)存的整理。
- 參考:尚硅谷周陽視頻及課件
- 資料提?。?/strong>https://pan.baidu.com/s/1w-M3S8777iR4oekw7S3crA 提取碼:6ea8















