Java GC(Garbage Collection唤锉,垃圾收集,垃圾回收)機(jī)制别瞭,是Java與C++/C的主要區(qū)別之一腌紧,作為Java開(kāi)發(fā)者,一般不需要專門編寫(xiě)內(nèi)存回收和垃圾清理代碼畜隶,對(duì)內(nèi)存泄露和溢出的問(wèn)題壁肋,也不需要像C程序員那樣戰(zhàn)戰(zhàn)兢兢号胚。這是因?yàn)樵贘ava虛擬機(jī)中,存在自動(dòng)內(nèi)存管理和垃圾清掃機(jī)制浸遗。概括地說(shuō)猫胁,該機(jī)制對(duì)JVM(Java Virtual Machine)中的內(nèi)存進(jìn)行標(biāo)記,并確定哪些內(nèi)存需要回收跛锌,根據(jù)一定的回收策略弃秆,自動(dòng)的回收內(nèi)存,永不停息(Nerver Stop)的保證JVM中的內(nèi)存空間髓帽,防止出現(xiàn)內(nèi)存泄露和溢出問(wèn)題菠赚。
Java GC機(jī)制主要完成3件事:確定哪些內(nèi)存需要回收,確定什么時(shí)候需要執(zhí)行GC郑藏,如何執(zhí)行GC衡查。經(jīng)過(guò)這么長(zhǎng)時(shí)間的發(fā)展(事實(shí)上,在Java語(yǔ)言出現(xiàn)之前必盖,就有GC機(jī)制的存在拌牲,如Lisp語(yǔ)言),Java GC機(jī)制已經(jīng)日臻完善歌粥,幾乎可以自動(dòng)的為我們做絕大多數(shù)的事情塌忽。然而,如果我們從事較大型的應(yīng)用軟件開(kāi)發(fā)失驶,曾經(jīng)出現(xiàn)過(guò)內(nèi)存優(yōu)化的需求土居,就必定要研究Java GC機(jī)制。
學(xué)習(xí)Java GC機(jī)制嬉探,可以幫助我們?cè)谌粘9ぷ髦信挪楦鞣N內(nèi)存溢出或泄露問(wèn)題擦耀,解決性能瓶頸,達(dá)到更高的并發(fā)量甲馋,寫(xiě)出更高效的程序埂奈。
我們將從4個(gè)方面學(xué)習(xí)Java GC機(jī)制:
- 內(nèi)存是如何分配的;
- 如何保證內(nèi)存不被錯(cuò)誤回收(即:哪些內(nèi)存需要回收)定躏;
- 在什么情況下執(zhí)行GC以及執(zhí)行GC的方式账磺;
- 如何監(jiān)控和優(yōu)化GC機(jī)制。
Java內(nèi)存區(qū)域
了解Java GC機(jī)制痊远,必須先清楚在JVM中內(nèi)存區(qū)域的劃分垮抗。在Java運(yùn)行時(shí)的數(shù)據(jù)區(qū)里,由JVM管理的內(nèi)存區(qū)域分為下圖幾個(gè)模塊:
其中:
1碧聪,程序計(jì)數(shù)器(Program Counter Register):程序計(jì)數(shù)器是一個(gè)比較小的內(nèi)存區(qū)域冒版,用于指示當(dāng)前線程所執(zhí)行的字節(jié)碼執(zhí)行到了第幾行,可以理解為是當(dāng)前線程的行號(hào)指示器逞姿。字節(jié)碼解釋器在工作時(shí)辞嗡,會(huì)通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)取下一條語(yǔ)句指令捆等。
每個(gè)程序計(jì)數(shù)器只用來(lái)記錄一個(gè)線程的行號(hào),所以它是線程私有(一個(gè)線程就有一個(gè)程序計(jì)數(shù)器)的续室。
如果程序執(zhí)行的是一個(gè)Java方法栋烤,則計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址;如果正在執(zhí)行的是一個(gè)本地(native挺狰,由C語(yǔ)言編寫(xiě)完成)方法明郭,則計(jì)數(shù)器的值為Undefined,由于程序計(jì)數(shù)器只是記錄當(dāng)前指令地址丰泊,所以不存在內(nèi)存溢出的情況薯定,因此,程序計(jì)數(shù)器也是所有JVM內(nèi)存區(qū)域中唯一一個(gè)沒(méi)有定義OutOfMemoryError的區(qū)域瞳购。
2话侄,虛擬機(jī)棧(JVM Stack):一個(gè)線程的每個(gè)方法在執(zhí)行的同時(shí),都會(huì)創(chuàng)建一個(gè)棧幀(Statck Frame)苛败,棧幀中存儲(chǔ)的有局部變量表满葛、操作站径簿、動(dòng)態(tài)鏈接罢屈、方法出口等,當(dāng)方法被調(diào)用時(shí)篇亭,棧幀在JVM棧中入棧缠捌,當(dāng)方法執(zhí)行完成時(shí),棧幀出棧译蒂。
局部變量表中存儲(chǔ)著方法的相關(guān)局部變量曼月,包括各種基本數(shù)據(jù)類型,對(duì)象的引用柔昼,返回地址等哑芹。在局部變量表中,只有l(wèi)ong和double類型會(huì)占用2個(gè)局部變量空間(Slot捕透,對(duì)于32位機(jī)器聪姿,一個(gè)Slot就是32個(gè)bit),其它都是1個(gè)Slot乙嘀。需要注意的是末购,局部變量表是在編譯時(shí)就已經(jīng)確定好的,方法運(yùn)行所需要分配的空間在棧幀中是完全確定的虎谢,在方法的生命周期內(nèi)都不會(huì)改變盟榴。
虛擬機(jī)棧中定義了兩種異常,如果線程調(diào)用的棧深度大于虛擬機(jī)允許的最大深度婴噩,則拋出StatckOverFlowError(棧溢出)擎场;不過(guò)多數(shù)Java虛擬機(jī)都允許動(dòng)態(tài)擴(kuò)展虛擬機(jī)棧的大小(有少部分是固定長(zhǎng)度的)羽德,所以線程可以一直申請(qǐng)棧,直到內(nèi)存不足迅办,此時(shí)玩般,會(huì)拋出OutOfMemoryError(內(nèi)存溢出)。
每個(gè)線程對(duì)應(yīng)著一個(gè)虛擬機(jī)棧礼饱,因此虛擬機(jī)棧也是線程私有的坏为。
3,本地方法棧(Native Method Statck):本地方法棧在作用镊绪,運(yùn)行機(jī)制匀伏,異常類型等方面都與虛擬機(jī)棧相同,唯一的區(qū)別是:虛擬機(jī)棧是執(zhí)行Java方法的蝴韭,而本地方法棧是用來(lái)執(zhí)行native方法的够颠,在很多虛擬機(jī)中(如Sun的JDK默認(rèn)的HotSpot虛擬機(jī)),會(huì)將本地方法棧與虛擬機(jī)棧放在一起使用榄鉴。
本地方法棧也是線程私有的履磨。
4,堆區(qū)(Heap):堆區(qū)是理解Java GC機(jī)制最重要的區(qū)域庆尘,沒(méi)有之一剃诅。在JVM所管理的內(nèi)存中,堆區(qū)是最大的一塊驶忌,堆區(qū)也是Java GC機(jī)制所管理的主要內(nèi)存區(qū)域矛辕,堆區(qū)由所有線程共享,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建付魔。堆區(qū)的存在是為了存儲(chǔ)對(duì)象實(shí)例聊品,原則上講,所有的對(duì)象都在堆區(qū)上分配內(nèi)存(不過(guò)現(xiàn)代技術(shù)里几苍,也不是這么絕對(duì)的翻屈,也有棧上直接分配的)。
一般的妻坝,根據(jù)Java虛擬機(jī)規(guī)范規(guī)定伸眶,堆內(nèi)存需要在邏輯上是連續(xù)的(在物理上不需要),在實(shí)現(xiàn)時(shí)惠勒,可以是固定大小的赚抡,也可以是可擴(kuò)展的,目前主流的虛擬機(jī)都是可擴(kuò)展的纠屋。如果在執(zhí)行垃圾回收之后涂臣,仍沒(méi)有足夠的內(nèi)存分配,也不能再擴(kuò)展,將會(huì)拋出OutOfMemoryError:Java heap space異常赁遗。
5署辉,方法區(qū)(Method Area):在Java虛擬機(jī)規(guī)范中,將方法區(qū)作為堆的一個(gè)邏輯部分來(lái)對(duì)待岩四,但事實(shí)上哭尝,方法區(qū)并不是堆(Non-Heap);另外剖煌,不少人的博客中材鹦,將Java GC的分代收集機(jī)制分為3個(gè)代:青年代,老年代耕姊,永久代桶唐,這些作者將方法區(qū)定義為“永久代”,這是因?yàn)檐岳迹瑢?duì)于之前的HotSpot Java虛擬機(jī)的實(shí)現(xiàn)方式中尤泽,將分代收集的思想擴(kuò)展到了方法區(qū),并將方法區(qū)設(shè)計(jì)成了永久代规脸。不過(guò)坯约,除HotSpot之外的多數(shù)虛擬機(jī),并不將方法區(qū)當(dāng)做永久代莫鸭,HotSpot本身闹丐,也計(jì)劃取消永久代。本文中黔龟,由于筆者主要使用Oracle JDK6.0妇智,因此仍將使用永久代一詞滥玷。
方法區(qū)是各個(gè)線程共享的區(qū)域氏身,用于存儲(chǔ)已經(jīng)被虛擬機(jī)加載的類信息(即加載類時(shí)需要加載的信息,包括版本惑畴、field蛋欣、方法、接口等信息)如贷、final常量陷虎、靜態(tài)變量、編譯器即時(shí)編譯的代碼等杠袱。
方法區(qū)在物理上也不需要是連續(xù)的尚猿,可以選擇固定大小或可擴(kuò)展大小,并且方法區(qū)比堆還多了一個(gè)限制:可以選擇是否執(zhí)行垃圾收集楣富。一般的凿掂,方法區(qū)上執(zhí)行的垃圾收集是很少的,這也是方法區(qū)被稱為永久代的原因之一(HotSpot),但這也不代表著在方法區(qū)上完全沒(méi)有垃圾收集庄萎,其上的垃圾收集主要是針對(duì)常量池的內(nèi)存回收和對(duì)已加載類的卸載踪少。
在方法區(qū)上進(jìn)行垃圾收集,條件苛刻而且相當(dāng)困難碌奉,效果也不令人滿意跨晴,所以一般不做太多考慮挚躯,可以留作以后進(jìn)一步深入研究時(shí)使用。
在方法區(qū)上定義了OutOfMemoryError:PermGen space異常集漾,在內(nèi)存不足時(shí)拋出。
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分砸脊,用于存儲(chǔ)編譯期就生成的字面常量帆竹、符號(hào)引用、翻譯出來(lái)的直接引用(符號(hào)引用就是編碼是用字符串表示某個(gè)變量脓规、接口的位置栽连,直接引用就是根據(jù)符號(hào)引用翻譯出來(lái)的地址,將在類鏈接階段完成翻譯)侨舆;運(yùn)行時(shí)常量池除了存儲(chǔ)編譯期常量外秒紧,也可以存儲(chǔ)在運(yùn)行時(shí)間產(chǎn)生的常量(比如String類的intern()方法,作用是String維護(hù)了一個(gè)常量池挨下,如果調(diào)用的字符“abc”已經(jīng)在常量池中熔恢,則返回池中的字符串地址,否則臭笆,新建一個(gè)常量加入池中叙淌,并返回地址)。
6愁铺,直接內(nèi)存(Direct Memory):直接內(nèi)存并不是JVM管理的內(nèi)存鹰霍,可以這樣理解,直接內(nèi)存茵乱,就是JVM以外的機(jī)器內(nèi)存茂洒,比如,你有4G的內(nèi)存瓶竭,JVM占用了1G督勺,則其余的3G就是直接內(nèi)存,JDK中有一種基于通道(Channel)和緩沖區(qū)(Buffer)的內(nèi)存分配方式斤贰,將由C語(yǔ)言實(shí)現(xiàn)的native函數(shù)庫(kù)分配在直接內(nèi)存中智哀,用存儲(chǔ)在JVM堆中的DirectByteBuffer來(lái)引用。由于直接內(nèi)存收到本機(jī)器內(nèi)存的限制荧恍,所以也可能出現(xiàn)OutOfMemoryError的異常瓷叫。
Java內(nèi)存分配機(jī)制
這里所說(shuō)的內(nèi)存分配,主要指的是在堆上的分配,一般的赞辩,對(duì)象的內(nèi)存分配都是在堆上進(jìn)行雌芽,但現(xiàn)代技術(shù)也支持將對(duì)象拆成標(biāo)量類型(標(biāo)量類型即原子類型,表示單個(gè)值辨嗽,可以是基本類型或String等)世落,然后在棧上分配,在棧上分配的很少見(jiàn)糟需,我們這里不考慮屉佳。
Java內(nèi)存分配和回收的機(jī)制概括的說(shuō),就是:分代分配洲押,分代回收武花。對(duì)象將根據(jù)存活的時(shí)間被分為:年輕代(Young Generation)、年老代(Old Generation)杈帐、永久代(Permanent Generation体箕,也就是方法區(qū))
年輕代(Young Generation)
對(duì)象被創(chuàng)建時(shí),內(nèi)存的分配首先發(fā)生在年輕代(大對(duì)象可以直接被創(chuàng)建在年老代)挑童,大部分的對(duì)象在創(chuàng)建后很快就不再使用累铅,因此很快變得不可達(dá),于是被年輕代的GC機(jī)制清理掉(IBM的研究表明站叼,98%的對(duì)象都是很快消亡的)娃兽,這個(gè)GC機(jī)制被稱為Minor GC或叫Young GC。注意尽楔,Minor GC并不代表年輕代內(nèi)存不足投储,它事實(shí)上只表示在Eden區(qū)上的GC。
年輕代上的內(nèi)存分配是這樣的阔馋,年輕代可以分為3個(gè)區(qū)域:Eden區(qū)(伊甸園玛荞,亞當(dāng)和夏娃偷吃禁果生娃娃的地方,用來(lái)表示內(nèi)存首次分配的區(qū)域垦缅,再貼切不過(guò))和兩個(gè)存活區(qū)(Survivor 0 冲泥、Survivor 1)。
- 絕大多數(shù)剛創(chuàng)建的對(duì)象會(huì)被分配在Eden區(qū)壁涎,其中的大多數(shù)對(duì)象很快就會(huì)消亡。Eden區(qū)是連續(xù)的內(nèi)存空間志秃,因此在其上分配內(nèi)存極快怔球;
- 最初一次,當(dāng)Eden區(qū)滿的時(shí)候浮还,執(zhí)行Minor GC竟坛,將消亡的對(duì)象清理掉,并將剩余的對(duì)象復(fù)制到一個(gè)存活區(qū)Survivor0(此時(shí),Survivor1是空白的担汤,兩個(gè)Survivor總有一個(gè)是空白的)涎跨;
- 下次Eden區(qū)滿了,再執(zhí)行一次Minor GC崭歧,將消亡的對(duì)象清理掉隅很,將存活的對(duì)象復(fù)制到Survivor1中,然后清空Eden區(qū)率碾;
- 將Survivor0中消亡的對(duì)象清理掉叔营,將其中可以晉級(jí)的對(duì)象晉級(jí)到Old區(qū),將存活的對(duì)象也復(fù)制到Survivor1區(qū)所宰,然后清空Survivor0區(qū)绒尊;
- 當(dāng)兩個(gè)存活區(qū)切換了幾次(HotSpot虛擬機(jī)默認(rèn)15次,用-XX:MaxTenuringThreshold控制仔粥,大于該值進(jìn)入老年代婴谱,但這只是個(gè)最大值,并不代表一定是這個(gè)值)之后躯泰,仍然存活的對(duì)象(其實(shí)只有一小部分勘究,比如,我們自己定義的對(duì)象)斟冕,將被復(fù)制到老年代口糕。
從上面的過(guò)程可以看出,Eden區(qū)是連續(xù)的空間磕蛇,且Survivor總有一個(gè)為空景描。經(jīng)過(guò)一次GC和復(fù)制,一個(gè)Survivor中保存著當(dāng)前還活著的對(duì)象秀撇,而Eden區(qū)和另一個(gè)Survivor區(qū)的內(nèi)容都不再需要了超棺,可以直接清空,到下一次GC時(shí)呵燕,兩個(gè)Survivor的角色再互換棠绘。因此,這種方式分配內(nèi)存和清理內(nèi)存的效率都極高再扭,這種垃圾回收的方式就是著名的“停止-復(fù)制(Stop-and-copy)”清理法(將Eden區(qū)和一個(gè)Survivor中仍然存活的對(duì)象拷貝到另一個(gè)Survivor中)氧苍,這不代表著停止復(fù)制清理法很高效,其實(shí)泛范,它也只在這種情況下高效让虐,如果在老年代采用停止復(fù)制,則挺悲劇的罢荡。
在Eden區(qū)赡突,HotSpot虛擬機(jī)使用了兩種技術(shù)來(lái)加快內(nèi)存分配对扶。分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),這兩種技術(shù)的做法分別是:由于Eden區(qū)是連續(xù)的惭缰,因此bump-the-pointer技術(shù)的核心就是跟蹤最后創(chuàng)建的一個(gè)對(duì)象浪南,在對(duì)象創(chuàng)建時(shí),只需要檢查最后一個(gè)對(duì)象后面是否有足夠的內(nèi)存即可漱受,從而大大加快內(nèi)存分配速度络凿;而對(duì)于TLAB技術(shù)是對(duì)于多線程而言的,將Eden區(qū)分為若干段拜效,每個(gè)線程使用獨(dú)立的一段喷众,避免相互影響。TLAB結(jié)合bump-the-pointer技術(shù)紧憾,將保證每個(gè)線程都使用Eden區(qū)的一段到千,并快速的分配內(nèi)存。
年老代(Old Generation)
對(duì)象如果在年輕代存活了足夠長(zhǎng)的時(shí)間而沒(méi)有被清理掉(即在幾次Young GC后存活了下來(lái))赴穗,則會(huì)被復(fù)制到年老代憔四,年老代的空間一般比年輕代大,能存放更多的對(duì)象般眉,在年老代上發(fā)生的GC次數(shù)也比年輕代少了赵。當(dāng)年老代內(nèi)存不足時(shí),將執(zhí)行Major GC甸赃,也叫 Full GC柿汛。
可以使用-XX:+UseAdaptiveSizePolicy開(kāi)關(guān)來(lái)控制是否采用動(dòng)態(tài)控制策略,如果動(dòng)態(tài)控制埠对,則動(dòng)態(tài)調(diào)整Java堆中各個(gè)區(qū)域的大小以及進(jìn)入老年代的年齡络断。
如果對(duì)象比較大(比如長(zhǎng)字符串或大數(shù)組),Young空間不足项玛,則大對(duì)象會(huì)直接分配到老年代上(大對(duì)象可能觸發(fā)提前GC貌笨,應(yīng)少用,更應(yīng)避免使用短命的大對(duì)象)襟沮。用-XX:PretenureSizeThreshold來(lái)控制直接升入老年代的對(duì)象大小锥惋,大于這個(gè)值的對(duì)象會(huì)直接分配在老年代上。
可能存在年老代對(duì)象引用新生代對(duì)象的情況开伏,如果需要執(zhí)行Young GC膀跌,則可能需要查詢整個(gè)老年代以確定是否可以清理回收,這顯然是低效的硅则。解決的方法是淹父,年老代中維護(hù)一個(gè)512 byte的塊——”card table“,所有老年代對(duì)象引用新生代對(duì)象的記錄都記錄在這里怎虫。Young GC時(shí)暑认,只要查這里即可,不用再去查全部老年代大审,因此性能大大提高蘸际。
Java GC機(jī)制
GC機(jī)制的基本算法是:分代收集,這個(gè)不用贅述徒扶。下面闡述每個(gè)分代的收集方法粮彤。
年輕代:
事實(shí)上,在上一節(jié)姜骡,已經(jīng)介紹了新生代的主要垃圾回收方法导坟,在新生代中,使用“停止-復(fù)制”算法進(jìn)行清理圈澈,將新生代內(nèi)存分為2部分惫周,1部分 Eden區(qū)較大,1部分Survivor比較小康栈,并被劃分為兩個(gè)等量的部分递递。每次進(jìn)行清理時(shí),將Eden區(qū)和一個(gè)Survivor中仍然存活的對(duì)象拷貝到 另一個(gè)Survivor中啥么,然后清理掉Eden和剛才的Survivor登舞。
這里也可以發(fā)現(xiàn),停止復(fù)制算法中悬荣,用來(lái)復(fù)制的兩部分并不總是相等的(傳統(tǒng)的停止復(fù)制算法兩部分內(nèi)存相等菠秒,但新生代中使用1個(gè)大的Eden區(qū)和2個(gè)小的Survivor區(qū)來(lái)避免這個(gè)問(wèn)題)
由于絕大部分的對(duì)象都是短命的,甚至存活不到Survivor中氯迂,所以践叠,Eden區(qū)與Survivor的比例較大,HotSpot默認(rèn)是 8:1囚戚,即分別占新生代的80%酵熙,10%,10%驰坊。如果一次回收中匾二,Survivor+Eden中存活下來(lái)的內(nèi)存超過(guò)了10%,則需要將一部分對(duì)象分配到 老年代拳芙。用-XX:SurvivorRatio參數(shù)來(lái)配置Eden區(qū)域Survivor區(qū)的容量比值察藐,默認(rèn)是8,代表Eden:Survivor1:Survivor2=8:1:1.
老年代:
老年代存儲(chǔ)的對(duì)象比年輕代多得多舟扎,而且不乏大對(duì)象分飞,對(duì)老年代進(jìn)行內(nèi)存清理時(shí),如果使用停止-復(fù)制算法睹限,則相當(dāng)?shù)托┟āR话阊堕埽夏甏玫乃惴ㄊ菢?biāo)記-整理算法,即:標(biāo)記出仍然存活的對(duì)象(存在引用的)染服,將所有存活的對(duì)象向一端移動(dòng)别洪,以保證內(nèi)存的連續(xù)。
在發(fā)生Minor GC時(shí)柳刮,虛擬機(jī)會(huì)檢查每次晉升進(jìn)入老年代的大小是否大于老年代的剩余空間大小挖垛,如果大于,則直接觸發(fā)一次Full GC秉颗,否則痢毒,就查看是否設(shè)置了-XX:+HandlePromotionFailure(允許擔(dān)保失敗)蚕甥,如果允許哪替,則只會(huì)進(jìn)行MinorGC,此時(shí)可以容忍內(nèi)存分配失斏颐稹夷家;如果不允許,則仍然進(jìn)行Full GC(這代表著如果設(shè)置-XX:+Handle PromotionFailure敏释,則觸發(fā)MinorGC就會(huì)同時(shí)觸發(fā)Full GC库快,哪怕老年代還有很多內(nèi)存,所以钥顽,最好不要這樣做)义屏。
方法區(qū)(永久代):
永久代的回收有兩種:常量池中的常量,無(wú)用的類信息蜂大,常量的回收很簡(jiǎn)單闽铐,沒(méi)有引用了就可以被回收。對(duì)于無(wú)用的類進(jìn)行回收奶浦,必須保證3點(diǎn):
- 類的所有實(shí)例都已經(jīng)被回收
- 加載類的ClassLoader已經(jīng)被回收
- 類對(duì)象的Class對(duì)象沒(méi)有被引用(即沒(méi)有通過(guò)反射引用該類的地方)