一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

JVM合理理解大總結(jié)(一)_耐心閱讀

 Java幫幫 2020-01-02


1.0 Java虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)


經(jīng)常有人把Java內(nèi)存區(qū)分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack),這種區(qū)分方法比較粗糙,Java內(nèi)存區(qū)域的劃分實(shí)際上遠(yuǎn)比這復(fù)雜。Java虛擬機(jī)在執(zhí)行Java程序的過(guò)程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,主要包括:

  • 虛擬機(jī)棧

  • 本地方法棧

  • PC寄存器

  • 方法區(qū)

  • 堆區(qū)

    這些數(shù)據(jù)區(qū)域中大致可以劃分為2類:線程獨(dú)享、線程共享。

線程共享數(shù)據(jù)區(qū):方法區(qū)、堆區(qū)。隨著虛擬機(jī)啟動(dòng)而創(chuàng)建,隨著虛擬機(jī)退出而銷毀,并且為為進(jìn)程的所有子線程共享 。

線程獨(dú)享數(shù)據(jù)區(qū):虛擬機(jī)棧、本地方法棧、PC寄存器。這些數(shù)據(jù)區(qū)與線程一一對(duì)應(yīng)的,這些與線程對(duì)應(yīng)的數(shù)據(jù)區(qū)域會(huì)隨著線程開始和結(jié)束而創(chuàng)建和銷毀。每個(gè)線程都有自己私有的虛擬機(jī)棧、本地方法棧、PC寄存器。

下圖顯示了兩個(gè)線程的情況下,JVM中的運(yùn)行時(shí)數(shù)據(jù)區(qū):

一、線程獨(dú)享數(shù)據(jù)區(qū)

PC寄存器

Java 虛擬機(jī)可以支持多條線程同時(shí)執(zhí)行,每一條 Java虛擬機(jī)線程都有自己的 PC( Program Counter)寄存器。在任意時(shí)刻,一條 Java 虛擬機(jī)線程只會(huì)執(zhí)行一個(gè)方法的代碼,這個(gè)正在被線程執(zhí)行的方法稱為該線程的當(dāng)前方法( Current Method)。PC寄存器中存儲(chǔ)的就是當(dāng)前方法經(jīng)過(guò)JVM匯編后字節(jié)碼指令的地址。如果這個(gè)方法不是 native 的,那 PC 寄存器就保存 Java 虛擬機(jī)正在執(zhí)行的字節(jié)碼指令的地址,如果該方法是 native 的,那 PC 寄存器的值是 undefined。

Java虛擬機(jī)棧

此棧中的元素叫做棧幀(Stack Frame),線程在調(diào)用java方法時(shí),會(huì)為每個(gè)方法創(chuàng)建一個(gè)棧幀,來(lái)存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口等信息。每個(gè)方法被調(diào)用和完成的過(guò)程,都對(duì)應(yīng)著一個(gè)棧幀從虛擬機(jī)棧上入棧和出棧的過(guò)程。虛擬機(jī)棧的生命周期和線程相同。

下圖展示了棧幀的內(nèi)部結(jié)構(gòu):

棧幀( Frame)是用來(lái)存儲(chǔ)數(shù)據(jù)和部分過(guò)程結(jié)果的數(shù)據(jù)結(jié)構(gòu),同時(shí)也被用來(lái)處理動(dòng)態(tài)鏈接( Dynamic Linking)、方法返回值和異常分派( Dispatch Exception)。棧幀隨著方法調(diào)用而創(chuàng)建,隨著方法結(jié)束而銷毀——無(wú)論方法是正常完成還是異常完成(拋出了在方法內(nèi)未被捕獲的異常)都算作方法結(jié)束。棧幀的存儲(chǔ)空間分配在 Java 虛擬機(jī)棧之中,每一個(gè)棧幀都有自己的局部變量表( Local Variables)、操作數(shù)棧(OperandStack)和指向當(dāng)前方法所屬的類的運(yùn)行時(shí)常量池的引用。局部變量表和操作數(shù)棧的容量是在編譯期確定,并通過(guò)方法的 Code 屬性保存及提供給棧幀使用。因此,棧幀容量的大小僅僅取決于 Java 虛擬機(jī)的實(shí)現(xiàn)和方法調(diào)用時(shí)可被分配的內(nèi)存。在一條線程之中,只有目前正在執(zhí)行的那個(gè)方法的棧幀是活動(dòng)的。這個(gè)棧幀就被稱為是當(dāng)前棧幀( Current Frame),這個(gè)棧幀對(duì)應(yīng)的方法就被稱為是當(dāng)前方法( Current Method),定義這個(gè)方法的類就稱作當(dāng)前類( Current Class)。對(duì)局部變量表和操作數(shù)棧的各種操作,通常都指的是對(duì)當(dāng)前棧幀的對(duì)局部變量表和操作數(shù)棧進(jìn)行的操作。法返回之后,當(dāng)前棧幀就隨之被丟棄,前一個(gè)棧幀就重新成為當(dāng)前棧幀了。

請(qǐng)讀者特別注意,棧幀是線程本地私有的數(shù)據(jù),不可能在一個(gè)棧幀之中引用另外一條線程的棧幀。

本地方法棧

Java 虛擬機(jī)實(shí)現(xiàn)可能會(huì)使用到傳統(tǒng)的棧(通常稱之為“ C Stacks”)來(lái)支持 native 方法( 指使用 Java 以外的其他語(yǔ)言編寫的方法)的執(zhí)行,這個(gè)棧就是本地方法棧( Native Method Stack)。當(dāng) Java 虛擬機(jī)使用其他語(yǔ)言(例如 C 語(yǔ)言)來(lái)實(shí)現(xiàn)指令集解釋器時(shí),也會(huì)使用到本地方法棧。這個(gè)棧一般會(huì)在線程創(chuàng)建的時(shí)候按線程分配,用來(lái)存儲(chǔ)線程調(diào)用本地方法時(shí),本地方法的局部變量表,操作棧等信息。

Java 虛擬機(jī)規(guī)范允許Java虛擬機(jī)棧和本地方法棧被實(shí)現(xiàn)成固定大小的或者是根據(jù)計(jì)算動(dòng)態(tài)擴(kuò)展和收縮的。

本地方法棧和虛擬機(jī)棧所發(fā)揮的作用是非常相似的,區(qū)別是虛擬機(jī)棧執(zhí)行Java方法,而本地方法棧則為虛擬機(jī)使用的Native方法服務(wù),在sun hotspot 虛擬機(jī)中已經(jīng)把兩者合二為一了,本地方法棧區(qū)也會(huì)拋出StackOverFlowError和OutOfMemeoryError異常。

堆(Heap)

Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。 此區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有對(duì)象的實(shí)例都在這分配內(nèi)存。 Java堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱為GC堆。Java 堆在虛擬機(jī)啟動(dòng)的時(shí)候就被創(chuàng)建,它存儲(chǔ)了被自動(dòng)內(nèi)存管理系統(tǒng)( Automatic Storage Management System,也即是常說(shuō)的“ Garbage Collector(垃圾收集器)”)所管理的各種對(duì)象,這些受管理的對(duì)象無(wú)需,也無(wú)法顯式地被銷毀。 Java 堆的容量可以是固定大小的,也可以隨著程序執(zhí)行的需求動(dòng)態(tài)擴(kuò)展,并在不需要過(guò)多空間時(shí)自動(dòng)收縮。

方法區(qū)

在 Java 虛擬機(jī)中,方法區(qū)( Method Area) 是可供各條線程共享的運(yùn)行時(shí)內(nèi)存區(qū)域。方法區(qū)與傳統(tǒng)語(yǔ)言中的編譯代碼儲(chǔ)存區(qū)( Storage Area Of Compiled Code)或者操作系統(tǒng)進(jìn)程的正文段( Text Segment)的作用非常類似,它存儲(chǔ)了每一個(gè)類的結(jié)構(gòu)信息,例如運(yùn)行時(shí)常量池( Runtime Constant Pool)、字段和方法數(shù)據(jù)、構(gòu)造函數(shù)和普通方法的字節(jié)碼內(nèi)容、還包括一些在類、實(shí)例、接口初始化時(shí)用到的特殊方法。 方法區(qū)在虛擬機(jī)啟動(dòng)的時(shí)候被創(chuàng)建,雖然方法區(qū)是堆的邏輯組成部分,但是簡(jiǎn)單的虛擬機(jī)實(shí)現(xiàn)可以選擇在這個(gè)區(qū)域不實(shí)現(xiàn)垃圾收集。 方法區(qū)的容量可以是固定大小的,也可以隨著程序執(zhí)行的需求動(dòng)態(tài)擴(kuò)展,并在不需要過(guò)多空間時(shí)自動(dòng)收縮。

下圖展示了方法區(qū)的內(nèi)部結(jié)構(gòu)

