1、java虛擬機(jī)發(fā)展史
? ? 1.1 Sun Classic
? ? ? ? jdk1.0-jdk1.4只能用解釋器方式解釋代碼,或者使用外掛的編譯器,兩者不能共存。
? ? 1.2 Sun HotSpot VM
? ? ? ? 是目前使用最廣的虛擬機(jī)也是java自帶的虛擬機(jī)。
? ? 1.3 BEA JRockit/IBM J9 VM
? ? ? ? ?JRockit快速服務(wù)器,專注服務(wù)器應(yīng)用,啟動(dòng)速度不快。
????1.4 Azul VM/BEA Liquid VM
? ? ? ? 專用高速虛擬機(jī)
2、java內(nèi)存區(qū)域
? ? 2.1 運(yùn)行時(shí)數(shù)據(jù)區(qū)域

? ? ? ? 2.2.1 程序計(jì)數(shù)器
? ??????程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lá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ū)域。
????????2.2.2 Java虛擬機(jī)棧
? ??????與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程。
? ??????局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference類型,它不等同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是指向一個(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。
????????其中64位長(zhǎng)度的long和double類型的數(shù)據(jù)會(huì)占用2個(gè)局部變量空間(Slot),其余的數(shù)據(jù)類型只占用1個(gè)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。
????????在Java虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可動(dòng)態(tài)擴(kuò)展,只不過(guò)Java虛擬機(jī)規(guī)范中也允許固定長(zhǎng)度的虛擬機(jī)棧),如果擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出OutOfMemoryError異常。
? ??????2.2.3 本地方法棧
? ??????本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過(guò)是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)。在虛擬機(jī)規(guī)范中對(duì)本地方法棧中方法使用的語(yǔ)言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它。甚至有的虛擬機(jī)(譬如Sun HotSpot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會(huì)拋出StackOverflowError和OutOfMemoryError異常。
? ??????2.2.4 Java堆
? ??????對(duì)于大多數(shù)應(yīng)用來(lái)說(shuō),Java堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。這一點(diǎn)在Java虛擬機(jī)規(guī)范中的描述是:所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配。
? ??????Java堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱做“GC堆”。從內(nèi)存回收的角度來(lái)看,由于現(xiàn)在收集器基本都采用分代收集算法,所以Java堆中還可以細(xì)分為:新生代和老年代;再細(xì)致一點(diǎn)的有Eden空間、From Survivor空間、To Survivor空間等。
? ??????從內(nèi)存分配的角度來(lái)看,線程共享的Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)。
? ??????根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,就像我們的磁盤空間一樣。在實(shí)現(xiàn)時(shí),既可以實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,不過(guò)當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來(lái)實(shí)現(xiàn)的(通過(guò)-Xmx和-Xms控制)。如果在堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無(wú)法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemoryError異常。
? ? ? ? 2.2.5? ? 方法區(qū)
? ??????方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。雖然Java虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫做Non-Heap(非堆),目的應(yīng)該是與Java堆區(qū)分開來(lái)。
? ??????對(duì)于習(xí)慣在HotSpot虛擬機(jī)上開發(fā)、部署程序的開發(fā)者來(lái)說(shuō),很多人都更愿意把方法區(qū)稱為“永久代”,本質(zhì)上兩者并不等價(jià),僅僅是因?yàn)镠otSpot虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)選擇把GC分代收集擴(kuò)展至方法區(qū),或者說(shuō)使用永久代來(lái)實(shí)現(xiàn)方法區(qū)而已,這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分內(nèi)存,能夠省去專門為方法區(qū)編寫內(nèi)存管理代碼的工作。對(duì)于其他虛擬機(jī)(如BEA JRockit、IBM J9等)來(lái)說(shuō)是不存在永久代的概念的。原則上,如何實(shí)現(xiàn)方法區(qū)屬于虛擬機(jī)實(shí)現(xiàn)細(xì)節(jié),不受虛擬機(jī)規(guī)范約束,但使用永久代來(lái)實(shí)現(xiàn)方法區(qū),現(xiàn)在看來(lái)并不是一個(gè)好主意,因?yàn)檫@樣更容易遇到內(nèi)存溢出問(wèn)題。
? ??????Java虛擬機(jī)規(guī)范對(duì)方法區(qū)的限制非常寬松,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,還可以選擇不實(shí)現(xiàn)垃圾收集。相對(duì)而言,垃圾收集行為在這個(gè)區(qū)域是比較少出現(xiàn)的,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就如永久代的名字一樣“永久”存在了。這區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載,一般來(lái)說(shuō),這個(gè)區(qū)域的回收“成績(jī)”比較難以令人滿意,尤其是類型的卸載,條件相當(dāng)苛刻,但是這部分區(qū)域的回收確實(shí)是必要的。在Sun公司的BUG列表中,曾出現(xiàn)過(guò)的若干個(gè)嚴(yán)重的BUG就是由于低版本的HotSpot虛擬機(jī)對(duì)此區(qū)域未完全回收而導(dǎo)致內(nèi)存泄漏。根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,當(dāng)方法區(qū)無(wú)法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemoryError異常。
? ? ????2.3????對(duì)象的創(chuàng)建
? ??????虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化過(guò)。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過(guò)程。
? ??????在類加載檢查通過(guò)后,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類加載完成后便可完全確定(如何確定將在2.3.2節(jié)中介紹),為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來(lái)。假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過(guò)的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)。如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒有辦法簡(jiǎn)單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱為“空閑列表”。
? ? ? ? 選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過(guò)程的收集器時(shí),系統(tǒng)采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時(shí),通常采用空閑列表。
? ??????除如何劃分可用空間之外,還有另外一個(gè)需要考慮的問(wèn)題是對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使是僅僅修改一個(gè)指針?biāo)赶虻奈恢茫诓l(fā)情況下也并不是線程安全的,可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒來(lái)得及修改,對(duì)象B又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存的情況。解決這個(gè)問(wèn)題有兩種方案,一種是對(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,可以通過(guò)-XX:+/-UseTLAB參數(shù)來(lái)設(shè)定。
????????內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭),如果使用TLAB,這一工作過(guò)程也可以提前至TLAB分配時(shí)進(jìn)行。這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問(wèn)到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。
? ? ? ? 接下來(lái),虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)之中。根據(jù)虛擬機(jī)當(dāng)前的運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式。
? ??????在上面工作都完成之后,從虛擬機(jī)的視角來(lái)看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了,但從Java程序的視角來(lái)看,對(duì)象創(chuàng)建才剛剛開始——<init>方法還沒有執(zhí)行,所有的字段都還為零。所以,一般來(lái)說(shuō)(由字節(jié)碼中是否跟隨invokespecial指令所決定),執(zhí)行new指令之后會(huì)接著執(zhí)行<init>方法,把對(duì)象按照程序員的意愿進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全產(chǎn)生出來(lái)。
? ??????在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。
? ??????HotSpot虛擬機(jī)的對(duì)象頭包括兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別為32bit和64bit,官方稱它為“Mark Word”。對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多,其實(shí)已經(jīng)超出了32位、64位Bitmap結(jié)構(gòu)所能記錄的限度,但是對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,考慮到虛擬機(jī)的空間效率,Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息。


