JVM內(nèi)存模型
根據(jù)Java虛擬機(jī)規(guī)范奈应,Java數(shù)據(jù)區(qū)域分為五大數(shù)據(jù)區(qū)域。
其中方法區(qū)和堆是所有線程共享的前硫,虛擬機(jī)棧、本地方法棧和程序計(jì)數(shù)器則為線程私有的涡扼。
有的博客稱方法區(qū)是永久代稼跳,那是因?yàn)榍罢呤荍VM的規(guī)范,而后者則是JVM規(guī)范的一種實(shí)現(xiàn)吃沪,并且只有HotSpot才有永久代汤善,
JDK8中已經(jīng)徹底移除了方法區(qū),JDK8中引入了一個(gè)新的內(nèi)存區(qū)域叫metaspace(元空間)票彪,后邊詳細(xì)介紹红淡。
棧區(qū)
棧分為虛擬機(jī)棧和本地方法棧。
虛擬機(jī)棧
每個(gè)方法執(zhí)行都會(huì)創(chuàng)建一個(gè)棧幀降铸,用于存放局部變量表在旱,操作棧,動(dòng)態(tài)鏈接推掸,方法出口等桶蝎。每個(gè)方法從被調(diào)用,直到被執(zhí)行完谅畅。對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)中從入棧到出棧的過(guò)程登渣。
通常說(shuō)的棧就是指局部變量表部分,存放編譯期間可知的8種基本數(shù)據(jù)類型及對(duì)象引用和指令地址毡泻。局部變量表是在編譯期間完成分配胜茧,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)棧中的局部變量分配內(nèi)存大小是確定的仇味。
常見(jiàn)的的兩種異常StackOverFlowError和OutOfMemoneyError竹揍。當(dāng)線程請(qǐng)求棧深度大于虛擬機(jī)所允許的深度就會(huì)拋出StackOverFlowError錯(cuò)誤;虛擬機(jī)棧動(dòng)態(tài)擴(kuò)展邪铲,當(dāng)擴(kuò)展無(wú)法申請(qǐng)到足夠的內(nèi)存空間時(shí)候,拋出OutOfMemoneyError无拗。
本地方法棧
本地方法棧與虛擬機(jī)棧的作用十分類似带到,不過(guò)本地方法是為native方法服務(wù)的。部分虛擬機(jī)(比如Sun HotSpot虛擬機(jī))直接將本地方法棧與虛擬機(jī)棧合二為一英染。與虛擬機(jī)棧一樣揽惹,本地方法棧也會(huì)拋出StactOverflowError與OutOfMemoryError異常。
程序計(jì)數(shù)器
當(dāng)前線程所執(zhí)行的行號(hào)指示器四康。通過(guò)改變計(jì)數(shù)器的值來(lái)確定下一條指令搪搏,比如循環(huán),分支闪金,跳轉(zhuǎn)疯溺,異常處理论颅,線程恢復(fù)等都是依賴計(jì)數(shù)器來(lái)完成。
Java虛擬機(jī)多線程是通過(guò)線程輪流切換并分配處理器執(zhí)行時(shí)間的方式實(shí)現(xiàn)的囱嫩。為了線程切換能恢復(fù)到正確的位置恃疯,每條線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器,所以它是線程私有的墨闲。
唯一一塊Java虛擬機(jī)沒(méi)有規(guī)定任何OutofMemoryError的區(qū)塊今妄。
方法區(qū)
方法區(qū)/永久代是被所有線程共享區(qū)域,用于存放已被虛擬機(jī)加載的類信息鸳碧、常量盾鳞、靜態(tài)變量等數(shù)據(jù)。永久代的垃圾回收和老年代的垃圾回收是綁定的瞻离,一旦其中一個(gè)區(qū)域被占滿腾仅,這兩個(gè)區(qū)都要進(jìn)行垃圾回收。
在JDK1.7之前運(yùn)行時(shí)常量池邏輯包含字符串常量池存放在方法區(qū)琐脏,此時(shí)hotspot虛擬機(jī)對(duì)方法區(qū)的實(shí)現(xiàn)為永久代
在JDK1.7字符串常量池被從方法區(qū)拿到了堆中攒砖,這里沒(méi)有提到運(yùn)行時(shí)常量池,也就是說(shuō)字符串常量池被單獨(dú)拿到堆日裙,運(yùn)行時(shí)常量池剩下的東西還在方法區(qū)吹艇,也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之,這時(shí)候字符串常量池還在堆, 運(yùn)行時(shí)常量池還在方法區(qū)昂拂,只不過(guò)方法區(qū)的實(shí)現(xiàn)從永久代變成了元空間(Metaspace)
移除永久代的影響
永久代在JDK8中被刪除受神,被一個(gè)叫做元空間的區(qū)域所替代了。這項(xiàng)改動(dòng)是很有必要的格侯,因?yàn)閷?duì)永久代進(jìn)行調(diào)優(yōu)是很困難的鼻听。永久代中的元數(shù)據(jù)可能會(huì)隨著每一次Full GC發(fā)生而進(jìn)行移動(dòng)。并且為永久代設(shè)置空間大小也是很難確定的联四,因?yàn)檫@其中有很多影響因素撑碴,比如類的總數(shù),常量池的大小和方法數(shù)量等朝墩。
默認(rèn)情況下醉拓,元空間的最大可分配空間就是系統(tǒng)可用內(nèi)存空間。因此收苏,我們就不會(huì)遇到永久代存在時(shí)的內(nèi)存溢出錯(cuò)誤亿卤,也不會(huì)出現(xiàn)泄漏的數(shù)據(jù)移到交換區(qū)這樣的事情。最終用戶可以為元空間設(shè)置一個(gè)可用空間最大值鹿霸,如果不進(jìn)行設(shè)置排吴,JVM會(huì)自動(dòng)根據(jù)類的元數(shù)據(jù)大小動(dòng)態(tài)增加元空間的容量。
注意:永久代的移除并不代表自定義的類加載器泄露問(wèn)題就解決了懦鼠。還必須監(jiān)控內(nèi)存消耗情況钻哩,因?yàn)橐坏┌l(fā)生泄漏屹堰,會(huì)占用大量的本地內(nèi)存,
堆區(qū)
堆被所有線程共享區(qū)域憋槐,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建双藕,唯一目的是存放對(duì)象實(shí)例。
堆區(qū)是垃圾回收的主要區(qū)域阳仔,通常情況下分為兩個(gè)區(qū)塊年輕代和老年代忧陪。年輕代又分為Eden區(qū)(存放新創(chuàng)建對(duì)象),F(xiàn)rom survivor區(qū)和To survivor區(qū)(兩個(gè)survivor區(qū)保存gc后幸存下的對(duì)象)近范。默認(rèn)情況下各自占比 8:1:1嘶摊。
java虛擬機(jī)規(guī)范對(duì)這塊的描述是:所有對(duì)象實(shí)例及數(shù)組都要在堆上分配內(nèi)存,但隨著逃逸分析技術(shù)的成熟评矩,這個(gè)說(shuō)法也不是那么絕對(duì)叶堆,但是大多數(shù)情況都是這樣的。
逃逸分析:通過(guò)逃逸分析來(lái)決定某些實(shí)例或者變量是否要在堆中進(jìn)行分配斥杜,如果開(kāi)啟了逃逸分析虱颗,即可將這些變量直接在棧上進(jìn)行分配,而非堆上進(jìn)行分配蔗喂。這些變量的指針可以被全局所引用忘渔,或者被其它線程所引用。
在JVM運(yùn)行時(shí)缰儿,可以通過(guò)配置以下參數(shù)改變整個(gè)JVM堆的配置比例
- JVM運(yùn)行時(shí)堆的大小
-Xms畦粮,堆的最小值
-Xmx,堆空間的最大值 - 新生代堆空間大小調(diào)整
-XX:NewSize新生代的最小值
-XX:MaxNewSize乖阵,新生代的最大值
-XX:NewRatio宣赔,設(shè)置新生代與老年代在堆空間的大小
-XX:SurvivorRatio,新生代中Eden所占區(qū)域的大小 - 永久代大小調(diào)整
-XX:MaxPermSize
4.其他
-XX:MaxTenuringThreshold瞪浸,設(shè)置將新生代對(duì)象轉(zhuǎn)到老年代時(shí)需要經(jīng)過(guò)多少次垃圾回收儒将,但是仍然沒(méi)有被回收
OutOfMemoryError報(bào)錯(cuò)及解決方法
java.lang.OutOfMemoryError:java heap space
這種是java堆內(nèi)存不夠,一個(gè)原因是內(nèi)存真不夠对蒲,另一個(gè)原因是程序中有死循環(huán)钩蚊。如果是java堆內(nèi)存不夠的話,可以通過(guò)調(diào)整JVM下面的配置來(lái)解決:-Xms
齐蔽、-Xmx
-
java.lang.OutOfMemoryError:GC overhead limit exceeded
這是JDK6新增錯(cuò)誤類型,當(dāng)GC為釋放很小空間占用大量時(shí)間時(shí)拋出床估;一般是因?yàn)槎烟『危瑢?dǎo)致異常的原因,沒(méi)有足夠的內(nèi)存丐巫。解決方案:- 查看系統(tǒng)是否有使用大內(nèi)存的代碼或死循環(huán)谈况;
- 通過(guò)添加JVM配置勺美,來(lái)限制使用內(nèi)存:-XX:-UseGCOverheadLimit
java.lang.OutOfMemoryError: PermGen space
這一部分用于存放Class和Meta的信息,Class在被Load的時(shí)候被放入PermGen space區(qū)域碑韵。所以如果你的APP會(huì)LOAD很多CLASS的話,就很可能出現(xiàn)PermGen space錯(cuò)誤赡茸。這種是永久代內(nèi)存不夠,可通過(guò)調(diào)整JVM的配置:
-XX:MaxPermSize
祝闻、-XXermSize
java.lang.OutOfMemoryError: Direct buffer memory
可能原因是本身資源不夠或者申請(qǐng)的太多內(nèi)存占卧。如果不是內(nèi)存泄漏的話,可以使用參數(shù)-XX:MaxDirectMemorySize
參數(shù)联喘,或者-XX:MaxDirectMemorySize
-
java.lang.OutOfMemoryError: unable to create new native thread
可能原因是系統(tǒng)內(nèi)存耗盡华蜒,無(wú)法為新線程分配內(nèi)存或者創(chuàng)建線程數(shù)超過(guò)了操作系統(tǒng)的限制。通過(guò)兩個(gè)途徑解決:- 排查應(yīng)用是否創(chuàng)建了過(guò)多的線程豁遭。通過(guò)jstack確定應(yīng)用創(chuàng)建了多少線程
- 調(diào)整操作系統(tǒng)線程數(shù)閾值叭喜。操作系統(tǒng)會(huì)限制進(jìn)程允許創(chuàng)建的線程數(shù),使用ulimit -u命令查看限制蓖谢。某些服務(wù)器上此閾值設(shè)置的過(guò)小捂蕴,比如1024。一旦應(yīng)用創(chuàng)建超過(guò)1024個(gè)線程闪幽,就會(huì)遇到j(luò)ava.lang.OutOfMemoryError: unable to create new native thread問(wèn)題啥辨。如果是這種情況,可以調(diào)大操作系統(tǒng)線程數(shù)閾值沟使。
- 增加機(jī)器內(nèi)存委可。如果上述兩項(xiàng)未能排除問(wèn)題,可能是正常增長(zhǎng)的業(yè)務(wù)確實(shí)需要更多內(nèi)存來(lái)創(chuàng)建更多線程腊嗡。如果是這種情況着倾,增加機(jī)器內(nèi)存。
- 減小堆內(nèi)存燕少。一個(gè)老司機(jī)也經(jīng)常忽略的非常重要的知識(shí)點(diǎn):線程不在堆內(nèi)存上創(chuàng)建卡者,線程在堆內(nèi)存之外的內(nèi)存上創(chuàng)建。所以如果分配了堆內(nèi)存之后只剩下很少的可用內(nèi)存客们,依然可能遇到j(luò)ava.lang.OutOfMemoryError: unable to create new native thread崇决。考慮如下場(chǎng)景:系統(tǒng)總內(nèi)存6G底挫,堆內(nèi)存分配了5G恒傻,永久代512M。在這種情況下建邓,JVM占用了5.5G內(nèi)存盈厘,系統(tǒng)進(jìn)程、其他用戶進(jìn)程和線程將共用剩下的0.5G內(nèi)存官边,很有可能沒(méi)有足夠的可用內(nèi)存創(chuàng)建新的線程沸手。如果是這種情況外遇,考慮減小堆內(nèi)存。
- 減小線程棧大小契吉。線程會(huì)占用內(nèi)存跳仿,如果每個(gè)線程都占用更多內(nèi)存,整體上將消耗更多的內(nèi)存捐晶。每個(gè)線程默認(rèn)占用內(nèi)存大小取決于JVM實(shí)現(xiàn)菲语。可以利用-Xss參數(shù)限制線程內(nèi)存大小租悄,降低總內(nèi)存消耗谨究。例如,JVM默認(rèn)每個(gè)線程占用1M內(nèi)存泣棋,應(yīng)用有500個(gè)線程胶哲,那么將消耗500M內(nèi)存空間。如果實(shí)際上256K內(nèi)存足夠線程正常運(yùn)行潭辈,配置-Xss256k鸯屿,那么500個(gè)線程將只需要消耗125M內(nèi)存。(注意把敢,如果-Xss設(shè)置的過(guò)低寄摆,將會(huì)產(chǎn)生java.lang.StackOverflowError錯(cuò)誤)。
java.lang.StackOverflowError
這也內(nèi)存溢出錯(cuò)誤的一種修赞,即線程棧的溢出婶恼,要么是方法調(diào)用層次過(guò)多(比如存在無(wú)限遞歸調(diào)用),要么是線程棧太小柏副。
可以通過(guò)優(yōu)化程序設(shè)計(jì)勾邦,減少方法調(diào)用層次;調(diào)整-Xss
參數(shù)增加線程棧大小割择。
垃圾回收算法
新生代采用復(fù)制算法眷篇。老年代采用標(biāo)記/清除算法或標(biāo)記/整理算法。由于老年代存活率高荔泳,沒(méi)有額外空間給他做擔(dān)保蕉饼,必須使用這兩種算法。
標(biāo)記-清除(Mark Sweep)算法
算法分為2個(gè)階段:
- 標(biāo)記處需要回收的對(duì)象
- 回收被標(biāo)記的對(duì)象
標(biāo)記算法分為兩種:
- 引用計(jì)數(shù)算法(Reference Counting)
- 可達(dá)性分析算法(Reachability Analysis)
由于引用技術(shù)算法無(wú)法解決循環(huán)引用的問(wèn)題玛歌,所以這里使用的標(biāo)記算法均為可達(dá)性分析算法昧港。下文將介紹兩種標(biāo)記算法。
如圖所示支子,當(dāng)進(jìn)行過(guò)標(biāo)記清除算法之后创肥,出現(xiàn)了大量的非連續(xù)內(nèi)存。當(dāng)java堆需要分配一段連續(xù)的內(nèi)存給一個(gè)新對(duì)象時(shí),發(fā)現(xiàn)雖然內(nèi)存清理出了很多的空閑瓤的,但是仍然需要繼續(xù)清理以滿足“連續(xù)空間”的要求。所以說(shuō)吞歼,這種方法比較基礎(chǔ)圈膏,效率也比較低下。
復(fù)制(Copying)算法
為了解決效率與內(nèi)存碎片問(wèn)題篙骡,復(fù)制(Copying)算法出現(xiàn)了稽坤,它將內(nèi)存劃分為兩塊相等的大小,每次使用一塊糯俗,當(dāng)這一塊用完了尿褪,就將還存活的對(duì)象復(fù)制到另外一塊內(nèi)存區(qū)域中,然后將當(dāng)前內(nèi)存空間一次性清理掉得湘。這樣的對(duì)整個(gè)半?yún)^(qū)進(jìn)行回收杖玲,分配時(shí)按照順序從內(nèi)存頂端依次分配,這種實(shí)現(xiàn)簡(jiǎn)單淘正,運(yùn)行高效摆马。不過(guò)這種算法將原有的內(nèi)存空間減少為實(shí)際的一半,代價(jià)比較高鸿吆。
從圖中可以看出囤采,整理后的內(nèi)存十分規(guī)整,但是白白浪費(fèi)一般的內(nèi)存成本太高惩淳。然而這其實(shí)是很重要的一個(gè)收集算法蕉毯,因?yàn)楝F(xiàn)在的商業(yè)虛擬機(jī)都采用這種算法來(lái)回收新生代。IBM公司的專門(mén)研究表明思犁,新生代中的對(duì)象98%都是“朝生夕死”的代虾,所以不需要按照1:1的比例來(lái)劃分內(nèi)存。HotSpot虛擬機(jī)將Java堆劃分為年輕代(Young Generation)抒倚、老年代(Tenured Generation)褐着,其中年輕代又分為一塊Eden和兩塊Survivor。
所有的新建對(duì)象都放在年輕代中托呕,年輕代使用的GC算法就是復(fù)制算法含蓉。其中Eden與Survivor的內(nèi)存大小比例為8:2,其中Eden由1大塊組成项郊,Survivor由2小塊組成馅扣。每次使用內(nèi)存為1Eden+1Survivor,即90%的內(nèi)存着降。由于年輕代中的對(duì)象生命周期往往很短差油,所以當(dāng)需要進(jìn)行GC的時(shí)候就將當(dāng)前90%中存活的對(duì)象復(fù)制到另外一塊Survivor中,原來(lái)的Eden與Survivor將被清空。但是這就有一個(gè)問(wèn)題蓄喇,我們無(wú)法保證每次年輕代GC后存活的對(duì)象都不高于10%发侵。所以在當(dāng)活下來(lái)的對(duì)象高于10%的時(shí)候,這部分對(duì)象將由Tenured進(jìn)行擔(dān)保妆偏,即無(wú)法復(fù)制到Survivor中的對(duì)象將移動(dòng)到老年代刃鳄。
標(biāo)記-整理算法
復(fù)制算法在極端情況下(存活對(duì)象較多)效率變得很低,并且需要有額外的空間進(jìn)行分配擔(dān)保钱骂。所以在老年代中這種情況一般是不適合的叔锐。
所以就出現(xiàn)了標(biāo)記-整理(Mark-Compact)算法。與標(biāo)記清除算法一樣见秽,首先是標(biāo)記對(duì)象愉烙,然而第二步是將存貨的對(duì)象向內(nèi)存一段移動(dòng),整理出一塊較大的連續(xù)內(nèi)存空間解取。
垃圾回收的幾種形式
Minor GC
在年輕代(包括Eden區(qū)和Survivor區(qū))中的垃圾回收稱之為 Minor GC步责。Minor GC當(dāng)年輕代中eden區(qū)分配滿的時(shí)候觸發(fā),只會(huì)清理年輕代禀苦。
經(jīng)過(guò)這次GC后勺择,Eden區(qū)和From區(qū)已經(jīng)被清空。這個(gè)時(shí)候伦忠,“From”和“To”會(huì)交換他們的角色省核,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”昆码。不管怎樣气忠,都會(huì)保證名為T(mén)o的Survivor區(qū)域是空的。
Full GC
full gc是收集整個(gè)堆赋咽,包括young gen旧噪、old gen、perm gen(如果存在的話)脓匿、元空間(1.8及以上)等所有部分的模式淘钟。
- 手動(dòng)調(diào)用System.gc()方法 [增加了full GC頻率,不建議使用而是讓jvm自己管理內(nèi)存陪毡,可以設(shè)置-XX:+ DisableExplicitGC來(lái)禁止RMI調(diào)用System.gc]
- 發(fā)現(xiàn)perm gen(如果存在永久代的話)需分配空間但已經(jīng)沒(méi)有足夠空間
- 老年代空間不足米母,比如說(shuō)新生代的大對(duì)象大數(shù)組晉升到老年代就可能導(dǎo)致老年代空間不足。
mixed GC(G1特有)
混合GC
收集整個(gè)young gen以及部分old gen的GC毡琉。只有G1有這個(gè)模式
垃圾回收的兩種判定方法
1. 引用計(jì)數(shù)算法
在JDK1.2之前铁瞒,使用的是引用計(jì)數(shù)器算法,即當(dāng)這個(gè)類被加載到內(nèi)存之后桅滋,就會(huì)產(chǎn)生方法區(qū)慧耍,堆棧、程序計(jì)數(shù)器等一系列信息,當(dāng)創(chuàng)建對(duì)象的時(shí)候芍碧,為這個(gè)對(duì)象在堆椈蜕海空間中分配對(duì)象,同時(shí)會(huì)產(chǎn)生一個(gè)引用計(jì)數(shù)器泌豆,同時(shí)引用計(jì)數(shù)器+1怪瓶,當(dāng)有新的引用時(shí),引用計(jì)數(shù)器繼續(xù)+1践美,而當(dāng)其中一個(gè)引用銷(xiāo)毀時(shí),引用計(jì)數(shù)器-1找岖,當(dāng)引用計(jì)數(shù)器減為0的時(shí)候陨倡,標(biāo)志著這個(gè)對(duì)象已經(jīng)沒(méi)有引用了,可以回收了许布!但是這樣會(huì)有一個(gè)問(wèn)題:當(dāng)我們的代碼出現(xiàn)這樣的情況時(shí):
ObjA.obj=ObjB
ObjB.obj=ObjA
這樣的代碼會(huì)產(chǎn)生如下引用情形ObjA指向ObjB兴革,而ObjB又指向objA,這樣當(dāng)其他所有的引用都消失了之后蜜唾,ObjA和ObjB還有一個(gè)相互的引用杂曲,也就是說(shuō)兩個(gè)對(duì)象的引用計(jì)數(shù)器各為1,而實(shí)際上這兩個(gè)對(duì)象都已經(jīng)沒(méi)有額外的引用袁余,已經(jīng)是垃圾了擎勘。
2.可達(dá)性分析算法
可達(dá)性分析算法是從離散數(shù)學(xué)中的圖論引入的,程序把所有的引用關(guān)系看做一張圖颖榜,從一個(gè)節(jié)點(diǎn)GC Root開(kāi)始棚饵,尋找對(duì)應(yīng)的引用節(jié)點(diǎn),找到這個(gè)節(jié)點(diǎn)之后掩完,繼續(xù)尋找這個(gè)節(jié)點(diǎn)的引用節(jié)點(diǎn)噪漾,當(dāng)所有的引用節(jié)點(diǎn)尋找完畢之后,剩余的節(jié)點(diǎn)則被認(rèn)為是沒(méi)有被引用到的節(jié)點(diǎn)且蓬,即無(wú)用的節(jié)點(diǎn)欣硼。
目前Java中可作為GC Root的對(duì)象有:
- 虛擬機(jī)棧中引用的對(duì)象(本地變量表)
- 方法區(qū)中靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象(final的常量值)
- 本地方法棧中引用的對(duì)象(Native對(duì)象)。
java中存在的四種引用
- 強(qiáng)引用:只要引用存在恶阴,垃圾回收器永遠(yuǎn)不會(huì)回收诈胜。
- 軟引用:非必須引用,內(nèi)存溢出之前進(jìn)行回收冯事。代碼示例:
Object obj=new Object();
SoftReference<Object> sf=new SoftRerence<Object>(obj);
obj=null;
sf.get();//有時(shí)會(huì)返回null
這時(shí)候sf是對(duì)obj的一個(gè)軟引用耘斩,通過(guò)sf.get()方法可以取到這個(gè)對(duì)象,當(dāng)然這個(gè)對(duì)象被標(biāo)記為需要回收的對(duì)象時(shí)桅咆,則返回null括授;
軟引用主要用于用戶實(shí)現(xiàn)類似緩存的功能,在內(nèi)存不足的情況下直接通過(guò)軟引用取值,無(wú)需從繁忙的真實(shí)來(lái)源查詢數(shù)據(jù)荚虚,提升速度薛夜;當(dāng)內(nèi)存不足時(shí),自動(dòng)刪除這部分緩存數(shù)據(jù)版述,從真實(shí)的來(lái)源查詢這些數(shù)據(jù)梯澜。
- 弱引用:弱引用也是用來(lái)描述非必需對(duì)象的,當(dāng)JVM進(jìn)行垃圾回收時(shí)渴析,無(wú)論內(nèi)存是否充足晚伙,都會(huì)回收被弱引用關(guān)聯(lián)的對(duì)象〖蠹耄可以通過(guò)如下代碼實(shí)現(xiàn)
Object obj=new Object();
WeakReference<Object> wf=new WeakReference<Object>(obj);
obj=null;
wf.get();//有時(shí)會(huì)返回null
wf.isEnQueued();//返回是否被垃圾回收器標(biāo)記為即將回收的垃圾
弱引用是在第二次垃圾回收時(shí)回收咆疗,短時(shí)間內(nèi)通過(guò)弱引用取對(duì)應(yīng)的數(shù)據(jù),可以取到母债,當(dāng)執(zhí)行過(guò)第二次垃圾回收時(shí)午磁,將返回null。弱引用主要用于監(jiān)控對(duì)象是否已經(jīng)被標(biāo)記為即將回收的垃圾毡们,可以通過(guò)弱引用的isEnQueues方法返回對(duì)象是否被垃圾回收器標(biāo)記迅皇。
- 虛引用:虛引用和前面的軟引用、弱引用不同衙熔,它并不影響對(duì)象的生命周期登颓。在java中用java.lang.ref.PhantomReference類表示。如果一個(gè)對(duì)象與虛引用關(guān)聯(lián)红氯,則跟沒(méi)有引用與之關(guān)聯(lián)一樣挺据,在任何時(shí)候都可能被垃圾回收器回收。
要注意的是脖隶,虛引用必須和引用隊(duì)列關(guān)聯(lián)使用扁耐,當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對(duì)象時(shí),如果發(fā)現(xiàn)它還有虛引用产阱,就會(huì)把這個(gè)虛引用加入到與之 關(guān)聯(lián)的引用隊(duì)列中婉称。程序可以通過(guò)判斷引用隊(duì)列中是否已經(jīng)加入了虛引用,來(lái)了解被引用的對(duì)象是否將要被垃圾回收构蹬。如果程序發(fā)現(xiàn)某個(gè)虛引用已經(jīng)被加入到引用隊(duì)列德频,那么就可以在所引用的對(duì)象的內(nèi)存被回收之前采取必要的行動(dòng)闽铐∑翘荩可以通過(guò)如下代碼實(shí)現(xiàn)
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
垃圾收集器
如果說(shuō)垃圾回收算法是內(nèi)存回收的方法論沽讹,那么垃圾收集器就是具體實(shí)現(xiàn)。jvm會(huì)結(jié)合針對(duì)不同的場(chǎng)景及用戶的配置使用不同的收集器藻烤。
年輕代收集器:
Serial绷雏、ParNew头滔、Parallel Scavenge
老年代收集器:
Serial Old、Parallel Old涎显、CMS收集器
特殊收集器:
G1收集器(新型坤检,不在年輕、老年代范疇內(nèi))
年輕代收集器
Serial
最基本期吓、發(fā)展最久的收集器早歇,在jdk3以前是gc收集器的唯一選擇,Serial是單線程收集器讨勤,Serial收集器只能使用一條線程進(jìn)行收集工作箭跳,在收集的時(shí)候必須得停掉其它線程,等待收集工作完成其它線程才可以繼續(xù)工作潭千。
雖然Serial看起來(lái)很坑谱姓,需停掉別的線程以完成自己的gc工作,但是也不是完全沒(méi)用的脊岳,比如說(shuō)Serial在運(yùn)行在Client模式下優(yōu)于其它收集器(簡(jiǎn)單高效,不過(guò)一般都是用Server模式,64bit的jvm甚至沒(méi)Client模式)
串行收集器組合 Serial + Serial Old
優(yōu)點(diǎn):對(duì)于Client模式下的jvm來(lái)說(shuō)是個(gè)好的選擇垛玻。適用于單核CPU(現(xiàn)在基本都是多核了)
缺點(diǎn):收集時(shí)要暫停其它線程割捅,有點(diǎn)浪費(fèi)資源,多核下顯得帚桩。
ParNew收集器
可以認(rèn)為是Serial的升級(jí)版亿驾,因?yàn)樗С侄嗑€程[GC線程],而且收集算法账嚎、Stop The World莫瞬、回收策略和Serial一樣,就是可以有多個(gè)GC線程并發(fā)運(yùn)行郭蕉,它是HotSpot第一個(gè)真正意義實(shí)現(xiàn)并發(fā)的收集器疼邀。默認(rèn)開(kāi)啟線程數(shù)和當(dāng)前cpu數(shù)量相同,如果cpu核數(shù)很多不想用那么多召锈,可以通過(guò)-XX:ParallelGCThreads來(lái)控制垃圾收集線程的數(shù)量旁振。
優(yōu)點(diǎn):
- 支持多線程,多核CPU下可以充分的利用CPU資源
- 運(yùn)行在Server模式下新生代首選的收集器【重點(diǎn)是因?yàn)樾律倪@幾個(gè)收集器只有它和Serial可以配合CMS收集器一起使用】
缺點(diǎn): 在單核下表現(xiàn)不會(huì)比Serial好涨岁,由于在單核能利用多核的優(yōu)勢(shì)拐袜,在線程收集過(guò)程中可能會(huì)出現(xiàn)頻繁上下文切換,導(dǎo)致額外的開(kāi)銷(xiāo)梢薪。
Parallel Scavenge
采用復(fù)制算法的收集器蹬铺,和ParNew一樣支持多線程。
但是該收集器重點(diǎn)關(guān)心的是吞吐量(吞吐量 = 代碼運(yùn)行時(shí)間 / (代碼運(yùn)行時(shí)間 + 垃圾收集時(shí)間) 如果代碼運(yùn)行100min垃圾收集1min秉撇,則為99%)
對(duì)于用戶界面甜攀,適合使用GC停頓時(shí)間短,不然因?yàn)榭D導(dǎo)致交互界面卡頓將很影響用戶體驗(yàn)秋泄。
對(duì)于后臺(tái)高吞吐量可以高效率的利用cpu盡快完成程序運(yùn)算任務(wù),適合后臺(tái)運(yùn)算
并行收集器組合 Parallel Scavenge + Parallel Old
Parallel Scavenge注重吞吐量赴邻,所以也成為"吞吐量?jī)?yōu)先"收集器印衔。
JDK7和8中,作為年輕代默認(rèn)的收集器
老年代收集器
Serial Old
和新生代的Serial一樣為單線程姥敛,Serial的老年代版本奸焙,不過(guò)它采用"標(biāo)記-整理算法",這個(gè)模式主要是給Client模式下的JVM使用彤敛。
如果是Server模式有兩大用途:
- jdk5前和Parallel Scavenge搭配使用与帆,jdk5前也只有這個(gè)老年代收集器可以和它搭配。
- 作為CMS收集器的后備墨榄。
Parallel Old
支持多線程玄糟,Parallel Scavenge的老年版本,jdk6開(kāi)始出現(xiàn)袄秩,采用"標(biāo)記-整理算法"阵翎。
在jdk6以前,新生代的Parallel Scavenge只能和Serial Old配合使用之剧,而且Serial Old為單線程Server模式下無(wú)法充分利用多核cpu郭卫,這種結(jié)合并不能讓?xiě)?yīng)用的吞吐量最大化。
Parallel Old的出現(xiàn)結(jié)合Parallel Scavenge背稼,真正的形成“吞吐量?jī)?yōu)先”的收集器組合贰军。
JDK7和8中,作為老年代默認(rèn)的收集器
CMS收集器
CMS收集器(Concurrent Mark Sweep)是以一種獲取最短回收停頓時(shí)間為目標(biāo)的收集器蟹肘。(重視響應(yīng)词疼,可以帶來(lái)好的用戶體驗(yàn),被sun稱為并發(fā)低停頓收集器)啟用CMS:-XX:+UseConcMarkSweepGC
正如其名帘腹,CMS采用的是"標(biāo)記-清除"(Mark Sweep)算法贰盗,而且是支持并發(fā)的。
它的運(yùn)作分為4個(gè)階段:
- 初始標(biāo)記(initial mark):標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象
- 并發(fā)標(biāo)記(concurrent mark):并發(fā)標(biāo)記就需要標(biāo)記出GC roots 關(guān)聯(lián)到的對(duì)象的引用對(duì)象有哪些阳欲。比如說(shuō) A -> B (A引用B童太,假設(shè)A是GC Roots關(guān)聯(lián)到的對(duì)象),那么這個(gè)階段就是標(biāo)記出B對(duì)象胸完,A對(duì)象會(huì)在初始標(biāo)記中標(biāo)記出來(lái)书释。這個(gè)過(guò)程是可以和用戶線程并發(fā)執(zhí)行的。所謂的并發(fā)的實(shí)現(xiàn)赊窥,可以有幾種方式爆惧,比如說(shuō),標(biāo)記了100個(gè)對(duì)象锨能,那么就停一停扯再,讓用戶線程跑一會(huì)芍耘;再比如說(shuō),標(biāo)記了10ms熄阻,再停一停斋竞,之類的實(shí)現(xiàn)。
- 重新標(biāo)記(remark):為了修正因并發(fā)標(biāo)記期間用戶程序運(yùn)作而產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄秃殉,會(huì)有些許停頓坝初,時(shí)間上一般 初始標(biāo)記 < 重新標(biāo)記 < 并發(fā)標(biāo)記
- 并發(fā)清除(sweep):將前面標(biāo)記對(duì)象的內(nèi)存回收,這個(gè)階段GC線程與用戶線程并發(fā)運(yùn)行钾军。
以上初始標(biāo)記和重新標(biāo)記需要停掉其它運(yùn)行java線程鳄袍。之所以說(shuō)CMS的用戶體驗(yàn)好,是因?yàn)镃MS收集器的內(nèi)存回收工作是可以和用戶線程一起并發(fā)執(zhí)行吏恭。
總體上CMS是款優(yōu)秀的收集器拗小,但是它也有缺點(diǎn):
- cms對(duì)cpu特別敏感,cms運(yùn)行線程和應(yīng)用程序并發(fā)執(zhí)行需要多核cpu樱哼,如果cpu核數(shù)多的話可以發(fā)揮它并發(fā)執(zhí)行的優(yōu)勢(shì)哀九,但是cms默認(rèn)配置啟動(dòng)的時(shí)候垃圾線程數(shù)為 (cpu數(shù)量+3)/4,它的性能很容易受cpu核數(shù)影響搅幅,當(dāng)cpu的數(shù)目少的時(shí)候比如說(shuō)為為2核阅束,如果這個(gè)時(shí)候cpu運(yùn)算壓力比較大,還要分一半給cms運(yùn)作盏筐,這可能會(huì)很大程度的影響到計(jì)算機(jī)性能围俘。
- cms無(wú)法處理浮動(dòng)垃圾砸讳,可能導(dǎo)致Concurrent Mode Failure(并發(fā)模式故障)而觸發(fā)full GC
- 由于cms是采用"標(biāo)記-清除“算法,因此就會(huì)存在垃圾碎片的問(wèn)題琢融,為了解決這個(gè)問(wèn)題cms提供了
-XX:+UseCMSCompactAtFullCollection
選項(xiàng),這個(gè)選項(xiàng)相當(dāng)于一個(gè)開(kāi)關(guān)(默認(rèn)開(kāi)啟)簿寂,用于CMS要進(jìn)行full GC時(shí)開(kāi)啟內(nèi)存碎片合并漾抬,內(nèi)存整理的過(guò)程是無(wú)法并發(fā)的,且開(kāi)啟這個(gè)選項(xiàng)會(huì)影響性能(比如停頓時(shí)間變長(zhǎng))
Concurrent mode failure:如果CMS回收過(guò)程還沒(méi)有執(zhí)行完常遂,老年代的剩余空間就用完了纳令,或者,當(dāng)前老年代空間不能滿足一次內(nèi)存分配請(qǐng)求(可能對(duì)象較大)克胳,那么此時(shí)將觸發(fā)擔(dān)保機(jī)制平绩,停頓所有用戶線程,串行老年代收集器將會(huì)以STW的方式進(jìn)行一次GC漠另,從而造成較大停頓時(shí)間捏雌;
浮動(dòng)垃圾:由于cms支持運(yùn)行的時(shí)候用戶線程也在運(yùn)行,程序運(yùn)行的時(shí)候會(huì)產(chǎn)生新的垃圾笆搓,這里產(chǎn)生的垃圾就是浮動(dòng)垃圾性湿,cms無(wú)法當(dāng)次處理纬傲,得等下次才可以。
假如有一個(gè)對(duì)象GC線程沒(méi)有標(biāo)記(用戶線程之前沒(méi)在用)肤频,然后輪到了用戶線程叹括,用戶線程說(shuō),這個(gè)對(duì)象我重新又要用了宵荒,不要把這個(gè)對(duì)象GC掉汁雷,這個(gè)時(shí)候怎么辦?假如這個(gè)時(shí)候處理不了骇扇,還是GC了摔竿,那么程序就直接報(bào)錯(cuò)了,這個(gè)是不允許的少孝,解決辦法可以百度搜索“cms 三色標(biāo)記法”獲取答案
G1收集器
G1(garbage first)收集器是當(dāng)前最為前沿的收集器之一(1.7以后才開(kāi)始有)继低,同cms一樣也是關(guān)注降低延遲,是用于替代cms功能更為強(qiáng)大的新型收集器稍走,因?yàn)樗鉀Q了cms產(chǎn)生空間碎片等一系列缺陷袁翁。
當(dāng)G1確定有必要進(jìn)行垃圾回收時(shí),它會(huì)先收集存活數(shù)據(jù)最少的區(qū)域(垃圾優(yōu)先)
g1的特別之處在于它強(qiáng)化了分區(qū)婿脸,弱化了分代的概念粱胜,是區(qū)域化、增量式的收集器狐树,它不屬于新生代也不屬于老年代收集器焙压。
用到的算法為標(biāo)記-清理、復(fù)制算法
G1是區(qū)域化的抑钟,它將java堆內(nèi)存劃分為若干個(gè)大小相同的區(qū)域"region“涯曲,JVM可以設(shè)置每個(gè)region的大小(1-32m,大小得看堆內(nèi)存大小,必須是2的冪)在塔,它會(huì)根據(jù)當(dāng)前的堆內(nèi)存分配合理的region大小幻件。
g1通過(guò)并發(fā)(并行)標(biāo)記階段查找老年代存活對(duì)象,通過(guò)并行復(fù)制壓縮存活對(duì)象(這樣可以省出連續(xù)空間供大對(duì)象使用)蛔溃。
g1將一組或多組區(qū)域中存活對(duì)象以增量并行的方式復(fù)制到不同區(qū)域進(jìn)行壓縮绰沥,從而減少堆碎片,目標(biāo)是盡可能多回收堆空間贺待,且盡可能不超出暫停目標(biāo)以達(dá)到低延遲的目的徽曲。
g1提供三種垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器麸塞,根據(jù)區(qū)域而不是分代秃臣,新生代老年代的對(duì)象它都能回收。
幾個(gè)重要的默認(rèn)值喘垂,更多的查看官方文檔oracle官方g1中文文檔
g1是自適應(yīng)的回收器甜刻,提供了若干個(gè)默認(rèn)值绍撞,無(wú)需修改就可高效運(yùn)作
- -XX:G1HeapRegionSize=n 設(shè)置g1 region大小,不設(shè)置的話自己會(huì)根據(jù)堆大小算得院,目標(biāo)是根據(jù)最小堆內(nèi)存劃分2048個(gè)區(qū)域
- -XX:MaxGCPauseMillis=200 最大停頓時(shí)間 默認(rèn)200毫秒
JDK9中傻铣,G1作為默認(rèn)的收集器
JDK7/8,默認(rèn)關(guān)閉的祥绞,開(kāi)啟選項(xiàng) -XX:+UseG1GC
更詳細(xì)的G1垃圾回收介紹非洲,請(qǐng)查看這篇文章:
https://blog.csdn.net/coderlius/article/details/79272773
(希望博主別刪博客啊。蜕径。两踏。)