1.1 Java代碼說(shuō)明JVM內(nèi)存布局

通常情況下,我們?cè)谘芯縅VM內(nèi)存布局的時(shí)候,主要研究的就是Java虛擬機(jī)棧和堆(Heap)。這也是大多人將Java虛擬機(jī)內(nèi)存粗分為棧內(nèi)存和堆內(nèi)存的原因。然后從另外一個(gè)角度,HotSpot虛擬機(jī)已經(jīng)將Java虛擬機(jī)棧和本地方法棧合并了,和方法區(qū)也是堆內(nèi)存的邏輯組成部分。因此這里我們也粗分為堆內(nèi)存和棧內(nèi)存,用具體的Java代碼來(lái)說(shuō)明對(duì)象在內(nèi)存中的布局。

這張圖演示了Java內(nèi)存模型的邏輯視圖。

每一個(gè)運(yùn)行在Java虛擬機(jī)里的線程都擁有自己的線程棧。這個(gè)線程棧包含了這個(gè)線程調(diào)用的方法當(dāng)前執(zhí)行點(diǎn)相關(guān)的信息。一個(gè)線程僅能訪問(wèn)自己的線程棧。一個(gè)線程創(chuàng)建的本地變量對(duì)其它線程不可見,僅自己可見。即使兩個(gè)線程執(zhí)行同樣的代碼,這兩個(gè)線程任然在在自己的線程棧中的代碼來(lái)創(chuàng)建本地變量。因此,每個(gè)線程 擁有每個(gè)本地變量的獨(dú)有版本。

所有原始類型(int,long...)的本地變量都存放在線程棧上,因此對(duì)其它線程不可見。一個(gè)線程可能向另一個(gè)線程傳遞一個(gè)原始類型變量的拷貝,但是它不能共享這個(gè)原始類型變量自身。

堆上包含在Java程序中創(chuàng)建的所有對(duì)象,無(wú)論是哪一個(gè)對(duì)象創(chuàng)建的。這包括原始類型的對(duì)象版本。如果一個(gè)對(duì)象被創(chuàng)建然后賦值給一個(gè)局部變量,或者用來(lái)作為另一個(gè)對(duì)象的成員變量,這個(gè)對(duì)象任然是存放在堆上。

下面這張圖演示了調(diào)用棧和本地變量存放在線程棧上,對(duì)象存放在堆上。

  • 一個(gè)本地變量可能是原始類型,在這種情況下,它總是“呆在”線程棧上。

  • 一個(gè)本地變量也可能是指向一個(gè)對(duì)象的一個(gè)引用。在這種情況下,引用(這個(gè)本地變量)存放在線程棧上,但是對(duì)象本身存放在堆上。

  • 一個(gè)對(duì)象可能包含方法,這些方法可能包含本地變量。這些本地變量任然存放在線程棧上,即使這些方法所屬的對(duì)象存放在堆上。

  • 一個(gè)對(duì)象的成員變量可能隨著這個(gè)對(duì)象自身存放在堆上。不管這個(gè)成員變量是原始類型還是引用類型。

  • 靜態(tài)成員變量跟隨著類定義一起也存放在堆上。

  • 存放在堆上的對(duì)象可以被所有持有對(duì)這個(gè)對(duì)象引用的線程訪問(wèn)。當(dāng)一個(gè)線程可以訪問(wèn)一個(gè)對(duì)象時(shí),它也可以訪問(wèn)這個(gè)對(duì)象的成員變量。如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)對(duì)象上的同一個(gè)方法,它們將會(huì)都訪問(wèn)這個(gè)對(duì)象的成員變量,但是每一個(gè)線程都擁有這個(gè)本地變量的私有拷貝。

下圖演示了上面提到的點(diǎn):

兩個(gè)線程擁有一些列的本地變量。其中一個(gè)本地變量(Local Variable 2)執(zhí)行堆上的一個(gè)共享對(duì)象(Object 3)。這兩個(gè)線程分別擁有同一個(gè)對(duì)象的不同引用。這些引用都是本地變量,因此存放在各自線程的線程棧上。這兩個(gè)不同的引用指向堆上同一個(gè)對(duì)象。

注意,這個(gè)共享對(duì)象(Object 3)持有Object2和Object4一個(gè)引用作為其成員變量(如圖中Object3指向Object2和Object4的箭頭)。通過(guò)在Object3中這些成員變量引用,這兩個(gè)線程就可以訪問(wèn)Object2和Object4。

這張圖也展示了指向堆上兩個(gè)不同對(duì)象的一個(gè)本地變量。在這種情況下,指向兩個(gè)不同對(duì)象的引用不是同一個(gè)對(duì)象。理論上,兩個(gè)線程都可以訪問(wèn)Object1和Object5,如果兩個(gè)線程都擁有兩個(gè)對(duì)象的引用。但是在上圖中,每一個(gè)線程僅有一個(gè)引用指向兩個(gè)對(duì)象其中之一。

因此,什么類型的Java代碼會(huì)導(dǎo)致上面的內(nèi)存圖呢?如下所示:

  1. public class MyRunnable implements Runnable() {

  2.  

  3.     public void run() {

  4.         methodOne();

  5.     }

  6.  

  7.     public void methodOne() {

  8.         int localVariable1 = 45;

  9.  

  10.         MySharedObject localVariable2 =

  11.             MySharedObject.sharedInstance;

  12.  

  13.         //... do more with local variables.

  14.  

  15.         methodTwo();

  16.     }

  17.  

  18.     public void methodTwo() {

  19.         Integer localVariable1 = new Integer(99);

  20.  

  21.         //... do more with local variable.

  22.     }

  23. }

  24.  

  25.  

  26. public class MySharedObject {

  27.  

  28.     //static variable pointing to instance of MySharedObject

  29.  

  30.     public static final MySharedObject sharedInstance =

  31.         new MySharedObject();

  32.  

  33.  

  34.     //member variables pointing to two objects on the heap

  35.  

  36.     public Integer object2 = new Integer(22);

  37.     public Integer object4 = new Integer(44);

  38.  

  39.     public long member1 = 12345;

  40.     public long member1 = 67890;

  41. }

如果兩個(gè)線程同時(shí)執(zhí)行run()方法,就會(huì)出現(xiàn)上圖所示的情景。run()方法調(diào)用methodOne()方法,methodOne()調(diào)用methodTwo()方法。

methodOne()聲明了一個(gè)原始類型的本地變量和一個(gè)引用類型的本地變量。

每個(gè)線程執(zhí)行methodOne()都會(huì)在它們對(duì)應(yīng)的線程棧上創(chuàng)建localVariable1localVariable2的私有拷貝。localVariable1變量彼此完全獨(dú)立,僅“生活”在每個(gè)線程的線程棧上。一個(gè)線程看不到另一個(gè)線程對(duì)它的localVariable1私有拷貝做出的修改。

每個(gè)線程執(zhí)行methodOne()時(shí)也將會(huì)創(chuàng)建它們各自的localVariable2拷貝。然而,兩個(gè)localVariable2的不同拷貝都指向堆上的同一個(gè)對(duì)象。代碼中通過(guò)一個(gè)靜態(tài)變量設(shè)置localVariable2指向一個(gè)對(duì)象引用。僅存在一個(gè)靜態(tài)變量的一份拷貝,這份拷貝存放在堆上。因此,localVariable2的兩份拷貝都指向由MySharedObject指向的靜態(tài)變量的同一個(gè)實(shí)例。MySharedObject實(shí)例也存放在堆上。它對(duì)應(yīng)于上圖中的Object3。

注意,MySharedObject類也包含兩個(gè)成員變量。這些成員變量隨著這個(gè)對(duì)象存放在堆上。這兩個(gè)成員變量指向另外兩個(gè)Integer對(duì)象。這些Integer對(duì)象對(duì)應(yīng)于上圖中的Object2和Object4.

注意,methodTwo()創(chuàng)建一個(gè)名為localVariable的本地變量。這個(gè)成員變量是一個(gè)指向一個(gè)Integer對(duì)象的對(duì)象引用。這個(gè)方法設(shè)置localVariable1引用指向一個(gè)新的Integer實(shí)例。在執(zhí)行methodTwo方法時(shí),localVariable1引用將會(huì)在每個(gè)線程中存放一份拷貝。這兩個(gè)Integer對(duì)象實(shí)例化將會(huì)被存儲(chǔ)堆上,但是每次執(zhí)行這個(gè)方法時(shí),這個(gè)方法都會(huì)創(chuàng)建一個(gè)新的Integer對(duì)象,兩個(gè)線程執(zhí)行這個(gè)方法將會(huì)創(chuàng)建兩個(gè)不同的Integer實(shí)例。methodTwo方法創(chuàng)建的Integer對(duì)象對(duì)應(yīng)于上圖中的Object1和Object5。

