深入理解Java虛擬機(jī)---自動(dòng)內(nèi)存管理機(jī)制

寫在前面
本文介紹的Java虛擬機(jī)(JVM)的自動(dòng)內(nèi)存管理機(jī)制主要是參照《深入理解Java虛擬機(jī)》(第2版)一書中的內(nèi)容,主要分為兩個(gè)部分:Java內(nèi)存區(qū)域和內(nèi)存溢出異常、垃圾回收和內(nèi)存分配策略。因此我也會(huì)分為兩個(gè)部分來講解,但這并不代表這兩個(gè)部分在JVM中是分割的。反之,其實(shí)這兩個(gè)部分關(guān)聯(lián)性很強(qiáng)。只不過為了便于介紹,所以我才分開來講。在介紹它們?cè)敿?xì)內(nèi)容之前,我首先會(huì)給出兩幅思維導(dǎo)圖以便讀者可以了解一下里面所包含的內(nèi)容,然后我會(huì)根據(jù)思維導(dǎo)圖中的知識(shí)點(diǎn)一一為大家進(jìn)行介紹。

第一部分 Java內(nèi)存區(qū)域和內(nèi)存溢出異常

Java內(nèi)存區(qū)域與內(nèi)存溢出異常

下面我將對(duì)圖中所涉及到的部分進(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程序的過程中會(huì)把它所管理的內(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é)碼的行號(hào)指示器。學(xué)過《計(jì)算機(jī)組成原理》這門課之后我們知道----在計(jì)算機(jī)中,其實(shí)程序計(jì)數(shù)器就是一個(gè)寄存器,依據(jù)不同計(jì)算機(jī)細(xì)節(jié)的差異,它可以存放當(dāng)前正在被執(zhí)行的指令,也可以存放下一個(gè)要被執(zhí)行的指令。由此,我們可以對(duì)“當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器”有更好的理解。
在虛擬機(jī)的概念模型中,字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。由于Java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對(duì)于多核處理器來說是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此為了線程切換之后能夠恢復(fù)到正確的執(zhí)行位置,每條線程都需要擁有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互補(bǔ)影響,獨(dú)立存儲(chǔ)。所以程序計(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í)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。
我們常常說的棧內(nèi)存其實(shí)就是現(xiàn)在講的虛擬機(jī)棧,或者說是虛擬機(jī)棧中局部變量表部分。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference類型,它不等同于對(duì)象本身,可能是指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苁侵赶蛞粋€(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。
其中64位長度的long和double類型的數(shù)據(jù)會(huì)占用2個(gè)局部變量空間(Slot),其余數(shù)據(jù)類型只占用1個(gè)。局部變量表所需要的內(nèi)存空間在編譯時(shí)期完成分配。當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。

本地方法棧

本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別就是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)。其實(shí)虛擬機(jī)規(guī)范中對(duì)本地方發(fā)棧中方法所使用的語言、使用方式以及數(shù)據(jù)結(jié)構(gòu)都沒有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由地實(shí)現(xiàn)它。甚至在有的虛擬機(jī)(如Sun HotSpot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會(huì)拋出StackOverflowError和OutOfMemory異常。

Java堆

對(duì)于大多數(shù)應(yīng)用來說,Java堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊數(shù)據(jù)區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存但是隨著JIT編譯器的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化發(fā)生,所有的對(duì)象都分配在堆上也逐漸變得不是那么“絕對(duì)”
Java堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱為“GC堆”。Java堆還可以細(xì)分為新生代和老年代等等。這一部分在講垃圾回收算法的時(shí)候還會(huì)繼續(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í),將會(huì)拋出OutOfMemortError異常。

方法區(qū)

方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(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í)期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。Java虛擬機(jī)對(duì)于運(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)存的分配不會(huì)受到Java堆大小的限制。但是肯定還是會(huì)受到本機(jī)總內(nèi)存大小以及處理器尋址空間的限制。管理員在配置虛擬機(jī)參數(shù)時(shí),會(huì)根據(jù)實(shí)際內(nèi)存設(shè)置-Xmx等參數(shù)信息,但經(jīng)常忽略直接內(nèi)存,使得各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)級(jí)的限制),從而導(dǎo)致動(dòng)態(tài)拓展時(shí)出現(xiàn)OutOfMemoryError異常。

對(duì)象的創(chuàng)建方式

