Java內(nèi)存分配機制 這里所說的內(nèi)存分配,,主要指的是在堆上的分配,,一般的,對象的內(nèi)存分配都是在堆上進行,,但現(xiàn)代技術(shù)也支持將對象拆成標(biāo)量類型(標(biāo)量類型即原子類型,,表示單個值,可以是基本類型或String等),,然后在棧上分配,,在棧上分配的很少見,我們這里不考慮。 Java內(nèi)存分配和回收的機制概括的說,,就是:分代分配,,分代回收。對象將根據(jù)存活的時間被分為:年輕代(Young Generation),、年老代(Old Generation),、永久代(Permanent Generation,也就是方法區(qū)),。如下圖(來源于《成為JavaGC專家part I》,,http://www./1993.html): 年輕代(Young Generation):對象被創(chuàng)建時,內(nèi)存的分配首先發(fā)生在年輕代(大對象可以直接被創(chuàng)建在年老代),,大部分的對象在創(chuàng)建后很快就不再使用,,因此很快變得不可達,于是被年輕代的GC機制清理掉(IBM的研究表明,,98%的對象都是很快消亡的),,這個GC機制被稱為Minor GC或叫Young GC。注意,,Minor GC并不代表年輕代內(nèi)存不足,,它事實上只表示在Eden區(qū)上的GC,。 年輕代上的內(nèi)存分配是這樣的,,年輕代可以分為3個區(qū)域:Eden區(qū)(伊甸園,亞當(dāng)和夏娃偷吃禁果生娃娃的地方,,用來表示內(nèi)存首次分配的區(qū)域,,再貼切不過)和兩個存活區(qū)(Survivor 0 、Survivor 1),。內(nèi)存分配過程為(來源于《成為JavaGC專家part I》,,http://www./1993.html): 絕大多數(shù)剛創(chuàng)建的對象會被分配在Eden區(qū),其中的大多數(shù)對象很快就會消亡,。Eden區(qū)是連續(xù)的內(nèi)存空間,,因此在其上分配內(nèi)存極快; 最初一次,,當(dāng)Eden區(qū)滿的時候,,執(zhí)行Minor GC,將消亡的對象清理掉,,并將剩余的對象復(fù)制到一個存活區(qū)Survivor0(此時,,Survivor1是空白的,兩個Survivor總有一個是空白的),; 下次Eden區(qū)滿了,,再執(zhí)行一次Minor GC,將消亡的對象清理掉,,將存活的對象復(fù)制到Survivor1中,,然后清空Eden區(qū),; 將Survivor0中消亡的對象清理掉,將其中可以晉級的對象晉級到Old區(qū),,將存活的對象也復(fù)制到Survivor1區(qū),,然后清空Survivor0區(qū); - 當(dāng)兩個存活區(qū)切換了幾次(HotSpot虛擬機默認(rèn)15次,,用-XX:MaxTenuringThreshold控制,,大于該值進入老年代,但這只是個最大值,,并不代表一定是這個值)之后,,仍然存活的對象(其實只有一小部分,比如,,我們自己定義的對象),,將被復(fù)制到老年代。
從上面的過程可以看出,,Eden區(qū)是連續(xù)的空間,,且Survivor總有一個為空。經(jīng)過一次GC和復(fù)制,,一個Survivor中保存著當(dāng)前還活著的對象,,而Eden區(qū)和另一個Survivor區(qū)的內(nèi)容都不再需要了,可以直接清空,,到下一次GC時,,兩個Survivor的角色再互換。因此,,這種方式分配內(nèi)存和清理內(nèi)存的效率都極高,,這種垃圾回收的方式就是著名的“停止-復(fù)制(Stop-and-copy)”清理法(將Eden區(qū)和一個Survivor中仍然存活的對象拷貝到另一個Survivor中),這不代表著停止復(fù)制清理法很高效,,其實,,它也只在這種情況下高效,如果在老年代采用停止復(fù)制,,則挺悲劇的,。 在Eden區(qū),HotSpot虛擬機使用了兩種技術(shù)來加快內(nèi)存分配,。分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),,這兩種技術(shù)的做法分別是:由于Eden區(qū)是連續(xù)的,因此bump-the-pointer技術(shù)的核心就是跟蹤最后創(chuàng)建的一個對象,,在對象創(chuàng)建時,,只需要檢查最后一個對象后面是否有足夠的內(nèi)存即可,從而大大加快內(nèi)存分配速度;而對于TLAB技術(shù)是對于多線程而言的,,將Eden區(qū)分為若干段,,每個線程使用獨立的一段,避免相互影響,。TLAB結(jié)合bump-the-pointer技術(shù),,將保證每個線程都使用Eden區(qū)的一段,并快速的分配內(nèi)存,。 年老代(Old Generation):對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Young GC后存活了下來),,則會被復(fù)制到年老代,年老代的空間一般比年輕代大,,能存放更多的對象,,在年老代上發(fā)生的GC次數(shù)也比年輕代少。當(dāng)年老代內(nèi)存不足時,,將執(zhí)行Major GC,,也叫 Full GC?! ?/p> 可以使用-XX:+UseAdaptiveSizePolicy開關(guān)來控制是否采用動態(tài)控制策略,,如果動態(tài)控制,則動態(tài)調(diào)整Java堆中各個區(qū)域的大小以及進入老年代的年齡,。 如果對象比較大(比如長字符串或大數(shù)組),,Young空間不足,則大對象會直接分配到老年代上(大對象可能觸發(fā)提前GC,,應(yīng)少用,,更應(yīng)避免使用短命的大對象),。用-XX:PretenureSizeThreshold來控制直接升入老年代的對象大小,,大于這個值的對象會直接分配在老年代上。 可能存在年老代對象引用新生代對象的情況,,如果需要執(zhí)行Young GC,,則可能需要查詢整個老年代以確定是否可以清理回收,這顯然是低效的,。解決的方法是,,年老代中維護一個512 byte的塊——”card table“,所有老年代對象引用新生代對象的記錄都記錄在這里,。Young GC時,,只要查這里即可,不用再去查全部老年代,,因此性能大大提高,。 Java GC機制 GC機制的基本算法是:分代收集,這個不用贅述。下面闡述每個分代的收集方法,。 年輕代: 事實上,,在上一節(jié),已經(jīng)介紹了新生代的主要垃圾回收方法,,在新生代中,,使用“停止-復(fù)制”算法進行清理,將新生代內(nèi)存分為2部分,,1部分 Eden區(qū)較大,,1部分Survivor比較小,并被劃分為兩個等量的部分,。每次進行清理時,,將Eden區(qū)和一個Survivor中仍然存活的對象拷貝到 另一個Survivor中,然后清理掉Eden和剛才的Survivor,。 這里也可以發(fā)現(xiàn),,停止復(fù)制算法中,用來復(fù)制的兩部分并不總是相等的(傳統(tǒng)的停止復(fù)制算法兩部分內(nèi)存相等,,但新生代中使用1個大的Eden區(qū)和2個小的Survivor區(qū)來避免這個問題) 由于絕大部分的對象都是短命的,,甚至存活不到Survivor中,所以,,Eden區(qū)與Survivor的比例較大,,HotSpot默認(rèn)是 8:1,即分別占新生代的80%,,10%,,10%。如果一次回收中,,Survivor+Eden中存活下來的內(nèi)存超過了10%,,則需要將一部分對象分配到 老年代。用-XX:SurvivorRatio參數(shù)來配置Eden區(qū)域Survivor區(qū)的容量比值,,默認(rèn)是8,,代表Eden:Survivor1:Survivor2=8:1:1. 老年代: 老年代存儲的對象比年輕代多得多,而且不乏大對象,,對老年代進行內(nèi)存清理時,,如果使用停止-復(fù)制算法,則相當(dāng)?shù)托?。一般,,老年代用的算法是?biāo)記-整理算法,即:標(biāo)記出仍然存活的對象(存在引用的),,將所有存活的對象向一端移動,,以保證內(nèi)存的連續(xù),。 在發(fā)生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大于老年代的剩余空間大小,,如果大于,,則直接觸發(fā)一次Full GC,否則,,就查看是否設(shè)置了-XX:+HandlePromotionFailure(允許擔(dān)保失?。绻试S,,則只會進行MinorGC,,此時可以容忍內(nèi)存分配失敗,;如果不允許,,則仍然進行Full GC(這代表著如果設(shè)置-XX:+Handle PromotionFailure,則觸發(fā)MinorGC就會同時觸發(fā)Full GC,,哪怕老年代還有很多內(nèi)存,,所以,最好不要這樣做),。 方法區(qū)(永久代): 永久代的回收有兩種:常量池中的常量,,無用的類信息,常量的回收很簡單,,沒有引用了就可以被回收,。對于無用的類進行回收,必須保證3點: - 類的所有實例都已經(jīng)被回收
- 加載類的ClassLoader已經(jīng)被回收
- 類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)
永久代的回收并不是必須的,,可以通過參數(shù)來設(shè)置是否對類進行回收,。 HotSpot提供-Xnoclassgc進行控制 使用-verbose,-XX:+TraceClassLoading,、-XX:+TraceClassUnLoading可以查看類加載和卸載信息 -verbose,、-XX:+TraceClassLoading可以在Product版HotSpot中使用; -XX:+TraceClassUnLoading需要fastdebug版HotSpot支持 垃圾收集器 在GC機制中,,起重要作用的是垃圾收集器,,垃圾收集器是GC的具體實現(xiàn),,Java虛擬機規(guī)范中對于垃圾收集器沒有任何規(guī)定,,所以不同廠商實現(xiàn)的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下圖(圖來源于《深入理解Java虛擬機:JVM高級特效與最佳實現(xiàn)》,,圖中兩個收集器之間有連線,,說明它們可以配合使用): 在介紹垃圾收集器之前,需要明確一點,,就是在新生代采用的停止復(fù)制算法中,,“停 止(Stop-the-world)”的意義是在回收內(nèi)存時,,需要暫停其他所 有線程的執(zhí)行。這個是很低效的,,現(xiàn)在的各種新生代收集器越來越優(yōu)化這一點,,但仍然只是將停止的時間變短,并未徹底取消停止,。 - Serial收集器:新生代收集器,,使用停止復(fù)制算法,使用一個線程進行GC,,串行,,其它工作線程暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運行進行內(nèi)存回收(這也是虛擬機在Client模式下運行的默認(rèn)值)
- ParNew收集器:新生代收集器,,使用停止復(fù)制算法,,Serial收集器的多線程版,用多個線程進行GC,,并行,,其它工作線程暫停,關(guān)注縮短垃圾收集時間,。使用-XX:+UseParNewGC開關(guān)來控制使用ParNew+Serial Old收集器組合收集內(nèi)存,;使用-XX:ParallelGCThreads來設(shè)置執(zhí)行內(nèi)存回收的線程數(shù)。
- Parallel Scavenge 收集器:新生代收集器,,使用停止復(fù)制算法,,關(guān)注CPU吞吐量,即運行用戶代碼的時間/總時間,,比如:JVM運行100分鐘,,其中運行用戶代碼99分鐘,垃 圾收集1分鐘,,則吞吐量是99%,,這種收集器能最高效率的利用CPU,適合運行后臺運算(關(guān)注縮短垃圾收集時間的收集器,,如CMS,,等待時間很少,所以適 合用戶交互,,提高用戶體驗),。使用-XX:+UseParallelGC開關(guān)控制使用Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認(rèn)值);使用-XX:GCTimeRatio來設(shè)置用戶執(zhí)行時間占總時間的比例,,默認(rèn)99,,即1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設(shè)置GC的最大停頓時間(這個參數(shù)只對Parallel Scavenge有效),,用開關(guān)參數(shù)-XX:+UseAdaptiveSizePolicy可以進行動態(tài)控制,,如自動調(diào)整Eden/Survivor比例,,老年代對象年齡,新生代大小等,,這個參數(shù)在ParNew下沒有,。
- Serial Old收集器:老年代收集器,單線程收集器,,串行,,使用標(biāo)記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對象干掉,,只留幸存的對象,,壓縮是將移動對象,將空間填滿保證內(nèi)存分為2塊,,一塊全是對象,,一塊空閑)算法,使用單線程進行GC,,其它工作線程暫停(注意,,在老年代中進行標(biāo)記整理算法清理,也需要暫停其它線程),,在JDK1.5之前,,Serial Old收集器與ParallelScavenge搭配使用。
- Parallel Old收集器:老年代收集器,,多線程,,并行,多線程機制與Parallel Scavenge差不錯,,使用標(biāo)記整理(與Serial Old不同,,這里的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對象復(fù)制到預(yù)先準(zhǔn)備好的區(qū)域,,而不是像Sweep(清理)那樣清理廢棄的對象)算法,,在Parallel Old執(zhí)行時,仍然需要暫停其它線程,。Parallel Old在多核計算中很有用,。Parallel Old出現(xiàn)后(JDK 1.6),與Parallel Scavenge配合有很好的效果,,充分體現(xiàn)Parallel Scavenge收集器吞吐量優(yōu)先的效果,。使用-XX:+UseParallelOldGC開關(guān)控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。
- CMS(Concurrent Mark Sweep)收集器:老年代收集器,,致力于獲取最短回收停頓時間(即縮短垃圾回收的時間),,使用標(biāo)記清除算法,多線程,,優(yōu)點是并發(fā)收集(用戶線程可以和GC線程同時工作),,停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行內(nèi)存回收,,優(yōu)先使用ParNew+CMS(原因見后面),,當(dāng)用戶線程內(nèi)存不足時,采用備用方案Serial Old收集,。
CMS收集的執(zhí)行過程是:初始標(biāo)記(CMS-initial-mark) -> 并發(fā)標(biāo)記(CMS-concurrent-mark) -->預(yù)清理(CMS-concurrent-preclean)-->可控預(yù)清理(CMS-concurrent-abortable-preclean)-> 重新標(biāo)記(CMS-remark) -> 并發(fā)清除(CMS-concurrent-sweep) ->并發(fā)重設(shè)狀態(tài)等待下次CMS的觸發(fā)(CMS-concurrent-reset) 具體的說,,先2次標(biāo)記,1次預(yù)清理,,1次重新標(biāo)記,,再1次清除。 1,,首先jvm根據(jù)-XX:CMSInitiatingOccupancyFraction,,-XX:+UseCMSInitiatingOccupancyOnly來決定什么時間開始垃圾收集; 2,,如果設(shè)置了-XX:+UseCMSInitiatingOccupancyOnly,,那么只有當(dāng)old代占用確實達到了-XX:CMSInitiatingOccupancyFraction參數(shù)所設(shè)定的比例時才會觸發(fā)cms gc; 3,,如果沒有設(shè)置-XX:+UseCMSInitiatingOccupancyOnly,,那么系統(tǒng)會根據(jù)統(tǒng)計數(shù)據(jù)自行決定什么時候觸發(fā)cms gc;因此有時會遇到設(shè)置了80%比例才cms gc,,但是50%時就已經(jīng)觸發(fā)了,,就是因為這個參數(shù)沒有設(shè)置的原因; 4,,當(dāng)cms gc開始時,,首先的階段是初始標(biāo)記(CMS-initial-mark),是stop the world階段,,因此此階段標(biāo)記的對象只是從root集最直接可達的對象,; CMS-initial-mark:961330K(1572864K),指標(biāo)記時,,old代的已用空間和總空間 5,,下一個階段是并發(fā)標(biāo)記(CMS-concurrent-mark),此階段是和應(yīng)用線程并發(fā)執(zhí)行的,,所謂并發(fā)收集器指的就是這個,,主要作用是標(biāo)記可達的對象,此階段不需要用戶停頓,。 此階段會打印2條日志:CMS-concurrent-mark-start,,CMS-concurrent-mark 6,下一個階段是CMS-concurrent-preclean,,此階段主要是進行一些預(yù)清理,,因為標(biāo)記和應(yīng)用線程是并發(fā)執(zhí)行的,,因此會有些對象的狀態(tài)在標(biāo)記后會改變,此階段正是解決這個問題因為之后的Rescan階段也會stop the world,,為了使暫停的時間盡可能的小,,也需要preclean階段先做一部分工作以節(jié)省時間 此階段會打印2條日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean 7,,下一階段是CMS-concurrent-abortable-preclean階段,,加入此階段的目的是使cms gc更加可控一些,作用也是執(zhí)行一些預(yù)清理,,以減少Rescan階段造成應(yīng)用暫停的時間 此階段涉及幾個參數(shù): -XX:CMSMaxAbortablePrecleanTime:當(dāng)abortable-preclean階段執(zhí)行達到這個時間時才會結(jié)束 -XX:CMSScheduleRemarkEdenSizeThreshold(默認(rèn)2m):控制abortable-preclean階段什么時候開始執(zhí)行,, 即當(dāng)eden使用達到此值時,才會開始abortable-preclean階段 -XX:CMSScheduleRemarkEdenPenetratio(默認(rèn)50%):控制abortable-preclean階段什么時候結(jié)束執(zhí)行 此階段會打印一些日志如下: CMS-concurrent-abortable-preclean-start,,CMS-concurrent-abortable-preclean,, CMS:abort preclean due to time XXX 8,再下一個階段是第二個stop the world階段了,,即Rescan階段,,此階段暫停應(yīng)用線程,停頓時間比并發(fā)標(biāo)記小得多,,但比初始標(biāo)記稍長,。對對象進行重新掃描并標(biāo)記; YG occupancy:964861K(2403008K),,指執(zhí)行時young代的情況 CMS remark:961330K(1572864K),,指執(zhí)行時old代的情況 此外,還打印出了弱引用處理,、類卸載等過程的耗時 9,,再下一個階段是CMS-concurrent-sweep,進行并發(fā)的垃圾清理 10,,最后是CMS-concurrent-reset,,為下一次cms gc重置相關(guān)數(shù)據(jù)結(jié)構(gòu) 有2種情況會觸發(fā)CMS 的悲觀full gc,在悲觀full gc時,,整個應(yīng)用會暫停 A,,concurrent-mode-failure:預(yù)清理階段可能出現(xiàn),當(dāng)cms gc正進行時,,此時有新的對象要進行old代,,但是old代空間不足造成的。其可能性有:1,,O區(qū)空間不足以讓新生代晉級,,2,O區(qū)空間用完之前,無法完成對無引用的對象的清理,。這表明,,當(dāng)前有大量數(shù)據(jù)進入內(nèi)存且無法釋放,。 B,,promotion-failed:新生代young gc可能出現(xiàn),,當(dāng)進行young gc時,,有部分young代對象仍然可用,但是S1或S2放不下,,因此需要放到old代,,但此時old代空間無法容納此。 影響cms gc時長及觸發(fā)的參數(shù)是以下2個: -XX:CMSMaxAbortablePrecleanTime=5000 -XX:CMSInitiatingOccupancyFraction=80 解決也是針對這兩個參數(shù)來的,,根本的原因是每次請求消耗的內(nèi)存量過大 解決方式: A,,針對cms gc的觸發(fā)階段,調(diào)整-XX:CMSInitiatingOccupancyFraction=50,,提早觸發(fā)cms gc,就可以緩解當(dāng)old代達到80%,,cms gc處理不完,,從而造成concurrent mode failure引發(fā)full gc B,,修改-XX:CMSMaxAbortablePrecleanTime=500,縮小CMS-concurrent-abortable-preclean階段的時間 C,,考慮到cms gc時不會進行compact,因此加入-XX:+UseCMSCompactAtFullCollection (cms gc后會進行內(nèi)存的compact)和-XX:CMSFullGCsBeforeCompaction=4(在full gc4次后會進行compact)參數(shù) 在CMS清理過程中,,只有初始標(biāo)記和重新標(biāo)記需要短暫停頓,并發(fā)標(biāo)記和并發(fā)清除都不需要暫停用戶線程,,因此效率很高,,很適合高交互的場合。 CMS也有缺點,,它需要消耗額外的CPU和內(nèi)存資源,,在CPU和內(nèi)存資源緊張,CPU較少時,,會加重系統(tǒng)負(fù)擔(dān)(CMS默認(rèn)啟動線程數(shù)為(CPU數(shù)量+3)/4),。 另外,在并發(fā)收集過程中,用戶線程仍然在運行,,仍然產(chǎn)生內(nèi)存垃圾,,所以可能產(chǎn)生“浮動垃圾”,,本次無法清理,,只能下一次Full GC才清理,因此在GC期間,,需要預(yù)留足夠的內(nèi)存給用戶線程使用,。所以使用CMS的收集器并不是老年代滿了才觸發(fā)Full GC,而是在使用了一大半(默認(rèn)68%,,即2/3,,使用-XX:CMSInitiatingOccupancyFraction來設(shè)置)的時候就要進行Full GC,如果用戶線程消耗內(nèi)存不是特別大,,可以適當(dāng)調(diào)高-XX:CMSInitiatingOccupancyFraction以降低GC次數(shù),,提高性能,如果預(yù)留的用戶線程內(nèi)存不夠,,則會觸發(fā)Concurrent Mode Failure,,此時,將觸發(fā)備用方案:使用Serial Old 收集器進行收集,,但這樣停頓時間就長了,,因此-XX:CMSInitiatingOccupancyFraction不宜設(shè)的過大。 還有,,CMS采用的是標(biāo)記清除算法,,會導(dǎo)致內(nèi)存碎片的產(chǎn)生,可以使用-XX:+UseCMSCompactAtFullCollection來設(shè)置是否在Full GC之后進行碎片整理,,用-XX:CMSFullGCsBeforeCompaction來設(shè)置在執(zhí)行多少次不壓縮的Full GC之后,,來一次帶壓縮的Full GC。
- G1收集器:在JDK1.7中正式發(fā)布,,與現(xiàn)狀的新生代,、老年代概念有很大不同,目前使用較少,,不做介紹,。
注意并發(fā)(Concurrent)和并行(Parallel)的區(qū)別: 并發(fā)是指用戶線程與GC線程同時執(zhí)行(不一定是并行,可能交替,,但總體上是在同時執(zhí)行的),,不需要停頓用戶線程(其實在CMS中用戶線程還是需要停頓的,只是非常短,,GC線程在另一個CPU上執(zhí)行),; 并行收集是指多個GC線程并行工作,但此時用戶線程是暫停的; 所以,,Serial是串行的,,Parallel收集器是并行的,而CMS收集器是并發(fā)的.
|