????????接下來(lái)的實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容。無(wú)論是從父類繼承下來(lái)的,還是在子類中定義的,都需要記錄起來(lái)。這部分的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù)(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛擬機(jī)默認(rèn)的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的字段總是被分配到一起。在滿足這個(gè)前提條件的情況下,在父類中定義的變量會(huì)出現(xiàn)在子類之前。如果CompactFields參數(shù)值為true(默認(rèn)為true),那么子類之中較窄的變量也可能會(huì)插入到父類變量的空隙之中。
????????第三部分對(duì)齊填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話說(shuō),就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍。而對(duì)象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊時(shí),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。
? ? ? ? 2.3.3????對(duì)象的訪問(wèn)定位
? ??????建立對(duì)象是為了使用對(duì)象,我們的Java程序需要通過(guò)棧上的reference數(shù)據(jù)來(lái)操作堆上的具體對(duì)象。由于reference類型在Java虛擬機(jī)規(guī)范中只規(guī)定了一個(gè)指向?qū)ο蟮囊?,并沒有定義這個(gè)引用應(yīng)該通過(guò)何種方式去定位、訪問(wèn)堆中的對(duì)象的具體位置,所以對(duì)象訪問(wèn)方式也是取決于虛擬機(jī)實(shí)現(xiàn)而定的。目前主流的訪問(wèn)方式有使用句柄和直接指針兩種。
????????如果使用句柄訪問(wèn)的話,那么Java堆中將會(huì)劃分出一塊內(nèi)存來(lái)作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息,如圖2-2所示。

????????如果使用直接指針訪問(wèn),那么Java堆對(duì)象的布局中就必須考慮如何放置訪問(wèn)類型數(shù)據(jù)的相關(guān)信息,而reference中存儲(chǔ)的直接就是對(duì)象地址,如圖2-3所示。

? ??????這兩種對(duì)象訪問(wèn)方式各有優(yōu)勢(shì),使用句柄來(lái)訪問(wèn)的最大好處就是reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要修改。
? ??????使用直接指針訪問(wèn)方式的最大好處就是速度更快,它節(jié)省了一次指針定位的時(shí)間開銷,由于對(duì)象的訪問(wèn)在Java中非常頻繁,因此這類開銷積少成多后也是一項(xiàng)非??捎^的執(zhí)行成本。就本書討論的主要虛擬機(jī)Sun HotSpot而言,它是使用第二種方式進(jìn)行對(duì)象訪問(wèn)的,但從整個(gè)軟件開發(fā)的范圍來(lái)看,各種語(yǔ)言和框架使用句柄來(lái)訪問(wèn)的情況也十分常見。
? ? ? ? 2.4?OutOfMemoryError異常

