第一章 走進(jìn)JAVA
java的優(yōu)點(diǎn):擺脫了硬件平臺(tái)的束縛,實(shí)現(xiàn)了“一次編寫(xiě),到處運(yùn)行”;它提供了一個(gè)相對(duì)安全的內(nèi)存管理和訪問(wèn)機(jī)制,避免了絕大部分的內(nèi)存泄漏和指針越界問(wèn)題;它實(shí)現(xiàn)了熱點(diǎn)代碼檢測(cè)和運(yùn)行時(shí)編譯及優(yōu)化,這使得java應(yīng)用能隨著運(yùn)行時(shí)間的增加而獲得更高的性能。
1 java虛擬機(jī)發(fā)展史
1.1 Sun Classic/Exact VM(jdk1.0~jdk1.2)
世界上第一款商用java虛擬機(jī),它只能使用純解釋器方式來(lái)執(zhí)行Java代碼,如果要使用JIT編譯器,就必須進(jìn)行外掛,但是外掛JIT后,JIT編譯器就會(huì)完全接管虛擬機(jī)的執(zhí)行系統(tǒng),解釋器就不工作了。由于Classic VM不能和JIT配合工作,這就意味著如果要使用編譯器執(zhí)行,編譯器就不得不對(duì)每一個(gè)方法、每一行代碼都進(jìn)行編譯,而無(wú)論他們執(zhí)行的頻率是否具有編譯的價(jià)值。基于程序響應(yīng)時(shí)間的壓力,這些編譯器不敢用編譯耗時(shí)稍高的優(yōu)化技術(shù) ,即使用了JIT,其執(zhí)行效率也和C++有很大差距。
1.2 Sun HotSpot VM
HotSpot VM繼承了Sun之前兩款商用虛擬機(jī)優(yōu)點(diǎn),HotSpot指是的它的熱點(diǎn)代碼探測(cè)技術(shù)。HotSpot VM的熱點(diǎn)代碼探測(cè)技術(shù)可以通過(guò)執(zhí)行計(jì)數(shù)器找出最有編譯價(jià)值的代碼,然后通知JIT編譯器以方法為單位進(jìn)行編譯,如果一個(gè)方法被頻繁調(diào)用,或方法中的有效循環(huán)次數(shù)很多,將會(huì)分別觸發(fā)標(biāo)準(zhǔn)編譯和OSR編譯動(dòng)作。通過(guò)編譯與解釋器恰當(dāng)?shù)膮f(xié)同工作,可以在最優(yōu)化的程序響應(yīng)時(shí)間和最佳執(zhí)行性能取得平衡,而且無(wú)需等待本地代碼輸出才能執(zhí)行程序,即時(shí)編譯的時(shí)間壓力也相對(duì)減小,這樣有助于更多的代碼優(yōu)化技術(shù)秘輸出質(zhì)量更高的本地代碼。
2 模塊化
它是解決應(yīng)用系統(tǒng)與技術(shù)平臺(tái)越來(lái)越復(fù)雜、越來(lái)越龐大 的一個(gè)重要途徑。
3 64位虛擬機(jī)
幾年之前,java程序運(yùn)行在64位虛擬機(jī)上需要付出比較大的額外代價(jià):首先是內(nèi)存問(wèn)題,由于指針和各種數(shù)據(jù)類(lèi)型對(duì)齊補(bǔ)白的原因,運(yùn)行于64位系統(tǒng)上的java應(yīng)用程序需要耗費(fèi)更多的內(nèi)存,通常要比32位系統(tǒng)額外增加10%~30%的內(nèi)存消耗;其次,64位虛擬機(jī)性能也全面落后于32位。
第二章 Java內(nèi)存區(qū)域與內(nèi)存溢出異常
2.1 概述
對(duì)于java程序員來(lái)說(shuō),在虛擬機(jī)自動(dòng)內(nèi)存管理機(jī)制下,不再需要為每一個(gè)new操作,去寫(xiě)配對(duì)的delete/free代碼,不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問(wèn)題,由java虛擬機(jī)管理內(nèi)存。但是也正是java程序員把內(nèi)存控制權(quán)交給了java虛擬機(jī),一旦出現(xiàn)內(nèi)存泄漏和溢出方面的問(wèn)題,不了解虛擬機(jī)是怎樣實(shí)用內(nèi)存的,那么排查錯(cuò)誤會(huì)是一項(xiàng)艱難的工作。
2.2運(yùn)行時(shí)數(shù)據(jù)區(qū)域