在Java程序當(dāng)中每時(shí)每刻都有對(duì)象被創(chuàng)建出來。在語言層面上,創(chuàng)建對(duì)象通常僅僅是使用一個(gè)new關(guān)鍵字而已,而在虛擬機(jī)中,對(duì)象(僅限于普通Java對(duì)象)的創(chuàng)建又是怎樣一個(gè)過程呢?

虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)能否在常量池中定位到一個(gè)類的符號(hào)引用。并且檢查這個(gè)符號(hào)引用代表的類是否已經(jīng)被加載、解析和初始化過。如果沒有,那就先執(zhí)行類加載的過程(關(guān)于類加載過程在后面的博客中會(huì)進(jìn)行介紹)。

在類加載檢查通過后,接下來虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類加載完成之后便可完全確定(在對(duì)象的內(nèi)存布局部分會(huì)介紹)。

為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。有兩種方式:

  • 指針碰撞:假設(shè)Java堆中內(nèi)存是規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那分配內(nèi)存就是將指針往空間空間挪動(dòng)一段與對(duì)象大小相等的距離,這種分配內(nèi)存的方式就被稱為指針碰撞;
  • 空閑列表:如果Java堆中的內(nèi)存并不是規(guī)整的,已經(jīng)使用的內(nèi)存和空閑內(nèi)存相互交錯(cuò),那就沒有辦法簡單地使用指針碰撞的方法進(jìn)行內(nèi)存分配了。虛擬機(jī)此時(shí)必須維護(hù)一個(gè)列表用來記錄哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間為分配給對(duì)象實(shí)例,并且更新列表上的記錄,這種分配方式就被稱為空閑列表。

選擇哪一種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
除了如何劃分可用空間之外,還要考慮的一個(gè)問題就是對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使是僅僅修改一個(gè)指針的位置,在并發(fā)的情況之下也并不是線程安全的----可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒來得及修改,對(duì)象B同時(shí)使用了原來的指針來分配內(nèi)存的情況。解決方案也有兩種:

  • 一種是對(duì)分配內(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)存空間都初始化為零值(不包括對(duì)象頭),如果使用TLAB,則此工作可以提前至TLAB分配時(shí)進(jìn)行。這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初值就可以直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。
接下來,虛擬機(jī)要對(duì)對(duì)象進(jìn)行一些必要的設(shè)置,比如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。

在上面的工作完成之后,從虛擬機(jī)的角度來看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了。但從Java程序的角度來看,對(duì)象創(chuàng)建才剛剛開始----<init>方法還沒執(zhí)行,所有的字段都還為零。一般來說(由字節(jié)碼中是否跟隨invokespecial指令所決定),執(zhí)行new指令之后會(huì)接著執(zhí)行<init>方法,把對(duì)象按照程序員的意愿進(jìn)行初始化,這樣一個(gè)真正的對(duì)象才算創(chuàng)建完成。

對(duì)象的內(nèi)存布局

對(duì)象頭

  • 第一部分:用于存儲(chǔ)自身的運(yùn)行時(shí)數(shù)據(jù),包括哈希碼、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等。
  • 第二部分:類型指針,即對(duì)象指向它的元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。不過并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,換句話說,查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過對(duì)象本身。另外,如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小,但是從數(shù)組的元數(shù)據(jù)中卻無法確定數(shù)組的大小。

實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù)和字段在Java源碼中定義順序的影響。
對(duì)齊填充
對(duì)齊填充并不是必然存在的,也沒有特殊的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話說,就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍。而對(duì)象頭部分正好是8字節(jié)的倍數(shù)(一倍或者兩倍),因此,當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊時(shí),就需要通過對(duì)齊填充來補(bǔ)全。

對(duì)象的訪問定位

建立對(duì)象是為了使用對(duì)象,我們的Java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對(duì)象。由于reference類型在Java虛擬機(jī)規(guī)范中只規(guī)定了一個(gè)指向?qū)ο蟮囊茫]有定義這個(gè)引用應(yīng)該通過何種方式去定位、訪問堆中的對(duì)象的具體位置,所以對(duì)象訪問方法也是取決于虛擬機(jī)的實(shí)現(xiàn)而決定的。目前主流的訪問方式有使用句柄和直接指針兩種。

通過句柄訪問對(duì)象

通過句柄訪問對(duì)象

優(yōu)點(diǎn):reference存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要改變;
缺點(diǎn):增加了一次指針定位的時(shí)間開銷。

通過直接指針訪問對(duì)象

通過直接指針訪問對(duì)象