還有一點(diǎn),MySharedObject類中的兩個(gè)long類型的成員變量是原始類型的。因?yàn)?,這些變量是成員變量,所以它們?nèi)稳浑S著該對(duì)象存放在堆上,僅有本地變量存放在線程棧上。

1.2 JVM對(duì)象分代內(nèi)存劃分與垃圾回收

前面的內(nèi)容分析了JVM運(yùn)行時(shí)的內(nèi)存區(qū)域劃分,并代碼進(jìn)行了實(shí)際的講解。下面我們對(duì)象分代年齡的角度對(duì)JVM內(nèi)存進(jìn)行劃分,這與JVM的垃圾回收息息相關(guān)。

  我們知道,JVM會(huì)動(dòng)態(tài)的幫我們進(jìn)行內(nèi)存分配,對(duì)象沒(méi)有引用的時(shí)候稱為垃圾,垃圾回收機(jī)就會(huì)進(jìn)行回收。

 有些對(duì)象會(huì)頻繁的創(chuàng)建于銷毀,例如我們?cè)诜椒ɡ锩媸褂玫骄植孔兞?,每次調(diào)用,方法里的局部變量就要經(jīng)歷創(chuàng)建與銷毀的過(guò)程。

 而另外一些對(duì)象可能會(huì)常駐內(nèi)存,例如靜態(tài)成員變量,又或者我們?cè)贘2EE開發(fā)中的Servelt、Filter等。

來(lái)自IBM的一組統(tǒng)計(jì)數(shù)據(jù):

98%的java對(duì)象,在創(chuàng)建之后不久就變成了非活動(dòng)對(duì)象;只有2%的對(duì)象,會(huì)在長(zhǎng)時(shí)間一直處于活動(dòng)狀態(tài)。

如果能對(duì)這兩種對(duì)象區(qū)分對(duì)象,那么會(huì)提高GC的效率。在sun jdk gc中(具體的說(shuō),是在jdk1.4之后的版本),提出了不同生命周期的GC策略。

 現(xiàn)在我們從另外一個(gè)角度對(duì)JVM的運(yùn)行時(shí)內(nèi)存進(jìn)行劃分:堆(Heap)堆和非堆(Non-heap)內(nèi)存

 按照官方的說(shuō)法:“Java 虛擬機(jī)具有一個(gè)堆,堆是運(yùn)行時(shí)數(shù)據(jù)區(qū)域,所有類實(shí)例和數(shù)組的內(nèi)存均從此處分配。堆是在 Java 虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建的?!薄霸贘VM中堆之外的內(nèi)存稱為非堆內(nèi)存(Non-heap memory)”。

簡(jiǎn)單來(lái)說(shuō)堆就是Java代碼可及的內(nèi)存,是留給開發(fā)人員使用的;

非堆就是JVM留給 自己用的,所以方法區(qū)、JVM內(nèi)部處理或優(yōu)化所需的內(nèi)存(如JIT編譯后的代碼緩存)、每個(gè)類結(jié)構(gòu)(如運(yùn)行時(shí)常數(shù)池、字段和方法數(shù)據(jù))以及方法和構(gòu)造方法 的代碼都在非堆內(nèi)存中。

一、基于對(duì)象分代年齡的內(nèi)存劃分

從對(duì)象分代的角度來(lái)說(shuō),JVM的內(nèi)存劃分大致分為3塊:分別是Permanent Generation(簡(jiǎn)稱PermGen、持久代)、New Generation(又叫Young Generation,年輕代)和Tenured Generation(又叫Old Generation,年老代)。其中Permanent Generation位于non-heap區(qū)。New和Old是Java應(yīng)用的Heap區(qū),用來(lái)存放類的實(shí)例Instance的。我們?cè)谶@里主要討論的Heap區(qū)如何根據(jù)對(duì)象分代年齡進(jìn)行劃分。如下圖所示:

其中New Generation的目標(biāo)就是盡可能快速的收集掉那些生命周期短的對(duì)象;New Generation又分為Eden SpaceFrom Survivor Space和To Survivor Space三塊,Eden Space用于存放新創(chuàng)建的對(duì)象,F(xiàn)rom區(qū)和To區(qū)都是救助空間Survivor Space(圖中的S0和S1);當(dāng)Eden區(qū)滿時(shí),JVM執(zhí)行垃圾回收GC(Garbage Collection),垃圾收集器暫停應(yīng)用程序,并會(huì)將Eden Space還存活的對(duì)象復(fù)制到當(dāng)前的From救助空間,一旦當(dāng)前的From救助空間充滿,此區(qū)的存活對(duì)象將被復(fù)制到另外一個(gè)To區(qū),當(dāng)To區(qū)也滿了的時(shí)候,從From區(qū)復(fù)制過(guò)來(lái)并且依然存活的對(duì)象復(fù)制到Old區(qū),從而From和To救助空間互換角色,維持活動(dòng)的對(duì)象將在救助空間不斷復(fù)制,直到最終轉(zhuǎn)入Old域。需要注意,Survivor的兩個(gè)區(qū)是對(duì)稱的,沒(méi)先后關(guān)系,所以同一個(gè)區(qū)中可能同時(shí)存在從Eden復(fù)制過(guò)來(lái)對(duì)象,和從前一個(gè)Survivor復(fù)制過(guò)來(lái)的對(duì)象,而且,Survivor區(qū)總有一個(gè)是空的。同時(shí),根據(jù)程序需要,Survivor區(qū)是可以配置為多個(gè)的(多于兩個(gè)),這樣可以增加對(duì)象在年輕代中的存在時(shí)間,減少被放到年老代的可能。每一次垃圾回收后,Eden Space都會(huì)被清空。

Old區(qū)用于存放長(zhǎng)壽的對(duì)象,在New區(qū)中經(jīng)歷了N次垃圾回收后仍然存活的對(duì)象,就會(huì)被放到Old區(qū)中;如那些與業(yè)務(wù)信息相關(guān)的對(duì)象,包括Http請(qǐng)求中的Session對(duì)象、線程、Socket連接,這類對(duì)象跟業(yè)務(wù)直接掛鉤,因此生命周期比較長(zhǎng)。

二、基于對(duì)象分代年齡的垃圾回收

在以上提到的任何一個(gè)空間不夠用時(shí),都會(huì)促使JVM執(zhí)行垃圾回收。基于回收類型的不同,我們將垃圾回收劃分為:

Minor GC(又叫Young GC):回收New Generation內(nèi)存空間(Eden、From Survivor、To Survivor);

Full GC:回收New Generation和Tenured Generation、PermGen內(nèi)存空間

Minor GC/Young GC觸發(fā)

當(dāng)新對(duì)象生成,并且在Eden申請(qǐng)空間失敗時(shí),就會(huì)觸發(fā)Minor GC,對(duì)Eden區(qū)域進(jìn)行GC,清除非存活對(duì)象,并且把尚且存活的對(duì)象移動(dòng)到Survivor區(qū),然后整理Survivor的兩個(gè)區(qū)。這種方式的GC是對(duì)New區(qū)的Eden區(qū)進(jìn)行,不會(huì)影響到Old區(qū)。因?yàn)榇蟛糠謱?duì)象都是從Eden區(qū)開始的,同時(shí)Eden區(qū)不會(huì)分配的很大,所以Eden區(qū)的Minor GC會(huì)頻繁進(jìn)行。因而,一般在這里需要使用速度快、效率高的算法,使Eden去能盡快空閑出來(lái)。

Full GC觸發(fā)

Full GC要對(duì)整個(gè)Heap區(qū)進(jìn)行回收,包括New、Old和PermGen,所以比Minor GC要慢,因此應(yīng)該盡可能減少Full GC的次數(shù)。在對(duì)JVM性能調(diào)優(yōu)的過(guò)程中,很大一部分工作就是對(duì)于Full GC的調(diào)節(jié)。有如下原因可能導(dǎo)致Full GC:

·Tenured區(qū)被寫滿

·PermGen區(qū)被寫滿

·System.gc()被顯示調(diào)用

·上一次GC之后Heap的各域分配策略動(dòng)態(tài)變化

需要注意的是,F(xiàn)ull GC與YoungGC是無(wú)法完全區(qū)分開來(lái)的,很多情況下,F(xiàn)ull GC是由YoungGC導(dǎo)致的。例如在Eden Space中的對(duì)象經(jīng)歷過(guò)幾次垃圾回收,依然還存活,就會(huì)移動(dòng)到Tenured區(qū),而如果此時(shí)Tenured區(qū)空間不夠,就會(huì)出發(fā)垃圾回收,過(guò)程如下圖所示:


對(duì)于Survivor區(qū)域,其實(shí)際上就是經(jīng)歷過(guò)幾次垃圾回收依然沒(méi)有被回收掉的對(duì)象(稱之為幸存者)過(guò)渡到Tenured區(qū)的臨時(shí)存儲(chǔ)空間。