? ??????DirectMemory容量可通過(guò)-XX:MaxDirectMemorySize指定,如果不指定,則默認(rèn)與Java堆最大值(-Xmx指定)一樣,代碼清單2-9越過(guò)了DirectByteBuffer類,直接通過(guò)反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配(Unsafe類的getUnsafe()方法限制了只有引導(dǎo)類加載器才會(huì)返回實(shí)例,也就是設(shè)計(jì)者希望只有rt.jar中的類才能使用Unsafe的功能)。因?yàn)椋m然使用DirectByteBuffer分配內(nèi)存也會(huì)拋出內(nèi)存溢出異常,但它拋出異常時(shí)并沒有真正向操作系統(tǒng)申請(qǐng)分配內(nèi)存,而是通過(guò)計(jì)算得知內(nèi)存無(wú)法分配,于是手動(dòng)拋出異常,真正申請(qǐng)分配內(nèi)存的方法是unsafe.allocateMemory()。
????3、垃圾收集器與內(nèi)存分配策略
????????Java與C++之間有一堵由內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)所圍成的“高墻”,墻外面的人想進(jìn)去,墻里面的人卻想出來(lái)。
? ??????在主流的商用程序語(yǔ)言(Java、C#,甚至包括前面提到的古老的Lisp)的主流實(shí)現(xiàn)中,都是稱通過(guò)可達(dá)性分析(Reachability Analysis)來(lái)判定對(duì)象是否存活的。這個(gè)算法的基本思路就是通過(guò)一系列的稱為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過(guò)的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連(用圖論的話來(lái)說(shuō),就是從GC Roots到這個(gè)對(duì)象不可達(dá))時(shí),則證明此對(duì)象是不可用的。如圖3-1所示,對(duì)象object 5、object 6、object 7雖然互相有關(guān)聯(lián),但是它們到GC Roots是不可達(dá)的,所以它們將會(huì)被判定為是可回收的對(duì)象。

? ??????在Java語(yǔ)言中,可作為GC Roots的對(duì)象包括下面幾種:
????????虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
? ??????方法區(qū)中類靜態(tài)屬性引用的對(duì)象。
? ??????方法區(qū)中常量引用的對(duì)象。
? ??????本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象。
????????在JDK 1.2之后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(WeakReference)、虛引用(Phantom Reference)4種,這4種引用強(qiáng)度依次逐漸減弱。
? ??????強(qiáng)引用就是指在程序代碼之中普遍存在的,類似“Object obj=new Object()”這類的引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
? ??????軟引用是用來(lái)描述一些還有用但并非必需的對(duì)象。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。在JDK 1.2之后,提供了SoftReference類來(lái)實(shí)現(xiàn)軟引用。
? ??????弱引用也是用來(lái)描述非必需對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。在JDK 1.2之后,提供了WeakReference類來(lái)實(shí)現(xiàn)弱引用。
? ??????虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。在JDK 1.2之后,提供了PhantomReference類來(lái)實(shí)現(xiàn)虛引用。
? ? ? ? 3.2.5回收方法區(qū)
? ??????很多人認(rèn)為方法區(qū)(或者HotSpot虛擬機(jī)中的永久代)是沒有垃圾收集的,Java虛擬機(jī)規(guī)范中確實(shí)說(shuō)過(guò)可以不要求虛擬機(jī)在方法區(qū)實(shí)現(xiàn)垃圾收集,而且在方法區(qū)中進(jìn)行垃圾收集的“性價(jià)比”一般比較低:在堆中,尤其是在新生代中,常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠(yuǎn)低于此。
? ??????永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無(wú)用的類。回收廢棄常量與回收J(rèn)ava堆中的對(duì)象非常類似。
? ??????判定一個(gè)常量是否是“廢棄常量”比較簡(jiǎn)單,而要判定一個(gè)類是否是“無(wú)用的類”的條件則相對(duì)苛刻許多。類需要同時(shí)滿足下面3個(gè)條件才能算是“無(wú)用的類”:
????????????????????該類所有的實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實(shí)例。
????????????????????加載該類的ClassLoader已經(jīng)被回收。
????????????????????該類對(duì)應(yīng)的java.lang.Class對(duì)象沒有在任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類的方法。
????????虛擬機(jī)可以對(duì)滿足上述3個(gè)條件的無(wú)用類進(jìn)行回收,這里說(shuō)的僅僅是“可以”,而并不是和對(duì)象一樣,不使用了就必然會(huì)回收。是否對(duì)類進(jìn)行回收,HotSpot虛擬機(jī)提供了-Xnoclassgc參數(shù)進(jìn)行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機(jī)中使用,-XX:+TraceClassUnLoading參數(shù)需要FastDebug版的虛擬機(jī)支持。
????????在大量使用反射、動(dòng)態(tài)代理、CGLib等ByteCode框架、動(dòng)態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場(chǎng)景都需要虛擬機(jī)具備類卸載的功能,以保證永久代不會(huì)溢出。
? ??????由于垃圾收集算法的實(shí)現(xiàn)涉及大量的程序細(xì)節(jié),而且各個(gè)平臺(tái)的虛擬機(jī)操作內(nèi)存的方法又各不相同,因此本節(jié)不打算過(guò)多地討論算法的實(shí)現(xiàn),只是介紹幾種算法的思想及其發(fā)展過(guò)程。
????????最基礎(chǔ)的收集算法是“標(biāo)記-清除”(Mark-Sweep)算法,如同它的名字一樣,算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象,它的標(biāo)記過(guò)程其實(shí)在前一節(jié)講述對(duì)象標(biāo)記判定時(shí)已經(jīng)介紹過(guò)了。之所以說(shuō)它是最基礎(chǔ)的收集算法,是因?yàn)楹罄m(xù)的收集算法都是基于這種思路并對(duì)其不足進(jìn)行改進(jìn)而得到的。它的主要不足有兩個(gè):一個(gè)是效率問(wèn)題,標(biāo)記和清除兩個(gè)過(guò)程的效率都不高;另一個(gè)是空間問(wèn)題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。標(biāo)記—清除算法的執(zhí)行過(guò)程如圖3-2所示。