優(yōu)點(diǎn):節(jié)省了一次指針定位的開銷
缺點(diǎn):在對(duì)象被移動(dòng)時(shí)reference本身需要被修改。

常見的內(nèi)存溢出異常

Java堆溢出
Java堆用于存儲(chǔ)對(duì)象實(shí)例,只要不停地創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑類避免垃圾回收機(jī)制清除這些對(duì)象對(duì)象,那么在對(duì)象數(shù)量達(dá)到最大堆的容量限制后就會(huì)產(chǎn)生內(nèi)存溢出異常。
虛擬機(jī)棧和本地方法棧溢出
關(guān)于虛擬機(jī)棧和本地方法棧,在Java虛擬機(jī)規(guī)范中描述了兩種異常:

  • 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常;
  • 如果虛擬機(jī)在擴(kuò)展棧時(shí)無法申請(qǐng)到足夠的內(nèi)存空間,則拋出OutOfMemoryError異常。

這里把異常分為兩種情況,看似較為嚴(yán)謹(jǐn),但卻存在著一些互相重疊的地方:當(dāng)棧空間無法繼續(xù)分配時(shí),到底是已使用的棧空間太大,還是內(nèi)存太小,其本質(zhì)上都只是對(duì)同一件事情的兩種描述而已。
方法區(qū)和運(yùn)行時(shí)常量池溢出
本機(jī)直接內(nèi)存溢出

第二部分 垃圾收集器與內(nèi)存分配策略

垃圾收集器與內(nèi)存分配策略

其實(shí)當(dāng)我們?cè)谟懻摾厥盏臅r(shí)候,我們常常要思考垃圾收集(Garbage Collection)需要完成的三件事情:

  • 哪些內(nèi)存需要回收?(What?)
  • 什么時(shí)候回收?(When?)
  • 如何回收?(How?)

那么對(duì)于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)行期間才能知道到底會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配是動(dòng)態(tài)的,是不確定的。所以我們要針對(duì)這兩塊區(qū)域制訂合適的垃圾收集策略。因此,在后面我們提到的對(duì)內(nèi)存進(jìn)行垃圾回收,說的主要也是針對(duì)Java堆和方法區(qū)這兩塊區(qū)域。

對(duì)象已死嗎?

在Java堆中存放著Java世界中幾乎所有的對(duì)象實(shí)例。垃圾收集器在進(jìn)行垃圾收集行為之前,需要對(duì)這些對(duì)象進(jìn)行判斷,看看哪些對(duì)象已經(jīng)“死”了,哪些對(duì)象依然“存活”著。

引用計(jì)數(shù)法

很多書上用來判斷對(duì)象是否存活的方法是這樣的:給對(duì)象添加一個(gè)引用計(jì)數(shù)器,如果有一個(gè)地方引用它的時(shí)候,這個(gè)計(jì)數(shù)器就加一;當(dāng)引用失效時(shí),計(jì)數(shù)器就減一;任何時(shí)刻計(jì)數(shù)器為零的對(duì)象意味著它已經(jīng)不能再被使用了
引用計(jì)數(shù)法看起來很簡單,也很容易理解。但是主流的Java虛擬機(jī)中沒有選用引用計(jì)數(shù)法來對(duì)內(nèi)存進(jìn)行管理。很大一部分原因就是因?yàn)榇怂惴ú荒芙鉀Q兩個(gè)對(duì)象相互引用的問題。如果不相信的話,下面可以用程序驗(yàn)證一下:

引用計(jì)數(shù)法的缺陷
運(yùn)行結(jié)果