2.0 垃圾收集器深入

Java與C++之間有一堵由內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)所圍成的高墻,墻外面的人想進(jìn)去,墻里面的人卻想出來(lái)。

GC的歷史遠(yuǎn)遠(yuǎn)比Java久遠(yuǎn),1960年誕生于MIT的Lisp是第一門真正使用內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)的語(yǔ)言。當(dāng)Lisp還在胚胎時(shí)期時(shí),人們就在思考GC需要完成的三件事情:

  1. 哪些內(nèi)存需要回收?

  2. 什么時(shí)候回收?

  3. 如何回收?

前面介紹了Java內(nèi)存運(yùn)行時(shí)區(qū)域的各個(gè)部分,其中程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧三個(gè)區(qū)域隨線程而生,隨線程而滅;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作。每一個(gè)棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來(lái)時(shí)就已知的(盡管在運(yùn)行期會(huì)由JIT編譯器進(jìn)行一些優(yōu)化,但在本章基于概念模型的討論中,大體上可以認(rèn)為是編譯期可知的),因此這幾個(gè)區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個(gè)區(qū)域內(nèi)不需要過(guò)多考慮回收的問(wèn)題,因?yàn)榉椒ńY(jié)束或線程結(jié)束時(shí),內(nèi)存自然就跟隨著回收了。

而Java堆和方法區(qū)則不一樣,一個(gè)接口中的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運(yùn)行期間時(shí)才能知道會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,垃圾收集器所關(guān)注的是這部分內(nèi)存,本教程后續(xù)討論中的“內(nèi)存”分配與回收也僅指這一部分內(nèi)存。  

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

HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為三塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)

1 對(duì)象頭

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ī)(暫不考慮開啟壓縮指針的場(chǎng)景)中分別為32個(gè)和64個(gè)Bits,官方 稱它為“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ǔ)盡量多的信息,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。例如在32位的HotSpot虛擬機(jī) 中對(duì)象未被鎖定的狀態(tài)下,Mark Word的32個(gè)Bits空間中的25Bits用于存儲(chǔ)對(duì)象哈希碼(HashCode),4Bits用于存儲(chǔ)對(duì)象分代年齡,2Bits用于存儲(chǔ)鎖標(biāo)志 位,1Bit固定為0,在其他狀態(tài)(輕量級(jí)鎖定、重量級(jí)鎖定、GC標(biāo)記、可偏向)下對(duì)象的存儲(chǔ)內(nèi)容如下表所示。

以下是HotSpot虛擬機(jī)markOop.cpp中的代碼(注釋)片段,它描述了32bits下MarkWord的存儲(chǔ)狀態(tài):對(duì)象頭的另外一部分是類型指針,即是對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象 數(shù)據(jù)上保留類型指針,換句話說(shuō)查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過(guò)對(duì)象本身,這點(diǎn)我們?cè)谙乱还?jié)討論。另外,如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必 須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過(guò)普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小,但是從數(shù)組的元數(shù)據(jù)中無(wú)法確定數(shù)組的大小。

  1. // Bit-format of an object header (most significant first, big endian layout below):

  2. //

  3. //  32 bits:

  4. //  --------

  5. //  hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)

  6. //  JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)

  7. //  size:32 ------------------------------------------>| (CMS free block)

  8. //  PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

接下來(lái)實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也既是我們?cè)诔绦虼a里面所定義的各種類型的字段內(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ì)齊填充并不是必然存在的,也沒(méi)有特別的含義,它僅僅起著占位符的作用。由于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ù)部分沒(méi)有對(duì)齊的話,就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。 

2 實(shí)例數(shù)據(jù)(Instance Data)

接下來(lái)實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也既是我們?cè)诔绦虼a里面所定義的各種類型的字段內(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ì)插入到父類變量的空隙之中。

3 對(duì)齊填充 

第三部分對(duì)齊填充并不是必然存在的,也沒(méi)有特別的含義,它僅僅起著占位符的作用。由于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ù)部分沒(méi)有對(duì)齊的話,就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。 

2.2 判斷對(duì)象是否已死

對(duì)象已死?

堆中幾乎存放著Java世界中所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆進(jìn)行回收前,第一件事情就是要確定這些對(duì)象有哪些還“存活”著,哪些已經(jīng)“死去”(即不可能再被任何途徑使用的對(duì)象)。判斷對(duì)象是否已死的方法包括。

1、引用計(jì)數(shù)算法判斷

很多教科書判斷對(duì)象是否存活的算法是這樣的:給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1;任何時(shí)刻計(jì)數(shù)器都為0的對(duì)象就是不可能再被使用的。筆者面試過(guò)很多的應(yīng)屆生和一些有多年工作經(jīng)驗(yàn)的開發(fā)人員,他們對(duì)于這個(gè)問(wèn)題給予的都是這個(gè)答案。

客觀地說(shuō),引用計(jì)數(shù)算法(Reference Counting)的實(shí)現(xiàn)簡(jiǎn)單,判定效率也很高,在大部分情況下它都是一個(gè)不錯(cuò)的算法,也有一些比較著名的應(yīng)用案例,例如微軟的COM(Component Object Model)技術(shù)、使用ActionScript 3的FlashPlayer、Python語(yǔ)言以及在游戲腳本領(lǐng)域中被廣泛應(yīng)用的Squirrel中都使用了引用計(jì)數(shù)算法進(jìn)行內(nèi)存管理。但是,Java語(yǔ)言中沒(méi)有選用引用計(jì)數(shù)算法來(lái)管理內(nèi)存,其中最主要的原因是它很難解決對(duì)象之間的相互循環(huán)引用的問(wèn)題。

2、根搜索算法

在主流的商用程序語(yǔ)言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索算法(GC Roots Tracing)判定對(duì)象是否存活的。這個(gè)算法的基本思路就是通過(guò)一系列的名為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過(guò)的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連(用圖論的話來(lái)說(shuō)就是從GC Roots到這個(gè)對(duì)象不可達(dá))時(shí),則證明此對(duì)象是不可用的。如下圖所示,對(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ì)象。

2.3 垃圾收集算法

由于垃圾收集算法的實(shí)現(xiàn)涉及大量的程序細(xì)節(jié),而且各個(gè)平臺(tái)的虛擬機(jī)操作內(nèi)存的方法又各不相同,因此本節(jié)不打算過(guò)多地討論算法的實(shí)現(xiàn),只是介紹幾種算法的思想及其發(fā)展過(guò)程。

目前常見垃圾回收算法有:

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

  • 復(fù)制算法

  • 標(biāo)記-整理算法

  • 分代收集算法

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

最基礎(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ì)其缺點(diǎn)進(jìn)行改進(jìn)而得到的。它的主要缺點(diǎn)有兩個(gè)

一個(gè)是效率問(wèn)題,標(biāo)記和清除過(guò)程的效率都不高;

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

復(fù)制算法

為了解決效率問(wèn)題,一種稱為“復(fù)制”(Copying)的收集算法出現(xiàn)了,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。這樣使得每次都是對(duì)其中的一塊進(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ò)程如圖所示。

現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來(lái)回收新生代,IBM的專門研究表明,新生代中的對(duì)象98%是朝生夕死的,所以并不需要按照1∶1的比例來(lái)劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當(dāng)回收時(shí),將Eden和Survivor中還存活著的對(duì)象一次性地拷貝到另外一塊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ù),我們沒(méi)有辦法保證每次回收都只有不多于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)為沒(méi)有風(fēng)險(xiǎn)了。內(nèi)存的分配擔(dān)保也一樣,如果另外一塊Survivor空間沒(méi)有足夠的空間存放上一次新生代收集下來(lái)的存活對(duì)象,這些對(duì)象將直接通過(guò)分配擔(dān)保機(jī)制進(jìn)入老年代。關(guān)于對(duì)新生代進(jìn)行分配擔(dān)保的內(nèi)容,本章稍后在講解垃圾收集器執(zhí)行規(guī)則時(shí)還會(huì)再詳細(xì)講解。

標(biāo)記-整理算法

復(fù)制收集算法在對(duì)象存活率較高時(shí)就要執(zhí)行較多的復(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)記-整理”算法的示意圖如圖所示。


分代收集算法

當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”(Generational Collection)算法,這種算法并沒(méi)有什么新的思想,只是根據(jù)對(duì)象的存活周期的不同將內(nèi)存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/span>在新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法來(lái)進(jìn)行回收。

2.4 垃圾收集器

