国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁(yè) > 編程 > Java > 正文

二、Java中的垃圾回收

2019-11-06 07:34:49
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友

  在垃圾回收中,標(biāo)記清除(Mark and Sweep)是最重要的一個(gè)思想。但是想要在實(shí)際場(chǎng)景中應(yīng)用這一思想,還是需要進(jìn)行一些調(diào)整的。本文接下來(lái)就通過簡(jiǎn)單的示例來(lái)分析JVM是如何保證安全持續(xù)的分配對(duì)象的。

一、碎片內(nèi)存區(qū)域整理

  JVM進(jìn)行垃圾回收的目的是想要重復(fù)使用內(nèi)存中那些不再被使用的對(duì)象所占的存儲(chǔ)空間。這些不可用對(duì)象在內(nèi)存中一般都不會(huì)是連續(xù)分布的,所以它們使用的空間也是比較零散的,而這種零散將會(huì)導(dǎo)致以下兩個(gè)問題:    - 內(nèi)存中的寫操作將會(huì)變得更加耗時(shí),因?yàn)樵趯懙臅r(shí)候需要花時(shí)間去找到一塊足夠的內(nèi)存碎片 - 當(dāng)創(chuàng)建一個(gè)新對(duì)象時(shí),JVM會(huì)分配給該對(duì)象一塊連續(xù)的內(nèi)存區(qū)域。所以,如果所有的內(nèi)存碎片大小都不足以容納該對(duì)象時(shí),就會(huì)拋出一個(gè)無(wú)法分配內(nèi)存空間的錯(cuò)誤。

  為了避免上述兩個(gè)問題,JVM在垃圾回收時(shí)除了進(jìn)行標(biāo)記清除之外,還會(huì)有一步內(nèi)存整理操作。內(nèi)存整理會(huì)將所有仍然在使用的對(duì)象在內(nèi)存中重新分配,使用連續(xù)的一塊內(nèi)存空間。對(duì)碎片內(nèi)存的整理過程如下圖所示,      這里寫圖片描述

二、對(duì)象分代理論

  在前面提到過,在進(jìn)行垃圾回收時(shí)會(huì)將整個(gè)應(yīng)用系統(tǒng)暫停,并且當(dāng)需要進(jìn)行回收的對(duì)象越多,這個(gè)暫停時(shí)間也會(huì)越長(zhǎng)。為了盡量縮短這個(gè)暫停時(shí)間,我們可以選擇對(duì)少部分空間進(jìn)行清理。研究表明,

大部分對(duì)象很快就不再使用了

并且對(duì)象也不會(huì)在很長(zhǎng)時(shí)間內(nèi)都可用

  基于上面這兩個(gè)現(xiàn)象,可以提出一個(gè)簡(jiǎn)單的分代理論。在JVM中的對(duì)象可以劃分為新生代(Young Generation)和老年代(Old Generation),老年代也可以成為老年區(qū)(Tenured)。      這里寫圖片描述

  從上圖中可以看出,大部分的對(duì)象很快就不再使用,隨著時(shí)間的推移,最后僅有少部分對(duì)象還繼續(xù)存活。

  將JVM劃分為兩個(gè)年代區(qū)后,就可以對(duì)不同區(qū)域采用不同的垃圾回收策略來(lái)進(jìn)行垃圾回收了。但是這也不是說(shuō)這樣就沒有問題了,比如說(shuō)有可能在新生代區(qū)域中有一個(gè)對(duì)象和老年代區(qū)域中的某個(gè)對(duì)象互相引用,那么由于這兩個(gè)對(duì)象始終有引用的存在,在垃圾回收時(shí)就無(wú)法將這兩個(gè)實(shí)質(zhì)無(wú)用的對(duì)象進(jìn)行回收。

  分代理論并不能很好的解決垃圾回收時(shí)的很多問題,比如,GC算法對(duì)于那些很快就不可用的對(duì)象,以及可能會(huì)存活很久的對(duì)象的處理效果都很好,但是對(duì)那些存活時(shí)間不長(zhǎng)不短的對(duì)象的回收效果就會(huì)很不好。