我們可以看到,如果虛擬機(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”的對(duì)象作為起始點(diǎn),然后從這些節(jié)點(diǎn)向下開始搜索,搜索走過的路徑被稱為引用鏈(Reference Chain),當(dāng)某個(gè)對(duì)象(節(jié)點(diǎn))到“GC Roots”之間不存在引用鏈的話,則證明此對(duì)象不可用。其實(shí)了解二叉樹的話這里就很好理解了:從根節(jié)點(diǎn)出發(fā),如果不能遍歷到某個(gè)對(duì)象,則此對(duì)象就不可用。
在Java語言中,可以作為GC Roots的對(duì)象有:

  • 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象;
  • 方法區(qū)中類靜態(tài)屬性引用的對(duì)象;
  • 方法區(qū)中常量引用的對(duì)象;
  • 本地方法棧中JNI(即常說的Native方法)引用的對(duì)象。

再談引用

無論是通過引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量,還是通過可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá),我們可以知道判定對(duì)象是否存活都與引用有關(guān)。
在JDK1.2之前,Java中的引用的定義很簡單粗暴:如果reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個(gè)引用。這種定義很簡單,但是太過于狹隘-----一個(gè)對(duì)象在這種定義之下就只有兩種狀態(tài):引用或者沒有引用。對(duì)于描述一些“食之無味,棄之可惜”的對(duì)象就顯得無能為力。
所以在JDK1.2之后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為:

  • 強(qiáng)引用(Strong Reference)
    強(qiáng)引用就是在內(nèi)存中普遍存在的。類似于“Object obj = new Object()”這樣的引用。只要強(qiáng)引用還存在,垃圾收集器就不會(huì)將被引用的對(duì)象回收。
  • 軟引用(Soft Reference)
    軟引用就是用來描述一些還有用但并非必需的對(duì)象。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出之前,將會(huì)把這些對(duì)象列入回收范圍中進(jìn)行二次回收。如果這次回收還沒有得到足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。SoftReference類可以實(shí)現(xiàn)軟引用
  • 弱引用(Weak Reference)
    弱引用也是用來描述非必需對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些。被弱引用關(guān)聯(lián)的對(duì)象只能存活到下一次垃圾回收發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。WeakReference類可以實(shí)現(xiàn)弱引用
  • 虛引用(Phantom Reference)
    虛引用又被稱為幽靈引用或者是幻影引用,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。PhantomReference類可以實(shí)現(xiàn)虛引用

生存 or 死亡

即使是在可達(dá)性分析算法中不可達(dá)的對(duì)象,其實(shí)也并非是“非死不可”的,這時(shí)候它們暫時(shí)處于“緩刑”階段,要真正宣告一個(gè)對(duì)象死亡,必須要經(jīng)歷兩次過程:

  • 如果對(duì)象在進(jìn)行可達(dá)性分析之后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那么它會(huì)被第一次標(biāo)記并進(jìn)行一次篩選。篩選的條件是此方法是否有必要執(zhí)行finalize()方法。當(dāng)對(duì)象沒有覆蓋finalize()方法或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”;
  • 如果這個(gè)對(duì)象被視為有必要執(zhí)行finalize()方法,那么這個(gè)對(duì)象將會(huì)放置在一個(gè)F-Queue的隊(duì)列之中,并在稍后由一個(gè)虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的Finalizer線程去執(zhí)行它。這里的“執(zhí)行”是指虛擬機(jī)會(huì)觸發(fā)這個(gè)方法,但并不會(huì)承諾等待它運(yùn)行結(jié)束。這樣做的目的是防止一個(gè)對(duì)象在finalize()方法中執(zhí)行緩慢或者是發(fā)生了死循環(huán)從而導(dǎo)致F-Queue隊(duì)列中其他對(duì)象永久處于等待狀態(tài),甚至導(dǎo)致程序崩潰。

回收方法分區(qū)

許多人認(rèn)為在方法區(qū)中不會(huì)發(fā)生垃圾回收行為。Java虛擬機(jī)規(guī)范中也說過可以不要求虛擬機(jī)在方法區(qū)實(shí)現(xiàn)垃圾回收。但是其實(shí)在方法區(qū)也是存在垃圾回收的,主要是針對(duì)兩部分:

  • 廢棄常量
  • 無用的類

判斷一個(gè)常量是否為廢棄常量是一件比較簡單的事情,而要判定一個(gè)類是否是“無用的類”的條件相對(duì)苛刻。類要同時(shí)滿足下面3個(gè)條件才能算是“無用的類”:

  • 該類所有的實(shí)例都已被回收,即Java堆中不存在此類的任何實(shí)例;
  • 加載該類的ClassLoader已被回收;
  • 該類對(duì)應(yīng)的java.lang.Class對(duì)象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

垃圾收集算法

這里主要介紹幾種算法的思想,不深究其實(shí)現(xiàn)過程

標(biāo)記 - 清除算法

“標(biāo)記 - 清除”(Mark-Sweep)算法是最基礎(chǔ)的算法。此算法共分為兩個(gè)階段:標(biāo)記階段和清除階段。其實(shí)很簡單,就是首先標(biāo)記出所有需要被回收的對(duì)象,然后在標(biāo)記完成之后統(tǒng)一回收所有被標(biāo)記的對(duì)象。
不足:

  • 效率問題,標(biāo)記和清除兩個(gè)過程的效率都不高;
  • 空間問題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后再程序運(yùn)行過程中需要分配較大對(duì)象時(shí),無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