如果說(shuō)收集算法是內(nèi)存回收的方法論,垃圾收集器就是內(nèi)存回收的具體實(shí)現(xiàn)。Java虛擬機(jī)規(guī)范中對(duì)垃圾收集器應(yīng)該如何實(shí)現(xiàn)并沒(méi)有任何規(guī)定,因此不同的廠商、不同版本的虛擬機(jī)所提供的垃圾收集器都可能會(huì)有很大的差別,并且一般都會(huì)提供參數(shù)供用戶根據(jù)自己的應(yīng)用特點(diǎn)和要求組合出各個(gè)年代所使用的收集器。這里討論的收集器基于Sun HotSpot虛擬機(jī)1.7版 Update 14,這個(gè)虛擬機(jī)包含的所有收集器如圖所示。

上圖展示了7種作用于不同分代的收集器(包括JDK 1.6_Update14后引入的Early Access版G1收集器),如果兩個(gè)收集器之間存在連線,就說(shuō)明它們可以搭配使用。虛擬機(jī)所處的區(qū)域,則表示它是屬于新生代收集器還是老年代收集器。

在介紹這些收集器各自的特性之前,我們先來(lái)明確一個(gè)觀點(diǎn):雖然我們是在對(duì)各個(gè)收集器進(jìn)行比較,但并非為了挑選一個(gè)最好的收集器出來(lái)。因?yàn)橹钡浆F(xiàn)在為止還沒(méi)有最好的收集器出現(xiàn),更加沒(méi)有萬(wàn)能的收集器,所以我們選擇的只是對(duì)具體應(yīng)用最合適的收集器。這點(diǎn)不需要多加解釋就能證明:如果有一種放之四海皆準(zhǔn)、任何場(chǎng)景下都適用的完美收集器存在,那HotSpot虛擬機(jī)就沒(méi)必要實(shí)現(xiàn)那么多不同的收集器了。

概念理解

1 并發(fā)和并行

這兩個(gè)名詞都是并發(fā)編程中的概念,在談?wù)摾占鞯纳舷挛恼Z(yǔ)境中,它們可以解釋如下。

  • 并行(Parallel):指多條垃圾收集線程并行工作,但此時(shí)用戶線程仍然處于等待狀態(tài)。

  • 并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時(shí)執(zhí)行(但不一定是并行的,可能會(huì)交替執(zhí)行),用戶程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行于另一個(gè)CPU上。

2 Minor GC 和 Full GC

  • 新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)镴ava對(duì)象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。

  • 老年代GC(Major GC / Full GC):指發(fā)生在老年代的GC,出現(xiàn)了Major GC,經(jīng)常會(huì)伴隨至少一次的Minor GC(但非絕對(duì)的,在Parallel Scavenge收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過(guò)程)。Major GC的速度一般會(huì)比Minor GC慢10倍以上。

3 吞吐量

吞吐量就是CPU用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值,即吞吐量 = 運(yùn)行用戶代碼時(shí)間 /(運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間)。

虛擬機(jī)總共運(yùn)行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

一、Serial收集器

Serial收集器是最基本、歷史最悠久的收集器,曾經(jīng)(在JDK 1.3.1之前)是虛擬機(jī)新生代收集的唯一選擇。大家看名字就知道,這個(gè)收集器是一個(gè)單線程的收集器,但它的“單線程”的意義并不僅僅是說(shuō)明它只會(huì)使用一個(gè)CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線程(Sun將這件事情稱之為“Stop The World”),直到它收集結(jié)束?!癝top The World”這個(gè)名字也許聽起來(lái)很酷,但這項(xiàng)工作實(shí)際上是由虛擬機(jī)在后臺(tái)自動(dòng)發(fā)起和自動(dòng)完成的,在用戶不可見的情況下把用戶的正常工作的線程全部停掉,這對(duì)很多應(yīng)用來(lái)說(shuō)都是難以接受的。你想想,要是你的電腦每運(yùn)行一個(gè)小時(shí)就會(huì)暫停響應(yīng)5分鐘,你會(huì)有什么樣的心情?圖3-6示意了Serial / Serial Old收集器的運(yùn)行過(guò)程。

對(duì)于“Stop The World”帶給用戶的惡劣體驗(yàn),虛擬機(jī)的設(shè)計(jì)者們表示完全理解,但也表示非常委屈:“你媽媽在給你打掃房間的時(shí)候,肯定也會(huì)讓你老老實(shí)實(shí)地在椅子上或房間外待著,如果她一邊打掃,你一邊亂扔紙屑,這房間還能打掃完嗎?”這確實(shí)是一個(gè)合情合理的矛盾,雖然垃圾收集這項(xiàng)工作聽起來(lái)和打掃房間屬于一個(gè)性質(zhì)的,但實(shí)際上肯定還要比打掃房間復(fù)雜得多??!

二、ParNew收集器

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收集器的工作過(guò)程如圖所示。

ParNew收集器除了多線程收集之外,其他與Serial收集器相比并沒(méi)有太多創(chuàng)新之處,但它卻是許多運(yùn)行在Server模式下的虛擬機(jī)中首選的新生代收集器,其中有一個(gè)與性能無(wú)關(guān)但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。在JDK 1.5時(shí)期,HotSpot推出了一款在強(qiáng)交互應(yīng)用中幾乎可稱為有劃時(shí)代意義的垃圾收集器—CMS收集器(Concurrent Mark Sweep,本節(jié)稍后將詳細(xì)介紹這款收集器),這款收集器是HotSpot虛擬機(jī)中第一款真正意義上的并發(fā)(Concurrent)收集器,它第一次實(shí)現(xiàn)了讓垃圾收集線程與用戶線程(基本上)同時(shí)工作,用前面那個(gè)例子的話來(lái)說(shuō),就是做到了在你媽媽打掃房間的時(shí)候你還能同時(shí)往地上扔紙屑。

不幸的是,它作為老年代的收集器,卻無(wú)法與JDK 1.4.0中已經(jīng)存在的新生代收集器Parallel Scavenge配合工作,所以在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ù)。

三、Parallel Scavenge收集器

Parallel Scavenge收集器也是一個(gè)新生代收集器,它也是使用復(fù)制算法的收集器,又是并行的多線程收集器……看上去和ParNew都一樣,那它有什么特別之處呢?

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è)定值。不過(guò)大家不要異想天開地認(rèn)為如果把這個(gè)參數(shù)的值設(shè)置得稍小一點(diǎn)就能使得系統(tǒng)的垃圾收集速度變得更快,GC停頓時(shí)間縮短是以犧牲吞吐量和新生代空間來(lái)?yè)Q取的:系統(tǒng)把新生代調(diào)小一些,收集300MB新生代肯定比收集500MB快吧,這也直接導(dǎo)致垃圾收集發(fā)生得更頻繁一些,原來(lái)10秒收集一次、每次停頓100毫秒,現(xiàn)在變成5秒收集一次、每次停頓70毫秒。停頓時(shí)間的確在下降,但吞吐量也降下來(lái)了。

GCTimeRatio參數(shù)的值應(yīng)當(dāng)是一個(gè)大于0小于100的整數(shù),也就是垃圾收集時(shí)間占總時(shí)間的比率,相當(dāng)于是吞吐量的倒數(shù)。如果把此參數(shù)設(shè)置為19,那允許的最大GC時(shí)間就占總時(shí)間的5%(即1 /(1+19)),默認(rèn)值為99,就是允許最大1%(即1 /(1+99))的垃圾收集時(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)。如果讀者對(duì)于收集器運(yùn)作原理不太了解,手工優(yōu)化存在困難的時(shí)候,使用Parallel Scavenge收集器配合自適應(yīng)調(diào)節(jié)策略,把內(nèi)存管理的調(diào)優(yōu)任務(wù)交給虛擬機(jī)去完成將是一個(gè)很不錯(cuò)的選擇。只需要把基本的內(nèi)存數(shù)據(jù)設(shè)置好(如-Xmx設(shè)置最大堆),然后使用MaxGCPauseMillis參數(shù)(更關(guān)注最大停頓時(shí)間)或GCTimeRatio參數(shù)(更關(guān)注吞吐量)給虛擬機(jī)設(shè)立一個(gè)優(yōu)化目標(biāo),那具體細(xì)節(jié)參數(shù)的調(diào)節(jié)工作就由虛擬機(jī)完成了。自適應(yīng)調(diào)節(jié)策略也是Parallel Scavenge收集器與ParNew收集器的一個(gè)重要區(qū)別。

四、Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個(gè)單線程收集器,使用“標(biāo)記-整理”算法。

這個(gè)收集器的主要意義也是被Client模式下的虛擬機(jī)使用。

如果在Server模式下,它主要還有兩大用途:一個(gè)是在JDK 1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另外一個(gè)就是作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure的時(shí)候使用。

這兩點(diǎn)都將在后面的內(nèi)容中詳細(xì)講解。Serial Old收集器的工作過(guò)程如圖所示。

五、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)用上獲得吞吐量最大化的效果,又因?yàn)槔夏甏占袩o(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ò)程如圖所示。