三、內(nèi)存區(qū)域劃分

  下圖中所展示的對(duì)JVM堆內(nèi)存區(qū)域的劃分和對(duì)象分代有點(diǎn)類似,需要注意的是要搞清楚垃圾回收在不同內(nèi)存區(qū)域時(shí)采取的是什么策略。不同GC算法某區(qū)域的垃圾回收實(shí)現(xiàn)細(xì)節(jié)可能并不相同,但是大體思路基本上還是一致的。      這里寫圖片描述   

  隨著對(duì)象的生命周期增加,該對(duì)象所處區(qū)域會(huì)逐漸后移。上圖中將內(nèi)存區(qū)域劃分為五個(gè)片段,分別是一個(gè)Eden區(qū),兩個(gè)Survivor區(qū),這三個(gè)區(qū)域組成一個(gè)Young區(qū)。一個(gè)Tenured區(qū),以及一個(gè)Permgen區(qū)。接下來(lái)的文章中,我們稱Eden區(qū)為新生區(qū),Survivor區(qū)為存活區(qū),而Eden區(qū)和Survivor區(qū)共同組成新生代,Tenured稱為老年代,Permgen稱為永久代。

1、Eden區(qū)(新生區(qū))

  新生區(qū) 是對(duì)象生成時(shí)所在的區(qū)域,即新建一個(gè)對(duì)象時(shí),首先會(huì)在內(nèi)存中的新生區(qū)域?yàn)槠浞峙淇臻g。由于可能會(huì)有多線程同時(shí)生成新對(duì)象,所以新生區(qū) 會(huì)進(jìn)一步劃分成一個(gè)或多個(gè)TLAB(Thread Local Allocation Buffer, 線程自有的內(nèi)存分配區(qū)域)區(qū)。一個(gè)線程可以在對(duì)應(yīng)的TLAB區(qū)域直接創(chuàng)建若干個(gè)對(duì)象,避免了多線程時(shí)的鎖消耗。當(dāng)一個(gè)TLAB區(qū)域的內(nèi)存不足以繼續(xù)創(chuàng)建對(duì)象時(shí),接下來(lái)就會(huì)使用新生代中劃分的一片共享區(qū)域。新生區(qū) 中TLAB和共享內(nèi)存區(qū)域的劃分如下圖所示,      這里寫圖片描述      如果在共享區(qū)域中仍然沒有足夠的內(nèi)存進(jìn)行分配,就會(huì)觸發(fā)新生區(qū)中的一次垃圾回收。如果新生區(qū)垃圾回收后,內(nèi)存還是不足,那么接下來(lái)就觸發(fā)老年代中的垃圾回收動(dòng)作了。

  當(dāng)Eden區(qū)進(jìn)行垃圾回收時(shí),GC會(huì)將所有從root可達(dá)的對(duì)象標(biāo)記為存活。

  前面提到,在JVM中可能會(huì)存在一種跨區(qū)域引用的情況,即可能在其他年代區(qū)域中會(huì)有對(duì)象引用了Eden區(qū)中的某對(duì)象。這種情況會(huì)破壞上面的內(nèi)存區(qū)域劃分策略。面對(duì)這種情況,JVM使用了一種叫做卡片標(biāo)記(card-marking)的方法。從本質(zhì)上來(lái)說(shuō),JVM只需要標(biāo)記出Eden區(qū)域中那些被老年區(qū)對(duì)象所引用的臟對(duì)象的大致位置。可以參考Nitsan的博客查看更多信息。

  標(biāo)記階段完成后,在Eden區(qū)域中的所有存活對(duì)象將會(huì)被復(fù)制到存活區(qū)中。此時(shí)Eden區(qū)就被清空了,可以進(jìn)行下一輪的對(duì)象生成。這一步被稱為是標(biāo)記和復(fù)制(Mark and Copy):存活的對(duì)象被標(biāo)記,然后將它們復(fù)制(不是移動(dòng))到存活區(qū)中。