程序計(jì)數(shù)器(線程私有內(nèi)存區(qū)域): 程序計(jì)數(shù)器(Program Counter Register) 是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在虛擬機(jī)的概念模型里,字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條執(zhí)行字節(jié)碼指令。每條線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器,獨(dú)立存儲(chǔ),互不影響,因此這類(lèi)內(nèi)存區(qū)域被稱(chēng)為線程私有的內(nèi)存。 如果執(zhí)行的是java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址。如果是native方法,計(jì)數(shù)器為空。此內(nèi)存區(qū)域是唯一一個(gè)在java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域。
Java虛擬機(jī)棧(線程私有內(nèi)存區(qū)域):與程序計(jì)數(shù)器一樣,它也是線程私有的,它的生命周期和線程相同。同樣是線程私有,描述Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。一個(gè)方法對(duì)應(yīng)一個(gè)棧幀。在java虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常情況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果虛擬機(jī)可以動(dòng)態(tài)擴(kuò)展,如果擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,則會(huì)拋出OutOfMemoryError異常。
本地方法棧(Native Method Stack):和Java虛擬機(jī)棧很類(lèi)似,虛擬機(jī)棧為虛擬機(jī)執(zhí)行java方法,不同的是本地方法棧為Native方法服務(wù)。與虛擬機(jī)棧一樣,本地方法棧也會(huì)拋出StackOverflowError異常和OutOfMemoryError異常。
Java堆:是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。由所有線程共享,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。堆區(qū)唯一目的就是存放對(duì)象實(shí)例。堆中可細(xì)分為新生代和老年代,再細(xì)分可分為Eden空間、From Survivor空間、To Survivor空間。 堆無(wú)法擴(kuò)展時(shí),拋出OutOfMemoryError異常。
方法區(qū)(Non-Heap):所有線程共享,存儲(chǔ)已被虛擬機(jī)加載的類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。當(dāng)方法區(qū)無(wú)法滿(mǎn)足內(nèi)存分配需求時(shí),拋出OutOfMemoryError.
運(yùn)行時(shí)常量池:它是方法區(qū)的一部分,Class文件中除了有類(lèi)的版本、字段、方法、接口等描述信息外,還有一項(xiàng)是常量池(Const Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用。并非預(yù)置入Class文件中常量池的內(nèi)容才進(jìn)入方法運(yùn)行時(shí)常量池,運(yùn)行期間也可能將新的常量放入池中,這種特性被開(kāi)發(fā)人員利用得比較多的便是String類(lèi)的intern()方法。當(dāng)方法區(qū)無(wú)法滿(mǎn)足內(nèi)存分配需求時(shí),拋出OutOfMemoryError。
直接內(nèi)存:并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。JDK1.4加入了NIO,引入一種基于通道與緩沖區(qū)的I/O方式,它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。因?yàn)楸苊饬嗽贘ava堆和Native堆中來(lái)回復(fù)制數(shù)據(jù),提高了性能。 當(dāng)各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制,拋出OutOfMemoryError異常。
2.3 Hotspot虛擬機(jī)
2.3.1 對(duì)象的創(chuàng)建
new 對(duì)象-------》常量池定位類(lèi)引用---------》檢查類(lèi)引用和引用的類(lèi)是否被加載、解析和初始化---------》沒(méi)有,就先執(zhí)行類(lèi)加載。----》創(chuàng)建對(duì)象分配內(nèi)存
類(lèi)加載通過(guò)后----》虛擬機(jī)從java堆中分配內(nèi)存---》內(nèi)存規(guī)整(已分配的內(nèi)存和未分配的內(nèi)存區(qū)域由一個(gè)指針劃分開(kāi)):分配內(nèi)存時(shí)把該指針移動(dòng)一個(gè)對(duì)象大小的距離,成為指針碰撞;內(nèi)存不規(guī)整時(shí):從一個(gè)大的空閑列表區(qū)域分配內(nèi)存。
分配內(nèi)存時(shí)存在并發(fā)問(wèn)題----》兩種解決方法:1 對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理(實(shí)際上虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性);2 把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行,即每個(gè)線程在java堆中預(yù)先分配一小塊內(nèi)存,成為本地線程分配緩沖。
new指令執(zhí)行時(shí),對(duì)象的所有字段仍為零,需要等init方法執(zhí)行后,字段才會(huì)賦予新值。
2.3.2 對(duì)象的內(nèi)存區(qū)域
在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í)間戳等。另一部分是類(lèi)型指針,即對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例。
2.3.3 對(duì)象的訪問(wèn)定位
對(duì)象的訪問(wèn)定位也取決于具體的虛擬機(jī)實(shí)現(xiàn)。當(dāng)我們?cè)诙焉蟿?chuàng)建一個(gè)對(duì)象實(shí)例后,就要通過(guò)虛擬機(jī)棧中的reference類(lèi)型數(shù)據(jù)來(lái)操作堆上的對(duì)象。現(xiàn)在主流的訪問(wèn)方式有兩種(HotSpot虛擬機(jī)采用的是第二種):
使用句柄訪問(wèn)對(duì)象。即reference中存儲(chǔ)的是對(duì)象句柄的地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類(lèi)型數(shù)據(jù)的具體地址信息,相當(dāng)于二級(jí)指針。
直接指針訪問(wèn)對(duì)象。即reference中存儲(chǔ)的就是對(duì)象地址,相當(dāng)于一級(jí)指針。
兩種方式有各自的優(yōu)缺點(diǎn)。當(dāng)垃圾回收移動(dòng)對(duì)象時(shí),對(duì)于方式一而言,reference中存儲(chǔ)的地址是穩(wěn)定的地址,不需要修改,僅需要修改對(duì)象句柄的地址;而對(duì)于方式二,則需要修改reference中存儲(chǔ)的地址。從訪問(wèn)效率上看,方式二優(yōu)于方式一,因?yàn)榉绞蕉贿M(jìn)行了一次指針定位,節(jié)省了時(shí)間開(kāi)銷(xiāo),而這也是HotSpot采用的實(shí)現(xiàn)方式
2.4 OutOfMemoryError異常
2.4.1 java堆溢出
2.4.2 虛擬機(jī)棧溢出和本地方法棧溢出
2.4.3 方法區(qū)和運(yùn)行時(shí)常量池區(qū)
2.4.4 本機(jī)直接內(nèi)存溢出
第三章 垃圾收集器與內(nèi)存分配策略
3.1 概述
程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧等3個(gè)區(qū)域隨線程而生,隨線程而滅,因?yàn)榉椒ńY(jié)束或線程結(jié)束時(shí),內(nèi)存自然就跟著回收了。而Java堆和方法區(qū)則不一樣,一個(gè)接口中的多個(gè)實(shí)現(xiàn)類(lèi)需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣,我們只有在程序出于運(yùn)行期間才能知道會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,垃圾收集器所關(guān)注的是這部分內(nèi)存。
3.2 判斷對(duì)象是否已死
3.2.1 引用計(jì)數(shù)算法
算法:給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器就加1,當(dāng)失效時(shí),計(jì)數(shù)器就減1,任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再被用的。缺點(diǎn):難以解決對(duì)象之間循環(huán)相互引用的問(wèn)題。
3.2.2 可達(dá)性分析算法
在Java中,是通過(guò)可達(dá)性分析(Reachability Analysis)來(lái)判定對(duì)象是否存活的。該算法的基本思路就是通過(guò)一些被稱(chēng)為(GC Roots)的對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索走過(guò)的路徑被稱(chēng)為(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí)(即從GC Roots節(jié)點(diǎn)到該節(jié)點(diǎn)不可達(dá)),則證明該對(duì)象是不可用的。