六、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è)步驟,包括:

  1. 初始標(biāo)記(CMS initial mark)

  2. 并發(fā)標(biāo)記(CMS concurrent mark)

  3. 重新標(biāo)記(CMS remark)

  4. 并發(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 Roots Tracing的過(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á)不到完美的程度,它有以下三個(gè)顯著的缺點(diǎn):

CMS收集器對(duì)CPU資源非常敏感。其實(shí),面向并發(fā)設(shè)計(jì)的程序都對(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í)垃圾收集線程最多占用不超過(guò)25%的CPU資源。但是當(dāng)CPU不足4個(gè)時(shí)(譬如2個(gè)),那么CMS對(duì)用戶程序的影響就可能變得很大,如果CPU負(fù)載本來(lái)就比較大的時(shí)候,還分出一半的運(yùn)算能力去執(zhí)行收集器線程,就可能導(dǎo)致用戶程序的執(zhí)行速度忽然降低了50%,這也很讓人受不了。為了解決這種情況,虛擬機(jī)提供了一種稱為“增量式并發(fā)收集器”(Incremental Concurrent Mark Sweep / i-CMS)的CMS收集器變種,所做的事情和單CPU年代PC機(jī)操作系統(tǒng)使用搶占式來(lái)模擬多任務(wù)機(jī)制的思想一樣,就是在并發(fā)標(biāo)記和并發(fā)清理的時(shí)候讓GC線程、用戶線程交替運(yùn)行,盡量減少GC線程的獨(dú)占資源的時(shí)間,這樣整個(gè)垃圾收集的過(guò)程會(huì)更長(zhǎng),但對(duì)用戶程序的影響就會(huì)顯得少一些,速度下降也就沒(méi)有那么明顯,但是目前版本中,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ú)法在本次收集中處理掉它們,只好留待下一次GC時(shí)再將其清理掉。這一部分垃圾就稱為“浮動(dòng)垃圾”。也是由于在垃圾收集階段用戶線程還需要運(yùn)行,即還需要預(yù)留足夠的內(nèi)存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用。在默認(rèn)設(shè)置下,CMS收集器在老年代使用了68%的空間后就會(huì)被激活,這是一個(gè)偏保守的設(shè)置,如果在應(yīng)用中老年代增長(zhǎng)不是太快,可以適當(dāng)調(diào)高參數(shù)-XX:CMSInitiatingOccupancyFraction的值來(lái)提高觸發(fā)百分比,以便降低內(nèi)存回收次數(shù)以獲取更好的性能。要是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:CMSInitiatingOccupancyFraction設(shè)置得太高將會(huì)很容易導(dǎo)致大量“Concurrent Mode Failure”失敗,性能反而降低。

還有最后一個(gè)缺點(diǎn),在本節(jié)在開頭說(shuō)過(guò),CMS是一款基于“標(biāo)記-清除”算法實(shí)現(xiàn)的收集器,如果讀者對(duì)前面這種算法介紹還有印象的話,就可能想到這意味著收集結(jié)束時(shí)會(huì)產(chǎn)生大量空間碎片。空間碎片過(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ù),用于在“享受”完Full GC服務(wù)之后額外免費(fèi)附送一個(gè)碎片整理過(guò)程,內(nèi)存整理的過(guò)程是無(wú)法并發(fā)的??臻g碎片問(wèn)題沒(méi)有了,但停頓時(shí)間不得不變長(zhǎng)了。虛擬機(jī)設(shè)計(jì)者們還提供了另外一個(gè)參數(shù)-XX: CMSFullGCsBeforeCompaction,這個(gè)參數(shù)用于設(shè)置在執(zhí)行多少次不壓縮的Full GC后,跟著來(lái)一次帶壓縮的。

七、G1收集器

G1(Garbage First)收集器是當(dāng)前收集器技術(shù)發(fā)展的最前沿成果,在JDK 1.6_Update14中提供了Early Access版本的G1收集器以供試用。在JDK 1.7正式發(fā)布的時(shí)候,G1收集器稱為成熟的商用版本隨之發(fā)布。這里只對(duì)G1收集器進(jìn)行簡(jiǎn)單介紹。

G1收集器是垃圾收集器理論進(jìn)一步發(fā)展的產(chǎn)物,它與前面的CMS收集器相比有兩個(gè)顯著的改進(jìn):一是G1收集器是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器,也就是說(shuō)它不會(huì)產(chǎn)生空間碎片,這對(duì)于長(zhǎng)時(shí)間運(yùn)行的應(yīng)用系統(tǒng)來(lái)說(shuō)非常重要。二是它可以非常精確地控制停頓,既能讓使用者明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過(guò)N毫秒,這幾乎已經(jīng)是實(shí)時(shí)Java(RTSJ)的垃圾收集器的特征了。

G1收集器可以實(shí)現(xiàn)在基本不犧牲吞吐量的前提下完成低停頓的內(nèi)存回收,這是由于它能夠極力地避免全區(qū)域的垃圾收集,之前的收集器進(jìn)行收集的范圍都是整個(gè)新生代或老年代,而G1將整個(gè)Java堆(包括新生代、老年代)劃分為多個(gè)大小固定的獨(dú)立區(qū)域(Region),并且跟蹤這些區(qū)域里面的垃圾堆積程度,在后臺(tái)維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收垃圾最多的區(qū)域(這就是Garbage First名稱的來(lái)由)。區(qū)域劃分及有優(yōu)先級(jí)的區(qū)域回收,保證了G1收集器在有限的時(shí)間內(nèi)可以獲得最高的收集效率。

3.0 JVM啟動(dòng)參數(shù)大全及默認(rèn)值

Java啟動(dòng)參數(shù)共分為三類;

其一是標(biāo)準(zhǔn)參數(shù)(-),所有的JVM實(shí)現(xiàn)都必須實(shí)現(xiàn)這些參數(shù)的功能,而且向后兼容;

其二是非標(biāo)準(zhǔn)參數(shù)(-X),默認(rèn)jvm實(shí)現(xiàn)這些參數(shù)的功能,但是并不保證所有jvm實(shí)現(xiàn)都滿足,且不保證向后兼容;

其三是非Stable參數(shù)(-XX),此類參數(shù)各個(gè)jvm實(shí)現(xiàn)會(huì)有所不同,將來(lái)可能會(huì)隨時(shí)取消,需要慎重使用;

一、JVM標(biāo)準(zhǔn)參數(shù)(-)

JVM的標(biāo)準(zhǔn)參數(shù)都是以"-"開頭,通過(guò)輸入"java -help"或者"java -?",可以查看JVM標(biāo)準(zhǔn)參數(shù)列表。如

以下是JVM標(biāo)準(zhǔn)參數(shù)的詳細(xì)介紹(紅色標(biāo)記的參數(shù)請(qǐng)著重注意):

-client 

 設(shè)置jvm使用client模式,特點(diǎn)是啟動(dòng)速度比較快,但運(yùn)行時(shí)性能和內(nèi)存管理效率不高,通常用于客戶端應(yīng)用程序或者PC應(yīng)用開發(fā)和調(diào)試。

-server

 設(shè)置jvm使server模式,特點(diǎn)是啟動(dòng)速度比較慢,但運(yùn)行時(shí)性能和內(nèi)存管理效率很高,適用于生產(chǎn)環(huán)境。在具有64位能力的jdk環(huán)境下將默認(rèn)啟用該模式,而忽略-client參數(shù)。

-agentlib:libname[=options] 

 用于裝載本地lib包;

 其中l(wèi)ibname為本地代理庫(kù)文件名,默認(rèn)搜索路徑為環(huán)境變量PATH中的路徑,options為傳給本地庫(kù)啟動(dòng)時(shí)的參數(shù),多個(gè)參數(shù)之間用逗號(hào)分隔。在Windows平臺(tái)上jvm搜索本地庫(kù)名為libname.dll的文件,在linux上jvm搜索本地庫(kù)名為libname.so的文件,搜索路徑環(huán)境變量在不同系統(tǒng)上有所不同,比如Solaries上就默認(rèn)搜索LD_LIBRARY_PATH。

 比如:-agentlib:hprof

 用來(lái)獲取jvm的運(yùn)行情況,包括CPU、內(nèi)存、線程等的運(yùn)行數(shù)據(jù),并可輸出到指定文件中;windows中搜索路徑為JRE_HOME/bin/hprof.dll。

-agentpath:pathname[=options] 

 按全路徑裝載本地庫(kù),不再搜索PATH中的路徑;其他功能和agentlib相同;更多的信息待續(xù),在后續(xù)的JVMTI部分會(huì)詳述。

-classpath classpath 