????????為了解決效率問(wèn)題,一種稱為“復(fù)制”(Copying)的收集算法出現(xiàn)了,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。這樣使得每次都是對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為了原來(lái)的一半,未免太高了一點(diǎn)。復(fù)制算法的執(zhí)行過(guò)程如圖3-3所示。

????????現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來(lái)回收新生代,IBM公司的專門研究表明,新生代中的對(duì)象98%是“朝生夕死”的,所以并不需要按照1:1的比例來(lái)劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor[1]。當(dāng)回收時(shí),將Eden和Survivor中還存活著的對(duì)象一次性地復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過(guò)的Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(80%+10%),只有10%的內(nèi)存會(huì)被“浪費(fèi)”。當(dāng)然,98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于10%的對(duì)象存活,當(dāng)Survivor空間不夠用時(shí),需要依賴其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保(Handle Promotion)。
????????內(nèi)存的分配擔(dān)保就好比我們?nèi)ャy行借款,如果我們信譽(yù)很好,在98%的情況下都能按時(shí)償還,于是銀行可能會(huì)默認(rèn)我們下一次也能按時(shí)按量地償還貸款,只需要有一個(gè)擔(dān)保人能保證如果我不能還款時(shí),可以從他的賬戶扣錢,那銀行就認(rèn)為沒有風(fēng)險(xiǎn)了。內(nèi)存的分配擔(dān)保也一樣,如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來(lái)的存活對(duì)象時(shí),這些對(duì)象將直接通過(guò)分配擔(dān)保機(jī)制進(jìn)入老年代。關(guān)于對(duì)新生代進(jìn)行分配擔(dān)保的內(nèi)容,在本章稍后在講解垃圾收集器執(zhí)行規(guī)則時(shí)還會(huì)再詳細(xì)講解。
? ??????內(nèi)存的分配擔(dān)保就好比我們?nèi)ャy行借款,如果我們信譽(yù)很好,在98%的情況下都能按時(shí)償還,于是銀行可能會(huì)默認(rèn)我們下一次也能按時(shí)按量地償還貸款,只需要有一個(gè)擔(dān)保人能保證如果我不能還款時(shí),可以從他的賬戶扣錢,那銀行就認(rèn)為沒有風(fēng)險(xiǎn)了。內(nèi)存的分配擔(dān)保也一樣,如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來(lái)的存活對(duì)象時(shí),這些對(duì)象將直接通過(guò)分配擔(dān)保機(jī)制進(jìn)入老年代。關(guān)于對(duì)新生代進(jìn)行分配擔(dān)保的內(nèi)容,在本章稍后在講解垃圾收集器執(zhí)行規(guī)則時(shí)還會(huì)再詳細(xì)講解。
????????3.3.3 標(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)記-整理”(Mark-Compact)算法,標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存,“標(biāo)記-整理”算法的示意圖如圖3-4所示。

? ??????3.3.4 分代收集算法
????????當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”(Generational Collection)算法,這種算法并沒有什么新的思想,只是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴?。在新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記—清理”或者“標(biāo)記—整理”算法來(lái)進(jìn)行回收。
? ? ?3.4 HotSpot的算法實(shí)現(xiàn)
????????3.2 節(jié)和3.3節(jié)從理論上介紹了對(duì)象存活判定算法和垃圾收集算法,而在HotSpot虛擬機(jī)上實(shí)現(xiàn)這些算法時(shí),必須對(duì)算法的執(zhí)行效率有嚴(yán)格的考量,才能保證虛擬機(jī)高效運(yùn)行。
? ??????從可達(dá)性分析中從GC Roots節(jié)點(diǎn)找引用鏈這個(gè)操作為例,可作為GC Roots的節(jié)點(diǎn)主要在全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中,現(xiàn)在很多應(yīng)用僅僅方法區(qū)就有數(shù)百兆,如果要逐個(gè)檢查這里面的引用,那么必然會(huì)消耗很多時(shí)間。
????????另外,可達(dá)性分析對(duì)執(zhí)行時(shí)間的敏感還體現(xiàn)在GC停頓上,因?yàn)檫@項(xiàng)分析工作必須在一個(gè)能確保一致性的快照中進(jìn)行——這里“一致性”的意思是指在整個(gè)分析期間整個(gè)執(zhí)行系統(tǒng)看起來(lái)就像被凍結(jié)在某個(gè)時(shí)間點(diǎn)上,不可以出現(xiàn)分析過(guò)程中對(duì)象引用關(guān)系還在不斷變化的情況,該點(diǎn)不滿足的話分析結(jié)果準(zhǔn)確性就無(wú)法得到保證。這點(diǎn)是導(dǎo)致GC進(jìn)行時(shí)必須停頓所有Java執(zhí)行線程(Sun將這件事情稱為“Stop The World”)的其中一個(gè)重要原因,即使是在號(hào)稱(幾乎)不會(huì)發(fā)生停頓的CMS收集器中,枚舉根節(jié)點(diǎn)時(shí)也是必須要停頓的。
????????由于目前的主流Java虛擬機(jī)使用的都是準(zhǔn)確式GC(這個(gè)概念在第1章介紹Exact VM對(duì)Classic VM的改進(jìn)時(shí)講過(guò)),所以當(dāng)執(zhí)行系統(tǒng)停頓下來(lái)后,并不需要一個(gè)不漏地檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機(jī)應(yīng)當(dāng)是有辦法直接得知哪些地方存放著對(duì)象引用。在HotSpot的實(shí)現(xiàn)中,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來(lái)達(dá)到這個(gè)目的的,在類加載完成的時(shí)候,HotSpot就把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來(lái),在JIT編譯過(guò)程中,也會(huì)在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時(shí)就可以直接得知這些信息了。下面的代碼清單3-3是HotSpot Client VM生成的一段String.hashCode()方法的本地代碼,可以看到在0x026eb7a9處的call指令有OopMap記錄,它指明了EBX寄存器和棧中偏移量為16的內(nèi)存區(qū)域中各有一個(gè)普通對(duì)象指針(Ordinary Object Pointer)的引用,有效范圍為從call指令開始直到0x026eb730(指令流的起始位置)+142(OopMap記錄的偏移量)=0x026eb7be,即hlt指令為止。