在Java中,可作為GC Root的對(duì)象包括以下幾種:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象
方法區(qū)中常量引用的對(duì)象
本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象
3.2.3 引用
在JDK1.2后,java對(duì)引用進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用、軟引用、弱引用和虛引用。這四種引用強(qiáng)度依次減弱。
強(qiáng)引用:是指創(chuàng)建一個(gè)對(duì)象并把這個(gè)對(duì)象賦給一個(gè)引用變量,比如:Object object =new Object(),強(qiáng)引用有引用變量指向時(shí)永遠(yuǎn)不會(huì)被垃圾回收,JVM寧愿拋出OutOfMemory錯(cuò)誤也不會(huì)回收這種對(duì)象。
軟引用:用來(lái)描述一些還有用但并非必須的對(duì)象。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收。如果這次回收還沒(méi)有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。在jdk1.2后,提供了SoftReference類(lèi)來(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ì)象。在JDK1.2后,提供了WeakReference類(lèi)來(lái)實(shí)現(xiàn)弱引用。
虛引用(也稱(chēng)幽靈引用或者幻影引用):它是最弱的一種引用關(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)來(lái)實(shí)現(xiàn)虛引用。
3.2.4 生存還是死亡
即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非是非死不可的,這時(shí)候它們暫時(shí)處于緩刑階段,要真正宣告一個(gè)對(duì)象的死亡,至少要經(jīng)過(guò)兩次標(biāo)記階段:
如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒(méi)有與GC Roots相連的引用鏈,那么它將會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法。
當(dāng)對(duì)象沒(méi)有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過(guò),虛擬機(jī)將這兩種情況都視為沒(méi)有必要執(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ì)象永久處于等待,甚至導(dǎo)致整個(gè)內(nèi)存回收系統(tǒng)崩潰。finalize()方法是對(duì)象逃脫死亡的最后一次機(jī)會(huì),稍后GC將對(duì)F-Queue中的對(duì)象進(jìn)行第二次小規(guī)模標(biāo)記,如果對(duì)象要在finalize()中成功拯救自己----------只要重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可,譬如把自己(this關(guān)鍵字)賦值給某個(gè)類(lèi)變量或者對(duì)象的成員變量,那在第二次標(biāo)記時(shí)它將被移除出‘即將回收’的集合;如果對(duì)象這時(shí)候還沒(méi)有逃脫,那么它就真的被回收了。例如:
public class FinalizeEscape { public static FinalizeEscape SAVE_HOOK = null; public void isAlive(){ System.out.println(" i am alive"); } @Override protected void finalize() throws Throwable { // TODO Auto-generated method stub super.finalize(); System.out.println("finallize method exec"); FinalizeEscape.SAVE_HOOK = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscape(); SAVE_HOOK = null; System.gc(); Thread.sleep(5000); if(SAVE_HOOK != null){ SAVE_HOOK.isAlive(); }else{ System.out.println(" i am dead"); } SAVE_HOOK = null; System.gc(); Thread.sleep(5000); if(SAVE_HOOK != null){ SAVE_HOOK.isAlive(); }else{ System.out.println(" i am dead"); } } }
輸出:
3.2.5 回收方法區(qū)
方法區(qū)是所有線程共享的一片內(nèi)存區(qū)域。它存儲(chǔ)的是已被JVM加載的類(lèi)信息,常量,靜態(tài)變量,編譯器編譯后的代碼等數(shù)據(jù)。在JDK1.8以前的HotSpot虛擬機(jī)中,方法區(qū)也被稱(chēng)為永久代,1.8后被元空間取代。方法區(qū)稱(chēng)為永久代并不意味這進(jìn)入方法區(qū)就永久存在,方法區(qū)也會(huì)發(fā)生內(nèi)存回收,此區(qū)域的內(nèi)存回收主要是針對(duì)常量池的回收以及對(duì)類(lèi)型的卸載。我們已經(jīng)知道,GC在進(jìn)行垃圾回收之前,先要進(jìn)行回收對(duì)象的判斷(回收對(duì)象判斷算法:引用計(jì)數(shù)法,可達(dá)性分析算法),然后再進(jìn)行垃圾回收。
很多人認(rèn)為方法區(qū)(或者Hotspot中的永久代)是沒(méi)有垃圾收集的,jvm規(guī)范中確實(shí)說(shuō)過(guò)可以不要求在方法區(qū)實(shí)現(xiàn)垃圾收集,而且在方法區(qū)進(jìn)行垃圾收集的性?xún)r(jià)比比較低:在堆中,新生代的常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠(yuǎn)低于此。
永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無(wú)用的類(lèi)。回收廢棄常量和回收java堆中的對(duì)象非常相似。以常量池中字面量的回收為例,假如一個(gè)字符串“abc"已經(jīng)進(jìn)入了常量池中,但是當(dāng)前系統(tǒng)沒(méi)有任何一個(gè)String對(duì)象是叫做“abc”的,換句話(huà)說(shuō),就是沒(méi)有任何String對(duì)象引用常量池中的“abc”常量,也沒(méi)有其他地方引用了這個(gè)字面量,如果這是發(fā)生內(nèi)存回收,而且必要的話(huà),這個(gè)“abc"常量就會(huì)被系統(tǒng)清理出常量池。常量池中的其他類(lèi)(接口)、方法、字段的符號(hào)引用也與此類(lèi)似。
判斷一個(gè)常量是否是廢棄常量比較簡(jiǎn)單,而要判定一個(gè)類(lèi)是否是無(wú)用的類(lèi)的條件相對(duì)苛刻很多。類(lèi)需要同時(shí)滿(mǎn)足下面三個(gè)條件才能算是無(wú)用的類(lèi):
該類(lèi)的所有的實(shí)例都已經(jīng)被回收,也就是java堆中不存在該類(lèi)的任何實(shí)例;
加載該類(lèi)的ClassLoader被回收;
該類(lèi)對(duì)應(yīng)的java.lang.Class對(duì)象沒(méi)有在任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類(lèi)的方法。
3.3 垃圾收集算法
JVM中的堆,一般分為三大部分:新生代、老年代、永久代。
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用分代收集算法,這種算法是根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴āR话闶前裫ava堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴āT谛律校看卫占瘯r(shí)都發(fā)現(xiàn)有大量對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高,沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用標(biāo)記-清除或者標(biāo)記-整理算法進(jìn)行回收。
新生代主要是用來(lái)存放新生的對(duì)象。一般占據(jù)堆的1/3空間。由于頻繁創(chuàng)建對(duì)象,所以新生代會(huì)頻繁觸發(fā)MinorGC進(jìn)行垃圾回收。新生代又分為 Eden區(qū)、ServivorFrom、ServivorTo三個(gè)區(qū)。當(dāng)JVM無(wú)法為新建對(duì)象分配內(nèi)存空間的時(shí)候(Eden滿(mǎn)了),Minor GC被觸發(fā)。因此新生代空間占用率越高,Minor GC越頻繁。
MinorGC的過(guò)程:采用復(fù)制算法。
首先,把Eden和ServivorFrom區(qū)域中存活的對(duì)象復(fù)制到ServicorTo區(qū)域(如果有對(duì)象的年齡以及達(dá)到了老年的標(biāo)準(zhǔn),一般是15,則賦值到老年代區(qū))
同時(shí)把這些對(duì)象的年齡+1(如果ServicorTo不夠位置了就放到老年區(qū))
然后,清空Eden和ServicorFrom中的對(duì)象;最后,ServicorTo和ServicorFrom互換,原ServicorTo成為下一次GC時(shí)的ServicorFrom區(qū)。

老年代的對(duì)象比較穩(wěn)定,所以MajorGC不會(huì)頻繁執(zhí)行。
在進(jìn)行MajorGC前一般都先進(jìn)行了一次MinorGC,使得有新生代的對(duì)象晉身入老年代,導(dǎo)致空間不夠用時(shí)才觸發(fā)。當(dāng)無(wú)法找到足夠大的連續(xù)空間分配給新創(chuàng)建的較大對(duì)象時(shí)也會(huì)提前觸發(fā)一次MajorGC進(jìn)行垃圾回收騰出空間。
MajorGC采用標(biāo)記—清除算法:
首先掃描一次所有老年代,標(biāo)記出存活的對(duì)象
然后回收沒(méi)有標(biāo)記的對(duì)象。
MajorGC的耗時(shí)比較長(zhǎng),因?yàn)橐獟呙柙倩厥铡ajorGC會(huì)產(chǎn)生內(nèi)存碎片,為了減少內(nèi)存損耗,我們一般需要進(jìn)行合并或者標(biāo)記出來(lái)方便下次直接分配。
當(dāng)老年代也滿(mǎn)了裝不下的時(shí)候,就會(huì)拋出OOM(Out of Memory)異常。
永久代指內(nèi)存的永久保存區(qū)域,主要存放Class和Meta(元數(shù)據(jù))的信息。
Class在被加載的時(shí)候被放入永久區(qū)域。它和和存放實(shí)例的區(qū)域不同,GC不會(huì)在主程序運(yùn)行期對(duì)永久區(qū)域進(jìn)行清理。所以這也導(dǎo)致了永久代的區(qū)域會(huì)隨著加載的Class的增多而脹滿(mǎn),最終拋出OOM異常。
在Java8中,永久代已經(jīng)被移除,被一個(gè)稱(chēng)為“元數(shù)據(jù)區(qū)”(元空間)的區(qū)域所取代。
元空間的本質(zhì)和永久代類(lèi)似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過(guò)元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。類(lèi)的元數(shù)據(jù)放入 native memory, 字符串池和類(lèi)的靜態(tài)變量放入java堆中. 這樣可以加載多少類(lèi)的元數(shù)據(jù)就不再由MaxPermSize控制, 而由系統(tǒng)的實(shí)際可用空間來(lái)控制。
Major GC和Full GC區(qū)別
Full GC:收集young gen、old gen、perm gen
Major GC:有時(shí)又叫old gc,只收集old gen
3.3.1 標(biāo)記-清除算法(老年代回收算法)
標(biāo)記-清除算法(最基礎(chǔ)的算法)分為標(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ò)了。
缺點(diǎn):一個(gè)是效率問(wèn)題,標(biāo)記和清除兩個(gè)過(guò)程的效率都不高;另一個(gè)是空間問(wèn)題,標(biāo)記清除之后會(huì)產(chǎn)生大量的不連續(xù)的內(nèi)存碎片,空間碎片較多時(shí)會(huì)導(dǎo)致以后在程序運(yùn)行過(guò)程中分配較大對(duì)象時(shí)無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。