2、Survivor區(qū)(存活區(qū))

  緊隨Eden區(qū)之后有兩個(gè)Survivor區(qū),分別命名為from和to。這兩個(gè)Survivor區(qū)總是有一個(gè)處于empty狀態(tài),并且處于empty狀態(tài)的Survivor區(qū)稱為to區(qū),另一個(gè)Survivor區(qū)稱為from區(qū)。

  在新生代(上圖中的整個(gè)Young區(qū)域)垃圾回收時(shí),新生代中(包括Eden區(qū)和from區(qū))的所有存活對(duì)象將會(huì)復(fù)制到to區(qū)中。

  這里寫圖片描述

  在新生代垃圾回收過程中,不再使用的對(duì)象將會(huì)在下次垃圾回收時(shí)清除,而一直存活的對(duì)象會(huì)在兩個(gè)Survivor區(qū)中來(lái)回復(fù)制。當(dāng)某個(gè)對(duì)象足夠老時(shí),這個(gè)對(duì)象將會(huì)被升級(jí)到老年代中。

  那么如何判斷一個(gè)對(duì)象的年齡呢?在每發(fā)生一次GC時(shí),那些仍然存活的對(duì)象的年齡就會(huì)增長(zhǎng)一次,當(dāng)某個(gè)對(duì)象的年齡增長(zhǎng)到一個(gè)閾值時(shí),這個(gè)對(duì)象就會(huì)被認(rèn)定為足夠老,可以升級(jí)到老年代了。

  這個(gè)年齡閾值是可以由參數(shù)進(jìn)行設(shè)置的,該參數(shù)是-XX:MaxTenuringThreshold。如果設(shè)置-XX:MaxTenuringThreshold=0,那么每次新生代GC時(shí),對(duì)象不再在from和to區(qū)之間進(jìn)行復(fù)制,而是存活對(duì)象直接進(jìn)入老年代。該參數(shù)默認(rèn)值是15,即發(fā)生15次GC,如果某個(gè)對(duì)象仍然存活,則該對(duì)象會(huì)被升級(jí)到老年代。

  需要注意,有另一種情況會(huì)提前將新生代中的存活對(duì)象升遷到老年代。如果在GC時(shí)發(fā)現(xiàn)Survivor區(qū)已經(jīng)無(wú)法容納所有的存活對(duì)象時(shí),也會(huì)有對(duì)象會(huì)被升遷到老年代中。

3、Old區(qū)(老年代)

  老年代的垃圾回收過程會(huì)更加復(fù)雜,這一部分區(qū)域更大,并且其中的對(duì)象可能存活的概率也更高。即在GC時(shí)在大量對(duì)象中可能只有少部分會(huì)被回收。

  老年代的GC要比新生代GC的頻率低得多。由于該區(qū)域中的大多數(shù)對(duì)象在GC時(shí)都可能存活,所以老年代中不會(huì)進(jìn)行標(biāo)記復(fù)制(Mark and Copy)來(lái)整理碎片空間,而是通過移動(dòng)對(duì)象來(lái)整理。基本上是按照如下步驟進(jìn)行,

將存活的對(duì)象通過標(biāo)記為(marked bit)進(jìn)行標(biāo)記刪除所有不再使用的對(duì)象將存活的對(duì)象進(jìn)行復(fù)制,從按照老年代空間開始處依次存放   

4、PermGen區(qū)(永久代)

  在java 8之前的JVM版本中,還有一個(gè)永久代區(qū)域。這個(gè)區(qū)域一般用于存放元數(shù)據(jù),比如類定義信息,內(nèi)部化的字符串(internalized strings)等。在實(shí)際應(yīng)用中很難確定這片區(qū)域需要使用多少空間,因此該區(qū)域很可能會(huì)拋出java.lang.OutOfMemoryError: Permgen space的錯(cuò)誤信息。除非該報(bào)錯(cuò)明確的是由于內(nèi)存泄漏導(dǎo)致,否則就需要通過調(diào)整永久代區(qū)域大小來(lái)解決這個(gè)問題。調(diào)整永久代區(qū)域內(nèi)存大小的方式如下,

java -XX:MaxPermSize=256m com.mycompany.Myapplication