????????在OopMap的協(xié)助下,HotSpot可以快速且準(zhǔn)確地完成GC Roots枚舉,但一個(gè)很現(xiàn)實(shí)的問(wèn)題隨之而來(lái):可能導(dǎo)致引用關(guān)系變化,或者說(shuō)OopMap內(nèi)容變化的指令非常多,如果為每一條指令都生成對(duì)應(yīng)的OopMap,那將會(huì)需要大量的額外空間,這樣GC的空間成本將會(huì)變得很高。
????????實(shí)際上,HotSpot也的確沒有為每條指令都生成OopMap,前面已經(jīng)提到,只是在“特定的位置”記錄了這些信息,這些位置稱為安全點(diǎn)(Safepoint),長(zhǎng)時(shí)間執(zhí)行”的最明顯特征就是指令序列復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,所以具有這些功能的指令才會(huì)產(chǎn)生Safepoint。
? ??????對(duì)于Sefepoint,另一個(gè)需要考慮的問(wèn)題是如何在GC發(fā)生時(shí)讓所有線程(這里不包括執(zhí)行JNI調(diào)用的線程)都“跑”到最近的安全點(diǎn)上再停頓下來(lái)。這里有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動(dòng)式中斷(Voluntary Suspension),其中搶先式中斷不需要線程的執(zhí)行代碼主動(dòng)去配合,在GC發(fā)生時(shí),首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點(diǎn)上,就恢復(fù)線程,讓它“跑”到安全點(diǎn)上?,F(xiàn)在幾乎沒有虛擬機(jī)實(shí)現(xiàn)采用搶先式中斷來(lái)暫停線程從而響應(yīng)GC事件。
? ??????而主動(dòng)式中斷的思想是當(dāng)GC需要中斷線程的時(shí)候,不直接對(duì)線程操作,僅僅簡(jiǎn)單地設(shè)置一個(gè)標(biāo)志,各個(gè)線程執(zhí)行時(shí)主動(dòng)去輪詢這個(gè)標(biāo)志,發(fā)現(xiàn)中斷標(biāo)志為真時(shí)就自己中斷掛起。輪詢標(biāo)志的地方和安全點(diǎn)是重合的,另外再加上創(chuàng)建對(duì)象需要分配內(nèi)存的地方。下面代碼清單3-4中的test指令是HotSpot生成的輪詢指令,當(dāng)需要暫停線程時(shí),虛擬機(jī)把0x160100的內(nèi)存頁(yè)設(shè)置為不可讀,線程執(zhí)行到test指令時(shí)就會(huì)產(chǎn)生一個(gè)自陷異常信號(hào),在預(yù)先注冊(cè)的異常處理器中暫停線程實(shí)現(xiàn)等待,這樣一條匯編指令便完成安全點(diǎn)輪詢和觸發(fā)線程中斷。