3.3.2 復(fù)制算法(新生代回收算法)
為了解決效率問(wèn)題,一種稱(chēng)為復(fù)制的收集算法出現(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)存即可。
優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。
缺點(diǎn):要犧牲內(nèi)存為代價(jià)。

3.3.3 標(biāo)記-整理算法(老年代回收算法)
由于復(fù)制算法在對(duì)象存活率較高時(shí)存在大量的復(fù)制操作,效率降低,而且浪費(fèi)空間,所以老年代一般不能選擇這種算法。
根據(jù)老年代的特點(diǎn),提出了標(biāo)記-整理算法,標(biāo)記過(guò)程與標(biāo)記-清除算法一致,整理過(guò)程是先將存活的對(duì)象移到一端,然后將存活的對(duì)象邊界外的內(nèi)存清理。

3.4 HotSpot的算法實(shí)現(xiàn)
3.5 垃圾收集器
3.5.1 Serial收集器
Serial收集器是最基本、發(fā)展歷史最悠久的收集器,曾經(jīng)(jdk1.3.1之前)是虛擬機(jī)新生代收集的唯一選擇。它是一個(gè)單線程的收集器,它使用一個(gè)CPU或一條收集線程完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線程,直到它收集結(jié)束。
直到現(xiàn)在為止,它依然是虛擬機(jī)運(yùn)行在CLient模式下的默認(rèn)新生代垃圾收集器。
優(yōu)點(diǎn):簡(jiǎn)單而高效,由于運(yùn)行在單個(gè)CPU的環(huán)境中,沒(méi)有線程交互的開(kāi)銷(xiāo),可以專(zhuān)心做垃圾收集從而獲得最高的單線程收集效率。
缺點(diǎn):有停頓時(shí)間,需要停止所有當(dāng)前工作線程。
3.5.2 ParNew收集器
ParNew收集器是Serial收集器的多線程版本,除了使用多線程進(jìn)行垃圾收集外,其余行為包括Serial收集器可用的所有控制參數(shù)、收集算法和回收策略等和Serial收集器完全一樣。
ParNew收集器是許多運(yùn)行在Server模式下的虛擬機(jī)首選的新生代收集器,其中一個(gè)與性能無(wú)關(guān)的原因是,它能和CMS收集器配合工作。在JDK1.5時(shí)期,HotSpot推出了一款在強(qiáng)交互應(yīng)用中幾乎可認(rèn)為有劃時(shí)代意義的垃圾收集器-----------CMS收集器,這款收集器是HotSpot中第一款真正意義上的并發(fā)收集器,它第一次實(shí)現(xiàn)了讓垃圾收集線程與用戶(hù)線程同時(shí)工作。
ParNew收集器在Cpu環(huán)境中不會(huì)比Serial收集器有更好的效果,甚至由于存在線程交互的開(kāi)銷(xiāo),該收集器在通過(guò)超線程技術(shù)實(shí)現(xiàn)的兩個(gè)CPU的環(huán)境中都不能百分之百的保證可以超越Serial收集器。當(dāng)然隨著可以使用的CPU的數(shù)量的增加,它對(duì)于GC時(shí)系統(tǒng)資源的有效利用還是很有好處的。它默認(rèn)開(kāi)啟的收集線程數(shù)與CPU的數(shù)量相同,在CPU非常多的環(huán)境下可以使用-XX:ParallelGCThreads參數(shù)來(lái)限制垃圾收集的線程數(shù)。
3.5.3 Parallel Scavenge收集器
Parallel Scavenge收集器是一個(gè)新生代收集器,它也是使用復(fù)制算法的收集器,又是并行的多線程收集器。
Parallel Scavenge收集器的特點(diǎn)是它的關(guān)注點(diǎn)與其它收集器不同,CMS等收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí)用戶(hù)線程的停頓時(shí)間,而Parallel Scavenge收集器的目標(biāo)則是達(dá)到一個(gè)可控制的吞吐量。所謂吞吐量就是CPU用于運(yùn)行用戶(hù)代碼的時(shí)間與CPU總消耗時(shí)間的比值,即吞吐量=運(yùn)行用戶(hù)代碼時(shí)間/(運(yùn)行用戶(hù)代碼時(shí)間+垃圾收集時(shí)間),虛擬機(jī)總共運(yùn)行了100分鐘,垃圾收集1分鐘,那吞吐量就是99%。
Parallel Scavenge收集器擁有自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics),它會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行狀況動(dòng)態(tài)調(diào)整停頓時(shí)間和吞吐量,這也是和ParNew收集器不同之處。
3.5.4 Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,它同樣是一個(gè)單線程收集器,使用‘標(biāo)記-整理’算法。這個(gè)收集器的主要意義是在于給Client模式下的虛擬機(jī)使用。如果在Server模式下,那么它還有兩大用途:一種是在JDK1.5以及之前版本中與Parallel Scavenge收集器搭配使用,另一種是作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時(shí)使用。
3.5.5 Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和標(biāo)記整理算法。這個(gè)收集器是在JDK1.6才開(kāi)始提供的,在此之前,新生代的Parallel Scavenge收集器處于比較尷尬的狀態(tài),因?yàn)樾律x擇了Parallel Scavenge收集器,老年代必須選擇Serial Old(因?yàn)镻arallel Scavenge收集器無(wú)法與CMS收集器搭配使用)。Serial Old由于是單線程,在服務(wù)端性能是拖累,因此即使使用了Parallel Scavenge收集器也未必能獲得吞吐量最大化的效果,而且單線程的老年代收集中無(wú)法充分利用服務(wù)器多CPU的處理能力,在老年代很大而且硬件比較高級(jí)的環(huán)境中,這種組合的吞吐量甚至還不一定有ParNew加CMS組合的能力。
3.5.6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。主要應(yīng)用在服務(wù)端,以提供較短的響應(yīng)時(shí)間。它是基于標(biāo)記-清除算法實(shí)現(xiàn)。整個(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)記依然會(huì)有停頓時(shí)間(Stop the world)。初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快,并發(fā)標(biāo)記就是進(jìn)行GC Roots Tracing的過(guò)程,而重新標(biāo)記則是為了修正并發(fā)標(biāo)記期間因用戶(hù)程序繼續(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í)間短。
CMS優(yōu)點(diǎn):并發(fā)收集,低停頓。
缺點(diǎn):
CMS收集器對(duì)CPU資源非常敏感。在CMS中并發(fā)階段,它依賴(lài)CPU資源,若CPU資源較少時(shí),CMS會(huì)消耗掉比較多的一部分CPU資源,使得程序的執(zhí)行速度變慢。
CMS收集器無(wú)法處理浮動(dòng)垃圾,可能出現(xiàn)“Concurrent Mode Failure”失敗而導(dǎo)致另一次Full GC的產(chǎn)生。由于CMS并發(fā)清理階段用戶(hù)程序還在運(yùn)行,就會(huì)產(chǎn)生新的垃圾,這一部分垃圾出現(xiàn)在標(biāo)記過(guò)程后,只好留到下一次GC時(shí)清理,這一部分垃圾成為
"浮動(dòng)垃圾"。
CMS收集器使用的時(shí)標(biāo)記-清除算法,所以會(huì)產(chǎn)生內(nèi)存碎片,導(dǎo)致大對(duì)象申請(qǐng)內(nèi)存時(shí),無(wú)法找到足夠大的連續(xù)空間來(lái)分配對(duì)象,不得不提前觸發(fā)Full GC。
3.5.7 G1收集器
為解決CMS算法產(chǎn)生空間碎片和其它一系列的問(wèn)題缺陷,HotSpot提供了另外一種垃圾回收策略,G1(Garbage First)算法,通過(guò)參數(shù)-XX:+UseG1GC來(lái)啟用,該算法在JDK 7u4版本被正式推出,官網(wǎng)對(duì)此描述如下:
G1垃圾收集算法主要應(yīng)用在多CPU大內(nèi)存的服務(wù)中,在滿(mǎn)足高吞吐量的同時(shí),竟可能的滿(mǎn)足垃圾回收時(shí)的暫停時(shí)間,該設(shè)計(jì)主要針對(duì)如下應(yīng)用場(chǎng)景:
垃圾收集線程和應(yīng)用線程并發(fā)執(zhí)行,和CMS一樣
空閑內(nèi)存壓縮時(shí)避免冗長(zhǎng)的暫停時(shí)間
應(yīng)用需要更多可預(yù)測(cè)的GC暫停時(shí)間
不希望犧牲太多的吞吐性能
不需要很大的Java堆 (翻譯的有點(diǎn)虛,多大才算大?)
第四章 虛擬機(jī)性能監(jiān)控與故障處理工具
4.1 概述
了解了虛擬機(jī)內(nèi)存分配與回收技術(shù)的介紹,再根據(jù)從實(shí)踐的角度去了解虛擬機(jī)內(nèi)存管理的世界。
給一個(gè)系統(tǒng)定位問(wèn)題的時(shí)候,知識(shí)、經(jīng)驗(yàn)是關(guān)鍵基礎(chǔ),數(shù)據(jù)是依據(jù),工具是運(yùn)用處理數(shù)據(jù)的手段。這里說(shuō)的數(shù)據(jù)包括:運(yùn)行日志、異常堆棧、GC日志、線程快照(threaddump/javacore文件)、堆轉(zhuǎn)儲(chǔ)快照(heapdump/hprof文件)等。經(jīng)常使用適當(dāng)?shù)奶摂M機(jī)監(jiān)控和分析的工具可以加快我們分析數(shù)據(jù)、定位解決問(wèn)題的速度,但在學(xué)習(xí)工具前,也應(yīng)當(dāng)意識(shí)到工具永遠(yuǎn)都是知識(shí)技能的一層包裝,沒(méi)有什么工具是秘密武器。
4.2 JDK的命令行工具
Sun jdk監(jiān)控和故障處理工具

