可以被所有線程共享的區(qū)域,包括堆區(qū)、方法區(qū)、運(yùn)行時常量池。
大多數(shù)時候,Java 堆是 Java 虛擬機(jī)管理的內(nèi)存里最大的一塊,所有的對象實(shí)例和數(shù)組都要在堆上分配內(nèi)存空間,Java 對象可以分為兩類,一類是快速創(chuàng)建快速消亡的,另一類是長期使用的。所以針對這種情況大多收集器都是基于分代收集算法進(jìn)行回收。
Java 的堆可以分為新生代(Young Generation)和老年代(Old Generation),而新生代(Young Generation)又可以分為 Eden Space 空間 (伊甸園區(qū))、From Survivor 空間(From 生存區(qū))、To Survivor 空間(To 生存區(qū))。
Java 堆是一塊共享的區(qū)域,會出現(xiàn)線程安全的問題,而操作共享區(qū)域就需要鎖和同步,通過- Xms
設(shè)置堆的最小值,堆內(nèi)存越小越容易發(fā)生內(nèi)存不夠用的情況而觸犯 Full GC(對新生代、老年代、永久代進(jìn)行垃圾回收)。官方推薦新生代大小占整個堆大小的 3/8,通過- Xmx
設(shè)置堆的最大值,堆內(nèi)存超過此值會發(fā)拋出 OutOfMemoryError 異常:
方法區(qū)(Method Area)在 HotSpot 虛擬機(jī)上可以看作是永久代(Permanent Generation),對于其他虛擬機(jī)(JRockit 、J9 等)來說是不存在永久代的。方法區(qū)也是所有線程共享的區(qū)域,主要存儲被虛擬機(jī)加載的類信息、常量、靜態(tài)變量,堆存儲對象數(shù)據(jù),方法區(qū)存儲靜態(tài)信息。
方法區(qū)不像 Java 堆區(qū)那樣經(jīng)常發(fā)生垃圾回收,但不表示不會發(fā)生。永久代的大小跟新生代、老年代比都是很小的,通過設(shè)置- XX:MaxPermSize
來指定最大內(nèi)存,方法區(qū)需要的內(nèi)存超過此值會拋出 OutOfMemoryError 異常。
Java 通過類加載機(jī)制可以把字節(jié)碼文件中的常量池表加載進(jìn)運(yùn)行時常量池,而我們也可使用 String 類的 intern() 方法在運(yùn)行期將新的常量放入池中,運(yùn)行時常量池是方法區(qū)的一部分,在 JDK1.7 的 HotSpot 中,已經(jīng)把原本放在方法區(qū)的常量池移出來了。
只允許被所屬的線程私自訪問的內(nèi)存區(qū),包括 PC 寄存器、Java 棧和本地方法棧。
Java Stack 描述的是 Java 方法執(zhí)行時的內(nèi)存模型,每個方法執(zhí)行時都會創(chuàng)建一個棧幀(Stack Frame),棧幀包含局部變量表(存放編譯期間的各種基本數(shù)據(jù)類型,對象引用等信息)、操作數(shù)棧、動態(tài)鏈接、方法出口等數(shù)據(jù)。
一個線程運(yùn)行時會分配??臻g,每個線程的??臻g數(shù)據(jù)是相互隔離的,所以棧是私有的,堆是共享的,一個線程執(zhí)行多個方法,會入棧出棧多個棧幀(多個方法),棧是先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),最先入棧的棧幀,最后出棧,可以通過-Xss
設(shè)置每個線程棧的大小,越小,能創(chuàng)建的線程數(shù)就越多,但并不是可以無限的,在一個進(jìn)程里(JVM 進(jìn)程)能生成的線程數(shù)最多不超過五千
虛擬機(jī)棧(Java Stack)為執(zhí)行 Java 方法(就是字節(jié)碼)服務(wù),而本地方法棧(Native Stack)則為 Native 方法(比如用 C/C++ 編寫的代碼)服務(wù),其他方面都很類似。
JVM 字節(jié)碼解析器通過改變 PC 寄存器的值來明確下一條需要執(zhí)行的字節(jié)碼指令,每個線程都會分配一個獨(dú)立的 PC 寄存器。
JVM 垃圾收集算法不同虛擬機(jī)的具體實(shí)現(xiàn)會不一樣,這里先講解幾種經(jīng)典的垃圾收集算法的思想,后面再以使用得最廣泛的 HotSpot 虛擬機(jī)為例講解具體的垃圾收集器算法。
給每個對象維護(hù)一個引用計(jì)數(shù)器,每當(dāng)被引用一次就加 1,每當(dāng)引用失效一次就減 1,引用計(jì)數(shù)器為 0,表明當(dāng)前對象沒有被任何對象引用,則可以當(dāng)作垃圾回收。但是當(dāng) A 對象和 B 對象相互引用對方的時候,大家的計(jì)數(shù)器值都不為 0,而如果對象 A 和對象 B 都已經(jīng)不被外部引用,就是說兩個無用的對象因?yàn)橄嗷ヒ枚鵁o法進(jìn)行垃圾回收。這就是循環(huán)引用的缺陷,故現(xiàn)在 JVM 虛擬機(jī)大多不采用這種方式做垃圾回收。
復(fù)制 (Coping)
標(biāo)記-清除 (Mark-Sweep)
標(biāo)記-壓縮(Mark-Compact)
分代收集算法(Generational Collection)
根搜索算法從那些被稱為 GC Roots 的對象(虛擬機(jī)棧中引用的對象、方法區(qū)中靜態(tài)屬性引用的對象、方法區(qū)中常量引用的對象、本地方法棧 JNI 引用的對象)作為起始節(jié)點(diǎn),從這些節(jié)點(diǎn)向下搜索,搜索所形成的路徑叫引用鏈。當(dāng)一個對象到 GC Roots 的所有對象都沒有一條引用鏈,則說明該對象不可用,所以根搜索算法又叫可達(dá)性算法,GC Roots 到該對象不可達(dá)則表明該對象不可用,表明該對象可回收。
根搜索算法有四種,其中復(fù)制算法應(yīng)用在新生代。
復(fù)制算法將內(nèi)存劃分相等的兩塊,當(dāng)一塊內(nèi)存用完了,將還存活的對象移動到另一塊內(nèi)存,將之前使用的內(nèi)存一次清理,新生代的內(nèi)存空間通常都是所有代里最大的,適用復(fù)制算法,實(shí)際上垃圾回收時是把 Eden Space 和 From Survivor 上還存活的對象復(fù)制到 To Survivor,而不需要按照 1:1 的比例來劃分。
通常 Eden Space:From Survivor:To Survivor = 8:1:1,如果出現(xiàn)狀況 To Survivor 空間不足以容納復(fù)制過來的還存活的對象,那通過分配擔(dān)保機(jī)制,這些對象可直接進(jìn)入老年代,然后下一次垃圾回收發(fā)生時 From Survivor 和 To Survivor 交換身份,內(nèi)存直接從 To Survivor 分配,回收到 From Survivor。
優(yōu)點(diǎn):沒有標(biāo)記和清除的過程,效率高,沒有內(nèi)存碎片,可以利用 Bump-the-pointer(指針碰撞)技術(shù)實(shí)現(xiàn)快速內(nèi)存分配,因?yàn)橐延煤臀从玫膬?nèi)存各自一邊,內(nèi)存分布規(guī)整有序,當(dāng)新對象分配時就可以通過修改指針偏移量將其分配在第一個空閑的內(nèi)存位置上,從而快速分配內(nèi)存,否則只能使用空閑列表(Free List)方式分配內(nèi)存,如下面要講的標(biāo)記-清除(Mark-Sweep)垃圾回收算法。
缺點(diǎn):開辟專門的空間存放存活對象,占用更多的內(nèi)存。
上面的內(nèi)存圖示是為了理解,其實(shí)內(nèi)存是線性的。
從根集合開始掃描,對存活動對象進(jìn)行標(biāo)記,然后重新掃描整個內(nèi)存空間清除未被標(biāo)記的對象,優(yōu)點(diǎn)是不需要額外的空間,缺點(diǎn)是重復(fù)掃描,性能低,而且產(chǎn)生內(nèi)存碎片。
一樣是從從根集合開始掃描,對存活動對象進(jìn)行標(biāo)記,然后重新掃描整個內(nèi)存空間,并往一個方向移動存活對象,雖然移動對象的消耗時間,但不產(chǎn)生內(nèi)存碎片,可以通過 Bump-the-pointer(指針碰撞)快速分配內(nèi)存。
是“標(biāo)記-清除”和“標(biāo)記-壓縮”算法的結(jié)合,唯一的不同是要等“標(biāo)記-清除”多次以后,也就是多次垃圾回收進(jìn)行以后才進(jìn)行移動對象(壓縮),以避免每次 GC(垃圾回收)后都壓縮一次,降低了移動對象的耗時。
JVM 幾種經(jīng)典的垃圾回收算法已經(jīng)講完,下面直接進(jìn)入 HotSpot 虛擬機(jī)的講解。
public class Earth
{ String name; // 單位:億年 int age; String size;
public Earth(String name, int age, String size)
{ super();
this.name = name;
this.age = age;
this.size = size;
} Earth e = new Earth('地球', 46, '1.0832×10^12立方千米');
}
當(dāng)我們?nèi)?new 一個對象時,首先會在??臻g為對象的引用 e 分配內(nèi)存,這是聲明 Earth e,但由于 Earth 還是一個空對象,無法使用,不指向任何實(shí)體,接著 new Earth 會在堆開辟內(nèi)存給成員變量 name、age、size,并初始化為各個數(shù)據(jù)類型的默認(rèn)值,然后才是初始化為自定義的賦值,接著調(diào)用構(gòu)造函數(shù)通過參數(shù)列表 (“地球”, 46, “1.0832×10^12 立方千米”) 為成員變量再賦值,最后返回對象的引用(首地址)給變量 e。
Earth e1 = new Earth('地球', 46, '1.0832×10^12立方千米');
Earth e2 = new Earth('地球', 46, '1.0832×10^12立方千米');
Earth e3 = new Earth('地球', 46, '1.0832×10^12立方千米');
就算創(chuàng)建多個對象,在堆中還是一個實(shí)例,在棧中 e1、e2、e3 共同指向一個實(shí)例,而由于對象創(chuàng)建都是頻繁而且短生命周期,故一般對象被分配在堆的新生代的 Eden 區(qū)。
而堆空間是非線程安全的,是線程共享的,為對象分配內(nèi)存時就要保證原子性,防止產(chǎn)生臟數(shù)據(jù),這是會消耗性能的。為此 JVM 做了優(yōu)化,優(yōu)先為加載完成對類在 TLAB(Thread Local Allocation,本地線程分配緩沖區(qū))中為對象實(shí)例分配內(nèi)存空間。
TLAB 是在堆中 Eden 區(qū)里開辟的空間,但卻是一塊線程私有區(qū)域,并不是所有對象都能在這成功分配,但在 TLAB 分配是個優(yōu)選項(xiàng),為了優(yōu)化內(nèi)存分配,還可以使用 JVM 的堆外內(nèi)存存放對象實(shí)例,堆空間不是唯一 一個可以存放對象實(shí)例的地方,當(dāng)一個對象作用域在方法體內(nèi),但隨時間推移,一旦其引用被方法體外的成員變量引用時,就發(fā)生了逃逸;
反過來,如果作用域還是局限于方法體內(nèi),JVM 就可以為對象在棧幀里分配空間,對象分配在方法的棧幀上,隨著方法創(chuàng)建而創(chuàng)建,隨著方法退出而消亡,無需垃圾回收。
我們再來看一下跟運(yùn)行時常量池相關(guān)的內(nèi)存分配的例子(面試??停?/span>
public class StringDemo
{
public static void stingDemo()
{ // 池化的思想,把共享的數(shù)據(jù)放入字符串池中,
以減少頻繁創(chuàng)建銷毀對象的開銷
// 引用s1被放入棧中,字符串字面量'123'如果
存在于字符串池中,返回其引用給s1,否則,在池中新建字符串
String s1 = '123'; // 池中已經(jīng)存在'123',
不會再創(chuàng)建,直接拿到引用(地址值)
String s2 = '123'; // 就是new對象的創(chuàng)建過程,不會去池查找,
直接在堆開辟空間存放實(shí)例對象,返回對象地址給s3
String s3 = new String('123');// String類本身是被final
修飾的,final修飾會保證s4的值不變,也就是s4=123,
而這個字符串字面量直接可以從池中拿到,不需要創(chuàng)建對象
String s4 = '1' + '23';
String str = '1';
// 由于被final修飾,str的值'1'不能改變,s5的值也不能變,其值是str+'23'創(chuàng)建對象所返回的地址(不是指對象內(nèi)容
// 123不許變),所以這里會新建一個對象
String s5 = str + '23'; // 兩個基本類型用==比較,比較的是兩個基本類型的值是否相等
// 一個包裝類型和一個基本類型用==比較,比較包裝類型的值和基本類型的值是否相等
// 兩個包裝類型用==比較,比較兩個對象返回的地址是否相等 // 兩個包裝類型用equals比較,比較兩個對象是否相等,
// 包括兩個對象類的類型是否相同,對象里的值是否完全相同,對象的hashcode是否相同,不比較地址,所以hashcode相同,對象不一定相等,對象相等,hashcode一定相同
// s1==s2:true
System.out.println('s1==s2:' + (s1 == s2)); // s1==s3:false
System.out.println('s1==s3:' + (s1 == s3)); // s1.equals(s3):true
// Sting類已經(jīng)幫我們覆寫了Object的equals方法,使得equals的比較正常,
// 如果沒覆寫,底層還是用==做的比較,我們自定義對象要用equals比較的前提是記得覆寫equals方法
System.out.println('s1.equals(s3):' + (s1.equals(s3)));
// s1=s4:true
System.out.println('s1=s4:' + (s1 == s4));
}
public static void main(String[] args)
{
StringDemo.stingDemo();
}
}
public class TestInteger
{
public static void main(String[] args)
{
int i1 = 129;
// java在編譯的時候,會變成 Integer i2 = Integer.valueOf(129)
Integer i2 = 129;
// int和integer(無論是否new出來的)比,都為true,因?yàn)闀袸nteger自動拆箱為int再去比較
System.out.println('int i1 = 129 == Integer i2= 129 :' + (i1 == i2));
Integer i3 = new Integer(129);
// Integer與new Integer不會相等。不會經(jīng)歷拆箱過程,i3與i2指向是兩個不同的地址,為false
System.out.println('Integer i2= 129 == Integer i3 = new Integer(129) :' + (i2 == i3));
Integer i4 = new Integer(129);
// 兩個都是new出來的,開辟不同的內(nèi)存空間,都為false
System.out.println('Integer i3 = new Integer(129) == Integer i4 =new Integer(129) :' + (i3 == i4));
Integer i5 = 129;
/*
* Integer i2 = 129 會被編譯成Integer.valueOf(129) 而valueOf的源碼如下 public static
* Integer valueOf(int i) { * assert IntegerCache.high >= 127;
* 如果值在-128到127之間,直接從緩存取值,不需要重新創(chuàng)建
* if (i >= IntegerCache.low && i <=integercache.high) ="" ="" ="" ="">=integercache.high)>
* return IntegerCache.cache[i + (-IntegerCache.low)];
* return new Integer(i); * }
*/
// 兩個都是非new出來的Integer,如果數(shù)在-128到127之間,則是true,否則為false,超過范圍不會在常量池里取,會重新創(chuàng)建兩個Integer,==比較Integer的值,即是地址,肯定false
System.out.println('Integer i2= 129 == Integer i5 = 129 :' + (i2 == i5)); i2 = 127; i5 = 127; // 在-128到127之間,從常量池取同一個引用給Integer,肯定是true
System.out.println('Integer i2= 127 == Integer i5 = 127 :' + (i2 == i5));
}
}
HotSpot 內(nèi)存管理里,新生代 80% 的對象生命周期較短,GC 頻率高,適合采用效率較高的復(fù)制算法,經(jīng)歷了多次 GC 仍然存活的對象或者一些超過設(shè)定值大小的對象會分配到老年代,老年代 GC 頻率較低,適合使用“標(biāo)記 - 清除 - 壓縮”這種綜合的算法。
回收算法還有回收方式的不同,串行回收(Serial),是指就算有多個 CPU 核,某一時刻也只能有一個 CPU 核可以執(zhí)行垃圾回收線程,此時用戶線程會被掛起處于暫停狀態(tài),只有等回收線程執(zhí)行完畢,用戶線程才能繼續(xù)執(zhí)行,也就是會產(chǎn)生所謂的 Stop-the-world,JVM 短時間內(nèi)卡頓不會工作。
并行回收是指某一時刻可以由多個 CPU 核同時執(zhí)行各自的垃圾回收線程,不過一樣會出現(xiàn) Stop-the-world,而并發(fā)回收是指用戶線程和垃圾回收線程交替執(zhí)行,大大縮短 Stop-the-world 的停頓時間?,F(xiàn)在大型項(xiàng)目動輒使用上百 G 的內(nèi)存,內(nèi)存越大,回收時間越久,而 Stop-the-world 的卡頓時間也會越久,目前還沒有算法可以做到零停頓的。
算法的思想講完了,下面就講垃圾收集算法的具體實(shí)現(xiàn)垃圾收集器。
上面紅色橫線的地方就是安全點(diǎn),用戶線程執(zhí)行時,要到達(dá)了安全點(diǎn),才能暫停,讓回收線程執(zhí)行;當(dāng)觸發(fā)回收線程執(zhí)行時,不會直接中斷用戶線程,而是設(shè)置一個標(biāo)志位,讓用戶線程輪詢。發(fā)現(xiàn)為中斷標(biāo)志時就運(yùn)行到最近的安全點(diǎn)再將自己掛起讓 CPU 執(zhí)行回收線程,但如果此時用戶線程處于 Waiting 或者 Blocked 狀態(tài),無法輪詢標(biāo)志位,就會造成回收線程長時間無法運(yùn)行的情況。
為此引入了安全區(qū),安全區(qū)就是引用關(guān)系不會發(fā)生變化的代碼,在這段代碼的任何地方發(fā)起 GC 都是安全的。所以當(dāng)用戶線程進(jìn)入到安全區(qū),恰好這時回收線程要執(zhí)行就會直接中斷用戶線程,用戶線程離開安全區(qū)時,只需檢查回收線程是否已經(jīng)完成,如果完成則可以離開,否則等待直到 GC 完畢。
Serial Coping(串行復(fù)制),Parallel Scavenge(并行復(fù)制),ParNew(并發(fā)復(fù)制)這三種回收器都是基于復(fù)制算法,復(fù)制 young eden 和 young from 中還存活的對象到 young to,或者根據(jù)設(shè)定對象的大小和 GC 次數(shù)直接晉升到 old,清空 young eden 和 young from 中的垃圾,下一次 GC 發(fā)生交換 young from 和 young to,只可使用于新生代,是在 young eden 內(nèi)存空間不足以分配給對象時觸發(fā) Minor GC(新生代垃圾回收)。
Serial Old (串行標(biāo)記-清理-壓縮)
Parallel Old(并行標(biāo)記-壓縮)
CMS Concurrent Mark-Sweep(并發(fā)標(biāo)記清除)
Serial Coping(串行復(fù)制)
適合客戶端工作,不適合在服務(wù)器運(yùn)行,針對單 CPU,小新生代,不太在乎暫停時間的應(yīng)用,可通過- XX:+UseSerialGC
手動指定新生代使用 Serial Coping(串行復(fù)制)收集器,老年代使用 Serial Old (串行標(biāo)記 - 清理 - 壓縮)收集器執(zhí)行內(nèi)存回收。
ParNew(并發(fā)復(fù)制)
是 Serial Coping(串行復(fù)制)的多線程版本,在多 CPU 核情況下可以提高收集能力,但如果是單 CPU 條件下,還要來回切換任務(wù),不一定比 Serial Coping(串行復(fù)制)收集能力強(qiáng),通過- XX:+UseParNewGC
手動指定新生代使用 ParNew(并發(fā)復(fù)制)收集器,老年代使用 Serial Old (串行標(biāo)記 - 清理 - 壓縮)收集器執(zhí)行內(nèi)存回收。
Parallel Scavenge(并行復(fù)制)
跟 ParNew(并發(fā)復(fù)制)相比更注重于吞吐量而不是低延遲,如果吞吐量優(yōu)先,必然會降低 GC 的頻次,也就造成 GC 回收垃圾量更多、時間更長。如果低延遲優(yōu)先,為了降低每次的暫停時間,就得高頻的回收,這頻繁的回收又會導(dǎo)致吞吐量的下降,所以吐吞量和低延遲是對矛盾體,適合多 CPU、高 IO 密集操作、高計(jì)算消耗的應(yīng)用,通過XX:+UseParallelGC
手動指定新生代使用 Parallel Scavenge(并行復(fù)制)收集器,老年代使用 Serial Old (串行標(biāo)記 - 清理 - 壓縮)收集器執(zhí)行內(nèi)存回收。
Serial Old (串行標(biāo)記 - 清理 - 壓縮)
單線程串行回收,停頓時間長,可以使用
- XX:+PrintGCApplicationStoppedTime
查看暫停時間,適合客戶端使用,不會產(chǎn)生內(nèi)存碎片
Parallel Old(并行標(biāo)記 - 壓縮)
根據(jù) GC 線程數(shù)劃分若干區(qū)域(Region),并行做標(biāo)記,重新掃描,定位到需要壓縮的 Region,統(tǒng)計(jì) Region 里所有存活對象的下次要移動的目的地地址,然后并行的往一端壓縮,不產(chǎn)生內(nèi)存碎片,整理后的空閑區(qū)域是連續(xù)的,通過- XX:+UseParallelOldGC
手動指定新生代使用 Parallel Scavenge(并行復(fù)制)收集器,老年代使用 Parallel Old(并行標(biāo)記 - 壓縮)收集器執(zhí)行內(nèi)存回收。
CMS Concurrent Mark-Sweep(并發(fā)標(biāo)記清除)
第一階段是初始標(biāo)記,需要 Stop-the-world,這階段標(biāo)記出那些與根對象集合所連接的不可達(dá)的對象,標(biāo)記完就會被暫停的應(yīng)用線程;
第二階段是并發(fā)標(biāo)記,這階段是應(yīng)用線程和回收線程交替執(zhí)行,把第一步標(biāo)記為不可達(dá)的對象標(biāo)記為垃圾對象,由于是交替進(jìn)行,一開始被標(biāo)記為垃圾的對象,后面應(yīng)用線程可能更改對象的引用關(guān)系導(dǎo)致標(biāo)記錯誤;
所以第三階段重新標(biāo)記,需要 Stop-the-world,修正上個階段由于對象引用或者新對象創(chuàng)建導(dǎo)致的標(biāo)記錯誤,這階段只有回收線程執(zhí)行,確保修正的正確性。
經(jīng)過三個階段的標(biāo)記,第四個階段會并發(fā)的清除無有的對象釋放內(nèi)存,這階段是應(yīng)用線程和回收線程交替執(zhí)行,如果用戶應(yīng)用線程產(chǎn)生了新的垃圾(浮動垃圾),只能留到下次 GC 進(jìn)行回收,極端情況如果產(chǎn)生的新的垃圾,而老年代的預(yù)留空間又不夠,就會產(chǎn)生 Concurrent Mode Failure,這個時候只能通過后備的 Serial Old (串行標(biāo)記 - 清理 - 壓縮)來進(jìn)行垃圾回收。
又因?yàn)?CMS 并沒有用到壓縮算法,回收后會產(chǎn)生內(nèi)存碎片,為新對象分配內(nèi)存無法使用 Bump-the-pointer(指針碰撞)技術(shù)實(shí)現(xiàn)快速內(nèi)存分配,只能使用空閑列表(Free List :JVM 會維護(hù)一張可用內(nèi)存地址的列表,當(dāng)需要分配空間,就從列表搜索一段和對象大小一樣的連續(xù)內(nèi)存塊用于存放要生成的對象實(shí)例)方式分配內(nèi)存。
但也可以通過- XX:CMSFullGCsBeforeCompaction
,用于指定經(jīng)過多少次 Full GC 后對內(nèi)存碎片整理壓縮,由于內(nèi)存碎片不是并發(fā)執(zhí)行,會帶來更長的停頓時間,通過- XX:+UseConcMarkSweepGC
設(shè)定新生代使用 ParNew(并發(fā)復(fù)制)收集器,老年代使用 CMS Concurrent Mark-Sweep(并發(fā)標(biāo)記清除)收集器執(zhí)行內(nèi)存回收,當(dāng)出現(xiàn)浮動垃圾導(dǎo)致 Concurrent Mode Failure 或者新對象分配內(nèi)存失敗時,通過備用組合新生代使用 ParNew(并發(fā)復(fù)制)收集器,老年代使用 Serial Old (串行標(biāo)記 - 清理 - 壓縮)收集器執(zhí)行內(nèi)存回收,適用于要求暫停時間短,追求快速響應(yīng)的應(yīng)用,如互聯(lián)網(wǎng)應(yīng)用。
JVM回收需要注意的點(diǎn):
在執(zhí)行 Minor GC 的時候,JVM 會檢查老年代中最大連續(xù)可用空間是否大于了當(dāng)前新生代所有對象的總大小,如果大于,則直接執(zhí)行 Minor GC;
如果小于了,JVM 會檢查是否開啟了空間分配擔(dān)保機(jī)制;如果開啟了,則 JVM 會檢查老年代中最大連續(xù)可用空間是否大于了歷次晉升到老年代中的平均大?。?/span>
如果大于則會執(zhí)行 Minor GC,如果小于則執(zhí)行改為執(zhí)行 Full GC,如果沒有開啟則直接改為執(zhí)行 Full GC。
當(dāng)老年代(Major GC)和永久代發(fā)生 GC 時,除了 CMS 外都會觸發(fā) Full GC,F(xiàn)ull GC 就是先按新生代 GC 方式進(jìn)行 Minor GC,再按照老年代的配置進(jìn)行 Major GC,包含對老年代和永久代進(jìn)行 GC,若 JVM 估計(jì) Minor GC 會產(chǎn)生晉升失敗,則會采用 Major GC 的配置進(jìn)行 Full GC。
如果 Minor GC 執(zhí)行失敗則會執(zhí)行 Full GC。
吞吐量:應(yīng)用運(yùn)行時間/總時間,暫停時間:每次 GC 造成的暫停
分區(qū)分代增量式式收集器:G1(Garbage-First)收集器
傳統(tǒng)的分代收集也提供了并發(fā)收集,但最致命的是分代收集把整個堆空間劃分成固定間隔的內(nèi)存塊,每次收集都很容易觸發(fā) Full GC 從而掃描整個堆空間,這會拖慢應(yīng)用,而且要對整個堆空間都做內(nèi)存碎片整理會很麻煩。
而增量式的收集方式是一種新的收集思想,增量收集把堆空間劃分成一系列的小內(nèi)存塊(內(nèi)存塊大小可配置),使用時只使用部分內(nèi)存塊,等這部分內(nèi)存塊空間不足時,再把存活對象移動到未被使用過的內(nèi)存塊,避免整個堆用完了再 Full GC,可以一邊使用內(nèi)存一邊收集垃圾。
G1 收集器將整個 Java 堆區(qū)分成約 2048 大小相同的 Region 塊(分新生 Region 塊、幸存 Region 塊、老年 Region 塊),Region 塊大小在 1MB 到 32MB 之間,每個對象會被分配到 Region 塊里,既可以被塊內(nèi)對象引用也可以被塊外對象引用,在判斷對象是否存活時,為了避免全堆掃描或者遺漏,是通過 Remembered Set 來檢查 Reference 引用的對象是否存在不同的 Region 塊中的。G1 在收集垃圾時,會對各個 Region 塊的回收價值和成本做排序,根據(jù)用戶配置的期望停頓時間來進(jìn)行回收。
G1 收集器與 CMS 收集器執(zhí)行過程類似。初始標(biāo)記階段,Stop-the-World,標(biāo)記 GC Roots 可直接訪問到的對象,為下一個階段并發(fā)標(biāo)記時,和應(yīng)用線程交替執(zhí)行時,有正確可有的 Region 來分配新建對象,并發(fā)標(biāo)記階段識別上個階段標(biāo)記對象的下層對象的活躍狀態(tài),找出存活的對象,也就是標(biāo)記 GC Roots 可達(dá)對象;
最終標(biāo)記階段,Stop-the-World,修正上次線程交替執(zhí)行產(chǎn)生的變動;
清除復(fù)制階段,Stop-the-World,這階段并不是最終標(biāo)記執(zhí)行完了就一定執(zhí)行,畢竟是要 Stop-the-World,為了達(dá)到準(zhǔn)實(shí)時(可配置在 M 毫秒內(nèi)最多只占用 N 毫秒的時間進(jìn)行垃圾回收)會根據(jù)用戶配置的 GC 時間去決定是否做清除。
還有,因?yàn)榍宄龔?fù)制階段使用的是復(fù)制算法,每次清理都必須保證”to space” 空間是足夠的(將存活的對象復(fù)制到未使用的 Region 塊),所以只有已用空間達(dá)到了(1-h)*堆大?。╤ 是 G1 定義的一個堆空間的百分比閾值,是個固定值)才執(zhí)行清除,把存活的對象往一個方向移動到”to space” 并整理內(nèi)存,不會產(chǎn)生內(nèi)存碎片。
接著把”Eden space” “from space” 的垃圾對象清理,根據(jù)維護(hù)的優(yōu)先列表,優(yōu)先回收價值最大的 Region,通過五個階段完成垃圾收集,可以通過設(shè)定 - XX:UseG1GC 在整個 Java 堆使用 G1 進(jìn)行垃圾回收,G1 適合高吞吐、低延時、大堆空間的應(yīng)用。
32 位操作系統(tǒng)限制堆大小介于 1.5G 到 2G,64 位操作系統(tǒng)無限制,同時系統(tǒng)可用虛擬內(nèi)存,可用物理內(nèi)存都會限制最大堆的配置。
堆空間分配典型設(shè)置:
-Xms
:初始堆大小
-Xmx
:最大堆大小
-XX:NewSize=n
:設(shè)置年輕代大小
-XX:NewRatio=n
:設(shè)置年輕代和年老代的比值。如 n 為 2,表示年輕代與年老代比值為 1:2,年輕代占整個年輕代年老代和的 1/3
-XX:SurvivorRatio=n
:年輕代中 Eden 區(qū)與兩個 Survivor 區(qū)的比值。注意 Survivor 區(qū)有兩個。如 n 為 2,表示 Eden:Survivor=1:2,一個 Survivor 區(qū)占整個年輕代的 1/4
-XX:MaxPermSize=
:設(shè)置持久代大小
-Xmx5120m –Xms5120m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-Xmn2g
:設(shè)置年輕代大小為 2G。整個 JVM 內(nèi)存大小 = 年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為 64m,所以增大年輕代后,將會減小年老代大小。此值對系統(tǒng)性能影響較大,Sun 官方推薦配置為整個堆的 3/8。
-Xss128k
:設(shè)置每個線程的棧大小。JDK5.0 以后每個線程堆棧大小為 1M,以前每個線程堆棧大小為 256K。在相同物理內(nèi)存下,減小這個值能生成更多的線程。但是操作系統(tǒng)對一個進(jìn)程內(nèi)的線程數(shù)還是有限制的在 3000~5000 間。
-XX:SurvivorRatio=4
:設(shè)置年輕代中 Eden 區(qū)與 Survivor 區(qū)的大小比值。
-XX:MaxPermSize=16m
:設(shè)置持久代大小為 16m。
-XX:MaxTenuringThreshold=0
:設(shè)置年輕代最大年齡。如果設(shè)置為 0 的話,則年輕代對象不經(jīng)過 Survivor 區(qū),直接進(jìn)入年老代。對于年老代比較多的應(yīng)用,可以提高效率。如果將此值設(shè)置為一個較大值,則年輕代對象會在 Survivor 區(qū)進(jìn)行多次復(fù)制,這樣可以增加對象再年輕代的存活時間,增加在年輕代即被回收的概率。
-XX:+DisableExplicitGC
:這個將會忽略手動調(diào)用 GC 的代碼使得 System.gc() 的調(diào)用就會變成一個空調(diào)用,完全不會觸發(fā)任何 GC
......
本文未完
掃碼查看余下內(nèi)容