01 前言
前面我們了解了JVM相關(guān)的理論知識(shí),這章節(jié)主要從實(shí)戰(zhàn)方面,去解讀JVM。
02 類(lèi)加載機(jī)制
Java源代碼經(jīng)過(guò)編譯器編譯成字節(jié)碼之后,最終都需要加載到虛擬機(jī)之后才能運(yùn)行。虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從
Class 文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java 類(lèi)型,這就是虛擬機(jī)的類(lèi)加載機(jī)制。
2.1 類(lèi)加載時(shí)機(jī)
一個(gè)類(lèi)型從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,它的整個(gè)生命周期將會(huì)經(jīng)歷加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個(gè)階段,其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱(chēng)為連接(Linking)。這七個(gè)階段的發(fā)生順序下圖所示。
上圖中,加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段的順序是確定的,類(lèi)型的加載過(guò)程必須按照這種順序按部就班地開(kāi)始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開(kāi)始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定特性(也稱(chēng)為動(dòng)態(tài)綁定或晚期綁定)。
關(guān)于在什么情況下需要開(kāi)始類(lèi)加載過(guò)程的第一個(gè)階段“加載”,《Java虛擬機(jī)規(guī)范》中并沒(méi)有進(jìn)行強(qiáng)制約束,這點(diǎn)可以交給虛擬機(jī)的具體實(shí)現(xiàn)來(lái)自由把握。
但是對(duì)于初始化階段,《Java虛擬機(jī)規(guī)范》則是嚴(yán)格規(guī)定了有且只有六種情況必須立即對(duì)類(lèi)進(jìn)行“初始化”(而加
載、驗(yàn)證、準(zhǔn)備自然需要在此之前開(kāi)始):
- 遇到new、getstatic、putstatic 或invokestatic 這4 條字節(jié)碼指令;
- 使用java.lang.reflect 包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用的時(shí)候;
- 當(dāng)初始化一個(gè)類(lèi)的時(shí)候,發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行初始化的時(shí)候,需要先觸發(fā)其父類(lèi)的初始化;
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(lèi),虛擬機(jī)會(huì)先初始化這個(gè)類(lèi);
- 當(dāng)使用JDK 1.7 的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle 實(shí)例最后的解析結(jié)果
- REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類(lèi)沒(méi)有初始化。
- 當(dāng)一個(gè)接口中定義了JDK 8新加入的默認(rèn)方法(被default關(guān)鍵字修飾的接口方法)時(shí),如果有這個(gè)接口的實(shí)現(xiàn)。類(lèi)發(fā)生了初始化,那該接口要在其之前被初始化。
對(duì)于這六種會(huì)觸發(fā)類(lèi)型進(jìn)行初始化的場(chǎng)景,《Java虛擬機(jī)規(guī)范》中使用了一個(gè)非常強(qiáng)烈的限定語(yǔ)——“有且只有”,這六種場(chǎng)景中的行為稱(chēng)為對(duì)一個(gè)類(lèi)型進(jìn)行主動(dòng)引用。除此之外,所有引用類(lèi)型的方式都不會(huì)觸發(fā)初始化,稱(chēng)為被動(dòng)引用。
比如如下幾種場(chǎng)景就是被動(dòng)引用:
- 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,不會(huì)導(dǎo)致子類(lèi)的初始化;
- 通過(guò)數(shù)組定義來(lái)引用類(lèi),不會(huì)觸發(fā)此類(lèi)的初始化;
- 常量在編譯階段會(huì)存入調(diào)用類(lèi)的常量池中,本質(zhì)上并沒(méi)有直接引用到定義常量的類(lèi),因此不會(huì)觸發(fā)定義常量的類(lèi)的初始化;
2.2 類(lèi)加載過(guò)程
加載
在加載階段,Java虛擬機(jī)需要完成以下三件事情:
通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(lèi)的二進(jìn)制字節(jié)流。
將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
在內(nèi)存中生成一個(gè)代表這個(gè)類(lèi)的java.lang. Class對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的訪問(wèn)入口。
驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是確保Class文件的字節(jié)流中包含的信息符合《Java虛擬機(jī)規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運(yùn)行后不會(huì)危害虛擬機(jī)自身的安全。
驗(yàn)證階段大致上會(huì)完成下面4 個(gè)階段的檢驗(yàn)動(dòng)作:
- 文件格式驗(yàn)證 第一階段要驗(yàn)證字節(jié)流是否符合Class 文件格式的規(guī)范,并且能夠被當(dāng)前版本的虛擬機(jī)處理。驗(yàn)證點(diǎn)主要包括:是否以魔數(shù)0xCAFEBABE 開(kāi)頭;主、次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi);常量池的常量中是否有不被支持的常量類(lèi)型;Class 文件中各個(gè)部分及文件本身是否有被刪除的或者附加的其它信息等等。
- 元數(shù)據(jù)驗(yàn)證 第二階段是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的信息符合Java 語(yǔ)言規(guī)范的要求,這個(gè)階段的驗(yàn)證點(diǎn)包括:這個(gè)類(lèi)是否有父類(lèi);這個(gè)類(lèi)的父類(lèi)是否繼承了不允許被繼承的類(lèi);如果這個(gè)類(lèi)不是抽象類(lèi),是否實(shí)現(xiàn)了其父類(lèi)或者接口之中要求實(shí)現(xiàn)的所有方法;類(lèi)中的字段、方法是否與父類(lèi)產(chǎn)生矛盾等等。
- 字節(jié)碼驗(yàn)證 第三階段是整個(gè)驗(yàn)證過(guò)程中最復(fù)雜的一個(gè)階段,主要目的是通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。
- 符號(hào)引用驗(yàn)證 最后一個(gè)階段的校驗(yàn)發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段--解析階段中發(fā)生。符號(hào)引用驗(yàn)證可以看做是對(duì)類(lèi)自身以外(常量池中的各種符號(hào)引用)的各類(lèi)信息進(jìn)行匹配性校驗(yàn),通俗來(lái)說(shuō)就是,該類(lèi)是否缺少或者被禁止訪問(wèn)它依賴(lài)的某些外部類(lèi)、方法、字段等資源。
準(zhǔn)備
準(zhǔn)備階段是正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段。
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。
初始化
類(lèi)初始化階段是類(lèi)加載過(guò)程中的最后一步,前面的類(lèi)加載過(guò)程中,除了在加載階段用戶應(yīng)用程序可以通過(guò)自定義類(lèi)加載器參與之外,其余動(dòng)作完全是由虛擬機(jī)主導(dǎo)和控制的。
到了初始化階段,才真正開(kāi)始執(zhí)行類(lèi)中定義的Java 程序代碼。
2.3 類(lèi)加載器
類(lèi)加載器雖然只用于實(shí)現(xiàn)類(lèi)的加載動(dòng)作,但它在Java程序中起到的作用卻遠(yuǎn)超類(lèi)加載階段。
對(duì)于任意一個(gè)類(lèi),都必須由加載它的類(lèi)加載器和這個(gè)類(lèi)本身一起共同確立其在Java虛擬機(jī)中的唯一性,每一個(gè)類(lèi)加載器,都擁有一個(gè)獨(dú)立的類(lèi)名稱(chēng)空間。
這句話可以表達(dá)得更通俗一些:比較兩個(gè)類(lèi)是否“相等”,只有在這兩個(gè)類(lèi)是由同一個(gè)類(lèi)加載器加載的前提下才有意義,否則,即使這兩個(gè)類(lèi)來(lái)源于同一個(gè)Class文件,被同一個(gè)Java虛擬機(jī)加載,只要加載它們的類(lèi)加載器不同,那這兩個(gè)類(lèi)就必定不相等。
雙親委派模型
從Java 虛擬機(jī)的角度來(lái)講,只存在兩種不同的類(lèi)加載器:一種是啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader),這個(gè)類(lèi)加載器使用C++ 來(lái)實(shí)現(xiàn),是虛擬機(jī)自身的一部分;另一種就是所有其他的類(lèi)加載器,這些類(lèi)加載器都由Java 來(lái)實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部,并且全都繼承自抽象類(lèi) java.lang.ClassLoader 。
從Java 開(kāi)發(fā)者的角度來(lái)看,類(lèi)加載器可以劃分為:
- 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader):這個(gè)類(lèi)加載器負(fù)責(zé)將存放在<java_home>\lib 目錄中的類(lèi)庫(kù)加載到虛擬機(jī)內(nèi)存中。啟動(dòng)類(lèi)加載器無(wú)法被Java 程序直接引用,用戶在編寫(xiě)自定義類(lèi)加載器時(shí),如果需要把加載請(qǐng)求委派給啟動(dòng)類(lèi)加載器,那直接使用null 代替即可;
- 擴(kuò)展類(lèi)加載器(Extension ClassLoader):這個(gè)類(lèi)加載器由 sun.misc.Launcher$ExtClassLoader 實(shí)現(xiàn),它負(fù)責(zé)加載<java_home>\lib\ext 目錄中,或者被java.ext.dirs 系統(tǒng)變量所指定的路徑中的所有類(lèi)庫(kù),開(kāi)發(fā)者可以直接使用擴(kuò)展類(lèi)加載器;
- 應(yīng)用程序類(lèi)加載器(Application ClassLoader):這個(gè)類(lèi)加載器由 sun.misc.Launcher$AppClassLoader 實(shí)現(xiàn)。 getSystemClassLoader() 方法返回的就是這個(gè)類(lèi)加載器,因此也被稱(chēng)為系統(tǒng)類(lèi)加載器。它負(fù)責(zé)加載用戶類(lèi)路徑(ClassPath)上所指定的類(lèi)庫(kù)。開(kāi)發(fā)者可以直接使用這個(gè)類(lèi)加載器,如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類(lèi)加載器,一般情況下這個(gè)就是程序中默認(rèn)的類(lèi)加載器。
我們的應(yīng)用程序都是由這3 種類(lèi)加載器互相配合進(jìn)行加載的,在必要時(shí)還可以自己定義類(lèi)加載器。它們的關(guān)系如下圖所示:
雙親委派模型要求除了頂層的啟動(dòng)類(lèi)加載器外,其余的類(lèi)加載器都應(yīng)有自己的父類(lèi)加載器。
雙親委派模型的工作過(guò)程是:
- 如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi)
- 而是把這個(gè)請(qǐng)求委派給父類(lèi)加載器去完成,每一個(gè)層次的類(lèi)加載器都是如此
- 因此所有的加載請(qǐng)求最終都應(yīng)該傳送到最頂層的啟動(dòng)類(lèi)加載器中
- 只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒(méi)有找到所需的類(lèi))時(shí),子加載器才會(huì)嘗試自己去完成加載。
這樣做的好處就是Java 類(lèi)隨著它的類(lèi)加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系。例如java.lang. Object,它放在rt.jar 中,無(wú)論哪一個(gè)類(lèi)加載器要加載這個(gè)類(lèi),最終都是委派給處于模型頂端的啟動(dòng)類(lèi)加載器來(lái)加載,因此Object 類(lèi)在程序的各種類(lèi)加載器環(huán)境中都是同一個(gè)類(lèi)。
相反,如果沒(méi)有使用雙親委派模型,由各個(gè)類(lèi)加載器自行去加載的話,如果用戶自己編寫(xiě)了一個(gè)稱(chēng)為java.lang. Object 的類(lèi),并放在程序的ClassPath 中,那系統(tǒng)中將會(huì)出現(xiàn)多個(gè)不同的Object 類(lèi),Java 類(lèi)型體系中最基本的行為也就無(wú)法保證了。
雙親委派模型對(duì)于保證Java程序的穩(wěn)定運(yùn)作極為重要,但它的實(shí)現(xiàn)卻異常簡(jiǎn)單,用以實(shí)現(xiàn)雙親委派的代碼只有短短十余行,全部集中在java.lang. ClassLoader的loadClass()方法之中:
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先,檢查請(qǐng)求的類(lèi)是不是已經(jīng)被加載過(guò)
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父類(lèi)拋出 ClassNotFoundException 說(shuō)明父類(lèi)加載器無(wú)法完成加載
}
if (c == null) {
// 如果父類(lèi)加載器無(wú)法加載,則調(diào)用自己的 findClass 方法來(lái)進(jìn)行類(lèi)加載
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.
03 JVM調(diào)優(yōu)實(shí)戰(zhàn)
3.1 JVM運(yùn)行參數(shù)
在jvm中有很多的參數(shù)可以進(jìn)行設(shè)置,這樣可以讓jvm在各種環(huán)境中都能夠高效的運(yùn)行。絕大部分的參數(shù)保持默認(rèn)即可。
三種參數(shù)類(lèi)型
- 標(biāo)準(zhǔn)參數(shù)
- -help
- -version
- -X參數(shù)(非標(biāo)準(zhǔn)參數(shù))
- -Xint
- -Xcomp
- XX參數(shù)(使用率較高)
- -XX:newSize
- -XX:+UseSerialGC
-X參數(shù)
jvm的-X參數(shù)是非標(biāo)準(zhǔn)參數(shù),在不同版本的jvm中,參數(shù)可能會(huì)有所不同,可以通過(guò)java -X查看非標(biāo)準(zhǔn)參數(shù)。
-XX參數(shù)
-XX參數(shù)也是非標(biāo)準(zhǔn)參數(shù),主要用于jvm的調(diào)優(yōu)和debug操作。
-XX參數(shù)的使用有2種方式,一種是boolean類(lèi)型,一種是非boolean類(lèi)型:
- boolean類(lèi)型
- 格式:-XX:[+-] 表示啟用或禁用屬性
- 如:-XX:+DisableExplicitGC 表示禁用手動(dòng)調(diào)用gc操作,也就是說(shuō)調(diào)用System.gc()無(wú)效
- 非boolean類(lèi)型
- 格式:-XX:= 表示屬性的值為
- 如:-XX:NewRatio=4 表示新生代和老年代的比值為1:4
-Xms和-Xmx參數(shù)
-Xms與-Xmx分別是設(shè)置jvm的堆內(nèi)存的初始大小和最大大小。
-Xmx2048m:等價(jià)于-XX:MaxHeapSize,設(shè)置JVM最大堆內(nèi)存為2048M。
-Xms512m:等價(jià)于-XX:InitialHeapSize,設(shè)置JVM初始堆內(nèi)存為512M。
適當(dāng)?shù)恼{(diào)整jvm的內(nèi)存大小,可以充分利用服務(wù)器資源,讓程序跑得更快。
示例:
[root@node01 test]# java -Xms512m -Xmx2048m TestJVM
itcast
1.2.
jstat
jstat命令可以查看堆內(nèi)存各部分的使用量,以及加載類(lèi)的數(shù)量。命令的格式如下:
jstat [-命令選項(xiàng)] [vmid] [間隔時(shí)間/毫秒] [查詢(xún)次數(shù)]
查看class加載統(tǒng)計(jì)
F:\t>jstat -class 12076
Loaded Bytes Unloaded Bytes Time
5962 10814.2 0 0.0 3.75
1.2.3.
說(shuō)明:
Loaded:加載class的數(shù)量
Bytes:所占用空間大小
Unloaded:未加載數(shù)量
Bytes:未加載占用空間
Time:時(shí)間
查看編譯統(tǒng)計(jì)
F:\t>jstat -compiler 12076
Compiled Failed Invalid Time FailedType FailedMethod
3115 0 0 3.43 0
1.2.3.
說(shuō)明:
Compiled:編譯數(shù)量。
Failed:失敗數(shù)量
Invalid:不可用數(shù)量
Time:時(shí)間
FailedType:失敗類(lèi)型
FailedMethod:失敗的方法
垃圾回收統(tǒng)計(jì)
F:\t>jstat -gc 12076
S0C S1C S0U S1U EC EU OC OU MC MU
CCSC CCSU YGC YGCT FGC FGCT GCT
3584.0 6656.0 3412.1 0.0 180224.0 89915.4 61440.0 5332.1 27904.0 2626
7.3 3840.0 3420.8 6 0.036 1 0.026 0.062
#也可以指定打印的間隔和次數(shù),每1秒中打印一次,共打印5次
F:\t>jstat -gc 12076 1000 5
S0C S1C S0U S1U EC EU OC OU MC MU
CCSC CCSU YGC YGCT FGC FGCT GCT
3584.0 6656.0 3412.1 0.0 180224.0 89915.4 61440.0 5332.1 27904.0 2626
7.3 3840.0 3420.8 6 0.036 1 0.026 0.062
3584.0 6656.0 3412.1 0.0 180224.0 89915.4 61440.0 5332.1 27904.0 2626
7.3 3840.0 3420.8 6 0.036 1 0.026 0.062
3584.0 6656.0 3412.1 0.0 180224.0 89915.4 61440.0 5332.1 27904.0 2626
7.3 3840.0 3420.8 6 0.036 1 0.026 0.062
3584.0 6656.0 3412.1 0.0 180224.0 89915.4 61440.0 5332.1 27904.0 2626
7.3 3840.0 3420.8 6 0.036 1 0.026 0.062
3584.0 6656.0 3412.1 0.0 180224.0 89915.4 61440.0 5332.1 27904.0 2626
7.3 3840.0 3420.8 6 0.036 1 0.026 0.062
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.
說(shuō)明:
S0C:第一個(gè)Survivor區(qū)的大?。↘B)
S1C:第二個(gè)Survivor區(qū)的大小(KB)
S0U:第一個(gè)Survivor區(qū)的使用大?。↘B)
S1U:第二個(gè)Survivor區(qū)的使用大?。↘B)
EC:Eden區(qū)的大?。↘B)
EU:Eden區(qū)的使用大小(KB)
OC:Old區(qū)大?。↘B)
OU:Old使用大?。↘B)
MC:方法區(qū)大?。↘B)
MU:方法區(qū)使用大小(KB)
CCSC:壓縮類(lèi)空間大?。↘B)
CCSU:壓縮類(lèi)空間使用大?。↘B)
YGC:年輕代垃圾回收次數(shù)
YGCT:年輕代垃圾回收消耗時(shí)間
FGC:老年代垃圾回收次數(shù)
FGCT:老年代垃圾回收消耗時(shí)間
GCT:垃圾回收消耗總時(shí)間
3.2 Jmap的使用以及內(nèi)存溢出分析
前面通過(guò)jstat可以對(duì)jvm堆的內(nèi)存進(jìn)行統(tǒng)計(jì)分析,而jmap可以獲取到更加詳細(xì)的內(nèi)容,如:內(nèi)存使用情況的匯總、對(duì)內(nèi)存溢出的定位與分析。
查看內(nèi)存使用情況
[root@node01 ~]# jmap -heap 6219
Attaching to process ID 6219, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.141-b15
using thread-local object allocation.
Parallel GC with 2 thread(s)
Heap Configuration: #堆內(nèi)存配置信息
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 488636416 (466.0MB)
NewSize = 10485760 (10.0MB)
MaxNewSize = 162529280 (155.0MB)
OldSize = 20971520 (20.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage: # 堆內(nèi)存的使用情況
PS Young Generation #年輕代
Eden Space:
capacity = 123731968 (118.0MB)
used = 1384736 (1.320587158203125MB)
free = 122347232 (116.67941284179688MB)
1.1191416594941737% used
From Space:
capacity = 9437184 (9.0MB)
used = 0 (0.0MB)
free = 9437184 (9.0MB)
0.0% used
To Space:
capacity = 9437184 (9.0MB)
used = 0 (0.0MB)
free = 9437184 (9.0MB)
0.0% used
PS Old Generation #年老代
capacity = 28311552 (27.0MB)
used = 13698672 (13.064071655273438MB)
free = 14612880 (13.935928344726562MB)
48.38545057508681% used
13648 interned Strings occupying 1866368 bytes.
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.
查看內(nèi)存中對(duì)象數(shù)量及大小
#查看所有對(duì)象,包括活躍以及非活躍的
jmap -histo <pid> | more
#查看活躍對(duì)象
jmap -histo:live <pid> | more
[root@node01 ~]# jmap -histo:live 6219 | more
num #instances #bytes class name
----------------------------------------------1: 37437 7914608 [C
2: 34916 837984 java.lang.String
3: 884 654848 [B
4: 17188 550016 java.util.HashMap$Node
5: 3674 424968 java.lang.Class
6: 6322 395512 [Ljava.lang.Object;
7: 3738 328944 java.lang.reflect.Method
8: 1028 208048 [Ljava.util.HashMap$Node;
9: 2247 144264 [I
10: 4305 137760 java.util.concurrent.ConcurrentHashMap$Node
11: 1270 109080 [Ljava.lang.String;
12: 64 84128 [Ljava.util.concurrent.ConcurrentHashMap$Node;
13: 1714 82272 java.util.HashMap
14: 3285 70072 [Ljava.lang.Class;
15: 2888 69312 java.util.ArrayList
16: 3983 63728 java.lang.Object
17: 1271 61008 org.apache.tomcat.util.digester.CallMethodRule
18: 1518 60720 java.util.LinkedHashMap$Entry
19: 1671 53472 com.sun.org.apache.xerces.internal.xni.QName
20: 88 50880 [Ljava.util.WeakHashMap$Entry;
21: 618 49440 java.lang.reflect.Constructor
22: 1545 49440 java.util.Hashtable$Entry
23: 1027 41080 java.util.TreeMap$Entry
24: 846 40608 org.apache.tomcat.util.modeler.AttributeInfo
25: 142 38032 [S
26: 946 37840 java.lang.ref.SoftReference
27: 226 36816 [[C
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
#對(duì)象說(shuō)明
B byte
C char
D double
F float
I int
J long
Z boolean
[ 數(shù)組,如[I表示int[]
[L+類(lèi)名 其他對(duì)象
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.
將內(nèi)存使用情況dump到文件中
#用法:
jmap -dump:format=b,file=dumpFileName <pid>
#示例
jmap -dump:format=b,file=/tmp/dump.dat 6219
1.2.3.4.
可以看到已經(jīng)在/tmp下生成了dump.dat的文件。
通過(guò)jhat對(duì)dump文件進(jìn)行分析
在上一小節(jié)中,我們將jvm的內(nèi)存dump到文件中,這個(gè)文件是一個(gè)二進(jìn)制的文件,不方便查看,這時(shí)我們可以借助于jhat工具進(jìn)行查看。
#用法:
jhat -port <port> <file>
#示例:
[root@node01 tmp]# jhat -port 9999 /tmp/dump.dat
Reading from /tmp/dump.dat...
Dump file created Mon Sep 10 01:04:21 CST 2018
Snapshot read, resolving...
Resolving 204094 objects...
Chasing references, expect 40 dots........................................
Eliminating duplicate references........................................
Snapshot resolved.
Started HTTP server on port 9999
Server is ready.
1.2.3.4.5.6.7.8.9.10.11.12.13.
打開(kāi)瀏覽器進(jìn)行訪問(wèn):http://192.168.40.133:9999/
在最后由OQL查詢(xún)功能
3.3 Jmp使用以及內(nèi)存溢出分析
使用MAT對(duì)內(nèi)存溢出的定位與分析
內(nèi)存溢出在實(shí)際的生產(chǎn)環(huán)境中經(jīng)常會(huì)遇到,比如,不斷地將數(shù)據(jù)寫(xiě)入到一個(gè)集合中,出現(xiàn)了死循環(huán),讀取超大的文件等等,都可能會(huì)造成內(nèi)存溢出。
如果出現(xiàn)了內(nèi)存溢出,首先我們需要定位到發(fā)生內(nèi)存溢出的環(huán)節(jié),并且進(jìn)行分析,是正常還是非正常情況,如果是正常的需求,就應(yīng)該考慮加大內(nèi)存的設(shè)置,如果是非正常需求,那么就要對(duì)代碼進(jìn)行修改,修復(fù)這個(gè)bug。首先,我們得先學(xué)會(huì)如何定位問(wèn)題,然后再進(jìn)行分析。如何定位問(wèn)題呢,我們需要借助于jmap與MAT工具進(jìn)行定位分析。
接下來(lái),我們模擬內(nèi)存溢出的場(chǎng)景。
模擬內(nèi)存溢出
編寫(xiě)代碼,向List集合中添加100萬(wàn)個(gè)字符串,每個(gè)字符串由1000個(gè)UUID組成。如果程序能夠正常執(zhí)行,最后打印ok。
public class TestJvmOutOfMemory {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
String str = "";
for (int j = 0; j < 1000; j++) {
str += UUID.randomUUID().toString();
}
list.add(str);
}
System.out.println("ok");
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.
為了演示效果,我們將設(shè)置執(zhí)行的參數(shù)
#參數(shù)如下:
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
1.2.
運(yùn)行測(cè)試
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid5348.hprof ...
Heap dump file created [8137186 bytes in 0.032 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at
java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at cn.itcast.jvm.TestJvmOutOfMemory.main(TestJvmOutOfMemory.java:14)
Process finished with exit code 1
1.2.3.4.5.6.7.8.9.10.11.
可以看到,當(dāng)發(fā)生內(nèi)存溢出時(shí),會(huì)dump文件到j(luò)ava_pid5348.hprof。
導(dǎo)入到MA T工具中進(jìn)行分析
可以看到,有91.03%的內(nèi)存由Object[]數(shù)組占有,所以比較可疑。
分析:這個(gè)可疑是正確的,因?yàn)橐呀?jīng)有超過(guò)90%的內(nèi)存都被它占有,這是非常有可能出現(xiàn)內(nèi)存溢出的。
可以看到集合中存儲(chǔ)了大量的uuid字符串
3.4 Jsatck的使用
有些時(shí)候我們需要查看下jvm中的線程執(zhí)行情況,比如,發(fā)現(xiàn)服務(wù)器的CPU的負(fù)載突然增高了、出現(xiàn)了死鎖、死循環(huán)等,我們?cè)撊绾畏治瞿兀?/p>
由于程序是正常運(yùn)行的,沒(méi)有任何的輸出,從日志方面也看不出什么問(wèn)題,所以就需要看下jvm的內(nèi)部線程的執(zhí)行情況,然后再進(jìn)行分析查找出原因。
這個(gè)時(shí)候,就需要借助于jstack命令了,jstack的作用是將正在運(yùn)行的jvm的線程情況進(jìn)行快照,并且打印出來(lái):
#用法:jstack <pid>
[root@node01 bin]# jstack 2203
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.141-b15 mixed mode):
"Attach Listener" #24 daemon prio=9 os_prio=0 tid=0x00007fabb4001000 nid=0x906 waiting on
condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"http-bio-8080-exec-5" #23 daemon prio=5 os_prio=0 tid=0x00007fabb057c000 nid=0x8e1
waiting on condition [0x00007fabd05b8000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-exec-4" #22 daemon prio=5 os_prio=0 tid=0x00007fab9c113800 nid=0x8e0
waiting on condition [0x00007fabd06b9000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-exec-3" #21 daemon prio=5 os_prio=0 tid=0x0000000001aeb800 nid=0x8df
waiting on condition [0x00007fabd09ba000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-exec-2" #20 daemon prio=5 os_prio=0 tid=0x0000000001aea000 nid=0x8de
waiting on condition [0x00007fabd0abb000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-exec-1" #19 daemon prio=5 os_prio=0 tid=0x0000000001ae8800 nid=0x8dd
waiting on condition [0x00007fabd0bbc000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"ajp-bio-8009-AsyncTimeout" #17 daemon prio=5 os_prio=0 tid=0x00007fabe8128000 nid=0x8d0
waiting on condition [0x00007fabd0ece000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at org.apache.tomcat.util.net.JIoEndpoint$AsyncTimeout.run(JIoEndpoint.java:152)
at java.lang.Thread.run(Thread.java:748)
"ajp-bio-8009-Acceptor-0" #16 daemon prio=5 os_prio=0 tid=0x00007fabe82d4000 nid=0x8cf
runnable [0x00007fabd0fcf000]
java.lang.Thread.State: RUNNABLE
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at
org.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(DefaultServerSocketFac
tory.java:60)
at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:220)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-AsyncTimeout" #15 daemon prio=5 os_prio=0 tid=0x00007fabe82d1800 nid=0x8ce
waiting on condition [0x00007fabd10d0000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at org.apache.tomcat.util.net.JIoEndpoint$AsyncTimeout.run(JIoEndpoint.java:152)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-Acceptor-0" #14 daemon prio=5 os_prio=0 tid=0x00007fabe82d0000 nid=0x8cd
runnable [0x00007fabd11d1000]
java.lang.Thread.State: RUNNABLE
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at
org.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(DefaultServerSocketFac
tory.java:60)
at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:220)
at java.lang.Thread.run(Thread.java:748)
"ContainerBackgroundProcessor[StandardEngine[Catalina]]" #13 daemon prio=5 os_prio=0
tid=0x00007fabe82ce000 nid=0x8cc waiting on condition [0x00007fabd12d2000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at
org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.jav
a:1513)
at java.lang.Thread.run(Thread.java:748)
"GC Daemon" #10 daemon prio=2 os_prio=0 tid=0x00007fabe83b4000 nid=0x8b3 in Object.wait()
[0x00007fabd1c2f000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000e315c2d0> (a sun.misc.GC$LatencyLock)
at sun.misc.GC$Daemon.run(GC.java:117)
- locked <0x00000000e315c2d0> (a sun.misc.GC$LatencyLock)
"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fabe80c3800 nid=0x8a5 runnable
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fabe80b6800 nid=0x8a4 waiting
on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007fabe80b3800 nid=0x8a3 waiting
on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007fabe80b2000 nid=0x8a2 runnable
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fabe807f000 nid=0x8a1 in Object.wait()
[0x00007fabd2a67000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000e3162918> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x00000000e3162918> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007fabe807a800 nid=0x8a0 in
Object.wait() [0x00007fabd2b68000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000e3162958> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000e3162958> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"main" #1 prio=5 os_prio=0 tid=0x00007fabe8009000 nid=0x89c runnable [0x00007fabed210000]
java.lang.Thread.State: RUNNABLE
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at org.apache.catalina.core.StandardServer.await(StandardServer.java:453)
at org.apache.catalina.startup.Catalina.await(Catalina.java:777)
at org.apache.catalina.startup.Catalina.start(Catalina.java:723)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:321)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:455)
"VM Thread" os_prio=0 tid=0x00007fabe8073000 nid=0x89f runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007fabe801e000 nid=0x89d runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007fabe8020000 nid=0x89e runnable
"VM Periodic Task Thread" os_prio=0 tid=0x00007fabe80d6800 nid=0x8a6 waiting on condition
JNI global references: 43
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.102.103.104.105.106.107.108.109.110.111.112.113.114.115.116.117.118.119.120.121.122.123.124.125.126.127.128.129.130.131.132.133.134.135.136.137.138.139.140.141.142.143.144.145.146.147.148.149.150.151.152.153.154.155.156.157.158.159.160.161.162.163.164.165.166.167.168.169.170.171.172.173.174.175.176.177.178.179.180.181.182.183.184.185.186.187.188.189.190.191.192.193.194.195.196.197.198.199.200.201.202.203.204.205.206.207.208.209.210.211.212.213.
3.5 VisualVM工具的使用
VisualVM,能夠監(jiān)控線程,內(nèi)存情況,查看方法的CPU時(shí)間和內(nèi)存中的對(duì) 象,已被GC的對(duì)象,反向查看分配的堆棧(如100個(gè)String對(duì)象分別由哪幾個(gè)對(duì)象分配出來(lái)的)。
VisualVM使用簡(jiǎn)單,幾乎0配置,功能還是比較豐富的,幾乎囊括了其它JDK自帶命令的所有功能。
- 內(nèi)存信息
- 線程信息
- Dump堆(本地進(jìn)程
- Dump線程(本地進(jìn)程)
- 打開(kāi)堆Dump。堆Dump可以用jmap來(lái)生成
- 打開(kāi)線程Dump
- 生成應(yīng)用快照(包含內(nèi)存信息、線程信息等等)
- 性能分析。CPU分析(各個(gè)方法調(diào)用時(shí)間,檢查哪些方法耗時(shí)多),內(nèi)存分析(各類(lèi)對(duì)象占用的內(nèi)存,檢查哪些類(lèi)占用內(nèi)存多)
- ......
啟動(dòng)
在jdk的安裝目錄的bin目錄下,找到j(luò)visualvm.exe,雙擊打開(kāi)即可。
查看 CPU、內(nèi)存、類(lèi)、線程運(yùn)行信息
參看線程信息
也可以點(diǎn)擊右上角Dump按鈕,將線程的信息導(dǎo)出,其實(shí)就是執(zhí)行的jstack命令。
監(jiān)控遠(yuǎn)程JVM
VisualJVM不僅是可以監(jiān)控本地jvm進(jìn)程,還可以監(jiān)控遠(yuǎn)程的jvm進(jìn)程,需要借助于JMX技術(shù)實(shí)現(xiàn)。
什么是JMX
JMX(Java Management Extensions,即Java管理擴(kuò)展)是一個(gè)為應(yīng)用程序、設(shè)備、系統(tǒng)等植入管理功能的框架。JMX可以跨越一系列異構(gòu)操作系統(tǒng)平臺(tái)、系統(tǒng)體系結(jié)構(gòu)和網(wǎng)絡(luò)傳輸協(xié)議,靈活地開(kāi)發(fā)無(wú)縫集成的系統(tǒng)、網(wǎng)絡(luò)和服務(wù)管理應(yīng)用。
監(jiān)控Tomcat
想要監(jiān)控遠(yuǎn)程的tomcat,就需要在遠(yuǎn)程的tomcat進(jìn)行對(duì)JMX配置,方法如下:
#在tomcat的bin目錄下,修改catalina.sh,添加如下的參數(shù)
JAVA_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false"
#這幾個(gè)參數(shù)的意思是:
#-Dcom.sun.management.jmxremote :允許使用JMX遠(yuǎn)程管理
#-Dcom.sun.management.jmxremote.port=9999 :JMX遠(yuǎn)程連接端口
#-Dcom.sun.management.jmxremote.authenticate=false :不進(jìn)行身份認(rèn)證,任何用戶都可以連接
#-Dcom.sun.management.jmxremote.ssl=false :不使用ssl
1.2.3.4.5.6.7.8.9.
使用VisualJVM遠(yuǎn)程連接Tomcat
添加主機(jī)
在一個(gè)主機(jī)下可能會(huì)有很多的jvm需要監(jiān)控,所以接下來(lái)要在該主機(jī)上添加需要監(jiān)控的jvm:
連接成功。使用方法和前面就一樣了,就可以和監(jiān)控本地jvm進(jìn)程一樣,監(jiān)控遠(yuǎn)程的tomcat進(jìn)程。
3.6 可視化GC日志分析工具
GC日志輸出參數(shù)
前面通過(guò)-XX:+PrintGCDetails可以對(duì)GC日志進(jìn)行打印,我們就可以在控制臺(tái)查看,這樣雖然可以查看GC的信息,但是并不直觀,可以借助于第三方的GC日志分析工具進(jìn)行查看。
在日志打印輸出涉及到的參數(shù)如下:
-XX:+PrintGC 輸出GC日志
-XX:+PrintGCDetails 輸出GC的詳細(xì)日志
-XX:+PrintGCTimeStamps 輸出GC的時(shí)間戳(以基準(zhǔn)時(shí)間的形式)
-XX:+PrintGCDateStamps 輸出GC的時(shí)間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進(jìn)行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的輸出路徑
1.2.3.4.5.6.7.8.9.10.11.
測(cè)試:
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xmx256m -XX:+PrintGCDetails
-XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
-Xloggc:F://test//gc.log
1.2.3.
運(yùn)行后就可以在E盤(pán)下生成gc.log文件。
使用GC Easy
它是一款在線的可視化工具,易用、功能強(qiáng)大,網(wǎng)站: http://gceasy.io/
3.7 調(diào)優(yōu)實(shí)戰(zhàn)
先部署一個(gè)web項(xiàng)目(自行準(zhǔn)備)
壓測(cè)
下面我們通過(guò)jmeter進(jìn)行壓力測(cè)試,先測(cè)得在初始狀態(tài)下的并發(fā)量等信息,然后我們?cè)趯?duì)jvm做調(diào)優(yōu)處理,再與初始狀態(tài)測(cè)得的數(shù)據(jù)進(jìn)行比較,看調(diào)好了還是調(diào)壞了。
首先需要對(duì)jmeter本身的參數(shù)調(diào)整,jmeter默認(rèn)的的內(nèi)存大小只有1g,如果并發(fā)數(shù)到達(dá)300以上時(shí),將無(wú)法
正常執(zhí)行,會(huì)拋出內(nèi)存溢出等異常,所以需要對(duì)內(nèi)存大小做出調(diào)整。
修改jmeter.bat文件:
set HEAP=-Xms1g -Xmx4g -XX:MaxMetaspaceSize=512m
在該文件中可以看到,jmeter默認(rèn)使用的垃圾收集器是G1.
Defaults to '-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:G1ReservePercent=20'
1.2.3.4.5.6.
添加gc相關(guān)參數(shù)
#內(nèi)存設(shè)置較小是為了更頻繁的gc,方便觀察效果,實(shí)際要比此設(shè)置的更大 JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms64m -Xmx128m - XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC - Xloggc:../logs/gc.log -Dcom.sun.management.jmxremote - Dcom.sun.management.jmxremote.port=9999 - Dcom.sun.management.jmxremote.authenticate=false - Dcom.sun.management.jmxremote.ssl=false"
1.
重啟tomcat
創(chuàng)建測(cè)試用例進(jìn)行壓測(cè)
調(diào)優(yōu)方向主要從以下幾個(gè)方面:
- 調(diào)整內(nèi)存
- 更換垃圾收集器
對(duì)于JVM的調(diào)優(yōu),給出大家?guī)讞l建議:
- 生產(chǎn)環(huán)境的JVM一定要進(jìn)行參數(shù)設(shè)定,不能全部默認(rèn)上生產(chǎn)。
- 對(duì)于參數(shù)的設(shè)定,不能拍腦袋,需要通過(guò)實(shí)際并發(fā)情況或壓力測(cè)試得出結(jié)論。
- 對(duì)于內(nèi)存中對(duì)象臨時(shí)存在居多的情況,將年輕代調(diào)大一些。如果是G1或ZGC,不需要設(shè)定。
- 仔細(xì)分析gceasy給出的報(bào)告,從中分析原因,找出問(wèn)題。
- 對(duì)于低延遲的應(yīng)用建議使用G1或ZGC垃圾收集器。
- 不要將焦點(diǎn)全部聚焦jvm參數(shù)上,影響性能的因素有很多,比如:操作系統(tǒng)、tomcat本身的參數(shù)等。
PerfMa
PerfMa提供了JVM參數(shù)分析、線程分析、堆內(nèi)存分析功能,界面美觀,功能強(qiáng)大,我們?cè)谧鰆vm調(diào)優(yōu)時(shí),可以作為一個(gè)輔助工具。官網(wǎng): https://www.perfma.com/
04 Tomcat8優(yōu)化
4.1 禁用AJP連接
在服務(wù)狀態(tài)頁(yè)面中可以看到,默認(rèn)狀態(tài)下會(huì)啟用AJP服務(wù),并且占用8009端口。
什么是AJP呢?
AJP(Apache JServer Protocol) AJPv13協(xié)議是面向包的。WEB服務(wù)器和Servlet容器通過(guò)TCP連接來(lái)交互;為了節(jié)省SOCKET創(chuàng)建的昂貴代價(jià),WEB服務(wù)器會(huì)嘗試維護(hù)一個(gè)永久TCP連接到servlet容器,并且在多個(gè)請(qǐng)求和響應(yīng)周期過(guò)程會(huì)重用連接。
我們一般是使用Nginx+tomcat的架構(gòu),所以用不著AJP協(xié)議,所以把AJP連接器禁用。
修改conf下的server.xml文件,將AJP服務(wù)禁用掉即可。
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
1.
4.2 執(zhí)行器(線程池)
在tomcat中每一個(gè)用戶請(qǐng)求都是一個(gè)線程,所以可以使用線程池提高性能。
修改server.xml文件:
<!--將注釋打開(kāi)--> <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="500" minSpareThreads="50" prestartminSpareThreads="true" maxQueueSize="100"/>
<!-- 參數(shù)說(shuō)明: maxThreads:最大并發(fā)數(shù),默認(rèn)設(shè)置 200,一般建議在 500 ~ 1000,根據(jù)硬件設(shè)施和業(yè)務(wù)來(lái)判斷 minSpareThreads:Tomcat 初始化時(shí)創(chuàng)建的線程數(shù),默認(rèn)設(shè)置 25 prestartminSpareThreads: 在 Tomcat 初始化的時(shí)候就初始化 minSpareThreads 的參數(shù)值,如果不等于 true,minSpareThreads 的值就沒(méi)啥效果了 maxQueueSize,最大的等待隊(duì)列數(shù),超過(guò)則拒絕請(qǐng)求 -->
<!--在Connector中設(shè)置executor屬性指向上面的執(zhí)行器-->
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
1.2.3.4.5.
保存退出,重啟tomcat,查看效果。
4.3 三種運(yùn)行模式
tomcat的運(yùn)行模式有3種:
\1. bio 默認(rèn)的模式,性能非常低下,沒(méi)有經(jīng)過(guò)任何優(yōu)化處理和支持.
\2. nio nio(new I/O),是Java SE 1.4及后續(xù)版本提供的一種新的I/O操作方式(即java.nio包及其子包)。Java nio是一個(gè)基于緩沖區(qū)、并能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的縮寫(xiě)。它擁有比傳統(tǒng)I/O操作(bio)更好的并發(fā)運(yùn)行性能。
\3. apr 安裝起來(lái)最困難,但是從操作系統(tǒng)級(jí)別來(lái)解決異步的IO問(wèn)題,大幅度的提高性能.
推薦使用nio,不過(guò),在tomcat8中有最新的nio2,速度更快,建議使用nio2.
設(shè)置nio2
<Connector executor="tomcatThreadPool" port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" connectionTimeout="20000" redirectPort="8443" />
1.
05 代碼優(yōu)化建議
(1)盡可能使用局部變量
調(diào)用方法時(shí)傳遞的參數(shù)以及在調(diào)用中創(chuàng)建的臨時(shí)變量都保存在棧中速度較快,其他變量,如靜態(tài)變量、實(shí)例變量等,都在堆中創(chuàng)建,速度較慢。另外,棧中創(chuàng)建的變量,隨著方法的運(yùn)行結(jié)束,這些內(nèi)容就沒(méi)了,不需要額外的垃圾回收。
(2)盡量減少對(duì)變量的重復(fù)計(jì)算
明確一個(gè)概念,對(duì)方法的調(diào)用,即使方法中只有一句語(yǔ)句,也是有消耗的。所以例如下面的操作:
for (int i = 0; i < list.size(); i++) {...}
1.
建議替換為:
int length = list.size(); for (int i = 0, i < length; i++) {...}
1.
這樣,在list.size()很大的時(shí)候,就減少了很多的消耗。
(3)盡量采用懶加載的策略,即在需要的時(shí)候才創(chuàng)建
String str = "aaa";
if (i == 1){
list.add(str);
}//建議替換成
if (i == 1){
String str = "aaa";
list.add(str);
}
1.2.3.4.5.6.7.8.
(4)異常不應(yīng)該用來(lái)控制流程
異常對(duì)性能不利。拋出異常首先要?jiǎng)?chuàng)建一個(gè)新的對(duì)象,Throwable接口的構(gòu)造函數(shù)調(diào)用名為fifillInStackTrace()的本地同步方 法,fifillInStackTrace()方法檢查堆棧,收集調(diào)用跟蹤信息。只要有異常被拋出,Java虛擬機(jī)就必須調(diào)整調(diào)用堆棧,因?yàn)樵谔幚磉^(guò)程中創(chuàng)建 了一個(gè)新的對(duì)象。異常只能用于錯(cuò)誤處理,不應(yīng)該用來(lái)控制程序流程。
(5)不要將數(shù)組聲明為public static final
因?yàn)檫@毫無(wú)意義,這樣只是定義了引用為static final,數(shù)組的內(nèi)容還是可以隨意改變的,將數(shù)組聲明為public更是一個(gè)安全漏洞,這意味著這個(gè)數(shù)組可以被外部類(lèi)所改變。
(6)不要?jiǎng)?chuàng)建一些不使用的對(duì)象,不要導(dǎo)入一些不使用的類(lèi)
這毫無(wú)意義,如果代碼中出現(xiàn)"The value of the local variable i is not used"、"The import java.util is never used",那么請(qǐng)刪除這些無(wú)用的內(nèi)容
(7)程序運(yùn)行過(guò)程中避免使用反射
反射是Java提供給用戶一個(gè)很強(qiáng)大的功能,功能強(qiáng)大往往意味著效率不高。不建議在程序運(yùn)行過(guò)程中使用尤其是頻繁使用反射機(jī)制,特別是 Method的invoke方法。
如果確實(shí)有必要,一種建議性的做法是將那些需要通過(guò)反射加載的類(lèi)在項(xiàng)目啟動(dòng)的時(shí)候通過(guò)反射實(shí)例化出一個(gè)對(duì)象并放入內(nèi)存。
(8)使用數(shù)據(jù)庫(kù)連接池和線程池
這兩個(gè)池都是用于重用對(duì)象的,前者可以避免頻繁地打開(kāi)和關(guān)閉連接,后者可以避免頻繁地創(chuàng)建和銷(xiāo)毀線程。
(9)容器初始化時(shí)盡可能指定長(zhǎng)度
容器初始化時(shí)盡可能指定長(zhǎng)度,如:new ArrayList<>(10); new HashMap<>(32); 避免容器長(zhǎng)度不足時(shí),擴(kuò)容帶來(lái)的性能損耗。
(10)ArrayList隨機(jī)遍歷快,LinkedList添加刪除快
(11)使用Entry遍歷map
(12)不要手動(dòng)調(diào)用System().gc;
(13)String盡量少用正則表達(dá)式
正則表達(dá)式雖然功能強(qiáng)大,但是其效率較低,除非是有需要,否則盡可能少用。
replace() 不支持正則 replaceAll() 支持正則
如果僅僅是字符的替換建議使用replace()。
(14)日志的輸出要注意級(jí)別
(15)對(duì)資源的close()建議分開(kāi)操作