4.2.1 jps:虛擬機(jī)進(jìn)程狀況工具

jps [option] [hostid]
-q 只輸出LVMID,省略主類(lèi)的名稱(chēng)
-m 輸出虛擬機(jī)進(jìn)程啟動(dòng)時(shí)傳遞給主類(lèi)main()函數(shù)的參數(shù)
-l 輸出主類(lèi)的全名,如果進(jìn)程執(zhí)行的是Jar包,輸出Jar路徑
-v 輸出虛擬機(jī)進(jìn)程啟動(dòng)JVM參數(shù)
4.2.2 jstat:虛擬機(jī)統(tǒng)計(jì)信息監(jiān)視工具
jstat [option vmid [interval[s|ms] [count] ]
-class 監(jiān)視類(lèi)裝載、卸載數(shù)量、總空間以及類(lèi)裝載所耗費(fèi)的時(shí)間
-gc 監(jiān)視java堆狀況,包括Eden區(qū)、兩個(gè)suivivor區(qū)、老年代、永久代等的容量、已用空間、GC時(shí)間合計(jì)等信息
-gccapacity 監(jiān)視內(nèi)容與gc基本相同,但輸出主要關(guān)注Java堆各個(gè)區(qū)域使用到的最大、最小空間
-gcutil 監(jiān)視內(nèi)容與gc基本相同,但輸出主要關(guān)注已使用空間占總空間的百分比
-gccause 與-gcutil功能一樣,但是會(huì)額外輸出導(dǎo)致上一次GC產(chǎn)生的原因
-gcnew 監(jiān)視新生代GC狀況
4.2.3 jinfo:Java配置信息工具
jinfo [option] pid
4.2.4 jmap:Java內(nèi)存映像工具
jmap [option] vmid
————————————————
版權(quán)聲明:本文為CSDN博主「24koby」的原創(chuàng)文章,遵循CC 4.0 by-sa版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_31583183/article/details/95059493