復(fù)制算法

為了解決效率問題,復(fù)制算法就出現(xiàn)了,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另一塊上,然后再把已使用過的內(nèi)存空間清理掉。這樣就使得每次都是對(duì)整個(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ù)制算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率會(huì)降低。更為關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況,所以老年代一般不能直接選用這種算法。
根據(jù)老年代的特點(diǎn),有人提出了另一種“標(biāo)記 - 整理”算法,標(biāo)記過程仍與“標(biāo)記 - 清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都往一端移動(dòng),然后直接清理端邊界以外的內(nèi)存。

分代收集算法

當(dāng)前商業(yè)虛擬機(jī)都采用“分代收集”(Generational Collection)算法,這種算法就是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)年代的不同來選擇最合適的垃圾收集算法。

  • 在新生代中,每次垃圾收集時(shí)都有大批的對(duì)象死去,只有少量對(duì)象存活。那就選用復(fù)制算法。這樣依賴只需付出少量存貨對(duì)象的復(fù)制成本即可完成垃圾收集。
  • 老年代中對(duì)象存活率較高、沒有額外空間進(jìn)行分配擔(dān)保,所以必須使用“標(biāo)記 - 清除”或者“標(biāo)記 - 整理”算法來進(jìn)行回收。

HotSpot算法實(shí)現(xiàn)(待完善)

垃圾收集器(待完善)

內(nèi)存分配與回收策略

Java的自動(dòng)內(nèi)存管理歸根結(jié)底其實(shí)就是解決了兩個(gè)問題:給對(duì)象分配內(nèi)存以及回收分配給對(duì)象的內(nèi)存空間。我們前面已經(jīng)講了非常多有關(guān)于內(nèi)存回收的知識(shí),下面將開始介紹有關(guān)于內(nèi)存分配的只是。
對(duì)象的內(nèi)存分配,在宏觀上來看,其實(shí)就是在堆上分配(也可能經(jīng)過JIT編譯后被拆散為標(biāo)量類型并間接地棧上分配),對(duì)象主要分配在新生代的Eden區(qū)上,如果啟動(dòng)了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配。少數(shù)情況下也可能會(huì)直接分配在老年代。其實(shí)分配的規(guī)則并不是固定的,其細(xì)節(jié)還取決于當(dāng)前使用的是哪一種垃圾收集器組合,還有虛擬機(jī)中能夠與內(nèi)存相關(guān)的參數(shù)設(shè)置。

對(duì)象優(yōu)先在Eden分配

大多數(shù)情況下,對(duì)象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC。

大對(duì)象直接進(jìn)入老年代

所謂大對(duì)象是指需要大量連續(xù)內(nèi)存空間的Java對(duì)象,最典型的大對(duì)象就是那種很長的字符串以及數(shù)組。大對(duì)象對(duì)虛擬機(jī)的內(nèi)存分配來說就是一個(gè)壞消息,經(jīng)常出現(xiàn)大對(duì)象容易導(dǎo)致內(nèi)存還有不少空間時(shí)就提前觸發(fā)垃圾收集以獲取足夠多的連續(xù)空間來“安置”它們。

長期存活的對(duì)象將進(jìn)入老年代

既然虛擬機(jī)采用了分代收集的思想來管理內(nèi)存,那么內(nèi)存回收時(shí)就必須能識(shí)別哪些對(duì)象應(yīng)放在新生代,哪些對(duì)象應(yīng)放在老年代。為了做到這點(diǎn),虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器。如果對(duì)象在Eden出生并經(jīng)過一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動(dòng)到Survivor空間中,并且對(duì)象年齡設(shè)為1.對(duì)象在Survivor區(qū)每“熬過”一次Minor GC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲),就會(huì)晉升到老年代中。對(duì)象晉升老年代的年齡閾值,可以通過-XX:MaxTenuringThreshold設(shè)置。

動(dòng)態(tài)對(duì)象年齡判定

為了更好地適應(yīng)不同程度的內(nèi)存情況,虛擬機(jī)并不是永遠(yuǎn)地要求對(duì)象的年齡必須達(dá)到了MaxTenuringThreshold才能晉升老年代,如果在Servivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代,無須等到MaxTenuringThreshold中要求的年齡。

空間分配擔(dān)保(待完善)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,477評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,617評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,807評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,049評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評(píng)論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,432評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,769評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容