????????3.4.3 安全區(qū)域
????????使用Safepoint似乎已經(jīng)完美地解決了如何進(jìn)入GC的問(wèn)題,但實(shí)際情況卻并不一定。Safepoint機(jī)制保證了程序執(zhí)行時(shí),在不太長(zhǎng)的時(shí)間內(nèi)就會(huì)遇到可進(jìn)入GC的Safepoint。但是,程序“不執(zhí)行”的時(shí)候呢?所謂的程序不執(zhí)行就是沒有分配CPU時(shí)間,典型的例子就是線程處于Sleep狀態(tài)或者Blocked狀態(tài),這時(shí)候線程無(wú)法響應(yīng)JVM的中斷請(qǐng)求,“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時(shí)間。對(duì)于這種情況,就需要安全區(qū)域(Safe Region)來(lái)解決。
????????安全區(qū)域是指在一段代碼片段之中,引用關(guān)系不會(huì)發(fā)生變化。在這個(gè)區(qū)域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被擴(kuò)展了的Safepoint。
????????在線程執(zhí)行到Safe Region中的代碼時(shí),首先標(biāo)識(shí)自己已經(jīng)進(jìn)入了Safe Region,那樣,當(dāng)在這段時(shí)間里JVM要發(fā)起GC時(shí),就不用管標(biāo)識(shí)自己為Safe Region狀態(tài)的線程了。在線程要離開Safe Region時(shí),它要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉(或者是整個(gè)GC過(guò)程),如果完成了,那線程就繼續(xù)執(zhí)行,否則它就必須等待直到收到可以安全離開Safe Region的信號(hào)為止。
????????3.5 垃圾收集器
????????如果說(shuō)收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實(shí)現(xiàn)。Java虛擬機(jī)規(guī)范中對(duì)垃圾收集器應(yīng)該如何實(shí)現(xiàn)并沒有任何規(guī)定,因此不同的廠商、不同版本的虛擬機(jī)所提供的垃圾收集器都可能會(huì)有很大差別,并且一般都會(huì)提供參數(shù)供用戶根據(jù)自己的應(yīng)用特點(diǎn)和要求組合出各個(gè)年代所使用的收集器。這里討論的收集器基于JDK 1.7 Update 14之后的HotSpot虛擬機(jī)(在這個(gè)版本中正式提供了商用的G1收集器,之前G1仍處于實(shí)驗(yàn)狀態(tài)),這個(gè)虛擬機(jī)包含的所有收集器如圖3-5所示。

????????3.5.1 Serial收集器.
????????Serial收集器是最基本、發(fā)展歷史最悠久的收集器,曾經(jīng)(在JDK 1.3.1之前)是虛擬機(jī)新生代收集的唯一選擇。大家看名字就會(huì)知道,這個(gè)收集器是一個(gè)單線程的收集器,但它的“單線程”的意義并不僅僅說(shuō)明它只會(huì)使用一個(gè)CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線程,直到它收集結(jié)束。
? ??????

????????Serial收集器依然是虛擬機(jī)運(yùn)行在Client模式下的默認(rèn)新生代收集器。它也有著優(yōu)于其他收集器的地方:簡(jiǎn)單而高效(與其他收集器的單線程比),對(duì)于限定單個(gè)CPU的環(huán)境來(lái)說(shuō),Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。所以,Serial收集器對(duì)于運(yùn)行在Client模式下的虛擬機(jī)來(lái)說(shuō)是一個(gè)很好的選擇。
? ??????ParNew收集器其實(shí)就是Serial收集器的多線程版本,除了使用多條線程進(jìn)行垃圾收集之外,其余行為包括Serial收集器可用的所有控制參數(shù)(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對(duì)象分配規(guī)則、回收策略等都與Serial收集器完全一樣,在實(shí)現(xiàn)上,這兩種收集器也共用了相當(dāng)多的代碼。

????????ParNew收集器除了多線程收集之外,其他與Serial收集器相比并沒有太多創(chuàng)新之處,但它卻是許多運(yùn)行在Server模式下的虛擬機(jī)中首選的新生代收集器,其中有一個(gè)與性能無(wú)關(guān)但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。
????????不幸的是,CMS作為老年代的收集器,卻無(wú)法與JDK 1.4.0中已經(jīng)存在的新生代收集器Parallel Scavenge配合工作[1],所以在JDK 1.5中使用CMS來(lái)收集老年代的時(shí)候,新生代只能選擇ParNew或者Serial收集器中的一個(gè)。ParNew收集器也是使用-XX:+UseConcMarkSweepGC選項(xiàng)后的默認(rèn)新生代收集器,也可以使用-XX:+UseParNewGC選項(xiàng)來(lái)強(qiáng)制指定它。
????????ParNew收集器在單CPU的環(huán)境中絕對(duì)不會(huì)有比Serial收集器更好的效果,甚至由于存在線程交互的開銷,該收集器在通過(guò)超線程技術(shù)實(shí)現(xiàn)的兩個(gè)CPU的環(huán)境中都不能百分之百地保證可以超越Serial收集器。當(dāng)然,隨著可以使用的CPU的數(shù)量的增加,它對(duì)于GC時(shí)系統(tǒng)資源的有效利用還是很有好處的。它默認(rèn)開啟的收集線程數(shù)與CPU的數(shù)量相同,在CPU非常多(譬如32個(gè),現(xiàn)在CPU動(dòng)輒就4核加超線程,服務(wù)器超過(guò)32個(gè)邏輯CPU的情況越來(lái)越多了)的環(huán)境下,可以使用-XX:ParallelGCThreads參數(shù)來(lái)限制垃圾收集的線程數(shù)。
? ??????注意 從ParNew收集器開始,后面還會(huì)接觸到幾款并發(fā)和并行的收集器。
????????●并行(Parallel):指多條垃圾收集線程并行工作,但此時(shí)用戶線程仍然處于等待狀態(tài)。
????????●并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時(shí)執(zhí)行(但不一定是并行的,可能會(huì)交替執(zhí)行),用戶程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行于另一個(gè)CPU上。
? ??????3.5.3 Parallel Scavenge收集器
? ??????Parallel Scavenge收集器的特點(diǎn)是它的關(guān)注點(diǎn)與其他收集器不同,CMS等收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí)用戶線程的停頓時(shí)間,而Parallel Scavenge收集器的目標(biāo)則是達(dá)到一個(gè)可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值,即吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收集時(shí)間),虛擬機(jī)總共運(yùn)行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
????????停頓時(shí)間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶體驗(yàn),而高吞吐量則可以高效率地利用CPU時(shí)間,盡快完成程序的運(yùn)算任務(wù),主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。
? ??????Parallel Scavenge收集器提供了兩個(gè)參數(shù)用于精確控制吞吐量,分別是控制最大垃圾收集停頓時(shí)間的-XX:MaxGCPauseMillis參數(shù)以及直接設(shè)置吞吐量大小的-XX:GCTimeRatio參數(shù)。
? ??????MaxGCPauseMillis參數(shù)允許的值是一個(gè)大于0的毫秒數(shù),收集器將盡可能地保證內(nèi)存回收花費(fèi)的時(shí)間不超過(guò)設(shè)定值。
????????GCTimeRatio參數(shù)的值應(yīng)當(dāng)是一個(gè)大于0且小于100的整數(shù),也就是垃圾收集時(shí)間占總時(shí)間的比率,相當(dāng)于是吞吐量的倒數(shù)。
????????由于與吞吐量關(guān)系密切,Parallel Scavenge收集器也經(jīng)常稱為“吞吐量?jī)?yōu)先”收集器。除上述兩個(gè)參數(shù)之外,Parallel Scavenge收集器還有一個(gè)參數(shù)-XX:+UseAdaptiveSizePolicy值得關(guān)注。這是一個(gè)開關(guān)參數(shù),當(dāng)這個(gè)參數(shù)打開之后,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對(duì)象年齡(-XX:PretenureSizeThreshold)等細(xì)節(jié)參數(shù)了,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或者最大的吞吐量,這種調(diào)節(jié)方式稱為GC自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics)自適應(yīng)調(diào)節(jié)策略也是Parallel Scavenge收集器與ParNew收集器的一個(gè)重要區(qū)別。
????????3.5.4 Serial Old收集器
????????Serial Old是Serial收集器的老年代版本,它同樣是一個(gè)單線程收集器,使用“標(biāo)記-整理”算法。這個(gè)收集器的主要意義也是在于給Client模式下的虛擬機(jī)使用。如果在Server模式下,那么它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用[1],另一種用途就是作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時(shí)使用。這兩點(diǎn)都將在后面的內(nèi)容中詳細(xì)講解。Serial Old收集器的工作過(guò)程如圖3-8所示。

