寫在前面
本文介紹的Java虛擬機(jī)(JVM)的自動(dòng)內(nèi)存管理機(jī)制主要是參照《深入理解Java虛擬機(jī)》(第2版)一書中的內(nèi)容,主要分為兩個(gè)部分:Java內(nèi)存區(qū)域和內(nèi)存溢出異常域携、垃圾回收和內(nèi)存分配策略。因此我也會分為兩個(gè)部分來講解,但這并不代表這兩個(gè)部分在JVM中是分割的。反之梆掸,其實(shí)這兩個(gè)部分關(guān)聯(lián)性很強(qiáng)。只不過為了便于介紹牙言,所以我才分開來講酸钦。在介紹它們詳細(xì)內(nèi)容之前,我首先會給出兩幅思維導(dǎo)圖以便讀者可以了解一下里面所包含的內(nèi)容咱枉,然后我會根據(jù)思維導(dǎo)圖中的知識點(diǎn)一一為大家進(jìn)行介紹卑硫。
第一部分 Java內(nèi)存區(qū)域和內(nèi)存溢出異常
下面我將對圖中所涉及到的部分進(jìn)行介紹
運(yùn)行時(shí)數(shù)據(jù)區(qū)域
由于直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域蚕断。但是這部分內(nèi)存也被頻繁地使用欢伏,而且也可能導(dǎo)致內(nèi)存溢出異常(OutOfMemoryError)出現(xiàn),所以也放到這部分進(jìn)行介紹亿乳。
Java虛擬機(jī)在執(zhí)行Java程序的過程中會把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域硝拧。這些區(qū)域都有各自的用途以及創(chuàng)建和銷毀的時(shí)間。有的區(qū)域(線程共享的數(shù)據(jù)區(qū)域)隨著虛擬機(jī)的啟動(dòng)而存在葛假,有的區(qū)域(線程隔離的數(shù)據(jù)區(qū)域)則要依賴用戶線程的啟動(dòng)和結(jié)束來創(chuàng)建或者是銷毀障陶。
程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器聊训。學(xué)過《計(jì)算機(jī)組成原理》這門課之后我們知道----在計(jì)算機(jī)中咸这,其實(shí)程序計(jì)數(shù)器就是一個(gè)寄存器,依據(jù)不同計(jì)算機(jī)細(xì)節(jié)的差異魔眨,它可以存放當(dāng)前正在被執(zhí)行的指令,也可以存放下一個(gè)要被執(zhí)行的指令酿雪。由此遏暴,我們可以對“當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器”有更好的理解。
在虛擬機(jī)的概念模型中指黎,字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令朋凉。由于Java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻醋安,一個(gè)處理器(對于多核處理器來說是一個(gè)內(nèi)核)都只會執(zhí)行一條線程中的指令杂彭。因此為了線程切換之后能夠恢復(fù)到正確的執(zhí)行位置,每條線程都需要擁有一個(gè)獨(dú)立的程序計(jì)數(shù)器吓揪,各條線程之間計(jì)數(shù)器互補(bǔ)影響亲怠,獨(dú)立存儲。所以程序計(jì)數(shù)器是線程私有的內(nèi)存(線程隔離)柠辞。
如果線程正在執(zhí)行的是一個(gè)Java方法团秽,這個(gè)計(jì)數(shù)器記錄的就是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是Native方法,那么這個(gè)計(jì)數(shù)器的值就為空(Undefined)习勤。此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域踪栋。
Java虛擬機(jī)棧
和程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stack)也是線程私有的图毕,即它的生命周期和線程的相同夷都。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行時(shí)都會創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧予颤、動(dòng)態(tài)鏈接囤官、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過程荣瑟,就對應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程治拿。
我們常常說的棧內(nèi)存其實(shí)就是現(xiàn)在講的虛擬機(jī)棧,或者說是虛擬機(jī)棧中局部變量表部分笆焰。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean劫谅、byte、char嚷掠、short捏检、int、float不皆、long贯城、double)、對象引用(reference類型霹娄,它不等同于對象本身能犯,可能是指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苁侵赶蛞粋€(gè)代表對象的句柄或其他與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)犬耻。
其中64位長度的long和double類型的數(shù)據(jù)會占用2個(gè)局部變量空間(Slot)踩晶,其余數(shù)據(jù)類型只占用1個(gè)。局部變量表所需要的內(nèi)存空間在編譯時(shí)期完成分配枕磁。當(dāng)進(jìn)入一個(gè)方法時(shí)渡蜻,這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會改變局部變量表的大小计济。
本地方法棧
本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的茸苇,它們之間的區(qū)別就是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)沦寂。其實(shí)虛擬機(jī)規(guī)范中對本地方發(fā)棧中方法所使用的語言学密、使用方式以及數(shù)據(jù)結(jié)構(gòu)都沒有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由地實(shí)現(xiàn)它传藏。甚至在有的虛擬機(jī)(如Sun HotSpot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一则果。與虛擬機(jī)棧一樣幔翰,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemory異常。
Java堆
對于大多數(shù)應(yīng)用來說西壮,Java堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊遗增。Java堆是被所有線程共享的一塊數(shù)據(jù)區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建款青。此內(nèi)存區(qū)域的唯一目的就是存放對象實(shí)例做修,幾乎所有的對象實(shí)例都在這里分配內(nèi)存。但是隨著JIT編譯器的發(fā)展與逃逸分析技術(shù)逐漸成熟抡草,棧上分配饰及、標(biāo)量替換優(yōu)化技術(shù)將會導(dǎo)致一些微妙的變化發(fā)生,所有的對象都分配在堆上也逐漸變得不是那么“絕對”康震。
Java堆是垃圾收集器管理的主要區(qū)域燎含,因此很多時(shí)候也被稱為“GC堆”。Java堆還可以細(xì)分為新生代和老年代等等腿短。這一部分在講垃圾回收算法的時(shí)候還會繼續(xù)介紹屏箍。
根據(jù)Java虛擬機(jī)規(guī)范規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中橘忱,即只要邏輯上是連續(xù)的即可赴魁,就像我們磁盤空間一樣。在實(shí)現(xiàn)時(shí)钝诚,可以固定大小颖御,也可是可拓展的,主流的虛擬機(jī)都是按照可拓展來實(shí)現(xiàn)的(通過-Xmx和-Xms來控制)凝颇。如果在堆中沒有內(nèi)存完成實(shí)例分配潘拱,并且堆也無法繼續(xù)拓展時(shí),將會拋出OutOfMemortError異常拧略。
方法區(qū)
方法區(qū)(Method Area)與Java堆一樣泽铛,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機(jī)加載的類信息辑鲤、常量、靜態(tài)變量杠茬、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)月褥。雖然Java虛擬機(jī)將其描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫做Non-Heap(非堆)瓢喉。目的是與Java堆區(qū)分開來宁赤。(以前很多人把方法區(qū)稱為永久代,現(xiàn)在JDK1.8中已經(jīng)用元數(shù)據(jù)區(qū)域取代了永久代)栓票。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)(Runtime Constant Pool)的一部分决左。Class文件中除了有類的版本愕够、字段、方法佛猛、接口等描述信息外惑芭,還有一項(xiàng)信息就是常量池,用于存放編譯時(shí)期生成的各種字面量和符號引用继找,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放遂跟。Java虛擬機(jī)對于運(yùn)行時(shí)常量池沒有做任何細(xì)節(jié)的要求。
運(yùn)行時(shí)常量池具備動(dòng)態(tài)性婴渡,Java語言并不要求常量一定只有編譯期才能產(chǎn)生幻锁,也就是并非預(yù)置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可能將新的常量放入池中边臼,這種特性被開發(fā)人員利用得比較多的便是String類的intern()方法哄尔。
直接內(nèi)存
由于直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域柠并。但是這部分內(nèi)存也被頻繁地使用岭接,而且也可能導(dǎo)致內(nèi)存溢出異常(OutOfMemoryError)出現(xiàn),所以也放到這部分進(jìn)行介紹堂鲤。
顯然亿傅,本機(jī)直接內(nèi)存的分配不會受到Java堆大小的限制。但是肯定還是會受到本機(jī)總內(nèi)存大小以及處理器尋址空間的限制瘟栖。管理員在配置虛擬機(jī)參數(shù)時(shí)葵擎,會根據(jù)實(shí)際內(nèi)存設(shè)置-Xmx等參數(shù)信息,但經(jīng)常忽略直接內(nèi)存半哟,使得各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)級的限制)酬滤,從而導(dǎo)致動(dòng)態(tài)拓展時(shí)出現(xiàn)OutOfMemoryError異常。
對象的創(chuàng)建方式
在Java程序當(dāng)中每時(shí)每刻都有對象被創(chuàng)建出來寓涨。在語言層面上盯串,創(chuàng)建對象通常僅僅是使用一個(gè)new關(guān)鍵字而已,而在虛擬機(jī)中戒良,對象(僅限于普通Java對象)的創(chuàng)建又是怎樣一個(gè)過程呢体捏?
虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)能否在常量池中定位到一個(gè)類的符號引用糯崎。并且檢查這個(gè)符號引用代表的類是否已經(jīng)被加載几缭、解析和初始化過。如果沒有沃呢,那就先執(zhí)行類加載的過程(關(guān)于類加載過程在后面的博客中會進(jìn)行介紹)年栓。
在類加載檢查通過后,接下來虛擬機(jī)將為新生對象分配內(nèi)存薄霜。對象所需內(nèi)存的大小在類加載完成之后便可完全確定(在對象的內(nèi)存布局部分會介紹)某抓。
為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來纸兔。有兩種方式:
- 指針碰撞:假設(shè)Java堆中內(nèi)存是規(guī)整的,所有用過的內(nèi)存都放在一邊否副,空閑的內(nèi)存放在另一邊汉矿,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那分配內(nèi)存就是將指針往空間空間挪動(dòng)一段與對象大小相等的距離副编,這種分配內(nèi)存的方式就被稱為指針碰撞负甸;
- 空閑列表:如果Java堆中的內(nèi)存并不是規(guī)整的,已經(jīng)使用的內(nèi)存和空閑內(nèi)存相互交錯(cuò)痹届,那就沒有辦法簡單地使用指針碰撞的方法進(jìn)行內(nèi)存分配了呻待。虛擬機(jī)此時(shí)必須維護(hù)一個(gè)列表用來記錄哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間為分配給對象實(shí)例队腐,并且更新列表上的記錄蚕捉,這種分配方式就被稱為空閑列表。
選擇哪一種分配方式由Java堆是否規(guī)整決定柴淘,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定迫淹。
除了如何劃分可用空間之外,還要考慮的一個(gè)問題就是對象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為为严,即使是僅僅修改一個(gè)指針的位置敛熬,在并發(fā)的情況之下也并不是線程安全的----可能出現(xiàn)正在給對象A分配內(nèi)存,指針還沒來得及修改第股,對象B同時(shí)使用了原來的指針來分配內(nèi)存的情況应民。解決方案也有兩種:
- 一種是對分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理----實(shí)際上虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性;
- 另一種是把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行夕吻,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存诲锹,稱為本地線程緩沖分配(Thread Local Allocation Buffer,TLAB)涉馅。哪個(gè)線程需要分派內(nèi)存归园,就在哪個(gè)線程的TLAB上分配,只有TLAB用完并分配新的TLAB時(shí)稚矿,才需要同步鎖定庸诱。虛擬機(jī)是否使用TLAB,可以通過-XX:+/-UseTLAB參數(shù)來設(shè)定晤揣。
內(nèi)存分配完成之后桥爽,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對象頭),如果使用TLAB碉渡,則此工作可以提前至TLAB分配時(shí)進(jìn)行。這一步操作保證了對象的實(shí)例字段在Java代碼中可以不賦初值就可以直接使用母剥,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值滞诺。
接下來形导,虛擬機(jī)要對對象進(jìn)行一些必要的設(shè)置,比如這個(gè)對象是哪個(gè)類的實(shí)例习霹、如何才能找到類的元數(shù)據(jù)朵耕、對象的哈希碼、對象的GC分代年齡等信息淋叶。
在上面的工作完成之后阎曹,從虛擬機(jī)的角度來看,一個(gè)新的對象已經(jīng)產(chǎn)生了煞檩。但從Java程序的角度來看处嫌,對象創(chuàng)建才剛剛開始----<init>方法還沒執(zhí)行,所有的字段都還為零斟湃。一般來說(由字節(jié)碼中是否跟隨invokespecial指令所決定)熏迹,執(zhí)行new指令之后會接著執(zhí)行<init>方法,把對象按照程序員的意愿進(jìn)行初始化凝赛,這樣一個(gè)真正的對象才算創(chuàng)建完成注暗。
對象的內(nèi)存布局
對象頭
- 第一部分:用于存儲自身的運(yùn)行時(shí)數(shù)據(jù),包括哈希碼墓猎、GC分代年齡捆昏、鎖狀態(tài)標(biāo)志、線程持有的鎖毙沾、偏向線程ID骗卜、偏向時(shí)間戳等。
- 第二部分:類型指針搀军,即對象指向它的元數(shù)據(jù)的指針膨俐,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對象是哪個(gè)類的實(shí)例。不過并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針罩句,換句話說焚刺,查找對象的元數(shù)據(jù)信息并不一定要經(jīng)過對象本身。另外门烂,如果對象是一個(gè)Java數(shù)組乳愉,那在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過普通Java對象的元數(shù)據(jù)信息確定Java對象的大小屯远,但是從數(shù)組的元數(shù)據(jù)中卻無法確定數(shù)組的大小蔓姚。
實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容慨丐。無論是從父類繼承下來的蹬耘,還是在子類中定義的兄纺,都需要記錄起來。這部分的存儲順序會受到虛擬機(jī)分配策略參數(shù)和字段在Java源碼中定義順序的影響贰镣。
對齊填充
對齊填充并不是必然存在的,也沒有特殊的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說咧纠,就是對象的大小必須是8字節(jié)的整數(shù)倍。而對象頭部分正好是8字節(jié)的倍數(shù)(一倍或者兩倍)泻骤,因此漆羔,當(dāng)對象實(shí)例數(shù)據(jù)部分沒有對齊時(shí),就需要通過對齊填充來補(bǔ)全狱掂。
對象的訪問定位
建立對象是為了使用對象演痒,我們的Java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對象。由于reference類型在Java虛擬機(jī)規(guī)范中只規(guī)定了一個(gè)指向?qū)ο蟮囊梅罚]有定義這個(gè)引用應(yīng)該通過何種方式去定位嫡霞、訪問堆中的對象的具體位置,所以對象訪問方法也是取決于虛擬機(jī)的實(shí)現(xiàn)而決定的希柿。目前主流的訪問方式有使用句柄和直接指針兩種诊沪。
通過句柄訪問對象
優(yōu)點(diǎn):reference存儲的是穩(wěn)定的句柄地址,在對象被移動(dòng)(垃圾收集時(shí)移動(dòng)對象是非常普遍的行為)時(shí)只會改變句柄中的實(shí)例數(shù)據(jù)指針曾撤,而reference本身不需要改變;
缺點(diǎn):增加了一次指針定位的時(shí)間開銷端姚。
通過直接指針訪問對象
優(yōu)點(diǎn):節(jié)省了一次指針定位的開銷
缺點(diǎn):在對象被移動(dòng)時(shí)reference本身需要被修改。
常見的內(nèi)存溢出異常
Java堆溢出
Java堆用于存儲對象實(shí)例挤悉,只要不停地創(chuàng)建對象渐裸,并且保證GC Roots到對象之間有可達(dá)路徑類避免垃圾回收機(jī)制清除這些對象對象,那么在對象數(shù)量達(dá)到最大堆的容量限制后就會產(chǎn)生內(nèi)存溢出異常装悲。
虛擬機(jī)棧和本地方法棧溢出
關(guān)于虛擬機(jī)棧和本地方法棧昏鹃,在Java虛擬機(jī)規(guī)范中描述了兩種異常:
- 如果線程請求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常诀诊;
- 如果虛擬機(jī)在擴(kuò)展棧時(shí)無法申請到足夠的內(nèi)存空間洞渤,則拋出OutOfMemoryError異常。
這里把異常分為兩種情況属瓣,看似較為嚴(yán)謹(jǐn)载迄,但卻存在著一些互相重疊的地方:當(dāng)棧空間無法繼續(xù)分配時(shí)抡蛙,到底是已使用的椈っ粒空間太大,還是內(nèi)存太小粗截,其本質(zhì)上都只是對同一件事情的兩種描述而已惋耙。
方法區(qū)和運(yùn)行時(shí)常量池溢出
本機(jī)直接內(nèi)存溢出
第二部分 垃圾收集器與內(nèi)存分配策略
其實(shí)當(dāng)我們在討論垃圾回收的時(shí)候,我們常常要思考垃圾收集(Garbage Collection)需要完成的三件事情:
- 哪些內(nèi)存需要回收?(What绽榛?)
- 什么時(shí)候回收遥金?(When?)
- 如何回收蒜田?(How?)
那么對于Java虛擬機(jī)來說选泻,垃圾收集主要是發(fā)生在哪些區(qū)域呢冲粤?
由于程序計(jì)數(shù)器、虛擬機(jī)棧页眯、本地方法棧這三個(gè)區(qū)域是隨線程而生梯捕,隨線程而亡的;棧中的棧幀隨著方法的進(jìn)入和退出有條不紊地執(zhí)行著入棧和出棧操作窝撵,每一個(gè)棧幀中分配多少內(nèi)存基本上都是在類結(jié)構(gòu)確定下來時(shí)就已知的傀顾。因此這幾個(gè)區(qū)域的內(nèi)存分配和回收策略都具備確定性,在這幾個(gè)區(qū)域就不需要過多考慮回收的問題碌奉。因?yàn)榉椒ńY(jié)束或者線程結(jié)束之后短曾。這部分內(nèi)存自然也就隨著回收了。
但是Java堆和方法區(qū)則不一樣赐劣,因?yàn)橐粋€(gè)接口中的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不一樣嫉拐,一個(gè)方法中的多個(gè)分支需要的內(nèi)存可能也不一樣,我們只有在程序運(yùn)行期間才能知道到底會創(chuàng)建哪些對象魁兼,這部分內(nèi)存的分配是動(dòng)態(tài)的婉徘,是不確定的。所以我們要針對這兩塊區(qū)域制訂合適的垃圾收集策略咐汞。因此盖呼,在后面我們提到的對內(nèi)存進(jìn)行垃圾回收,說的主要也是針對Java堆和方法區(qū)這兩塊區(qū)域化撕。
對象已死嗎几晤?
在Java堆中存放著Java世界中幾乎所有的對象實(shí)例。垃圾收集器在進(jìn)行垃圾收集行為之前侯谁,需要對這些對象進(jìn)行判斷锌仅,看看哪些對象已經(jīng)“死”了,哪些對象依然“存活”著墙贱。
引用計(jì)數(shù)法
很多書上用來判斷對象是否存活的方法是這樣的:給對象添加一個(gè)引用計(jì)數(shù)器热芹,如果有一個(gè)地方引用它的時(shí)候,這個(gè)計(jì)數(shù)器就加一惨撇;當(dāng)引用失效時(shí)伊脓,計(jì)數(shù)器就減一;任何時(shí)刻計(jì)數(shù)器為零的對象意味著它已經(jīng)不能再被使用了。
引用計(jì)數(shù)法看起來很簡單报腔,也很容易理解株搔。但是主流的Java虛擬機(jī)中沒有選用引用計(jì)數(shù)法來對內(nèi)存進(jìn)行管理。很大一部分原因就是因?yàn)榇怂惴ú荒芙鉀Q兩個(gè)對象相互引用的問題纯蛾。如果不相信的話纤房,下面可以用程序驗(yàn)證一下:
我們可以看到,如果虛擬機(jī)中采用的是引用計(jì)數(shù)法的話翻诉,那么objA和objB引用計(jì)數(shù)器的值都應(yīng)不為零炮姨,故不應(yīng)該發(fā)生垃圾回收。但是從運(yùn)行結(jié)果來看碰煌,此時(shí)確實(shí)發(fā)生了垃圾回收行為舒岸。這也就驗(yàn)證了在這里,Java虛擬機(jī)并不是采用引用計(jì)數(shù)法來管理內(nèi)存芦圾。
可達(dá)性分析
可達(dá)性分析算法的基本思想是通過一些列被稱為“GC Roots”的對象作為起始點(diǎn)蛾派,然后從這些節(jié)點(diǎn)向下開始搜索,搜索走過的路徑被稱為引用鏈(Reference Chain)个少,當(dāng)某個(gè)對象(節(jié)點(diǎn))到“GC Roots”之間不存在引用鏈的話洪乍,則證明此對象不可用。其實(shí)了解二叉樹的話這里就很好理解了:從根節(jié)點(diǎn)出發(fā)夜焦,如果不能遍歷到某個(gè)對象典尾,則此對象就不可用。
在Java語言中糊探,可以作為GC Roots的對象有:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象钾埂;
- 方法區(qū)中類靜態(tài)屬性引用的對象;
- 方法區(qū)中常量引用的對象科平;
- 本地方法棧中JNI(即常說的Native方法)引用的對象褥紫。
再談引用
無論是通過引用計(jì)數(shù)算法判斷對象的引用數(shù)量,還是通過可達(dá)性分析算法判斷對象的引用鏈?zhǔn)欠窨蛇_(dá)瞪慧,我們可以知道判定對象是否存活都與引用有關(guān)髓考。
在JDK1.2之前,Java中的引用的定義很簡單粗暴:如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另一塊內(nèi)存的起始地址弃酌,就稱這塊內(nèi)存代表著一個(gè)引用氨菇。這種定義很簡單,但是太過于狹隘-----一個(gè)對象在這種定義之下就只有兩種狀態(tài):引用或者沒有引用妓湘。對于描述一些“食之無味查蓉,棄之可惜”的對象就顯得無能為力。
所以在JDK1.2之后榜贴,Java對引用的概念進(jìn)行了擴(kuò)充豌研,將引用分為:
- 強(qiáng)引用(Strong Reference)
強(qiáng)引用就是在內(nèi)存中普遍存在的。類似于“Object obj = new Object()”這樣的引用。只要強(qiáng)引用還存在鹃共,垃圾收集器就不會將被引用的對象回收鬼佣。
- 軟引用(Soft Reference)
軟引用就是用來描述一些還有用但并非必需的對象。對于軟引用關(guān)聯(lián)著的對象霜浴,在系統(tǒng)將要發(fā)生內(nèi)存溢出之前晶衷,將會把這些對象列入回收范圍中進(jìn)行二次回收。如果這次回收還沒有得到足夠的內(nèi)存阴孟,才會拋出內(nèi)存溢出異常房铭。SoftReference類可以實(shí)現(xiàn)軟引用。 - 弱引用(Weak Reference)
弱引用也是用來描述非必需對象的温眉,但是它的強(qiáng)度比軟引用更弱一些。被弱引用關(guān)聯(lián)的對象只能存活到下一次垃圾回收發(fā)生之前翁狐。當(dāng)垃圾收集器工作時(shí)类溢,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象露懒。WeakReference類可以實(shí)現(xiàn)弱引用 - 虛引用(Phantom Reference)
虛引用又被稱為幽靈引用或者是幻影引用闯冷,它是最弱的一種引用關(guān)系。一個(gè)對象是否有虛引用的存在懈词,完全不會對其生存時(shí)間構(gòu)成影響蛇耀,也無法通過虛引用來取得一個(gè)對象實(shí)例。為一個(gè)對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知坎弯。PhantomReference類可以實(shí)現(xiàn)虛引用纺涤。
生存 or 死亡
即使是在可達(dá)性分析算法中不可達(dá)的對象,其實(shí)也并非是“非死不可”的抠忘,這時(shí)候它們暫時(shí)處于“緩刑”階段撩炊,要真正宣告一個(gè)對象死亡,必須要經(jīng)歷兩次過程:
- 如果對象在進(jìn)行可達(dá)性分析之后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈崎脉,那么它會被第一次標(biāo)記并進(jìn)行一次篩選拧咳。篩選的條件是此方法是否有必要執(zhí)行finalize()方法。當(dāng)對象沒有覆蓋finalize()方法或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過囚灼,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”骆膝;
- 如果這個(gè)對象被視為有必要執(zhí)行finalize()方法,那么這個(gè)對象將會放置在一個(gè)F-Queue的隊(duì)列之中灶体,并在稍后由一個(gè)虛擬機(jī)自動(dòng)建立的阅签、低優(yōu)先級的Finalizer線程去執(zhí)行它。這里的“執(zhí)行”是指虛擬機(jī)會觸發(fā)這個(gè)方法蝎抽,但并不會承諾等待它運(yùn)行結(jié)束愉择。這樣做的目的是防止一個(gè)對象在finalize()方法中執(zhí)行緩慢或者是發(fā)生了死循環(huán)從而導(dǎo)致F-Queue隊(duì)列中其他對象永久處于等待狀態(tài),甚至導(dǎo)致程序崩潰。
回收方法分區(qū)
許多人認(rèn)為在方法區(qū)中不會發(fā)生垃圾回收行為锥涕。Java虛擬機(jī)規(guī)范中也說過可以不要求虛擬機(jī)在方法區(qū)實(shí)現(xiàn)垃圾回收衷戈。但是其實(shí)在方法區(qū)也是存在垃圾回收的,主要是針對兩部分:
- 廢棄常量
- 無用的類
判斷一個(gè)常量是否為廢棄常量是一件比較簡單的事情层坠,而要判定一個(gè)類是否是“無用的類”的條件相對苛刻殖妇。類要同時(shí)滿足下面3個(gè)條件才能算是“無用的類”:
- 該類所有的實(shí)例都已被回收,即Java堆中不存在此類的任何實(shí)例破花;
- 加載該類的ClassLoader已被回收谦趣;
- 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法座每。
垃圾收集算法
這里主要介紹幾種算法的思想前鹅,不深究其實(shí)現(xiàn)過程
標(biāo)記 - 清除算法
“標(biāo)記 - 清除”(Mark-Sweep)算法是最基礎(chǔ)的算法。此算法共分為兩個(gè)階段:標(biāo)記階段和清除階段峭梳。其實(shí)很簡單舰绘,就是首先標(biāo)記出所有需要被回收的對象,然后在標(biāo)記完成之后統(tǒng)一回收所有被標(biāo)記的對象葱椭。
不足:
- 效率問題捂寿,標(biāo)記和清除兩個(gè)過程的效率都不高;
- 空間問題孵运,標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片秦陋,空間碎片太多可能會導(dǎo)致以后再程序運(yùn)行過程中需要分配較大對象時(shí),無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作治笨。
復(fù)制算法
為了解決效率問題驳概,復(fù)制算法就出現(xiàn)了,它將可用內(nèi)存按容量劃分為大小相等的兩塊旷赖,每次只使用其中的一塊抡句。當(dāng)這一塊內(nèi)存用完了,就將還存活著的對象復(fù)制到另一塊上杠愧,然后再把已使用過的內(nèi)存空間清理掉待榔。這樣就使得每次都是對整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,在進(jìn)行內(nèi)存分配的時(shí)候也無需考慮內(nèi)存碎片等復(fù)雜問題流济,只要移動(dòng)堆頂指針锐锣,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡單绳瘟,運(yùn)行高效雕憔。只是這種算法的代價(jià)是將內(nèi)存縮小為原來的一半。
標(biāo)記 - 整理算法
復(fù)制算法在對象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作糖声,效率會降低斤彼。更為關(guān)鍵的是分瘦,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保琉苇,以應(yīng)對被使用的內(nèi)存中所有對象都100%存活的極端情況嘲玫,所以老年代一般不能直接選用這種算法。
根據(jù)老年代的特點(diǎn)并扇,有人提出了另一種“標(biāo)記 - 整理”算法去团,標(biāo)記過程仍與“標(biāo)記 - 清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進(jìn)行清理穷蛹,而是讓所有存活的對象都往一端移動(dòng)土陪,然后直接清理端邊界以外的內(nèi)存。
分代收集算法
當(dāng)前商業(yè)虛擬機(jī)都采用“分代收集”(Generational Collection)算法肴熏,這種算法就是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊鬼雀。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)年代的不同來選擇最合適的垃圾收集算法蛙吏。
- 在新生代中源哩,每次垃圾收集時(shí)都有大批的對象死去,只有少量對象存活出刷。那就選用復(fù)制算法。這樣依賴只需付出少量存貨對象的復(fù)制成本即可完成垃圾收集坯辩。
- 老年代中對象存活率較高馁龟、沒有額外空間進(jìn)行分配擔(dān)保,所以必須使用“標(biāo)記 - 清除”或者“標(biāo)記 - 整理”算法來進(jìn)行回收漆魔。
HotSpot算法實(shí)現(xiàn)(待完善)
垃圾收集器(待完善)
內(nèi)存分配與回收策略
Java的自動(dòng)內(nèi)存管理歸根結(jié)底其實(shí)就是解決了兩個(gè)問題:給對象分配內(nèi)存以及回收分配給對象的內(nèi)存空間坷檩。我們前面已經(jīng)講了非常多有關(guān)于內(nèi)存回收的知識,下面將開始介紹有關(guān)于內(nèi)存分配的只是改抡。
對象的內(nèi)存分配矢炼,在宏觀上來看,其實(shí)就是在堆上分配(也可能經(jīng)過JIT編譯后被拆散為標(biāo)量類型并間接地棧上分配)阿纤,對象主要分配在新生代的Eden區(qū)上句灌,如果啟動(dòng)了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配欠拾。少數(shù)情況下也可能會直接分配在老年代胰锌。其實(shí)分配的規(guī)則并不是固定的,其細(xì)節(jié)還取決于當(dāng)前使用的是哪一種垃圾收集器組合藐窄,還有虛擬機(jī)中能夠與內(nèi)存相關(guān)的參數(shù)設(shè)置资昧。
對象優(yōu)先在Eden分配
大多數(shù)情況下,對象在新生代Eden區(qū)中分配荆忍。當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時(shí)格带,虛擬機(jī)將發(fā)起一次Minor GC撤缴。
大對象直接進(jìn)入老年代
所謂大對象是指需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象就是那種很長的字符串以及數(shù)組叽唱。大對象對虛擬機(jī)的內(nèi)存分配來說就是一個(gè)壞消息屈呕,經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少空間時(shí)就提前觸發(fā)垃圾收集以獲取足夠多的連續(xù)空間來“安置”它們。
長期存活的對象將進(jìn)入老年代
既然虛擬機(jī)采用了分代收集的思想來管理內(nèi)存尔觉,那么內(nèi)存回收時(shí)就必須能識別哪些對象應(yīng)放在新生代凉袱,哪些對象應(yīng)放在老年代。為了做到這點(diǎn)侦铜,虛擬機(jī)給每個(gè)對象定義了一個(gè)對象年齡(Age)計(jì)數(shù)器专甩。如果對象在Eden出生并經(jīng)過一次Minor GC后仍然存活,并且能被Survivor容納的話钉稍,將被移動(dòng)到Survivor空間中涤躲,并且對象年齡設(shè)為1.對象在Survivor區(qū)每“熬過”一次Minor GC,年齡就增加1歲贡未,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲)种樱,就會晉升到老年代中。對象晉升老年代的年齡閾值俊卤,可以通過-XX:MaxTenuringThreshold設(shè)置嫩挤。
動(dòng)態(tài)對象年齡判定
為了更好地適應(yīng)不同程度的內(nèi)存情況,虛擬機(jī)并不是永遠(yuǎn)地要求對象的年齡必須達(dá)到了MaxTenuringThreshold才能晉升老年代消恍,如果在Servivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半岂昭,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代,無須等到MaxTenuringThreshold中要求的年齡狠怨。