1.Java代碼是如何運(yùn)行起來(lái)的劲厌?
- 首先從".java"代碼文件,編譯成".class"字節(jié)碼文件米辐;
- 將".class"字節(jié)碼文件通過(guò)java -jar等方式打成jar包或者war包的形式;
- 然后類(lèi)加載器把".class"字節(jié)碼文件中的類(lèi)給加載到JVM中;
- 接著JVM執(zhí)行我們寫(xiě)好的那些類(lèi)中的代碼届良。
2.類(lèi)從加載到使用的幾個(gè)階段蚁趁?
一個(gè)類(lèi)從加載到到使用覆享,一般會(huì)經(jīng)歷下面的過(guò)程:
加載 -> 驗(yàn)證 -> 準(zhǔn)備 -> 解析 -> 初始化 -> 使用 -> 卸載
- 驗(yàn)證階段:簡(jiǎn)單來(lái)說(shuō),就是根據(jù)Java虛擬機(jī)規(guī)范浊伙,來(lái)校驗(yàn)?zāi)慵虞d進(jìn)來(lái)的"class"字節(jié)碼文件中的內(nèi)容样漆,是否符合指定的規(guī)范为障;
- 準(zhǔn)備階段:給加載進(jìn)來(lái)的類(lèi)分配一定的內(nèi)容空間,然后給它里面的類(lèi)變量(static修飾的變量)分配內(nèi)存空間放祟,并且賦予初始值鳍怨;
- 解析階段:把符號(hào)引用替換為直接引用;
-
初始化階段:執(zhí)行類(lèi)的初始化代碼跪妥;
3.什么時(shí)候會(huì)初始化一個(gè)類(lèi)鞋喇?
- 使用new 關(guān)鍵字實(shí)例化對(duì)象的時(shí)候、讀取或設(shè)置一個(gè)類(lèi)的靜態(tài)字段(被final修飾眉撵、一再編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候侦香,以及調(diào)用一個(gè)類(lèi)的靜態(tài)方法的時(shí)候落塑;
- 使用java.lang.reflect包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用的時(shí)候;
- 當(dāng)初始化一個(gè)類(lèi)的時(shí)候罐韩,如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行初始化憾赁,則需要先觸發(fā)其父類(lèi)的初始化;
注意:所有引用類(lèi)的方式都不會(huì)觸發(fā)初始化散吵,稱(chēng)為被動(dòng)引用龙考。
示例1:通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,不會(huì)導(dǎo)致子類(lèi)初始化
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class NotInitization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
執(zhí)行結(jié)果:
示例2:通過(guò)數(shù)組定義來(lái)引用類(lèi)矾睦,不會(huì)觸發(fā)此類(lèi)的初始化
public class NotInitization2 {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
執(zhí)行結(jié)果:
發(fā)現(xiàn)并沒(méi)有輸出"SuperClass init!"晦款。
示例3:常量在編譯階段會(huì)存入調(diào)用類(lèi)的常量池中,本質(zhì)上并沒(méi)有直接引用到定義常量的類(lèi)枚冗,因此不會(huì)觸發(fā)定義常量的類(lèi)的初始化
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitization3 {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
執(zhí)行結(jié)果:
也沒(méi)有輸出"ConstClass init!"缓溅。這是因?yàn)殡m然在Java源碼中引用了ConstClass類(lèi)中的常量HELLOWORLD,但其實(shí)在編譯階段通過(guò)常量傳播優(yōu)化赁温,已經(jīng)將此常量的值"hello world"存儲(chǔ)到NotInitization3類(lèi)的常量池中坛怪,以后NotInitization3對(duì)常量的引用實(shí)際上都被轉(zhuǎn)化為NotInitization3類(lèi)對(duì)自身常量池的引用了。
4.類(lèi)加載器與雙親委派機(jī)制束世?
Java的4中類(lèi)加載器:
- 啟動(dòng)類(lèi)加載器
BootStrap ClassLoader酝陈,他主要是負(fù)責(zé)加載我們?cè)跈C(jī)器上安裝的Java目錄下的核心類(lèi)的。在你的Java安裝目錄下毁涉,就有一個(gè)“l(fā)ib”目錄沉帮,這里就有Java最核心的一些類(lèi)庫(kù),支撐你的Java系統(tǒng)的運(yùn)行贫堰。所以一旦你的JVM啟動(dòng)穆壕,那么首先就會(huì)依托啟動(dòng)類(lèi)加載器,去加載你的Java安裝目錄下的“l(fā)ib”目錄中的核心類(lèi)庫(kù)其屏。 - 擴(kuò)展類(lèi)加載器
Extension ClassLoader喇勋,這個(gè)類(lèi)加載器其實(shí)也是類(lèi)似的,就是你的Java安裝目錄下偎行,有一個(gè)“l(fā)ib\ext”目錄川背,這里面有一些類(lèi),就是需要使用這個(gè)類(lèi)加載器來(lái)加載的蛤袒,支撐你的系統(tǒng)的運(yùn)行熄云。
那么你的JVM一旦啟動(dòng),是不是也得從Java安裝目錄下妙真,加載這個(gè)“l(fā)ib\ext”目錄中的類(lèi)缴允。 - 應(yīng)用程序類(lèi)加載器
Application ClassLoader,這類(lèi)加載器就負(fù)責(zé)去加載“ClassPath”環(huán)境變量所指定的路徑中的類(lèi)珍德,其實(shí)你大致就理解為去加載你寫(xiě)好的Java代碼吧练般,這個(gè)類(lèi)加載器就負(fù)責(zé)加載你寫(xiě)好的那些類(lèi)到內(nèi)存里矗漾。 - 自定義類(lèi)加載器
除了上面那幾種之外,還可以自定義類(lèi)加載器薄料,去根據(jù)你自己的需求加載你的類(lèi)敞贡。
自己實(shí)現(xiàn)一個(gè)類(lèi)加載器.
類(lèi)加載的雙親委派機(jī)制
假設(shè)你的應(yīng)用程序類(lèi)加載器需要加載一個(gè)類(lèi),他首先會(huì)委派給自己的父類(lèi)加載器去加載都办,最終傳導(dǎo)到頂層的類(lèi)加載器去加載嫡锌,但是如果父類(lèi)加載器在自己負(fù)責(zé)加載的范圍內(nèi),沒(méi)找到這個(gè)類(lèi)琳钉,那么就會(huì)下推加載權(quán)利給自己的子類(lèi)加載器。
5.Java虛擬機(jī)里有哪些內(nèi)存區(qū)域蛛倦,分別都是用來(lái)干什么的歌懒?
演示代碼:
public class Kafka {
public static void main(String[] args) {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.loadReplicasFromDisk();
}
}
public class ReplicaManager {
private long replicaCount;
public void loadReplicasFromDisk() {
Boolean hasFinished = false;
if (isLocalDataCorrupt()) {}
}
private boolean isLocalDataCorrupt() {
Boolean isCorrupt = false;
return isCorrupt;
}
}
演示代碼全流程示意圖:
- 首先,JVM啟動(dòng)溯壶,就會(huì)加載你的Kafka類(lèi)到內(nèi)存里及皂。然后有一個(gè)main線程,開(kāi)始執(zhí)行你的kafka中的main()方法且改。
- main線程是關(guān)聯(lián)了一個(gè)程序計(jì)數(shù)器的验烧,那么它執(zhí)行到那一條指令,就會(huì)記錄在這里又跛。
- 其次碍拆,main線程在執(zhí)行main()方法的時(shí)候,會(huì)在main線程關(guān)聯(lián)的Java虛擬機(jī)棧里慨蓝,壓入一個(gè)main()方法的棧幀感混。
- 接著會(huì)發(fā)現(xiàn)需要?jiǎng)?chuàng)建一個(gè)ReplicaManager類(lèi)的實(shí)例對(duì)象,此時(shí)會(huì)加載ReplicaManager類(lèi)到內(nèi)存里來(lái)礼烈。
- 然后會(huì)創(chuàng)建一個(gè)ReplicaManager的對(duì)象實(shí)例分配在Java堆內(nèi)存里弧满,并且在main()方法的棧幀里的局部變量表引入一個(gè)“replicaManager”變量,讓他引用ReplicaManager對(duì)象在Java堆內(nèi)存中的地址此熬。
- 接著庭呜,main線程開(kāi)始執(zhí)行ReplicaManager對(duì)象中的方法,會(huì)依次把自己執(zhí)行到的方法對(duì)應(yīng)的棧幀壓入自己的Java虛擬機(jī)棧犀忱,執(zhí)行完方法之后再把方法對(duì)應(yīng)的棧幀從Java虛擬機(jī)棧里出棧募谎。
7.tomcat的類(lèi)加載機(jī)制,打破了雙親委派機(jī)制
8.方法區(qū)內(nèi)的類(lèi)是否會(huì)被垃圾回收峡碉?
在以下幾種情況下近哟,方法區(qū)里的類(lèi)會(huì)被回收:
- 首先該類(lèi)的所有實(shí)例對(duì)象都已經(jīng)從Java堆內(nèi)存里被回收;
- 其次加載這個(gè)類(lèi)的ClassLoader已經(jīng)被回收鲫寄;
- 最后吉执,對(duì)該類(lèi)的Class對(duì)象沒(méi)有任何引用疯淫。
9.對(duì)象在JVM內(nèi)存中如何分配?如何流轉(zhuǎn)戳玫?
示例代碼:
public class Kafka {
private static ReplicaFetcher fetcher = new ReplicaFetcher();
public static void main(String[] args) throws InterruptedException {
loadReplicasFromDisk();
while (true) {
fetchReplicasFromRemote();
Thread.sleep(1000);
}
}
private static void loadReplicasFromDisk() {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.load();
}
private static void fetchReplicasFromRemote() {
fetcher.fetch();
}
}
public class ReplicaFetcher {
public void fetch() {
}
}
public class ReplicaManager {
public void load() {
}
}
-
實(shí)例對(duì)象和靜態(tài)對(duì)象都會(huì)先分配在新生代
-
一旦“l(fā)oadReplicasFromDisk()”方法執(zhí)行完畢之后熙掺,這個(gè)方法的棧幀出棧,會(huì)導(dǎo)致沒(méi)有任何局部變量引用那個(gè)“ReplicaManager”實(shí)例對(duì)象了
-
我們會(huì)在新生代里分配大量的對(duì)象咕宿,但是使用完之后立馬就沒(méi)人引用了币绩,此時(shí)新生代差不多滿(mǎn)了然后要分配新的對(duì)象的時(shí)候,發(fā)現(xiàn)新生代內(nèi)存空間不足府阀,就會(huì)觸發(fā)一次垃圾回收(young gc)缆镣,然后就把所有垃圾對(duì)象給干掉,騰出大量的內(nèi)存空間
-
實(shí)例對(duì)象在新生代中试浙,成功的在15次垃圾回收之后董瞻,還是沒(méi)被回收掉,就說(shuō)明他已經(jīng)15歲了田巴。這是對(duì)象的年齡钠糊,每垃圾回收一次,如果一個(gè)對(duì)象沒(méi)被回收掉壹哺,他的年齡就會(huì)增加1抄伍。所以如果上圖中的那個(gè)“ReplicaFetcher”對(duì)象在新生代中成功躲過(guò)10多次垃圾回收,成為一個(gè)“老年人”管宵,那么就會(huì)被認(rèn)為是會(huì)長(zhǎng)期存活在內(nèi)存里的對(duì)象截珍。然后他會(huì)被轉(zhuǎn)移到Java堆內(nèi)存的老年代中去
流程總結(jié):
- 對(duì)象優(yōu)先分配在新生代;
- 新生代如果對(duì)象滿(mǎn)了啄糙,會(huì)觸發(fā)Minor GC回收掉沒(méi)有人引用的垃圾對(duì)象笛臣;
- 如果有對(duì)象躲過(guò)了十多次垃圾回收,就會(huì)放入老年代里隧饼;
- 如果老年代也滿(mǎn)了沈堡,那么也會(huì)觸發(fā)垃圾回收,把老年代里沒(méi)人引用的垃圾對(duì)象清理掉燕雁。
10.JVM內(nèi)存相關(guān)的核心參數(shù)圖解及如何設(shè)置JVM參數(shù)
-Xms:Java堆內(nèi)存的大小
-Xmx:Java堆內(nèi)存的最大大小
-Xmn:Java堆內(nèi)存中的新生代大小诞丽,扣除新生代剩下的就是老年代的內(nèi)存大小了
-XX:PermSize:永久代大小
-XX:MaxPermSize:永久代最大大小
-Xss:每個(gè)線程的棧內(nèi)存大小
如何在啟動(dòng)系統(tǒng)的時(shí)候設(shè)置JVM參數(shù)?
-
在IDEA中設(shè)置如何設(shè)置JVM參數(shù)
- 啟動(dòng)一個(gè)jar包里的系統(tǒng)如何設(shè)置JVM參數(shù)
java -Xms512M -Xmx512M -Xmn256M -Xss1M -XX:PermSize=128M -XX:MaxPermSize=128M -jar App.jar - Tomcat部署系統(tǒng)怎么設(shè)置JVM參數(shù)
tomcat就是在bin目錄下的catalina.sh中的頭部可以加入JVM參數(shù):
JAVA_OPTS="$JAVA_OPTS -server -Xms4096m -Xmx6144m -XX:PermSize=256m -XX:MaxPermSize=2048m"
11.被哪些變量引用的對(duì)象是不能回收的拐格?
JVM中使用了一種可達(dá)性分析算法來(lái)判定哪些對(duì)象是可以被回收的僧免,哪些對(duì)象是不可以被回收的。這個(gè)算法的意思捏浊,就是說(shuō)對(duì)每個(gè)對(duì)象懂衩,都分析一下有誰(shuí)在引用他,然后一層一層往上去判斷,看是否有一個(gè)GC Roots浊洞。方法的局部變量牵敷、類(lèi)的靜態(tài)變量都是GC Roots,所以法希,只要你的對(duì)象被方法的局部變量枷餐、類(lèi)的靜態(tài)變量給引用了,就不會(huì)回收他們苫亦。
Java里有不同的引用類(lèi)型毛肋,分別是強(qiáng)引用、軟引用屋剑、弱引用和虛引用润匙。
只要是強(qiáng)引用的類(lèi)型穿香,那么垃圾回收的時(shí)候絕對(duì)不會(huì)去回收這個(gè)對(duì)象的袍榆。
正常情況下垃圾回收是不會(huì)回收軟引用對(duì)象的宾尚,但是如果你進(jìn)行垃圾回收之后隆檀,發(fā)現(xiàn)內(nèi)存空間還是不夠存放新的對(duì)象,內(nèi)存都快溢出了勤篮,此時(shí)就會(huì)把這些軟引用對(duì)象給回收掉,哪怕他被變量引用了,但是因?yàn)樗擒浺玫渑牵赃€是要回收。
弱引用就跟沒(méi)引用是類(lèi)似的益咬,如果發(fā)生垃圾回收逮诲,就會(huì)把這個(gè)對(duì)象回收掉。
虛引用幽告,這個(gè)大家其實(shí)暫時(shí)忽略他也行梅鹦,因?yàn)楹苌儆谩?br>
其實(shí)這里比較常用的,就是強(qiáng)引用和軟引用冗锁,強(qiáng)引用就是代表絕對(duì)不能回收的對(duì)象齐唆,軟引用就是說(shuō)有的對(duì)象可有可無(wú),如果內(nèi)存實(shí)在不夠了冻河,可以回收他箍邮。
finalize()方法的作用
假設(shè)沒(méi)有GC Roots引用的對(duì)象,是一定立馬被回收嗎叨叙?其實(shí)不是的锭弊,這里有一個(gè)finalize()方法可以拯救他自己,看下面的代碼擂错。
假設(shè)有一個(gè)ReplicaManager對(duì)象要被垃圾回收了味滞,那么假如這個(gè)對(duì)象重寫(xiě)了Object類(lèi)中的finialize()方法,此時(shí)會(huì)先嘗試調(diào)用一下他的finalize()方法,看是否把自己這個(gè)實(shí)例對(duì)象給了某個(gè)GC Roots變量剑鞍,比如說(shuō)代碼中就給了ReplicaManager類(lèi)的靜態(tài)變量昨凡。如果重新讓某個(gè)GC Roots變量引用了自己,那么就不用被垃圾回收了攒暇。不過(guò)說(shuō)實(shí)話土匀,這個(gè)東西沒(méi)必要過(guò)多解讀,因?yàn)槠鋵?shí)平時(shí)很少用形用,就是給大家梳理出來(lái)這些細(xì)節(jié)就轧,讓大家清楚而已。
12.新生代進(jìn)行垃圾回收的算法
-
一種不太好的垃圾回收思路—標(biāo)記-清除算法
標(biāo)記出哪些對(duì)象是可以被垃圾回收的田度,然后就直接對(duì)那塊內(nèi)存區(qū)域中的對(duì)象進(jìn)行垃圾回收妒御,把內(nèi)存空出來(lái)。
缺點(diǎn):形成大量內(nèi)存碎片镇饺,造成內(nèi)存浪費(fèi)乎莉,可能因?yàn)閮?nèi)存碎片太多的緣故,雖然所有的內(nèi)存碎片加起來(lái)其實(shí)有很大的一塊內(nèi)存奸笤,但是因?yàn)檫@些內(nèi)存都是碎片式分散的惋啃,所以導(dǎo)致沒(méi)有一塊完整的足夠的內(nèi)存空間來(lái)分配新的對(duì)象。
一種合理的垃圾回收思路—復(fù)制算法
所謂的“復(fù)制算法“监右,把新生代內(nèi)存劃分為兩塊內(nèi)存區(qū)域边灭,然后只使用其中一塊內(nèi)存待那塊內(nèi)存快滿(mǎn)的時(shí)候,就把里面的存活對(duì)象一次性轉(zhuǎn)移到另外一塊內(nèi)存區(qū)域健盒,保證沒(méi)有內(nèi)存碎片绒瘦,接著一次性回收原來(lái)那塊內(nèi)存區(qū)域的垃圾對(duì)象,再次空出來(lái)一塊內(nèi)存區(qū)域扣癣。兩塊內(nèi)存區(qū)域就這么重復(fù)著循環(huán)使用惰帽。
缺點(diǎn):從始至終,就只有一半的內(nèi)存可以用父虑,這樣的算法顯然對(duì)內(nèi)存的使用效率太低了该酗。
-
復(fù)制算法的優(yōu)化:Eden區(qū)和Survivor區(qū)
把新生代內(nèi)存區(qū)域劃分為三塊:1個(gè)Eden區(qū),2個(gè)Survivor區(qū)频轿,其中Eden區(qū)占80%內(nèi)存空間垂涯,每一塊Survivor區(qū)各占10%內(nèi)存空間,比如說(shuō)Eden區(qū)有800MB內(nèi)存航邢,每一塊Survivor區(qū)就100MB內(nèi)存耕赘。平時(shí)可以使用的,就是Eden區(qū)和其中一塊Survivor區(qū)膳殷,那么相當(dāng)于就是有900MB的內(nèi)存是可以使用的操骡。
但是剛開(kāi)始對(duì)象都是分配在Eden區(qū)內(nèi)的九火,如果Eden區(qū)快滿(mǎn)了,此時(shí)就會(huì)觸發(fā)垃圾回收册招,此時(shí)就會(huì)把Eden區(qū)中的存活對(duì)象都一次性轉(zhuǎn)移到一塊空著的Survivor區(qū)岔激。接著Eden區(qū)就會(huì)被清空,然后再次分配新對(duì)象到Eden區(qū)里是掰,然后就會(huì)如上圖所示虑鼎,Eden區(qū)和一塊Survivor區(qū)里是有對(duì)象的,其中Survivor區(qū)里放的是上一次Minor GC后存活的對(duì)象键痛。如果下次再次Eden區(qū)滿(mǎn)炫彩,那么再次觸發(fā)Minor GC,就會(huì)把Eden區(qū)和放著上一次Minor GC后存活對(duì)象的Survivor區(qū)內(nèi)的存活對(duì)象絮短,轉(zhuǎn)移到另外一塊Survivor區(qū)去江兢。
接著新對(duì)象繼續(xù)分配在Eden區(qū)和另外那塊開(kāi)始被使用的Survivor區(qū),然后始終保持一塊Survivor區(qū)是空著的丁频,就這樣一直循環(huán)使用這三塊內(nèi)存區(qū)域杉允。
這么做最大的好處,就是只有10%的內(nèi)存空間是被閑置的席里,90%的內(nèi)存都被使用上了無(wú)論是垃圾回收的性能叔磷,內(nèi)存碎片的控制,還是說(shuō)內(nèi)存使用的效率奖磁,都非常的好世澜。
13.對(duì)象是如何進(jìn)入老年代的?
1. 躲過(guò)15次GC之后進(jìn)入老年代
對(duì)象在新生代里躲過(guò)一次GC被轉(zhuǎn)移到一塊Survivor區(qū)域中署穗,此時(shí)他的年齡就會(huì)增長(zhǎng)一歲,默認(rèn)的設(shè)置下嵌洼,當(dāng)對(duì)象的年齡達(dá)到15歲的時(shí)候案疲,也就是躲過(guò)15次GC的時(shí)候,他就會(huì)轉(zhuǎn)移到老年代里去麻养。
這個(gè)具體是多少歲進(jìn)入老年代褐啡,可以通過(guò)JVM參數(shù)“-XX:MaxTenuringThreshold”來(lái)設(shè)置,默認(rèn)是15歲鳖昌。
2. 動(dòng)態(tài)對(duì)象年齡判斷
他的大致規(guī)則就是备畦,假如說(shuō)當(dāng)前放對(duì)象的Survivor區(qū)域里,一批對(duì)象的總大小大于了這塊Survivor區(qū)域的內(nèi)存大小的50%许昨,那么此時(shí)大于等于這批對(duì)象年齡的對(duì)象懂盐,就可以直接進(jìn)入老年代了。
實(shí)際這個(gè)規(guī)則運(yùn)行的時(shí)候是如下的邏輯:年齡1+年齡2+年齡n的多個(gè)年齡對(duì)象總和超過(guò)了Survivor區(qū)域的50%糕档,此時(shí)就會(huì)把年齡n以上的對(duì)象都放入老年代莉恼。
3. 大對(duì)象直接進(jìn)入老年代
有一個(gè)JVM參數(shù),就是“-XX:PretenureSizeThreshold”,可以把他的值設(shè)置為字節(jié)數(shù)俐银,比如“1048576”字節(jié)尿背,就是1MB。他的意思就是捶惜,如果你要?jiǎng)?chuàng)建一個(gè)大于這個(gè)大小的對(duì)象田藐,比如一個(gè)超大的數(shù)組,或者是別的啥東西吱七,此時(shí)就直接把這個(gè)大對(duì)象放到老年代里去汽久。壓根兒不會(huì)經(jīng)過(guò)新生代。之所以這么做陪捷,就是要避免新生代里出現(xiàn)那種大對(duì)象回窘,然后屢次躲過(guò)GC,還得把他在兩個(gè)Survivor區(qū)域里來(lái)回復(fù)制多次之后才能進(jìn)入老年代市袖,那么大的一個(gè)對(duì)象在內(nèi)存里來(lái)回復(fù)制啡直,不是很耗費(fèi)時(shí)間嗎?
4. Minor GC后的對(duì)象太多無(wú)法放入Survivor區(qū)直接轉(zhuǎn)移到老年代
14.老年代空間分配擔(dān)保規(guī)則(老年代何時(shí)進(jìn)行垃圾回收
)苍碟?
在執(zhí)行任何一次Minor GC之前酒觅,JVM會(huì)先檢查一下老年代可用的可用內(nèi)存空間,是否大于新生代所有對(duì)象的總大小微峰。如果說(shuō)發(fā)現(xiàn)老年代的內(nèi)存大小是大于新生代所有對(duì)象的舷丹,此時(shí)就可以放心大膽的對(duì)新生代發(fā)起一次Minor GC了,因?yàn)榧词筂inor GC之后所有對(duì)象都存活蜓肆,Survivor區(qū)放不下了颜凯,也可以轉(zhuǎn)移到老年代去。
假如Minor GC之前仗扬,發(fā)現(xiàn)老年代的可用內(nèi)存已經(jīng)小于了新生代的全部對(duì)象大小了症概,就會(huì)看一個(gè)“-XX:-HandlePromotionFailure”的參數(shù)是否設(shè)置了,如果有這個(gè)參數(shù)早芭,那么就會(huì)繼續(xù)嘗試進(jìn)行下一步判斷彼城。
下一步判斷,就是看看老年代的內(nèi)存大小退个,是否大于之前每一次Minor GC后進(jìn)入老年代的對(duì)象的平均大小募壕。
如果上面那個(gè)步驟判斷失敗了,或者是“-XX:-HandlePromotionFailure”參數(shù)沒(méi)設(shè)置语盈,此時(shí)就會(huì)直接觸發(fā)一次“Full GC”舱馅,就是對(duì)老年代進(jìn)行垃圾回收,盡量騰出來(lái)一些內(nèi)存空間刀荒,然后再執(zhí)行Minor GC习柠。
如果上面兩個(gè)步驟都判斷成功了匀谣,那么就是說(shuō)可以冒點(diǎn)風(fēng)險(xiǎn)嘗試一下Minor GC。此時(shí)進(jìn)行Minor GC有幾種可能资溃。
第一種可能武翎,Minor GC過(guò)后,剩余的存活對(duì)象的大小宝恶,是小于Survivor區(qū)的大小的,那么此時(shí)存活對(duì)象進(jìn)入Survivor區(qū)域即可趴捅。
第二種可能垫毙,Minor GC過(guò)后,剩余的存活對(duì)象的大小拱绑,是大于 Survivor區(qū)域的大小综芥,但是是小于老年代可用內(nèi)存大小的,此時(shí)就直接進(jìn)入老年代即可。
第三種可能,很不幸睦焕,Minor GC過(guò)后,剩余的存活對(duì)象的大小额各,大于了Survivor區(qū)域的大小,也大于了老年代可用內(nèi)存的大小吧恃。此時(shí)老年代都放不下這些存活對(duì)象了虾啦,就會(huì)發(fā)生“Handle Promotion Failure”的情況,這個(gè)時(shí)候就會(huì)觸發(fā)一次“Full GC”痕寓。
Full GC就是對(duì)老年代進(jìn)行垃圾回收傲醉,同時(shí)也一般會(huì)對(duì)新生代進(jìn)行垃圾回收。因?yàn)檫@個(gè)時(shí)候必須得把老年代里的沒(méi)人引用的對(duì)象給回收掉呻率,然后才可能讓Minor GC過(guò)后剩余的存活對(duì)象進(jìn)入老年代里面需频。
如果要是Full GC過(guò)后,老年代還是沒(méi)有足夠的空間存放Minor GC過(guò)后的剩余存活對(duì)象筷凤,那么此時(shí)就會(huì)導(dǎo)致所謂的“OOM”內(nèi)存溢出了。
簡(jiǎn)單來(lái)說(shuō)苞七,一句話總結(jié)藐守,對(duì)老年代觸發(fā)垃圾回收的時(shí)機(jī),一般就是兩個(gè):
要不然是在Minor GC之前蹂风,一通檢查發(fā)現(xiàn)很可能Minor GC之后要進(jìn)入老年代的對(duì)象太多了卢厂,老年代放不下,此時(shí)需要提前觸發(fā)Full GC然后再帶著進(jìn)行Minor GC惠啄;要不然是在Minor GC之后慎恒,發(fā)現(xiàn)剩余對(duì)象太多放入老年代都放不下了任内。
15.老年代垃圾回收算法—標(biāo)記整理算法
首先標(biāo)記出來(lái)老年代當(dāng)前存活的對(duì)象,這些對(duì)象可能是東一個(gè)西一個(gè)的融柬。
接著會(huì)讓這些存活對(duì)象在內(nèi)存里進(jìn)行移動(dòng)死嗦,把存活對(duì)象盡量都挪動(dòng)到一邊去,讓存活對(duì)象緊湊的靠在一起粒氧,避免垃圾回收過(guò)后出現(xiàn)過(guò)多的內(nèi)存碎片越除,然后再一次性把垃圾對(duì)象都回收掉。
老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍外盯。如果系統(tǒng)頻繁出現(xiàn)老年代的Full GC垃圾回收摘盆,會(huì)導(dǎo)致系統(tǒng)性能被嚴(yán)重影響,出現(xiàn)頻繁卡頓的情況饱苟。
所謂JVM優(yōu)化孩擂,就是盡可能讓對(duì)象都在新生代里分配和回收,盡量別讓太多對(duì)象頻繁進(jìn)入老年代箱熬,避免頻繁對(duì)老年代進(jìn)行垃圾回收类垦,同時(shí)給系統(tǒng)充足的內(nèi)存大小,避免新生代頻繁的進(jìn)行垃圾回收坦弟。
老年代為什么不采用復(fù)制算法护锤?
老年代存活對(duì)象太多了,如果采用復(fù)制算法酿傍,每次都挪動(dòng)可能90%的存活對(duì)象烙懦,這就不合適了。所以采用先把存活對(duì)象挪動(dòng)到一起緊湊一些赤炒,然后回收垃圾對(duì)象的方式氯析。
16.JVM的痛點(diǎn):Stop the World
平時(shí)使用JVM最大的痛點(diǎn),其實(shí)就是在垃圾回收的這個(gè)過(guò)程莺褒,因?yàn)樵诶厥盏臅r(shí)候掩缓,盡可能要讓垃圾回收器專(zhuān)心致志的干工作,不能隨便讓我們寫(xiě)的Java系統(tǒng)繼續(xù)對(duì)象了遵岩,所以此時(shí)JVM會(huì)在后臺(tái)
直接進(jìn)入“Stop the World”狀態(tài)你辣。也就是說(shuō),他會(huì)直接停止我們寫(xiě)的Java系統(tǒng)的所有工作線程尘执,讓我們寫(xiě)的代碼不再運(yùn)行舍哄!然后讓垃圾回收線程可以專(zhuān)心致志的進(jìn)行垃圾回收的工作。接著一旦垃圾回收完畢誊锭,就可以繼續(xù)恢復(fù)我們寫(xiě)的Java系統(tǒng)的工作線程的運(yùn)行了表悬,然后我們的那些代碼就可以繼續(xù)運(yùn)行。
現(xiàn)在就很清晰“Stop the World”會(huì)對(duì)系統(tǒng)造成的影響了丧靡, 假設(shè)我們的Minor GC要運(yùn)行100ms蟆沫,那么可能就會(huì)導(dǎo)致我們的系統(tǒng)直接停頓100ms不能處理任何請(qǐng)求籽暇,在這100ms期間用戶(hù)發(fā)起的所有請(qǐng)求都會(huì)出現(xiàn)短暫的卡頓,因?yàn)橄到y(tǒng)的工作線程不在運(yùn)行饭庞,不能處理請(qǐng)求戒悠。
假設(shè)你開(kāi)發(fā)的是一個(gè)Web系統(tǒng),那么可能導(dǎo)致你的用戶(hù)從網(wǎng)頁(yè)或者APP上點(diǎn)擊一個(gè)按鈕但绕,然后平時(shí)只要幾十ms就可以返回響應(yīng)了救崔,現(xiàn)在因?yàn)槟愕腤eb系統(tǒng)的JVM正在執(zhí)行Minor GC,暫停了所有的工作線程捏顺,導(dǎo)致你的請(qǐng)求過(guò)來(lái)到響應(yīng)返回六孵,這次需要等待幾百毫秒。
17.最常用的新生代垃圾回收器:ParNew
新生代的ParNew垃圾回收器主打的就是多線程垃圾回收機(jī)制幅骄,另外一種Serial垃圾回收器主打的是單線程垃圾回收劫窒,他們倆都是回收新生代的,唯一的區(qū)別就是單線程和多線程的區(qū)別拆座,但是垃圾回收算法是完全一樣的主巍。
在啟動(dòng)系統(tǒng)的時(shí)候如果要指定使用ParNew垃圾回收器,是用什么參數(shù)呢挪凑?
使用“-XX:+UseParNewGC”選項(xiàng)孕索,只要加入這個(gè)選項(xiàng),JVM啟動(dòng)之后對(duì)新生代進(jìn)行垃圾回收的躏碳,就是ParNew垃圾回收器了搞旭。
ParNew垃圾回收器默認(rèn)情況下的線程數(shù)量
一旦指定了使用ParNew垃圾回收器之后,他默認(rèn)給自己設(shè)置的垃圾回收線程的數(shù)量就是跟CPU的核數(shù)是一樣的菇绵。
但是如果你一定要自己調(diào)節(jié)ParNew的垃圾回收線程數(shù)量肄渗,也是可以的,使用“-XX:ParallelGCThreads”參數(shù)即可咬最,通過(guò)他可以設(shè)置線程的數(shù)量翎嫡,但是建議一般不要隨意動(dòng)這個(gè)參數(shù)。
18.老年代垃圾回收器CMS
CMS在執(zhí)行一次垃圾回收的過(guò)程一共分為4個(gè)階段:
初始標(biāo)記永乌、并發(fā)標(biāo)記惑申、重新標(biāo)記、并發(fā)清理
- 所謂的“初始標(biāo)記”翅雏,就是標(biāo)記出來(lái)所有GC Roots直接引用的對(duì)象圈驼,這個(gè)階段會(huì)讓系統(tǒng)的工作線程全部停止,進(jìn)入“Stop the World”狀態(tài)枚荣。雖然說(shuō)要造成“Stop the World”暫停一切工作線程,但是其實(shí)影響不大啼肩,因?yàn)樗乃俣群芸扉献保瑑H僅標(biāo)記GC Roots直接引用的那些對(duì)象罷了衙伶。
-
第二個(gè)階段,就是對(duì)老年代所有對(duì)象進(jìn)行GC Roots追蹤害碾,其實(shí)是最耗時(shí)的矢劲,他需要追蹤所有對(duì)象是否從根源上被GC Roots引用了,但是這個(gè)最耗時(shí)的階段慌随,是跟系統(tǒng)程序并發(fā)運(yùn)行的芬沉,系統(tǒng)程序會(huì)不停的工作,他可能會(huì)各種創(chuàng)建出來(lái)新的對(duì)象阁猜,部分對(duì)象可能成為垃圾丸逸,其實(shí)這個(gè)階段不會(huì)對(duì)系統(tǒng)運(yùn)行造成影響的。
-
在第二階段里剃袍,你一邊標(biāo)記存活對(duì)象和垃圾對(duì)象黄刚,一邊系統(tǒng)在不停運(yùn)行創(chuàng)建新對(duì)象,讓老對(duì)象變成垃圾所以第二階段結(jié)束之后民效,絕對(duì)會(huì)有很多存活對(duì)象和垃圾對(duì)象憔维,是之前第二階段沒(méi)標(biāo)記出來(lái)的。所以此時(shí)進(jìn)入第三階段畏邢,要繼續(xù)讓系統(tǒng)程序停下來(lái)业扒,再次進(jìn)入“Stop the World”階段。然后重新標(biāo)記下在第二階段里新創(chuàng)建的一些對(duì)象舒萎,還有一些已有對(duì)象可能失去引用變成垃圾的情況
- 并發(fā)清理這個(gè)階段就是讓系統(tǒng)程序隨意運(yùn)行程储,然后他來(lái)清理掉之前標(biāo)記為垃圾的對(duì)象即可。這個(gè)階段其實(shí)是很耗時(shí)的逆甜,因?yàn)樾枰M(jìn)行對(duì)象的清理虱肄,但是他也是跟系統(tǒng)程序并發(fā)運(yùn)行的,所以其實(shí)也不影響系統(tǒng)程序的執(zhí)行交煞。
對(duì)CMS的垃圾回收機(jī)制進(jìn)行性能分析
其實(shí)大家看完CMS的垃圾回收機(jī)制之后咏窿,就會(huì)發(fā)現(xiàn),他已經(jīng)盡可能的進(jìn)行了性能優(yōu)化了素征。因?yàn)樽詈臅r(shí)的集嵌,其實(shí)就是對(duì)老年代全部對(duì)相關(guān)進(jìn)行GC Roots追蹤,標(biāo)記出來(lái)到底哪些可以回收御毅,然后就是對(duì)各種垃圾對(duì)象從內(nèi)存里清理掉根欧,這是最耗時(shí)的。但是他的第二階段和第四階段端蛆,都是和系統(tǒng)程序并發(fā)執(zhí)行的凤粗,所以基本這兩個(gè)最耗時(shí)的階段對(duì)性能影響不大。只有第一個(gè)階段和第三個(gè)階段是需要“Stop the World”的今豆,但是這兩個(gè)階段都是簡(jiǎn)單的標(biāo)記而已嫌拣,速度非常的快柔袁,所以基本上對(duì)系統(tǒng)運(yùn)行響應(yīng)也不大。
CMS的垃圾回收的問(wèn)題以及參數(shù)設(shè)置
1.CPU資源緊張
CMS默認(rèn)啟動(dòng)的垃圾回收線程的數(shù)量是(CPU核數(shù) + 3)/ 4异逐。我們用最普通的2核4G機(jī)器和4核8G機(jī)器來(lái)計(jì)算一下捶索,假設(shè)是2核CPU,本來(lái)CPU資源就有限灰瞻,結(jié)果此時(shí)CMS還會(huì)有個(gè)“(2 + 3) / 4”= 1個(gè)垃圾回收線程腥例,去占用寶貴的一個(gè)CPU。所以其實(shí)CMS這個(gè)并發(fā)垃圾回收的機(jī)制酝润,第一個(gè)問(wèn)題就是會(huì)消耗CPU資源燎竖。
2.Concurrent Mode Failure問(wèn)題
在并發(fā)清理階段,CMS只不過(guò)是回收之前標(biāo)記好的垃圾對(duì)象袍祖,但是這個(gè)階段系統(tǒng)一直在運(yùn)行底瓣,可能會(huì)隨著系統(tǒng)運(yùn)行讓一些對(duì)象進(jìn)入老年代,同時(shí)還變成垃圾對(duì)象蕉陋,這種垃圾對(duì)象是“浮動(dòng)垃圾”捐凭。大家看下圖那個(gè)紅圈畫(huà)的地方,那個(gè)對(duì)象就是在并發(fā)清理期間凳鬓,系統(tǒng)程序可能先把某些對(duì)象分配在新生代茁肠,然后可能觸發(fā)了一次Minor GC,一些對(duì)象進(jìn)入了老年代缩举,然后短時(shí)間內(nèi)又沒(méi)人引用這些對(duì)象了垦梆。
因?yàn)樗m然成為了垃圾,但是CMS只能回收之前標(biāo)記出來(lái)的垃圾對(duì)象仅孩,不會(huì)回收他們托猩,需要等到下一次GC的時(shí)候才會(huì)回收他們。所以為了保證在CMS垃圾回收期間,還有一定的內(nèi)存空間讓一些對(duì)象可以進(jìn)入老年代,一般會(huì)預(yù)留一些空間彻舰。CMS垃圾回收的觸發(fā)時(shí)機(jī)滚躯,其中有一個(gè)就是當(dāng)老年代內(nèi)存占用達(dá)到一定比例了孵构,就自動(dòng)執(zhí)行GC。
“-XX:CMSInitiatingOccupancyFaction”參數(shù)可以用來(lái)設(shè)置老年代占用多少比例的時(shí)候觸發(fā)CMS垃圾回收,JDK 1.6里面默認(rèn)的值是92%。也就是說(shuō)欠气,老年代占用了92%空間了,就自動(dòng)進(jìn)行CMS垃圾回收镜撩,預(yù)留8%的空間給并發(fā)回收期間预柒,系統(tǒng)程序把一些新對(duì)象放入老年代中。
那么如果CMS垃圾回收期間,系統(tǒng)程序要放入老年代的對(duì)象大于了可用內(nèi)存空間宜鸯,此時(shí)會(huì)如何人灼?
這個(gè)時(shí)候,會(huì)發(fā)生Concurrent Mode Failure顾翼,就是說(shuō)并發(fā)垃圾回收失敗了,我一邊回收奈泪,你一邊把對(duì)象放入老年代适贸,內(nèi)存都不夠了。
此時(shí)就會(huì)自動(dòng)用“Serial Old”垃圾回收器替代CMS涝桅,就是直接強(qiáng)行把系統(tǒng)程序“Stop the World”拜姿,重新進(jìn)行長(zhǎng)時(shí)間的GC Roots追蹤,標(biāo)記出來(lái)全部垃圾對(duì)象冯遂,不允許新的對(duì)象產(chǎn)生蕊肥,然后一次性把垃圾對(duì)象都回收掉,完事兒了再恢復(fù)系統(tǒng)線程蛤肌。所以在生產(chǎn)實(shí)踐中壁却,這個(gè)自動(dòng)觸發(fā)CMS垃圾回收的比例需要合理優(yōu)化一下,避免“Concurrent Mode Failure”問(wèn)題裸准。
3.內(nèi)存碎片問(wèn)題
老年代的CMS采用“標(biāo)記-清理”算法展东,每次都是標(biāo)記出來(lái)垃圾對(duì)象,然后一次性回收掉炒俱,這樣會(huì)導(dǎo)致大量的內(nèi)存碎片產(chǎn)生盐肃。如果內(nèi)存碎片太多,會(huì)導(dǎo)致后續(xù)對(duì)象進(jìn)入老年代找不到可用的連續(xù)內(nèi)存空間了权悟,然后觸發(fā)Full GC砸王。所以CMS不是完全就僅僅用“標(biāo)記-清理”算法的,因?yàn)樘嗟膬?nèi)存碎片實(shí)際上會(huì)導(dǎo)致更加頻繁的Full GC峦阁。
CMS有一個(gè)參數(shù)是“-XX:+UseCMSCompactAtFullCollection”谦铃,默認(rèn)就打開(kāi)了
他意思是在Full GC之后要再次進(jìn)行“Stop the World”,停止工作線程拇派,然后進(jìn)行碎片整理荷辕,就是把存活對(duì)象挪到一起,空出來(lái)大片連續(xù)內(nèi)存空間件豌,避免內(nèi)存碎片疮方。
還有一個(gè)參數(shù)是“-XX:CMSFullGCsBeforeCompaction”,這個(gè)意思是執(zhí)行多少次Full GC之后再執(zhí)行一次內(nèi)存碎片整理的工作茧彤,默認(rèn)是0骡显,意思就是每次Full GC之后都會(huì)進(jìn)行一次內(nèi)存整理。
19.G1垃圾回收器
G1的核心設(shè)計(jì)思路:G1可以做到讓你來(lái)設(shè)定垃圾回收對(duì)系統(tǒng)的影響,他自己通過(guò)把內(nèi)存拆分為大量小Region惫谤,以及追蹤每個(gè)Region中可以回收的對(duì)象大小和預(yù)估時(shí)間壁顶,最后在垃圾回收的時(shí)候,盡量把垃圾回收對(duì)系統(tǒng)造成的影響控制在你指定的時(shí)間范圍內(nèi)溜歪,同時(shí)在有限的時(shí)間內(nèi)盡量回收盡可能多的垃圾對(duì)象若专。
Region可能屬于新生代也可能屬于老年代
在G1中,每一個(gè)Region時(shí)可能屬于新生代蝴猪,但是也可能屬于老年代的调衰,剛開(kāi)始Region可能誰(shuí)都不屬于,然后接著就分配給了新生代自阱,然后放了很多屬于新生代的對(duì)象嚎莉,接著就觸發(fā)了垃圾回收這個(gè)Region。
然后下一次同一個(gè)Region可能又被分配了老年代了沛豌,用來(lái)放老年代的長(zhǎng)生存周期的對(duì)象趋箩。
到底有多少個(gè)Region呢?每個(gè)Region的大小是多大呢加派?
其實(shí)這個(gè)默認(rèn)情況下自動(dòng)計(jì)算和設(shè)置的叫确,我們可以給整個(gè)堆內(nèi)存設(shè)置一個(gè)大小,比如說(shuō)用“-Xms”和“-Xmx”來(lái)設(shè)置堆內(nèi)存的大小芍锦。然后JVM啟動(dòng)的時(shí)候一旦發(fā)現(xiàn)你使用的是G1垃圾回收器启妹,可以使用“-XX:+UseG1GC”來(lái)指定使用G1垃圾回收器,此時(shí)會(huì)自動(dòng)用堆大小除以2048醉旦,因?yàn)镴VM最多可以有2048個(gè)Region饶米,然后Region的大小必須是2的倍數(shù),比如說(shuō)1MB车胡、2MB檬输、4MB之類(lèi)的。
比如說(shuō)堆大小是4G匈棘,那么就是4096MB丧慈,此時(shí)除以2048個(gè)Region,每個(gè)Region的大小就是2MB主卫。大概就是這樣子來(lái)決定Region的數(shù)量和大小的逃默,大家一般保持默認(rèn)的計(jì)算方式就可以,如果通過(guò)手動(dòng)方式來(lái)指定簇搅,則是“-XX:G1HeapRegionSize”完域。
剛開(kāi)始的時(shí)候,默認(rèn)新生代對(duì)堆內(nèi)存的占比是5%瘩将,也就是占據(jù)200MB左右的內(nèi)存吟税,對(duì)應(yīng)大概是100個(gè)Region凹耙,這個(gè)是可以通過(guò)“-XX:G1NewSizePercent”來(lái)設(shè)置新生代初始占比的,其實(shí)維持這個(gè)默認(rèn)值即可肠仪。
因?yàn)樵谙到y(tǒng)運(yùn)行中肖抱,JVM其實(shí)會(huì)不停的給新生代增加更多的Region,但是最多新生代的占比不會(huì)超過(guò)60%异旧,可以通過(guò)“-XX:G1MaxNewSizePercent”意述。而且一旦Region進(jìn)行了垃圾回收,此時(shí)新生代的Region數(shù)量還會(huì)減少吮蛹,這些其實(shí)都是動(dòng)態(tài)的欲险。
新生代還有Eden和Survivor的概念嗎?
沒(méi)錯(cuò)匹涮,其實(shí)在G1中雖然把內(nèi)存劃分為了很多的 Region,但是其實(shí)還是有新生代槐壳、老年代的區(qū)分然低,而且新生代里還是有Eden和Survivor的劃分的。
G1的新生代垃圾回收
隨著不停的在新生代的Eden對(duì)應(yīng)的Region中放對(duì)象务唐,JVM就會(huì)不停的給新生代加入更多的Region雳攘,直到新生代占據(jù)堆大小的最大比例60%。
一旦新生代達(dá)到了設(shè)定的占據(jù)堆內(nèi)存的最大大小60%枫笛,比如都有1200個(gè)Region了吨灭,里面的Eden可能占據(jù)了1000個(gè)Region,每個(gè)Survivor是100個(gè)Region刑巧,而且Eden區(qū)還占滿(mǎn)了對(duì)象喧兄,這個(gè)時(shí)候還是會(huì)觸發(fā)新生代的GC,G1就會(huì)用之前說(shuō)過(guò)的復(fù)制算法來(lái)進(jìn)行垃圾回收啊楚,進(jìn)入一個(gè)“Stop the World”狀態(tài)吠冤,然后把Eden對(duì)應(yīng)的Region中的存活對(duì)象放入S1對(duì)應(yīng)的Region中,接著回收掉Eden對(duì)應(yīng)的Region中的垃圾對(duì)象恭理。但是這個(gè)過(guò)程跟之前是有區(qū)別的拯辙,因?yàn)镚1是可以設(shè)定目標(biāo)GC停頓時(shí)間的,也就是G1執(zhí)行GC的時(shí)候最多可以讓系統(tǒng)停頓多長(zhǎng)時(shí)間颜价,可以通過(guò)“-XX:MaxGCPauseMills”參數(shù)來(lái)設(shè)定涯保,默認(rèn)值是200ms。
那么G1就會(huì)通過(guò)之前說(shuō)的周伦,對(duì)每個(gè)Region追蹤回收他需要多少時(shí)間夕春,可以回收多少對(duì)象來(lái)選擇回收一部分的Region,保證GC停頓時(shí)間控制在指定范圍內(nèi)专挪,盡可能多的回收掉一些對(duì)象撇他。
對(duì)象什么時(shí)候進(jìn)入老年代茄猫?
(1)對(duì)象在新生代躲過(guò)了很多次的垃圾回收,達(dá)到了一定的年齡了困肩,“-XX:MaxTenuringThreshold”參數(shù)可以設(shè)置這個(gè)年齡划纽,他就會(huì)進(jìn)入老年代;
(2)動(dòng)態(tài)年齡判定規(guī)則锌畸,如果一旦發(fā)現(xiàn)某次新生代GC過(guò)后勇劣,存活對(duì)象超過(guò)了Survivor的50%。
大對(duì)象的獨(dú)立Region存放和回收
G1提供了專(zhuān)門(mén)的Region來(lái)存放大對(duì)象潭枣,而不是讓大對(duì)象進(jìn)入老年代的Region中比默。在G1中,大對(duì)象的判定規(guī)則就是一個(gè)大對(duì)象超過(guò)了一個(gè)Region大小的50%盆犁,比如按照上面算的命咐,每個(gè)Region是2MB,只要一個(gè)大對(duì)象超過(guò)了1MB谐岁,就會(huì)被放入大對(duì)象專(zhuān)門(mén)的Region中而且一個(gè)大對(duì)象如果太大醋奠,可能會(huì)橫跨多個(gè)Region來(lái)存放。
大對(duì)象不屬于新生代和老年代伊佃,其實(shí)新生代窜司、老年代在回收的時(shí)候,會(huì)順帶帶著大對(duì)象Region一起回收航揉,所以這就是在G1內(nèi)存模型下對(duì)大對(duì)象的分配和回收的策略塞祈。
什么時(shí)候觸發(fā)新生代+老年代的混合垃圾回收?
G1有一個(gè)參數(shù)帅涂,是“-XX:InitiatingHeapOccupancyPercent”议薪,他的默認(rèn)值是45%。意思就是說(shuō)媳友,如果老年代占據(jù)了堆內(nèi)存的45%的Region的時(shí)候笙蒙,此時(shí)就會(huì)嘗試觸發(fā)一個(gè)新生代+老年代一起回收的混合回收階段。比如按照我們之前說(shuō)的庆锦,堆內(nèi)存有2048個(gè)Region捅位,如果老年代占據(jù)了其中45%的Region,也就是接近1000個(gè)Region的時(shí)候搂抒,就會(huì)開(kāi)始觸發(fā)一個(gè)混合回收艇搀。
G1垃圾回收的過(guò)程
-
初始標(biāo)記:先停止系統(tǒng)程序的運(yùn)行,然后對(duì)各個(gè)線程棧內(nèi)存中的局部變量代表的GC Roots求晶,以及方法區(qū)中的類(lèi)靜態(tài)變量代表的GCRoots焰雕,進(jìn)行掃描,標(biāo)記出來(lái)他們直接引用的那些對(duì)象芳杏。
-
并發(fā)標(biāo)記:這個(gè)階段會(huì)允許系統(tǒng)程序的運(yùn)行矩屁,同時(shí)進(jìn)行GC Roots追蹤辟宗,從GC Roots開(kāi)始追蹤所有的存活對(duì)象。JVM會(huì)對(duì)并發(fā)標(biāo)記階段對(duì)對(duì)象做出的一些修改記錄起來(lái)吝秕,比如說(shuō)哪個(gè)對(duì)象被新建了泊脐,哪個(gè)對(duì)象失去了引用。
-
最終標(biāo)記:這個(gè)階段會(huì)進(jìn)入“Stop the World”烁峭,系統(tǒng)程序是禁止運(yùn)行的容客,但是會(huì)根據(jù)并發(fā)標(biāo)記 階段記錄的那些對(duì)象修改,最終標(biāo)記一下有哪些存活對(duì)象约郁,有哪些是垃圾對(duì)象缩挑。
-
混合回收:這個(gè)階段會(huì)計(jì)算老年代中每個(gè)Region中的存活對(duì)象數(shù)量,存活對(duì)象的占比鬓梅,還有執(zhí)行垃圾回收的預(yù)期性能和效率供置。接著會(huì)停止系統(tǒng)程序,然后全力以赴盡快進(jìn)行垃圾回收绽快,此時(shí)會(huì)選擇部分Region進(jìn)行回收邢笙,因?yàn)楸仨氉尷厥盏耐nD時(shí)間控制在我們指定的范圍內(nèi)她肯。
而且需要在這里有一點(diǎn)認(rèn)識(shí)蒲列,其實(shí)老年代對(duì)堆內(nèi)存占比達(dá)到45%的時(shí)候膛锭,觸發(fā)的是“混合回收”
也就是說(shuō)寓辱,此時(shí)垃圾回收不僅僅是回收老年代艘绍,還會(huì)回收新生代,還會(huì)回收大對(duì)象秫筏。
G1垃圾回收器的一些參數(shù)
“-XX:G1MixedGCCountTarget”
最后一個(gè)階段混合回收的時(shí)候诱鞠,其實(shí)會(huì)停止所有程序運(yùn)行,所以說(shuō)G1是允許執(zhí)行多次混合回收这敬。
比如先停止工作航夺,執(zhí)行一次混合回收回收掉 一些Region,接著恢復(fù)系統(tǒng)運(yùn)行崔涂,然后再次停止系統(tǒng)運(yùn)行阳掐,再執(zhí)行一次混合回收回收掉一些Region。
有一些參數(shù)可以控制這個(gè)冷蚂,比如“-XX:G1MixedGCCountTarget”參數(shù)缭保,就是在一次混合回收的過(guò)程中,最后一個(gè)階段執(zhí)行幾次混合回收蝙茶,默認(rèn)值是8次艺骂,意味著最后一個(gè)階段,先停止系統(tǒng)運(yùn)行隆夯,混合回收一些Region钳恕,再恢復(fù)系統(tǒng)運(yùn)行别伏,接著再次禁止系統(tǒng)運(yùn)行,混合回收一些Region忧额,反復(fù)8次厘肮。為什么要反復(fù)回收多次呢?因?yàn)槟阃V瓜到y(tǒng)一會(huì)兒宙址,回收掉一些Region轴脐,再讓系統(tǒng)運(yùn)行一會(huì)兒,然后再次停止系統(tǒng)一會(huì)兒抡砂,再次回收掉一些Region大咱,這樣可以盡可能讓系統(tǒng)不要停頓時(shí)間過(guò)長(zhǎng),可以在多次回收的間隙注益,也運(yùn)行一下碴巾。-
“-XX:G1HeapWastePercent”
還有一個(gè)參數(shù),就是“-XX:G1HeapWastePercent”丑搔,默認(rèn)值是5%厦瓢,意思就是說(shuō),在混合回收的時(shí)候啤月,對(duì)Region回收都是基于復(fù)制算法進(jìn)行的煮仇,都是把要回收的Region里的存活對(duì)象放入其他
Region,然后這個(gè)Region中的垃圾對(duì)象全部清理掉谎仲。這樣的話在回收過(guò)程就會(huì)不斷空出來(lái)新的Region浙垫,一旦空閑出來(lái)的Region數(shù)量達(dá)到了堆內(nèi)存的5%,此時(shí)就會(huì)立即停止混合回收郑诺,意味著本次混合回收就結(jié)束了夹姥。
G1整體是基于復(fù)制算法進(jìn)行Region垃圾回收的,不會(huì)出現(xiàn)內(nèi)存碎片的問(wèn)題辙诞,不需要像CMS那樣標(biāo)記-清理之后辙售,再進(jìn)行內(nèi)存碎片的整理。
-
“-XX:G1MixedGCLiveThresholdPercent”
還有一個(gè)參數(shù)飞涂,“-XX:G1MixedGCLiveThresholdPercent”旦部,他的默認(rèn)值是85%,意思就是確定要回收的Region的時(shí)候较店,必須是存活對(duì)象低于85%的Region才可以進(jìn)行回收志鹃,否則要是一個(gè)Region的存活對(duì)象多余85%,你還回收他干什么泽西?這個(gè)時(shí)候要把85%的對(duì)象都拷貝到別的Region曹铃,這個(gè)成本是很高的。
回收失敗時(shí)的Full GC
如果在進(jìn)行Mixed回收的時(shí)候捧杉,無(wú)論是年輕代還是老年代都基于復(fù)制算法進(jìn)行回收陕见,都要把各個(gè)Region的存活對(duì)象拷貝到別的Region里去秘血,此時(shí)萬(wàn)一出現(xiàn)拷貝的過(guò)程中發(fā)現(xiàn)沒(méi)有空閑Region可以承載自己的存活對(duì)象了,就會(huì)觸發(fā) 一次失敗评甜。一旦失敗灰粮,立馬就會(huì)切換為停止系統(tǒng)程序,然后采用單線程進(jìn)行標(biāo)記忍坷、清理和壓縮整理粘舟,空閑出來(lái)一批Region,這個(gè)過(guò)程是極慢極慢的佩研。
20.G1新生代gc如何優(yōu)化柑肴?
對(duì)于G1而言,我們首先應(yīng)該給整個(gè)JVM的堆區(qū)域足夠的內(nèi)存旬薯,比如我們?cè)谶@里就給了JVM超過(guò)5G的內(nèi)存晰骑,其中堆內(nèi)存有4G的內(nèi)存。接著就應(yīng)該合理設(shè)置“-XX:MaxGCPauseMills”參數(shù)绊序,如果這個(gè)參數(shù)設(shè)置的小了硕舆,那么說(shuō)明每次gc停頓時(shí)間可能特別短,此時(shí)G1一旦發(fā)現(xiàn)你對(duì)幾十個(gè)Region占滿(mǎn)了就立即觸發(fā)新生代gc骤公,然后gc頻率特別頻繁抚官,雖然每次gc時(shí)間很短。比如說(shuō)30秒觸發(fā)一次新生代gc阶捆,每次就停頓30毫秒凌节。
如果這個(gè)參數(shù)設(shè)置大了呢?那么可能G1會(huì)允許你不停的在新生代理分配新的對(duì)象趁猴,然后積累了很多對(duì)象了刊咳,再一次性回收幾百個(gè)Region彪见,此時(shí)可能一次GC停頓時(shí)間就會(huì)達(dá)到幾百毫秒儡司,但是GC的頻率很低。比如說(shuō)30分鐘才觸發(fā)一次新生代GC余指,但是每次停頓500毫秒捕犬。
所以這個(gè)參數(shù)到底如何設(shè)置,需要結(jié)合后續(xù)給大家講解的系統(tǒng)壓測(cè)工具酵镜、gc日志碉碉、內(nèi)存分析工具結(jié)合起來(lái)進(jìn)行考慮,盡量讓系統(tǒng)的gc頻率別太高淮韭,同時(shí)每次gc停頓時(shí)間也別太長(zhǎng)垢粮,達(dá)到一個(gè)理想的合理值。
21.G1 mixed gc如何優(yōu)化靠粪?
對(duì)于這個(gè)mixed gc的觸發(fā)蜡吧,大家都知道是老年代在堆內(nèi)存里占比超過(guò)45%就會(huì)觸發(fā)毫蚓。
大家之前都很清楚了年輕代的對(duì)象進(jìn)入老年代的幾個(gè)條件了,要不然是新生代gc過(guò)后存活對(duì)象太多沒(méi)法放入Survivor區(qū)域昔善,要不然是對(duì)象年齡太大元潘,要不然是動(dòng)態(tài)年齡判定規(guī)則。其中尤其關(guān)鍵的君仆,就是新生代gc過(guò)后存活對(duì)象過(guò)多無(wú)法放入Survivor區(qū)域翩概,以及動(dòng)態(tài)年齡判定規(guī)則,這兩個(gè)條件尤其可能讓很多對(duì)象快速進(jìn)入老年代返咱,一旦老年代頻繁達(dá)到占用堆內(nèi)存45%的閾值钥庇,那么就會(huì)頻繁觸發(fā)mixed gc。所以mixed gc本身很復(fù)雜洛姑,很多參數(shù)可以?xún)?yōu)化上沐,但是優(yōu)化mixed gc的核心不是優(yōu)化他的參數(shù),而是跟我們之前分析的思路一樣楞艾,盡量避免對(duì)象過(guò)快進(jìn)入老年代参咙,盡量避免頻繁觸發(fā)mixed gc,就可以做到根本上優(yōu)化mixed gc了硫眯。
那么G1里面跟之前的ParNew+CMS的組合是不同的蕴侧,我們到底應(yīng)該如何來(lái)優(yōu)化參數(shù)呢?
其實(shí)核心的點(diǎn)两入,還是“-XX:MaxGCPauseMills”這個(gè)參數(shù)净宵。大家可以想一下,假設(shè)你“-XX:MaxGCPauseMills”參數(shù)設(shè)置的值很大裹纳,導(dǎo)致系統(tǒng)運(yùn)行很久择葡,新生代可能都占用了堆
內(nèi)存的60%了,此時(shí)才觸發(fā)新生代gc剃氧。那么存活下來(lái)的對(duì)象可能就會(huì)很多敏储,此時(shí)就會(huì)導(dǎo)致Survivor區(qū)域放不下那么多的對(duì)象,就會(huì)進(jìn)入老年代中朋鞍∫烟恚或者是你新生代gc過(guò)后,存活下來(lái)的對(duì)象過(guò)多滥酥,導(dǎo)致進(jìn)入Survivor區(qū)域后觸發(fā)了動(dòng)態(tài)年齡判定規(guī)則更舞,達(dá)到了Survivor區(qū)域的50%,也會(huì)快速導(dǎo)致一些對(duì)象進(jìn)入老年代中坎吻。
所以這里核心還是在于調(diào)節(jié)“-XX:MaxGCPauseMills”這個(gè)參數(shù)的值缆蝉,在保證他的新生代gc別太頻繁的同時(shí),還得考慮每次gc過(guò)后的存活對(duì)象有多少,避免存活對(duì)象太多快速進(jìn)入老年代刊头,頻繁觸發(fā)mixed gc贝搁。
至于到底如何優(yōu)化這個(gè)參數(shù),一切都要結(jié)合后續(xù)大量工具的講解和實(shí)操演練了芽偏,到這里為止雷逆,至少大家對(duì)原理性的東西都很了解了。
22.年輕代gc到底多久一次對(duì)系統(tǒng)影響不大污尉?
其實(shí)通常來(lái)說(shuō)是不大的膀哲,不知道大家發(fā)現(xiàn)沒(méi)有,其實(shí)年輕代gc幾乎沒(méi)什么好調(diào)優(yōu)的被碗,因?yàn)樗倪\(yùn)行邏輯非常簡(jiǎn)單某宪,就是Eden一旦滿(mǎn)了,無(wú)法放新對(duì)象就觸發(fā)一次gc锐朴。
一般來(lái)說(shuō)兴喂,真要說(shuō)對(duì)年輕代的gc進(jìn)行調(diào)優(yōu),只要你給系統(tǒng)分配足夠的內(nèi)存即可焚志,核心點(diǎn)還是在于堆內(nèi)存的分配衣迷、新生代內(nèi)存的分配內(nèi)存足夠的話,通常來(lái)說(shuō)系統(tǒng)可能在低峰時(shí)期在幾個(gè)小時(shí)才有一次新生代gc酱酬,高峰期最多也就幾分鐘一次新生代gc壶谒。而且一般的業(yè)務(wù)系統(tǒng)都是部署在2核4G或者4核8G的機(jī)器上,此時(shí)分配給堆的內(nèi)存不會(huì)超過(guò)3G膳沽,給新生代中的Eden區(qū)的內(nèi)存也就1G左右汗菜。
而且新生代采用的復(fù)制算法效率極高,因?yàn)樾律锎婊畹膶?duì)象很少挑社,只要迅速標(biāo)記出這少量存活對(duì)象陨界,移動(dòng)到Survivor區(qū),然后回收掉其他全部垃圾對(duì)象即可痛阻,速度很快菌瘪。
很多時(shí)候,一次新生代gc可能也就耗費(fèi)幾毫秒录平,幾十毫秒麻车。大家設(shè)想一下缀皱,假如說(shuō)你的系統(tǒng)運(yùn)行著斗这,然后每隔幾分鐘或者幾十分鐘執(zhí)行一次新生代gc,系統(tǒng)卡頓幾十毫秒啤斗,就這期間的請(qǐng)求會(huì)卡頓幾十毫秒表箭,幾乎用戶(hù)都是無(wú)感知的,所以新生代gc一般基本對(duì)系統(tǒng)性能影響不大钮莲。
23.什么時(shí)候新生代gc對(duì)系統(tǒng)影響很大免钻?
簡(jiǎn)單彼水,當(dāng)你的系統(tǒng)部署在大內(nèi)存機(jī)器上的時(shí)候,比如說(shuō)你的機(jī)器是32核64G的機(jī)器极舔,此時(shí)你分配給系統(tǒng)的內(nèi)存有幾十個(gè)G凤覆,新生代的Eden區(qū)可能30G~40G的內(nèi)存。比如類(lèi)似Kafka拆魏、Elasticsearch之類(lèi)的大數(shù)據(jù)相關(guān)的系統(tǒng)盯桦,都是部署在大內(nèi)存的機(jī)器上的,此時(shí)如果你的系統(tǒng)負(fù)載非常的高渤刃,對(duì)于大數(shù)據(jù)系統(tǒng)是很有可能的拥峦,比如每秒幾萬(wàn)的訪問(wèn)請(qǐng)求到Kafka、Elasticsearch上去卖子。
那么可能導(dǎo)致你Eden區(qū)的幾十G內(nèi)存頻繁塞滿(mǎn)要觸發(fā)垃圾回收略号,假設(shè)1分鐘會(huì)塞滿(mǎn)一次玖绿。然后每次垃圾回收要停頓掉Kafka房蝉、Elasticsearch的運(yùn)行,然后執(zhí)行垃圾回收大概需要幾秒鐘咱圆,此時(shí)你發(fā)現(xiàn)诫舅,可能每過(guò)一分鐘随闪,你的系統(tǒng)就要卡頓幾秒鐘,有的請(qǐng)求一旦卡死幾秒鐘就會(huì)超時(shí)報(bào)錯(cuò)骚勘,此時(shí)可能會(huì)導(dǎo)致你的系統(tǒng)頻繁出錯(cuò)铐伴。
如何解決這種幾十G的大內(nèi)存機(jī)器的新生代GC過(guò)慢的問(wèn)題呢?
用G1垃圾回收器俏讹,針對(duì)G1垃圾回收器当宴,可以設(shè)置一個(gè)期望的每次GC的停頓時(shí)間,比如我們可以設(shè)置一個(gè)20ms泽疆。
那么G1基于他的Region內(nèi)存劃分原理户矢,就可以在運(yùn)行一段時(shí)間之后,比如就針對(duì)2G內(nèi)存的Region進(jìn)行垃圾回收殉疼,此時(shí)就僅僅停頓20ms梯浪,然后回收掉2G的內(nèi)存空間,騰出來(lái)了部分內(nèi)存瓢娜,接著還可以繼續(xù)讓系統(tǒng)運(yùn)行挂洛。
G1天生就適合這種大內(nèi)存機(jī)器的JVM運(yùn)行,可以完美解決大內(nèi)存垃圾回收時(shí)間過(guò)長(zhǎng)的問(wèn)題眠砾。
24.要命的頻繁老年代gc問(wèn)題
老年代gc通常來(lái)說(shuō)都很耗費(fèi)時(shí)間虏劲,無(wú)論是CMS垃圾回收器還是G1垃圾回收器,因?yàn)楸热缯f(shuō)CMS就要經(jīng)歷初始標(biāo)記、并發(fā)標(biāo)記柒巫、重新標(biāo)記励堡、并發(fā)清理、碎片整理幾個(gè)環(huán)節(jié)堡掏,過(guò)程非常的復(fù)雜应结,G1同樣也是如此。
通常來(lái)說(shuō)泉唁,老年代gc至少比新生代gc慢10倍以上摊趾,比如新生代gc每次耗費(fèi)200ms,其實(shí)對(duì)用戶(hù)影響不大游两,但是老年代每次gc耗費(fèi)2s砾层,那可能就會(huì)導(dǎo)致老年代gc的時(shí)候用戶(hù)發(fā)現(xiàn)頁(yè)面上卡頓2s,影響就很大了贱案。
所以一旦你因?yàn)閖vm內(nèi)存分配不合理肛炮,導(dǎo)致頻繁進(jìn)行老年代gc,比如說(shuō)幾分鐘就有一次老年代gc宝踪,每次gc系統(tǒng)都停頓幾秒鐘侨糟,那簡(jiǎn)直對(duì)你的系統(tǒng)就是致命的打擊。此時(shí)用戶(hù)會(huì)發(fā)現(xiàn)頁(yè)面上或者APP上經(jīng)常性的出現(xiàn)點(diǎn)擊按鈕之后卡頓幾秒鐘瘩燥。
其實(shí)說(shuō)白了秕重,系統(tǒng)真正最大的問(wèn)題,就是因?yàn)?strong>內(nèi)存分配厉膀、參數(shù)設(shè)置不合理溶耘,導(dǎo)致你的對(duì)象頻繁的進(jìn)入老年代,然后頻繁觸發(fā)老年代gc服鹅,導(dǎo)致系統(tǒng)頻繁的每隔幾分鐘就要卡死幾秒鐘凳兵。
25.什么是內(nèi)存溢出??jī)?nèi)存溢出會(huì)在哪些區(qū)域發(fā)生企软?
內(nèi)存溢出可能會(huì)發(fā)生在:
- Metaspace庐扫;
- Java虛擬機(jī)棧;
- 堆內(nèi)存仗哨。
除了程序計(jì)數(shù)器形庭,其它區(qū)域都有可能發(fā)生OOM。
Metaspace區(qū)域是如何因?yàn)轭?lèi)太多而發(fā)生內(nèi)存溢出的厌漂?
以下兩個(gè)參數(shù)就是用來(lái)設(shè)置Metaspace區(qū)域大小的:
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m
一旦JVM不停地加載類(lèi)萨醒,加載了很多很多的類(lèi),然后Metaspace區(qū)域放滿(mǎn)了桩卵,當(dāng)Metaspace區(qū)域滿(mǎn)就會(huì)觸發(fā)Full GC验靡,F(xiàn)ull GC會(huì)帶著一塊進(jìn)行Old GC就是回收老年代的,也會(huì)帶著回收年輕代的Young GC雏节,當(dāng)然胜嗓,F(xiàn)ull GC的時(shí)候,必然會(huì)嘗試回收Metaspace區(qū)域中的類(lèi)钩乍。
那么什么樣的類(lèi)才是可以被回收的呢辞州?
這個(gè)條件是相當(dāng)?shù)目量蹋ú幌抻谝韵乱恍罕热邕@個(gè)類(lèi)的類(lèi)加載器先要被回收寥粹,比如這個(gè)類(lèi)的所有對(duì)象實(shí)例都要被回收变过,等等。
所以一旦你的Metaspace區(qū)域滿(mǎn)了涝涤,未必能回收掉里面很多的類(lèi)那么一旦回收不了多少類(lèi)媚狰,此時(shí)你的JVM還在拼命的加載類(lèi)放到Metaspace里去,你覺(jué)得此時(shí)會(huì)發(fā)生什么事情阔拳?
顯而易見(jiàn)崭孤,一旦你嘗試回收了Metaspace中的類(lèi)之后發(fā)現(xiàn)還是沒(méi)能騰出來(lái)太多空間,此時(shí)還要繼續(xù)往Metaspace中塞入更多的類(lèi)糊肠,直接就會(huì)引發(fā)內(nèi)存溢出的問(wèn)題辨宠。因?yàn)榇藭r(shí)Metaspace區(qū)域的內(nèi)存空間不夠了。一旦發(fā)生了內(nèi)存溢出就說(shuō)明JVM已經(jīng)沒(méi)辦法繼續(xù)運(yùn)行下去了货裹,此時(shí)可能你的系統(tǒng)就直接崩潰了嗤形,這就是Metaspace區(qū)域發(fā)生內(nèi)存溢出的一個(gè)根本的原理。
到底什么情況下會(huì)發(fā)生Metaspace內(nèi)存溢出弧圆?
- 很多工程師他不懂JVM的運(yùn)行原理赋兵,在上線系統(tǒng)的時(shí)候?qū)etaspace區(qū)域直接用默認(rèn)的參數(shù),即根本不設(shè)置其大小這會(huì)導(dǎo)致默認(rèn)的Metaspace區(qū)域可能才幾十MB而已搔预,此時(shí)對(duì)于一個(gè)稍微大型一點(diǎn)的系統(tǒng)毡惜,因?yàn)樗约河泻芏囝?lèi),還依賴(lài)了很多外部的jar包有有很多的類(lèi)斯撮,幾十MB的Metaspace很容易就不夠了经伙;
- 很多人寫(xiě)系統(tǒng)的時(shí)候會(huì)用cglib之類(lèi)的技術(shù)動(dòng)態(tài)生成一些類(lèi),一旦代碼中沒(méi)有控制好勿锅,導(dǎo)致你生成的類(lèi)過(guò)于多的時(shí)候帕膜,就很容易把Metaspace給塞滿(mǎn),進(jìn)而引發(fā)內(nèi)存溢出溢十。
對(duì)于第一種問(wèn)題垮刹,通常來(lái)說(shuō),有經(jīng)驗(yàn)的工程師上線系統(tǒng)往往會(huì)設(shè)置對(duì)應(yīng)的Metaspace大小张弛,推薦的值在512MB那樣荒典,一般都是足夠的酪劫。
無(wú)限制的調(diào)用方法是如何讓線程的棧內(nèi)存溢出的?
可以手動(dòng)設(shè)置每個(gè)線程的虛擬機(jī)棧的內(nèi)存大小的寺董,一般來(lái)說(shuō)現(xiàn)在默認(rèn)都是給設(shè)置1MB覆糟。假設(shè)你不停的讓這個(gè)線程去調(diào)用各種方法,然后不停的把方法調(diào)用的棧楨壓入棧中遮咖,是不是就會(huì)不斷的占用這個(gè)線程1MB的棧內(nèi)存滩字?那么如果不停的讓線程調(diào)用方法,不停的往棧里放入棧楨御吞,此時(shí)終有一個(gè)時(shí)刻麦箍,大量的棧楨會(huì)消耗完畢這個(gè)1MB的線程棧內(nèi)存,最終就會(huì)導(dǎo)致出現(xiàn)棧內(nèi)存溢出的情況
通常而言陶珠,哪怕你的線程的虛擬機(jī)棧內(nèi)存就128KB挟裂,或者256KB,通常都是足夠進(jìn)行一定深度的方法調(diào)用的揍诽。但是如果說(shuō)你要是走一個(gè)遞歸方法調(diào)用话瞧,一個(gè)線程就會(huì)不停的調(diào)用同一個(gè)方法,即使是同一個(gè)方法寝姿,每一次方法調(diào)用也會(huì)產(chǎn)生一個(gè)棧楨壓入棧里交排,比如說(shuō)對(duì)sayHello()進(jìn)行100次調(diào)用,那么就會(huì)有100個(gè)棧楨壓入中饵筑。所以如果瘋狂的運(yùn)行上述代碼埃篓,就會(huì)不停的將sayHello()方法的棧楨壓入棧里,最終一定會(huì)消耗掉線程的棧內(nèi)存根资,引發(fā)內(nèi)存溢出架专。所以一般來(lái)說(shuō),其實(shí)引發(fā)棧內(nèi)存溢出玄帕,往往都是代碼里寫(xiě)了一些bug才會(huì)導(dǎo)致的部脚,正常情況下發(fā)生的比較少。
對(duì)象太多了導(dǎo)致堆內(nèi)存實(shí)在是放不下裤纹,只能內(nèi)存溢出委刘!
有限的內(nèi)存中放了過(guò)多的對(duì)象,而且大多數(shù)都是存活的鹰椒,此時(shí)即使GC過(guò)后還是大部分都存活锡移,所以要繼續(xù)放入更多對(duì)象已經(jīng)不可能了,此時(shí)只能引發(fā)內(nèi)存溢出問(wèn)題漆际。
所以一般來(lái)說(shuō)發(fā)生內(nèi)存溢出有兩種主要的場(chǎng)景:
- 系統(tǒng)承載高并發(fā)請(qǐng)求淆珊,因?yàn)檎?qǐng)求量過(guò)大,導(dǎo)致大量對(duì)象都是存活的奸汇,所以要繼續(xù)放入新的對(duì)象實(shí)在是不行了施符,此時(shí)就會(huì)引發(fā)OOM系統(tǒng)崩潰往声;
- 系統(tǒng)有內(nèi)存泄漏的問(wèn)題,就是莫名其妙弄了很多的對(duì)象戳吝,結(jié)果對(duì)象都是存活的浩销,沒(méi)有及時(shí)取消對(duì)他們的引用,導(dǎo)致觸發(fā)GC還是無(wú)法回收骨坑,此時(shí)只能引發(fā)內(nèi)存溢出撼嗓,因?yàn)閮?nèi)存實(shí)在放不下更多對(duì)象了柬采。
因此總結(jié)起來(lái)欢唾,一般引發(fā)OOM,要不然是系統(tǒng)負(fù)載過(guò)高粉捻,要不然就是有內(nèi)存泄漏的問(wèn)題礁遣。
27.用CGLIB動(dòng)態(tài)生成類(lèi)的代碼演示Metaspace區(qū)域內(nèi)存溢出的場(chǎng)景
首先設(shè)置MetaSpace區(qū)域大小:
-XX:MetaspaceSize=10m
-XX:MaxMetaspaceSize=10m
演示代碼:
public class Demo8 {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Car.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if (method.getName().equals("run")) {
System.out.println("啟動(dòng)汽車(chē)之前肩刃,先進(jìn)行安全檢查...");
return methodProxy.invoke(o, objects);
} else {
return methodProxy.invoke(o, objects);
}
}
});
Car car = (Car) enhancer.create();
car.run();
}
}
static class Car {
public void run() {
System.out.println("汽車(chē)啟動(dòng)祟霍,開(kāi)始行駛...");
}
}
}
通過(guò)一個(gè)while(true)循環(huán)不斷動(dòng)態(tài)創(chuàng)建Car的子類(lèi),每創(chuàng)建一個(gè)代理子類(lèi)盈包,該類(lèi)信息都會(huì)被放到MetaSpace區(qū)域中沸呐,因?yàn)镸etaSpace區(qū)域設(shè)定為只有10M,所以很快就會(huì)出現(xiàn)OOM異常呢燥。
27.用遞歸調(diào)用方法的代碼演示棧內(nèi)存溢出的場(chǎng)景
首先設(shè)置棧內(nèi)存為:
-XX:ThreadStackSize=1m
演示代碼:
public class Demo9 {
private static long counter = 0;
public static void main(String[] args) {
work();
}
private static void work() {
System.out.println("目前是第" + ++counter + "次方法調(diào)用.");
work();
}
}
上面的代碼非常簡(jiǎn)單崭添,就是work()方法調(diào)用自己,進(jìn)入一個(gè)無(wú)限制的遞歸調(diào)用叛氨,陷入死循環(huán)呼渣,也就是說(shuō)在main線程的棧中,會(huì)不停的壓入work()方法調(diào)用的棧楨寞埠,直到1MB的內(nèi)存空間耗盡屁置。
27.通過(guò)代碼演示堆內(nèi)存溢出的場(chǎng)景
首先設(shè)置堆內(nèi)存大小:
-Xms10m
-Xmx10m
演示代碼:
public class Demo10 {
private static long counter = 0;
public static void main(String[] args) {
ArrayList<Object> objects = new ArrayList<>();
while (true) {
objects.add(new Object());
System.out.println("當(dāng)前創(chuàng)建了第" + (++counter) + "個(gè)對(duì)象.");
}
}
}
代碼很簡(jiǎn)單仁连,就是在一個(gè)while循環(huán)里不停的創(chuàng)建對(duì)象蓝角,而且對(duì)象全部都是放在List里面被引用的,也就是不能被回收饭冬。試想一下帅容,如果你不停的創(chuàng)建對(duì)象,Eden區(qū)滿(mǎn)了伍伤,他們?nèi)看婊顣?huì)全部轉(zhuǎn)移到老年代并徘,反復(fù)幾次之后老年代滿(mǎn)了。然后Eden區(qū)再次滿(mǎn)了扰魂,ygc后存活對(duì)象再次進(jìn)入老年代麦乞,此時(shí)老年代先f(wàn)ull gc蕴茴,但是回收不了任何對(duì)象,因此ygc后的存活對(duì)象就一定是無(wú)法進(jìn)入老年代的姐直。