-cp classpath 

 告知jvm搜索目錄名、jar文檔名、zip文檔名,之間用分號(hào);分隔;使用-classpath后jvm將不再使用CLASSPATH中的類搜索路徑,如果-classpath和CLASSPATH都沒(méi)有設(shè)置,則jvm使用當(dāng)前路徑(.)作為類搜索路徑。

 jvm搜索類的方式和順序?yàn)椋築ootstrap,Extension,User。

 Bootstrap中的路徑是jvm自帶的jar或zip文件,jvm首先搜索這些包文件,用System.getProperty("sun.boot.class.path")可得到搜索路徑。

 Extension是位于JRE_HOME/lib/ext目錄下的jar文件,jvm在搜索完Bootstrap后就搜索該目錄下的jar文件,用System.getProperty("java.ext.dirs")可得到搜索路徑。

 User搜索順序?yàn)楫?dāng)前路徑.、CLASSPATH、-classpath,jvm最后搜索這些目錄,用System.getProperty("java.class.path")可得到搜索路徑。

-Dproperty=value

 設(shè)置系統(tǒng)屬性名/值對(duì),運(yùn)行在此jvm之上的應(yīng)用程序可用System.getProperty("property")得到value的值。

 如果value中有空格,則需要用雙引號(hào)將該值括起來(lái),如-Dname="space string"。

 該參數(shù)通常用于設(shè)置系統(tǒng)級(jí)全局變量值,如配置文件路徑,以便該屬性在程序中任何地方都可訪問(wèn)。

-enableassertions[:<package name>"..." | :<class name> ] 

-ea[:<package name>"..." | :<class name> ] 

 上述參數(shù)就用來(lái)設(shè)置jvm是否啟動(dòng)斷言機(jī)制(從JDK 1.4開始支持),缺省時(shí)jvm關(guān)閉斷言機(jī)制。

 用-ea 可打開斷言機(jī)制,不加<packagename>和classname時(shí)運(yùn)行所有包和類中的斷言,如果希望只運(yùn)行某些包或類中的斷言,可將包名或類名加到-ea之后。例如要啟動(dòng)包c(diǎn)om.wombat.fruitbat中的斷言,可用命令java -ea:com.wombat.fruitbat...<Main Class>。

-disableassertions[:<package name>"..." | :<class ; ] 

-da[:<package name>"..." | :<class name> ]

 用來(lái)設(shè)置jvm關(guān)閉斷言處理,packagename和classname的使用方法和-ea相同,jvm默認(rèn)就是關(guān)閉狀態(tài)。

 該參數(shù)一般用于相同package內(nèi)某些class不需要斷言的場(chǎng)景,比如com.wombat.fruitbat需要斷言,但是com.wombat.fruitbat.Brickbat該類不需要,則可以如下運(yùn)行:

 java -ea:com.wombat.fruitbat...-da:com.wombat.fruitbat.Brickbat <Main Class>。

-enablesystemassertions 

-esa 

 激活系統(tǒng)類的斷言。

-disablesystemassertions 

-dsa 

 關(guān)閉系統(tǒng)類的斷言。

-jar 

 指定以jar包的形式執(zhí)行一個(gè)應(yīng)用程序。

 要這樣執(zhí)行一個(gè)應(yīng)用程序,必須讓jar包的manifest文件中聲明初始加載的Main-class,當(dāng)然那Main-class必須有public static void main(String[] args)方法。

-javaagent:jarpath[=options] 

 指定jvm啟動(dòng)時(shí)裝入java語(yǔ)言設(shè)備代理。

 Jarpath文件中的mainfest文件必須有Agent-Class屬性。代理類也必須實(shí)現(xiàn)公共的靜態(tài)public static void premain(String agentArgs, Instrumentation inst)方法(和main方法類似)。當(dāng)jvm初始化時(shí),將按代理類的說(shuō)明順序調(diào)用premain方法;具體參見java.lang.instrument軟件包的描述。

-verbose 

-verbose:class 

 輸出jvm載入類的相關(guān)信息,當(dāng)jvm報(bào)告說(shuō)找不到類或者類沖突時(shí)可此進(jìn)行診斷。

-verbose:gc 

 輸出每次GC的相關(guān)情況。

-verbose:jni 

 輸出native方法調(diào)用的相關(guān)情況,一般用于診斷jni調(diào)用錯(cuò)誤信息。

-version 

 輸出java的版本信息,比如jdk版本、vendor、model。

-version:release 

 指定class或者jar運(yùn)行時(shí)需要的jdk版本信息;若指定版本未找到,則以能找到的系統(tǒng)默認(rèn)jdk版本執(zhí)行;一般情況下,對(duì)于jar文件,可以在manifest文件中指定需要的版本信息,而不是在命令行。

 release中可以指定單個(gè)版本,也可以指定一個(gè)列表,中間用空格隔開,且支持復(fù)雜組合,比如:

 -version:"1.5.0_04 1.5*&1.5.1_02+"

 指定class或者jar需要jdk版本為1.5.0_04或者是1.5系列中比1.5.1_02更高的所有版本。

-showversion 

 輸出java版本信息(與-version相同)之后,繼續(xù)輸出java的標(biāo)準(zhǔn)參數(shù)列表及其描述。

-? 

-help 

 輸出java標(biāo)準(zhǔn)參數(shù)列表及其描述。

二、JVM非標(biāo)準(zhǔn)參數(shù)(-X)

通過(guò)"java -X"可以輸出非標(biāo)準(zhǔn)參數(shù)列表,如下所示:

非標(biāo)準(zhǔn)參數(shù)又稱為擴(kuò)展參數(shù),其列表如下:

-Xint

 設(shè)置jvm以解釋模式運(yùn)行,所有的字節(jié)碼將被直接執(zhí)行,而不會(huì)編譯成本地碼。

-Xbatch

 關(guān)閉后臺(tái)代碼編譯,強(qiáng)制在前臺(tái)編譯,編譯完成之后才能進(jìn)行代碼執(zhí)行;

 默認(rèn)情況下,jvm在后臺(tái)進(jìn)行編譯,若沒(méi)有編譯完成,則前臺(tái)運(yùn)行代碼時(shí)以解釋模式運(yùn)行。

-Xbootclasspath:bootclasspath

 讓jvm從指定路徑(可以是分號(hào)分隔的目錄、jar、或者zip)中加載bootclass,用來(lái)替換jdk的rt.jar;若非必要,一般不會(huì)用到;

-Xbootclasspath/a:path

 將指定路徑的所有文件追加到默認(rèn)bootstrap路徑中;

-Xbootclasspath/p:path

 讓jvm優(yōu)先于bootstrap默認(rèn)路徑加載指定路徑的所有文件;

-Xcheck:jni

 對(duì)JNI函數(shù)進(jìn)行附加check;此時(shí)jvm將校驗(yàn)傳遞給JNI函數(shù)參數(shù)的合法性,在本地代碼中遇到非法數(shù)據(jù)時(shí),jmv將報(bào)一個(gè)致命錯(cuò)誤而終止;使用該參數(shù)后將造成性能下降,請(qǐng)慎用。

-Xfuture

 讓jvm對(duì)類文件執(zhí)行嚴(yán)格的格式檢查(默認(rèn)jvm不進(jìn)行嚴(yán)格格式檢查),以符合類文件格式規(guī)范,推薦開發(fā)人員使用該參數(shù)。

-Xnoclassgc

 關(guān)閉針對(duì)class的gc功能;因?yàn)槠渥柚箖?nèi)存回收,所以可能會(huì)導(dǎo)致OutOfMemoryError錯(cuò)誤,慎用;

-Xincgc

 開啟增量gc(默認(rèn)為關(guān)閉);這有助于減少長(zhǎng)時(shí)間GC時(shí)應(yīng)用程序出現(xiàn)的停頓;但由于可能和應(yīng)用程序并發(fā)執(zhí)行,所以會(huì)降低CPU對(duì)應(yīng)用的處理能力。

-Xloggc:file

 與-verbose:gc功能類似,只是將每次GC事件的相關(guān)情況記錄到一個(gè)文件中,文件的位置最好在本地,以避免網(wǎng)絡(luò)的潛在問(wèn)題。

 若與verbose命令同時(shí)出現(xiàn)在命令行中,則以-Xloggc為準(zhǔn)。

-Xms<size>

 指定jvm堆的初始大小,默認(rèn)為物理內(nèi)存的1/64,最小為1M;可以指定單位,比如k、m,若不指定,則默認(rèn)為字節(jié)。

-Xmx<size>

 指定jvm堆的最大值,默認(rèn)為物理內(nèi)存的1/4或者1G,最小為2M;單位與-Xms一致。

-Xss<size>

 設(shè)置單個(gè)線程棧的大小,一般默認(rèn)為512k。 

-Xprof

 輸出 cpu 配置文件數(shù)據(jù)

