JVM性能優(yōu)化
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時間送達(dá)
作者 | IT王小二
來源 | urlify.cn/Vzu2Ij
一、內(nèi)存溢出
內(nèi)存溢出的原因:程序在申請內(nèi)存時,沒有足夠的空間。
1. 棧溢出
方法死循環(huán)遞歸調(diào)用(StackOverflowError)、不斷建立線程(OutOfMemoryError)。
2. 堆溢出
不斷創(chuàng)建對象,分配對象大于最大堆的大小(OutOfMemoryError)。
3. 直接內(nèi)存
JVM 分配的本地直接內(nèi)存大小大于 JVM 的限制,可以通過-XX:MaxDirectMemorySize 來設(shè)置(不設(shè)置的話默認(rèn)與堆內(nèi)存最大值一樣,也會出現(xiàn)OOM 異常)。
4. 方法區(qū)溢出
一個類要被垃圾收集器回收掉,判定條件是比較苛刻的,在經(jīng)常動態(tài)生產(chǎn)大量 Class 的應(yīng)用中,CGLIb 字節(jié)碼增強(qiáng),動態(tài)語言,大量 JSP(JSP 第一次運(yùn)行需要編譯成 Java 類),基于 OSGi 的應(yīng)用(同一個類,被不同的加載器加載也會設(shè)為不同的類),都可能會導(dǎo)致OOM。
二、內(nèi)存泄露
程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間,導(dǎo)致這一部分的原因主要是代碼寫的不合理,比如以下幾種情況。
1. 長生命周期的對象持有短生命周期對象的引用
例如將 ArrayList 設(shè)置為靜態(tài)變量,然后不斷地向ArrayList中添加對象,則 ArrayList 容器中的對象在程序結(jié)束之前將不能被釋放,從而造成內(nèi)存泄漏。
2. 連接未關(guān)閉
如數(shù)據(jù)庫連接、網(wǎng)絡(luò)連接和 IO 連接等,只有連接被關(guān)閉后,垃圾回收器才會回收對應(yīng)的對象。
3. 變量作用域不合理
例如:
一個變量的定義的作用范圍大于其使用范圍。
如果沒有及時地把對象設(shè)置為 null。
4. 內(nèi)部類持有外部類
Java 的 非靜態(tài)內(nèi)部類 的這種創(chuàng)建方式,會隱式地持有外部類的引用,而且默認(rèn)情況下這個引用是強(qiáng)引用,因此,如果內(nèi)部類的生命周期長于外部類的生命周期,程序很容易就產(chǎn)生內(nèi)存泄露(可以理解為:垃圾回收器會回收掉外部類的實(shí)例,但由于內(nèi)部類持有外部類的引用,導(dǎo)致垃圾回收器不能正常工作)。
解決辦法:將非靜態(tài)內(nèi)部類改為 靜態(tài)內(nèi)部類,即加上 static 修飾,例如:
public class Jvm5 {
private static String string = "SuunyBear";
public static void show() {
System.out.println("show");
}
public static void main(String[] args) {
Jvm5 m = new Jvm5();
// 非靜態(tài)內(nèi)部類的構(gòu)造方式
// Child c=m.new Child();
Child c = new Child();
c.test();
}
/**
* 內(nèi)部類Child --靜態(tài)的,防止內(nèi)存泄漏
*/
static class Child {
public int i;
public void test() {
System.out.println("string:" + string);
show();
}
}
}
5. Hash值改變
在集合中,如果修改了對象中的那些參與計(jì)算哈希值的字段,會導(dǎo)致無法從集合中單獨(dú)刪除當(dāng)前對象,造成內(nèi)存泄露。
使用例子來說明。
public class Jvm6 {
private int x;
private int y;
public Jvm6(int x, int y) {
super();
this.x = x;
this.y = y;
}
/**
* 重寫HashCode的方法
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
/**
* 改變y的值:同時改變hashcode
*/
public void setY(int y) {
this.y = y;
}
public static void main(String[] args) {
HashSet<Jvm6> hashSet = new HashSet<Jvm6>();
Jvm6 data1 = new Jvm6(1, 3);
Jvm6 data2 = new Jvm6(3, 5);
hashSet.add(data1);
hashSet.add(data2);
data2.setY(7); // data2的Hash值改變
hashSet.remove(data2); // 刪掉data2節(jié)點(diǎn)
System.out.println(hashSet.size()); // 2
}
}
三、內(nèi)存溢出和內(nèi)存泄漏辨析
內(nèi)存溢出:實(shí)實(shí)在在的內(nèi)存空間不足導(dǎo)致。
內(nèi)存泄漏:該釋放的對象沒有釋放,常見于使用容器保存元素的情況下。
如何避免:
內(nèi)存溢出:檢查代碼以及設(shè)置足夠的空間。
內(nèi)存泄漏:一定是代碼有問題,往往很多情況下,內(nèi)存溢出往往是內(nèi)存泄漏造成的。
四、了解MAT
mat是一個內(nèi)存泄露的分析工具。
1. 淺堆和深堆
淺堆(Shallow Heap):是指一個對象所消耗的內(nèi)存。
深堆(Retained Heap):這個對象被 GC 回收后,可以真實(shí)釋放的內(nèi)存大小,也就是只能通過對象被直接或間接訪問到的所有對象的集合。通俗地說,就是一個對象包含(引用)的所有對象的大小,如圖:

2. MAT的使用
1、下載MAT工具:下載地址
2、內(nèi)存溢出例子演示
參數(shù)說明:
-Xms5m 堆初始大小5M
-Xmx5m 堆最大大小5M
-XX:+PrintGCDetails 打印gc日志詳情
-XX:+HeapDumpOnOutOfMemoryError 輸出內(nèi)存溢出文件
-XX:HeapDumpPath=D:/oomDump/dump.hprof 內(nèi)存溢出文件保存位置,此文件用于MAT分析
/**
* VM Args:-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/oomDump/dump.hprof
*/
public class Jvm7 {
public static void main(String[] args) {
// 在方法執(zhí)行的過程中,它是GCRoots
List<Object> list = new LinkedList<>();
int i = 0;
while (true) {
i++;
if (i % 10000 == 0) {
System.out.println("i=" + i);
}
list.add(new Object());
}
}
}
設(shè)置參數(shù)運(yùn)行后,內(nèi)存溢出,程序結(jié)束,然后我們就可以用下載好的MAT來分析了,當(dāng)然MAT也只是分析猜想,并不代表一定是這個原因?qū)е聝?nèi)存溢出。
打開我們保存的文件目錄進(jìn)行分析。

分析結(jié)果。

此時可以查看詳情查看具體原因,當(dāng)然這個原因也只是一種猜想。
五、JDK提供的一些工具
| 分類 | 屬性值 | 描述 |
|---|---|---|
| 命令行工具 | jps | 虛擬機(jī)進(jìn)程狀況工具 |
| jstat | 虛擬機(jī)統(tǒng)計(jì)信息監(jiān)視工具 | |
| jinfo | Java配置信息工具 | |
| jmap | Java內(nèi)存映像工具 | |
| jhat | 虛擬機(jī)堆轉(zhuǎn)儲快照分析工具 | |
| jstack | Java堆棧跟蹤工具 | |
| 可視化工具 | JConsole | Java監(jiān)視與管理控制臺 |
| VisualVM | 多合一故障處理工具 |
所有的工具都在jdk的安裝bin目錄下,比如我的在C:\My Program Files\Java\jdk1.8.0_201\bin。
其中一般情況命令行在線上服務(wù)器上使用,可視化工具在本地使用,當(dāng)然如果你的線上服務(wù)器允許遠(yuǎn)程的話也可以使用可視化工具。
六、GC調(diào)優(yōu)
1. GC調(diào)優(yōu)重要參數(shù)
生產(chǎn)環(huán)境推薦開啟
-XX:+HeapDumpOnOutOfMemoryError
輸出內(nèi)存溢出文件
-XX:HeapDumpPath=D:/oomDump/dump.hprof
內(nèi)存溢出文件保存位置,此文件用于MAT分析
當(dāng)然,一般Linux服務(wù)器可以設(shè)置為
./java_pid<pid>.hprof默認(rèn)為Java進(jìn)程啟動位置
調(diào)優(yōu)之前開始,調(diào)優(yōu)之后關(guān)閉
-XX:+PrintGC
調(diào)試跟蹤之 打印簡單的 GC 信息參數(shù):
-XX:+PrintGCDetails和-XX:+PrintGCTimeStamps
打印詳細(xì)的 GC 信息
-Xlogger:logpath:log/gc.log
設(shè)置 gc 的日志路,將 gc.log 的路徑設(shè)置到當(dāng)前目錄的 log 目錄下. 應(yīng)用場景:將 gc 的日志獨(dú)立寫入日志文件,將 GC 日志與系統(tǒng)業(yè)務(wù)日志進(jìn)行了分離,方便開發(fā)人員進(jìn)行追蹤分析
考慮使用
-XX:+PrintHeapAtGC
打印推信息,獲取 Heap 在每次垃圾回收前后的使用狀況
-XX:+TraceClassLoading
在系統(tǒng)控制臺信息中看到 class 加載的過程和具體的 class 信息,可用以分析類的加載順序以及是否可進(jìn)行精簡操作
-XX:+DisableExplicitGC
禁止在運(yùn)行期顯式地調(diào)用 System.gc()
2. GC調(diào)優(yōu)的原則(很重要)
大多數(shù)的 java 應(yīng)用不需要 GC 調(diào)優(yōu)
大部分需要 GC 調(diào)優(yōu)的的,不是參數(shù)問題,是代碼問題
在實(shí)際使用中,分析 GC 情況優(yōu)化代碼 比 優(yōu)化 GC 參數(shù) 要多得多
GC 調(diào)優(yōu)是最后的手段
調(diào)優(yōu)的目的
GC 的時間夠小
GC 的次數(shù)夠少發(fā)生
Full GC 的周期足夠的長,時間合理,最好是不發(fā)生
注: 如果滿足下面的指標(biāo),則一般不需要進(jìn)行 GC調(diào)優(yōu)
Minor GC 執(zhí)行時間不到 50ms
Minor GC 執(zhí)行不頻繁,約 10 秒一次
Full GC 執(zhí)行時間不到 1s
Full GC 執(zhí)行頻率不算頻繁,不低于 10 分鐘 1 次
3. GC調(diào)優(yōu)步驟
1、監(jiān)控 GC 的狀態(tài)使用各種 JVM 工具,查看當(dāng)前日志,分析當(dāng)前 JVM 參數(shù)設(shè)置,并且分析當(dāng)前堆內(nèi)存快照和 gc 日志,根據(jù)實(shí)際的各區(qū)域內(nèi)存劃分和 GC 執(zhí)行時間,覺得是否進(jìn)行優(yōu)化。
2、分析結(jié)果,判斷是否需要優(yōu)化如果各項(xiàng)參數(shù)設(shè)置合理。
系統(tǒng)沒有超時日志出現(xiàn),GC 頻率不高,GC 耗時不高,那么沒有必要進(jìn)行 GC 優(yōu)化。
如果 GC 時間超過 1 秒,或者頻繁 GC,則必須優(yōu)化。
3、調(diào)整 GC 類型和內(nèi)存分配如果內(nèi)存分配過大或過小,或者采用的 GC 收集器比較慢,則應(yīng)該優(yōu)先調(diào)整這些參數(shù),并且先找 1 臺或幾臺機(jī)器進(jìn)行 測試,然后比較優(yōu)化過的機(jī)器和沒有優(yōu)化的機(jī)器的性能對比,并有針對性的做出最后選擇。
4、不斷的分析和調(diào)整通過不斷的試驗(yàn)和試錯,分析并找到最合適的參數(shù)5,全面應(yīng)用參數(shù)如果找到了最合適的參數(shù),則將這些參數(shù)應(yīng)用到所有服務(wù)器,并進(jìn)行后續(xù)跟蹤。
分析GC日志
主要關(guān)注 MinorGC 和 FullGC 的回收效率(回收前大小和回收比較)、回收的時間。
1、-XX:+UseSerialGC
以參數(shù)-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:+UseSerialGC 為例詳細(xì)說明。
[DefNew: 1855K->1855K(1856K), 0.0000148 secs][Tenured: 2815K->4095K(4096K), 0.0134819 secs] 4671K。
DefNew 指明了收集器類型,而且說明了收集發(fā)生在新生代。
1855K->1855K(1856K)表示,回收前 新生代占用 1855K,回收后占用 1855K,新生代大小 1856K
0.0000148 secs 表明新生代回收耗時。
Tenured 表明收集發(fā)生在老年代。
2815K->4095K(4096K), 0.0134819 secs:含義同新生代最后的 4671K 指明堆的大小。
2、-XX:+UseParNewGC
收集器參數(shù)變?yōu)?XX:+UseParNewGC。
日志變?yōu)椋篬ParNew: 1856K->1856K(1856K), 0.0000107 secs][Tenured: 2890K->4095K(4096K), 0.0121148 secs]。
收集器參數(shù)變?yōu)?XX:+ UseParallelGC 或 UseParallelOldGC。
日志變?yōu)椋篬PSYoungGen: 1024K->1022K(1536K)] [ParOldGen: 3783K->3782K(4096K)] 4807K->4804K(5632K)。
3、-XX:+UseConcMarkSweepGC 和 -XX:+UseG1GC
使用這兩個收集器的日志會和UseParNewGC一樣有明顯的相關(guān)字樣。
4. 項(xiàng)目啟動調(diào)優(yōu)
開啟日志分析-XX:+PrintGCDetails,啟動項(xiàng)目時,通過分析日志,不斷地調(diào)整參數(shù),減少GC次數(shù)。
例如:
1、碰到 Metadata空間 不足發(fā)生GC,那么調(diào)整 Metadata空間 -XX:MetaspaceSize=64m 減少 FullGC 。
2、碰到MinorGC,那么調(diào)整堆空間 -Xms1000m 大小減少FullGC 。
3、如果還是有MinorGC,那么繼續(xù)增大堆空間大小,或者增大新生代比例 -Xmn900m GC,此時新生代空間為900m,老年代大小100m 。
5. 項(xiàng)目運(yùn)行GC調(diào)優(yōu)
使用 jmeter 工具 來進(jìn)行壓測,然后分析原因,進(jìn)行調(diào)優(yōu),當(dāng)然 正式上線的項(xiàng)目請謹(jǐn)慎操作 。
jmeter工具安裝使用
1、下載好對應(yīng)版本的jmeter,注意jdk版本。