5、元數(shù)據(jù)區(qū)(Metaspace)

  由于很難去確定永久代的空間大小調(diào)節(jié),所以在Java 8中已經(jīng)去除了永久代的概念,新增了一個(gè)元數(shù)據(jù)區(qū)。

  在Java 8中,類定義信息都保存在元數(shù)據(jù)區(qū)中。元數(shù)據(jù)區(qū)使用的是本地內(nèi)存(native memory),并且不會(huì)有Java堆中對(duì)象的引用。這樣的話,元數(shù)據(jù)區(qū)的大小就由運(yùn)行Java進(jìn)程的機(jī)器上的本地內(nèi)存大小決定了。這樣就可以盡可能的避免出現(xiàn)java.lang.OutOfMemoryError: Permgen space報(bào)錯(cuò)。

  但是如果不加限制的使用本地內(nèi)存的話,可能會(huì)影響到本機(jī)的內(nèi)存交換或者導(dǎo)致本機(jī)內(nèi)存分配失敗。在這種情況下,最好還是對(duì)元數(shù)據(jù)區(qū)可使用的本地內(nèi)存大小進(jìn)行一定的限制,方法如下

java -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication

四、Minor GC vs Major GC vs Full GC

  GC主要有三種形式,Minor GC,Major GC以及Full GC。在本節(jié)中,會(huì)對(duì)這三種GC事件進(jìn)行分析。

1、Minor GC

  新生代(Young區(qū))中進(jìn)行的垃圾回收事件被稱為Minor GC。在理解Minor GC事件時(shí),需要注意以下幾點(diǎn),

當(dāng)JVM無(wú)法為新生成對(duì)象分配內(nèi)存空間時(shí)就會(huì)觸發(fā)Minor GC,比如Eden區(qū)滿時(shí)。所以新生成對(duì)象的頻率越高,越容易導(dǎo)致Minor GC。在Minor GC過程中,不會(huì)對(duì)永久代區(qū)域進(jìn)行處理。所有新生代區(qū)域中對(duì)象被永久代區(qū)域中某些對(duì)象引用的對(duì)象,都直接當(dāng)成是GC roots過來(lái)的引用。從新生代區(qū)域指向永久代區(qū)域的引用,在標(biāo)記過程中會(huì)被忽略。需要注意的是Minor GC是會(huì)導(dǎo)致應(yīng)用線程全部暫停的(stop-the-world)。對(duì)大多數(shù)應(yīng)用來(lái)說(shuō),Minor GC導(dǎo)致的暫停可以忽略不計(jì),因?yàn)镋den區(qū)中的對(duì)象大部分都是垃圾,在Minor GC時(shí)僅僅只有少量的對(duì)象會(huì)被復(fù)制到Survivor區(qū)或者Old區(qū)。

2、Major GC vs Full GC

  新生代發(fā)送的GC被稱為Minor GC,那么老年代區(qū)域發(fā)生的GC就被稱為Major GC,新生代和老年代同時(shí)發(fā)生的GC被稱為Full GC。

  按照上面的定義,想要區(qū)分開Major GC和Full GC是有點(diǎn)困難的,或者說(shuō)Major GC與Full GC之間的劃分并沒有那么明顯。許多Major GC都是由Minor GC觸發(fā)的。另外,對(duì)于一些GC算法,比如G1算法,在垃圾回收時(shí)只會(huì)清理部分內(nèi)存空間。

  所以,我們完全無(wú)需糾結(jié)于到底如何區(qū)分Major GC和Full GC。我們只需要關(guān)注GC是否會(huì)暫停應(yīng)用線程,或者GC是否可以與應(yīng)用線程并行執(zhí)行。

  下面通過一個(gè)實(shí)例來(lái)進(jìn)行分析,這個(gè)實(shí)例中使用的是并發(fā)標(biāo)記清除垃圾收集器。(-XX:+UseConcMarkSweepGC)

  觀察jstat工具的輸出