-Xrs

 減少jvm對(duì)操作系統(tǒng)信號(hào)(signals)的使用,該參數(shù)從1.3.1開始有效;

 從jdk1.3.0開始,jvm允許程序在關(guān)閉之前還可以執(zhí)行一些代碼(比如關(guān)閉數(shù)據(jù)庫(kù)的連接池),即使jvm被突然終止;

 jvm關(guān)閉工具通過(guò)監(jiān)控控制臺(tái)的相關(guān)事件而滿足以上的功能;更確切的說(shuō),通知在關(guān)閉工具執(zhí)行之前,先注冊(cè)控制臺(tái)的控制handler,然后對(duì)CTRL_C_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, and CTRL_SHUTDOWN_EVENT這幾類事件直接返回true。

 但如果jvm以服務(wù)的形式在后臺(tái)運(yùn)行(比如servlet引擎),他能接收CTRL_LOGOFF_EVENT事件,但此時(shí)并不需要初始化關(guān)閉程序;為了避免類似沖突的再次出現(xiàn),從jdk1.3.1開始提供-Xrs參數(shù);當(dāng)此參數(shù)被設(shè)置之后,jvm將不接收控制臺(tái)的控制handler,也就是說(shuō)他不監(jiān)控和處理CTRL_C_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, or CTRL_SHUTDOWN_EVENT事件。

上面這些參數(shù)中,比如-Xmsn、-Xmxn……都是我們性能優(yōu)化中很重要的參數(shù);

-Xprof、-Xloggc:file等都是在沒(méi)有專業(yè)跟蹤工具情況下排錯(cuò)的好手;

三、JVM非Stable參數(shù)(-XX)

Java 6(update 21oder 21之后)版本, HotSpot JVM 提供給了兩個(gè)新的參數(shù),在JVM啟動(dòng)后,在命令行中可以輸出所有XX參數(shù)和值。

  1. -XX:+PrintFlagsFinal and -XX:+PrintFlagsInitial

讀者可以使用以下語(yǔ)句輸出所有的參數(shù)和默認(rèn)值

  1. java -XX:+PrintFlagsInitial  -XX:+PrintFlagsInitial>>1.txt

由于非State參數(shù)非常的多,因此這里就不列出所有參數(shù)進(jìn)行講解。只介紹我們比較常用的。

Java HotSpot VM中-XX:的可配置參數(shù)列表進(jìn)行描述;

這些參數(shù)可以被松散的聚合成三類:

行為參數(shù)(Behavioral Options):用于改變jvm的一些基礎(chǔ)行為;

性能調(diào)優(yōu)(Performance Tuning):用于jvm的性能調(diào)優(yōu);

調(diào)試參數(shù)(Debugging Options):一般用于打開跟蹤、打印、輸出等jvm參數(shù),用于顯示jvm更加詳細(xì)的信息;

行為參數(shù)(功能開關(guān))

-XX:-DisableExplicitGC 禁止調(diào)用System.gc();但jvm的gc仍然有效

-XX:+MaxFDLimit 最大化文件描述符的數(shù)量限制

-XX:+ScavengeBeforeFullGC 新生代GC優(yōu)先于Full GC執(zhí)行

-XX:+UseGCOverheadLimit 在拋出OOM之前限制jvm耗費(fèi)在GC上的時(shí)間比例

-XX:-UseConcMarkSweepGC 對(duì)老生代采用并發(fā)標(biāo)記交換算法進(jìn)行GC

-XX:-UseParallelGC 啟用并行GC

-XX:-UseParallelOldGC 對(duì)Full GC啟用并行,當(dāng)-XX:-UseParallelGC啟用時(shí)該項(xiàng)自動(dòng)啟用

-XX:-UseSerialGC 啟用串行GC

-XX:+UseThreadPriorities 啟用本地線程優(yōu)先級(jí)

性能調(diào)優(yōu)

-XX:LargePageSizeInBytes=4m 設(shè)置用于Java堆的大頁(yè)面尺寸

-XX:MaxHeapFreeRatio=70 GC后java堆中空閑量占的最大比例

-XX:MaxNewSize=size 新生成對(duì)象能占用內(nèi)存的最大值

-XX:MaxPermSize=64m 老生代對(duì)象能占用內(nèi)存的最大值

-XX:MinHeapFreeRatio=40 GC后java堆中空閑量占的最小比例

-XX:NewRatio=2 新生代內(nèi)存容量與老生代內(nèi)存容量的比例

-XX:NewSize=2.125m 新生代對(duì)象生成時(shí)占用內(nèi)存的默認(rèn)值

-XX:ReservedCodeCacheSize=32m 保留代碼占用的內(nèi)存容量

-XX:ThreadStackSize=512 設(shè)置線程棧大小,若為0則使用系統(tǒng)默認(rèn)值

-XX:+UseLargePages 使用大頁(yè)面內(nèi)存

調(diào)試參數(shù)

-XX:-CITime 打印消耗在JIT編譯的時(shí)間

-XX:ErrorFile=./hs_err_pid<pid>.log 保存錯(cuò)誤日志或者數(shù)據(jù)到文件中

-XX:-ExtendedDTraceProbes 開啟solaris特有的dtrace探針

-XX:HeapDumpPath=./java_pid<pid>.hprof 指定導(dǎo)出堆信息時(shí)的路徑或文件名

-XX:-HeapDumpOnOutOfMemoryError 當(dāng)首次遭遇OOM時(shí)導(dǎo)出此時(shí)堆中相關(guān)信息

-XX:OnError="<cmd args>;<cmd args>" 出現(xiàn)致命ERROR之后運(yùn)行自定義命令

-XX:OnOutOfMemoryError="<cmd args>;<cmd args>" 當(dāng)首次遭遇OOM時(shí)執(zhí)行自定義命令

-XX:-PrintClassHistogram 遇到Ctrl-Break后打印類實(shí)例的柱狀信息,與jmap -histo功能相同

-XX:-PrintConcurrentLocks 遇到Ctrl-Break后打印并發(fā)鎖的相關(guān)信息,與jstack -l功能相同

-XX:-PrintCommandLineFlags 打印在命令行中出現(xiàn)過(guò)的標(biāo)記

-XX:-PrintCompilation 當(dāng)一個(gè)方法被編譯時(shí)打印相關(guān)信息

-XX:-PrintGC 每次GC時(shí)打印相關(guān)信息

-XX:-PrintGC Details 每次GC時(shí)打印詳細(xì)信息

-XX:-PrintGCTimeStamps 打印每次GC的時(shí)間戳

-XX:-TraceClassLoading 跟蹤類的加載信息

-XX:-TraceClassLoadingPreorder 跟蹤被引用到的所有類的加載信息

-XX:-TraceClassResolution 跟蹤常量池

-XX:-TraceClassUnloading 跟蹤類的卸載信息

-XX:-TraceLoaderConstraints 跟蹤類加載器約束的相關(guān)信息

4.0 Java自帶的性能監(jiān)測(cè)工具

JDK內(nèi)置工具使用

一、javah命令(C Header and Stub File Generator)

二、jps命令(Java Virtual Machine Process Status Tool)

三、jstack命令(Java Stack Trace)

四、jstat命令(Java Virtual Machine Statistics Monitoring Tool)

五、jmap命令(Java Memory Map)

六、jinfo命令(Java Configuration Info)

七、jconsole命令(Java Monitoring and Management Console)

八、jvisualvm命令(Java Virtual Machine Monitoring, Troubleshooting, and Profiling Tool)

九、jhat命令(Java Heap Analyse Tool)

十、Jdb命令(The Java Debugger)

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    91久久精品国产一区蜜臀| 午夜福利直播在线视频| 在线观看视频日韩精品 | 欧美精品在线播放一区二区| 国产亚洲精品久久99| 日本免费一级黄色录像| 欧美日韩国产综合在线| 久久女同精品一区二区| 91欧美日韩国产在线观看 | 欧美日韩国产一级91| 亚洲成人精品免费在线观看| 久久国产精品熟女一区二区三区| 亚洲欧美日韩国产综合在线| 亚洲一区二区三区有码| 国内女人精品一区二区三区| 中文字幕中文字幕一区二区| 亚洲中文字幕三区四区| 国产免费无遮挡精品视频| 午夜免费精品视频在线看| 国产日韩欧美在线播放| 91亚洲精品国产一区| 深夜少妇一区二区三区| 99久久国产综合精品二区| 久久夜色精品国产高清不卡| 亚洲精选91福利在线观看| 久热这里只有精品九九| 成人综合网视频在线观看| 亚洲最新中文字幕在线视频| 欧美一级特黄特色大色大片| 国产精品二区三区免费播放心| 一区二区三区亚洲国产| 极品少妇嫩草视频在线观看| 欧美尤物在线视频91| 亚洲精品中文字幕熟女| 可以在线看的欧美黄片| 久草视频在线视频在线观看| 日韩中文字幕人妻精品| 成年女人午夜在线视频| 熟妇久久人妻中文字幕| 空之色水之色在线播放| 欧美大黄片在线免费观看|