2、jmeter需要Java運(yùn)行時環(huán)境,所以如果報(bào)錯請先檢查你的Java環(huán)境變量設(shè)置,解壓到你想要的路徑,例如我解壓在C:\My Program Files\apache-jmeter-5.2.1,在bin目錄下有一個 jmeter.bat 文件,雙擊啟動。
至于具體怎么使用就百度吧,基本拿到軟件就知道使用了,畢竟這個說來就浪費(fèi)篇幅了。
聚合報(bào)告參數(shù)
這里放出我本地 jmeter 測試一個項(xiàng)目之后的 聚合報(bào)告參數(shù)解釋。

6. 推薦策略(僅作參考)
1、新生代大小選擇
盡可能設(shè)大,直到接近系統(tǒng)的最低響應(yīng)時間限制(根據(jù)實(shí)際情況選擇).在此種情況下,新生代收集發(fā)生的頻率也是最小的.同時,減少到達(dá)老年代的對象。
避免設(shè)置過小,當(dāng)新生代設(shè)置過小時會導(dǎo)致:MinorGC 次數(shù)更加頻繁、可能導(dǎo)致 MinorGC 對象直接進(jìn)入老年代,如果此時老年代滿了,會觸發(fā) FullGC。
2、老年代大小選擇
一般吞吐量優(yōu)先的應(yīng)用都有一個很大的新生代和一個較小的老年代.原因是,這樣可以盡可能回收掉大部分短期對象,減少中期的對象,而老年代盡存放長期存活對象
七、逃逸分析
補(bǔ)充知識,并非所有的對象都會在堆上面分配,而沒有在堆上分配的對象是因?yàn)榻?jīng)過逃逸分析,分析之后發(fā)現(xiàn)該對象的大小可以在棧上分配,不會造成棧溢出,這時,對象就可以在棧上分配。
當(dāng)然,如果經(jīng)過逃逸分析,發(fā)現(xiàn)該對象在棧上分配會照成棧溢出,那么該對象就會在堆空間分配。
參數(shù)jdk1.8默認(rèn)開啟
-XX:+DoEscapeAnalysis 啟用逃逸分析(默認(rèn)打開)
-XX:+EliminateAllocations 標(biāo)量替換(默認(rèn)打開)
-XX:+UseTLAB 本地線程分配緩沖(默認(rèn)打開)
八、常用的性能評價(jià)/測試指標(biāo)
一個 web 應(yīng)用不是一個孤立的個體,它是一個系統(tǒng)的部分,系統(tǒng)中的每一部分都會影響整個系統(tǒng)的性能。
1、響應(yīng)時間:提交請求和返回該請求的響應(yīng)之間使用的時間,一般比較關(guān)注平均響應(yīng)時間。
2、并發(fā)數(shù):同一時刻,對服務(wù)器有實(shí)際交互的請求數(shù),和網(wǎng)站在線用戶數(shù)的關(guān)聯(lián):1000 個同時在線用戶數(shù),可以估計(jì)并發(fā)數(shù)在 5%到 15%之間,也就是同時并發(fā)數(shù)在 50~150 之間。
3、吞吐量:對單位時間內(nèi)完成的工作量(請求)的量度,例如1秒處理5萬個請求。