my-PRecious: me$ jstat -gc -t 4235 1sTime S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT5.7 34048.0 34048.0 0.0 34048.0 272640.0 194699.7 1756416.0 181419.9 18304.0 17865.1 2688.0 2497.6 3 0.275 0 0.000 0.2756.7 34048.0 34048.0 34048.0 0.0 272640.0 247555.4 1756416.0 263447.9 18816.0 18123.3 2688.0 2523.1 4 0.359 0 0.000 0.3597.7 34048.0 34048.0 0.0 34048.0 272640.0 257729.3 1756416.0 345109.8 19072.0 18396.6 2688.0 2550.3 5 0.451 0 0.000 0.4518.7 34048.0 34048.0 34048.0 34048.0 272640.0 272640.0 1756416.0 444982.5 19456.0 18681.3 2816.0 2575.8 7 0.550 0 0.000 0.5509.7 34048.0 34048.0 34046.7 0.0 272640.0 16777.0 1756416.0 587906.3 20096.0 19235.1 2944.0 2631.8 8 0.720 0 0.000 0.72010.7 34048.0 34048.0 0.0 34046.2 272640.0 80171.6 1756416.0 664913.4 20352.0 19495.9 2944.0 2657.4 9 0.810 0 0.000 0.81011.7 34048.0 34048.0 34048.0 0.0 272640.0 129480.8 1756416.0 745100.2 20608.0 19704.5 2944.0 2678.4 10 0.896 0 0.000 0.89612.7 34048.0 34048.0 0.0 34046.6 272640.0 164070.7 1756416.0 822073.7 20992.0 19937.1 3072.0 2702.8 11 0.978 0 0.000 0.97813.7 34048.0 34048.0 34048.0 0.0 272640.0 211949.9 1756416.0 897364.4 21248.0 20179.6 3072.0 2728.1 12 1.087 1 0.004 1.09114.7 34048.0 34048.0 0.0 34047.1 272640.0 245801.5 1756416.0 597362.6 21504.0 20390.6 3072.0 2750.3 13 1.183 2 0.050 1.23315.7 34048.0 34048.0 0.0 34048.0 272640.0 21474.1 1756416.0 757347.0 22012.0 20792.0 3200.0 2791.0 15 1.336 2 0.050 1.38616.7 34048.0 34048.0 34047.0 0.0 272640.0 48378.0 1756416.0 838594.4 22268.0 21003.5 3200.0 2813.2 16 1.433 2 0.050 1.484

  上面這段輸出獲取的是JVM在啟動(dòng)17秒內(nèi)的信息。我們可以看到發(fā)生了12次Minor GC后連續(xù)進(jìn)行了2次Full GC,總共耗時(shí)50毫秒。這些信息也可以通過圖形界面工具,例如jconsole或者jvisualvm來(lái)獲取。

  接下來(lái)我們?cè)倏纯幢敬蜫VM啟動(dòng)后的垃圾回收日志,需要打印垃圾回收日志可以通過參數(shù)-XX:+PrintGCDetails來(lái)控制,