????????3.5.5 Parallel Old收集器
????????Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標(biāo)記-整理”算法。這個(gè)收集器是在JDK 1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處于比較尷尬的狀態(tài)。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無(wú)選擇(還記得上面說(shuō)過(guò)Parallel Scavenge收集器無(wú)法與CMS收集器配合工作嗎?)。由于老年代Serial Old收集器在服務(wù)端應(yīng)用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整體應(yīng)用上獲得吞吐量最大化的效果,由于單線程的老年代收集中無(wú)法充分利用服務(wù)器多CPU的處理能力,在老年代很大而且硬件比較高級(jí)的環(huán)境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”。
? ??????直到Parallel Old收集器出現(xiàn)后,“吞吐量?jī)?yōu)先”收集器終于有了比較名副其實(shí)的應(yīng)用組合,在注重吞吐量以及CPU資源敏感的場(chǎng)合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作過(guò)程如圖3-9所示。

????????3.5.6 CMS收集器
????????CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間最短,以給用戶帶來(lái)較好的體驗(yàn)。CMS收集器就非常符合這類應(yīng)用的需求。
????????從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“標(biāo)記—清除”算法實(shí)現(xiàn)的,它的運(yùn)作過(guò)程相對(duì)于前面幾種收集器來(lái)說(shuō)更復(fù)雜一些,整個(gè)過(guò)程分為4個(gè)步驟,包括:
????????初始標(biāo)記(CMS initial mark)
? ??????并發(fā)標(biāo)記(CMS concurrent mark)
????????重新標(biāo)記(CMS remark)
????????并發(fā)清除(CMS concurrent sweep)
????????其中,初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要“Stop The World”。初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快,并發(fā)標(biāo)記階段就是進(jìn)行GC RootsTracing的過(guò)程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段稍長(zhǎng)一些,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。
????????由于整個(gè)過(guò)程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過(guò)程收集器線程都可以與用戶線程一起工作,所以,從總體上來(lái)說(shuō),CMS收集器的內(nèi)存回收過(guò)程是與用戶線程一起并發(fā)執(zhí)行的。通過(guò)圖3-10可以比較清楚地看到CMS收集器的運(yùn)作步驟中并發(fā)和需要停頓的時(shí)間。

????????CMS是一款優(yōu)秀的收集器,它的主要優(yōu)點(diǎn)在名字上已經(jīng)體現(xiàn)出來(lái)了:并發(fā)收集、低停頓,Sun公司的一些官方文檔中也稱之為并發(fā)低停頓收集器(Concurrent Low Pause Collector)。但是CMS還遠(yuǎn)達(dá)不到完美的程度,它有以下3個(gè)明顯的缺點(diǎn):
????????CMS收集器對(duì)CPU資源非常敏感。在并發(fā)階段,它雖然不會(huì)導(dǎo)致用戶線程停頓,但是會(huì)因?yàn)檎加昧艘徊糠志€程(或者說(shuō)CPU資源)而導(dǎo)致應(yīng)用程序變慢,總吞吐量會(huì)降低。CMS默認(rèn)啟動(dòng)的回收線程數(shù)是(CPU數(shù)量+3)/4,也就是當(dāng)CPU在4個(gè)以上時(shí),并發(fā)回收時(shí)垃圾收集線程不少于25%的CPU資源,并且隨著CPU數(shù)量的增加而下降。為了應(yīng)付這種情況,虛擬機(jī)提供了一種稱為“增量式并發(fā)收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器變種,所做的事情和單CPU年代PC機(jī)操作系統(tǒng)使用搶占式來(lái)模擬多任務(wù)機(jī)制的思想一樣,就是在并發(fā)標(biāo)記、清理的時(shí)候讓GC線程、用戶線程交替運(yùn)行,盡量減少GC線程的獨(dú)占資源的時(shí)間,這樣整個(gè)垃圾收集的過(guò)程會(huì)更長(zhǎng),但對(duì)用戶程序的影響就會(huì)顯得少一些,也就是速度下降沒有那么明顯。實(shí)踐證明,增量時(shí)的CMS收集器效果很一般,在目前版本中,i-CMS已經(jīng)被聲明為“deprecated”,即不再提倡用戶使用。
????????CMS收集器無(wú)法處理浮動(dòng)垃圾(Floating Garbage),可能出現(xiàn)“Concurrent Mode Failure”失敗而導(dǎo)致另一次Full GC的產(chǎn)生。由于CMS并發(fā)清理階段用戶線程還在運(yùn)行著,伴隨程序運(yùn)行自然就還會(huì)有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過(guò)程之后,CMS無(wú)法在當(dāng)次收集中處理掉它們,只好留待下一次GC時(shí)再清理掉。這一部分垃圾就稱為“浮動(dòng)垃圾”。也是由于在垃圾收集階段用戶線程還需要運(yùn)行,那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用。在JDK 1.5的默認(rèn)設(shè)置下,CMS收集器當(dāng)老年代使用了68%的空間后就會(huì)被激活,這是一個(gè)偏保守的設(shè)置,如果在應(yīng)用中老年代增長(zhǎng)不是太快,可以適當(dāng)調(diào)高參數(shù)-XX:CMSInitiatingOccupancyFraction的值來(lái)提高觸發(fā)百分比,以便降低內(nèi)存回收次數(shù)從而獲取更好的性能,在JDK 1.6中,CMS收集器的啟動(dòng)閾值已經(jīng)提升至92%。要是CMS運(yùn)行期間預(yù)留的內(nèi)存無(wú)法滿足程序需要,就會(huì)出現(xiàn)一次“Concurrent Mode Failure”失敗,這時(shí)虛擬機(jī)將啟動(dòng)后備預(yù)案:臨時(shí)啟用Serial Old收集器來(lái)重新進(jìn)行老年代的垃圾收集,這樣停頓時(shí)間就很長(zhǎng)了。所以說(shuō)參數(shù)-XX:CM SInitiatingOccupancyFraction設(shè)置得太高很容易導(dǎo)致大量“Concurrent Mode Failure”失敗,性能反而降低。
還有最后一個(gè)缺點(diǎn),在本節(jié)開頭說(shuō)過(guò),CMS是一款基于“標(biāo)記—清除”算法實(shí)現(xiàn)的收集器,如果讀者對(duì)前面這種算法介紹還有印象的話,就可能想到這意味著收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生??臻g碎片過(guò)多時(shí),將會(huì)給大對(duì)象分配帶來(lái)很大麻煩,往往會(huì)出現(xiàn)老年代還有很大空間剩余,但是無(wú)法找到足夠大的連續(xù)空間來(lái)分配當(dāng)前對(duì)象,不得不提前觸發(fā)一次Full GC。為了解決這個(gè)問(wèn)題,CMS收集器提供了一個(gè)-XX:+UseCMSCompactAtFullCollection開關(guān)參數(shù)(默認(rèn)就是開啟的),用于在CMS收集器頂不住要進(jìn)行FullGC時(shí)開啟內(nèi)存碎片的合并整理過(guò)程,內(nèi)存整理的過(guò)程是無(wú)法并發(fā)的,空間碎片問(wèn)題沒有了,但停頓時(shí)間不得不變長(zhǎng)。虛擬機(jī)設(shè)計(jì)者還提供了另外一個(gè)參數(shù)-XX CMSFullGCsBeforeCompaction,這個(gè)參數(shù)是用于設(shè)置執(zhí)行多少次不壓縮的Full GC后,跟著來(lái)一次帶壓縮的(默認(rèn)值為0,表示每次進(jìn)入Full GC時(shí)都進(jìn)行碎片整理)。