3.157: [GC (Allocation Failure) 3.157: [ParNew: 272640K->34048K(306688K), 0.0844702 secs] 272640K->69574K(2063104K), 0.0845560 secs] [Times: user=0.23 sys=0.03, real=0.09 secs]4.092: [GC (Allocation Failure) 4.092: [ParNew: 306688K->34048K(306688K), 0.1013723 secs] 342214K->136584K(2063104K), 0.1014307 secs] [Times: user=0.25 sys=0.05, real=0.10 secs]... cut for brevity ...11.292: [GC (Allocation Failure) 11.292: [ParNew: 306686K->34048K(306688K), 0.0857219 secs] 971599K->779148K(2063104K), 0.0857875 secs] [Times: user=0.26 sys=0.04, real=0.09 secs]12.140: [GC (Allocation Failure) 12.140: [ParNew: 306688K->34046K(306688K), 0.0821774 secs] 1051788K->856120K(2063104K), 0.0822400 secs] [Times: user=0.25 sys=0.03, real=0.08 secs]12.989: [GC (Allocation Failure) 12.989: [ParNew: 306686K->34048K(306688K), 0.1086667 secs] 1128760K->931412K(2063104K), 0.1087416 secs] [Times: user=0.24 sys=0.04, real=0.11 secs]13.098: [GC (CMS Initial Mark) [1 CMS-initial-mark: 897364K(1756416K)] 936667K(2063104K), 0.0041705 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]13.102: [CMS-concurrent-mark-start]13.341: [CMS-concurrent-mark: 0.238/0.238 secs] [Times: user=0.36 sys=0.01, real=0.24 secs]13.341: [CMS-concurrent-preclean-start]13.350: [CMS-concurrent-preclean: 0.009/0.009 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]13.350: [CMS-concurrent-abortable-preclean-start]13.878: [GC (Allocation Failure) 13.878: [ParNew: 306688K->34047K(306688K), 0.0960456 secs] 1204052K->1010638K(2063104K), 0.0961542 secs] [Times: user=0.29 sys=0.04, real=0.09 secs]14.366: [CMS-concurrent-abortable-preclean: 0.917/1.016 secs] [Times: user=2.22 sys=0.07, real=1.01 secs]14.366: [GC (CMS Final Remark) [YG occupancy: 182593 K (306688 K)]14.366: [Rescan (parallel) , 0.0291598 secs]14.395: [weak refs processing, 0.0000232 secs]14.395: [class unloading, 0.0117661 secs]14.407: [scrub symbol table, 0.0015323 secs]14.409: [scrub string table, 0.0003221 secs][1 CMS-remark: 976591K(1756416K)] 1159184K(2063104K), 0.0462010 secs] [Times: user=0.14 sys=0.00, real=0.05 secs]14.412: [CMS-concurrent-sweep-start]14.633: [CMS-concurrent-sweep: 0.221/0.221 secs] [Times: user=0.37 sys=0.00, real=0.22 secs]14.633: [CMS-concurrent-reset-start]14.636: [CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

     從上面的信息中可以更清楚的看到,在12次Minor GC后,并不是真的觸發(fā)了兩次Full GC,而是實(shí)際上執(zhí)行了一次老年代的GC。這一過程的執(zhí)行步驟如下,

初始標(biāo)記過程,耗時(shí) 0.0041705秒。這一過程暫停了整個(gè)應(yīng)用線程。標(biāo)記和預(yù)清理過程。這一過程與應(yīng)用線程并發(fā)執(zhí)行。重新標(biāo)記過程,耗時(shí) 0.0462010。這一過程再次暫停了整個(gè)應(yīng)用線程。

清除過程。這一過程也是并發(fā)執(zhí)行,沒有暫停應(yīng)用線程。

  所以,在垃圾回收日志中我們獲取到的實(shí)際情況是,系統(tǒng)并不是真正的執(zhí)行了兩次Full GC操作,而是僅執(zhí)行了一次Major GC來(lái)清理老年代空間。這里Major GC導(dǎo)致的兩次暫停時(shí)間加起來(lái)剛好等于上面日志中Full GC導(dǎo)致的暫停時(shí)間。

  如果只關(guān)注系統(tǒng)延遲的話,使用jstat工具獲取到的信息就足夠進(jìn)行調(diào)優(yōu)決策了。而GC日志數(shù)據(jù)會(huì)清楚的列舉出總共50毫秒的暫停時(shí)間都消耗在哪些階段。如果需要調(diào)整系統(tǒng)吞吐量的話,jstat獲取的信息就會(huì)有誤導(dǎo)性了,因?yàn)樗涣信e出了GC過程中stop-the-world的總時(shí)間,將GC過程中并發(fā)執(zhí)行的操作給隱藏起來(lái)了。而實(shí)際上,GC過程中的并發(fā)操作過程也會(huì)影響到應(yīng)用的運(yùn)行。


發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 灵宝市| 海晏县| 马公市| 鹰潭市| 重庆市| 曲周县| 新沂市| 江源县| 敦煌市| 肃南| 龙江县| 肇州县| 乳山市| 绩溪县| 张家港市| 龙泉市| 澳门| 富民县| 万盛区| 云浮市| 临猗县| 天门市| 齐齐哈尔市| 肇东市| 瓮安县| 阿鲁科尔沁旗| 湘西| 关岭| 澜沧| 巩留县| 聊城市| 祁阳县| 尚义县| 甘谷县| 长顺县| 紫云| 青岛市| 措勤县| 沈丘县| 三亚市| 霍城县|