一、什么情況下GC會(huì)對(duì)程序產(chǎn)生影響
無(wú)論 Minor GC/Young GC 還是 Full GC,都會(huì)造成一定程度的程序卡頓,即Stop The World:JVM 因?yàn)閳?zhí)行 GC 線程,其他工作線程被掛起。它會(huì)在任何一種 GC 算法中發(fā)生,即使采用 ParNew、CMS 或者 G1 這些更先進(jìn)的垃圾回收算法,也只是在減少卡頓時(shí)間,而并不能完全消除卡頓。當(dāng)stop-the-world發(fā)生時(shí),除 GC 所需的線程外,所有的線程都進(jìn)入等待狀態(tài),直到 GC 任務(wù)完成。GC 優(yōu)化很多時(shí)候就是減少 stop-the-world 的發(fā)生。
根據(jù) GC 對(duì)程序產(chǎn)生影響的嚴(yán)重程度,從高到低包括以下四種情況:
1??【Full GC 過(guò)于頻繁】Full GC 通常是比較慢的,少則幾百毫秒,多則幾秒,正常情況 Full GC 每隔幾個(gè)小時(shí)甚至幾天才執(zhí)行一次,對(duì)系統(tǒng)的影響還能接受。一旦 Full GC 頻繁出現(xiàn)(比如幾十分鐘就會(huì)執(zhí)行一次),這種肯定是存在問(wèn)題的,它會(huì)導(dǎo)致工作線程頻繁被停止,讓系統(tǒng)看起來(lái)一直有卡頓現(xiàn)象,也會(huì)使得程序的整體性能變差。
2??【Minor GC/Young GC 耗時(shí)過(guò)長(zhǎng)】一般 Minor GC/Young GC 的總耗時(shí)在幾十或者上百毫秒是比較正常的,即便會(huì)引起系統(tǒng)卡頓幾毫秒或者幾十毫秒,但這種情況幾乎對(duì)用戶無(wú)感知,對(duì)程序的影響可以忽略不計(jì)。如果 Minor GC/Young GC 耗時(shí)達(dá)到了 1 秒甚至幾秒(都快趕上 Full GC 的耗時(shí)了),那卡頓時(shí)間就會(huì)增大,加上 Minor GC/Young GC 本身比較頻繁,就會(huì)導(dǎo)致比較多的服務(wù)超時(shí)問(wèn)題。
3??【Full GC 耗時(shí)過(guò)長(zhǎng)】Full GC 耗時(shí)增加,卡頓時(shí)間也會(huì)隨之增加,尤其對(duì)于高并發(fā)服務(wù),可能導(dǎo)致 Full GC 期間比較多的超時(shí)問(wèn)題,可用性降低,這種也需要關(guān)注。
4??【Minor GC/Young GC 過(guò)于頻繁】即使 Minor GC/Young GC 不會(huì)引起服務(wù)超時(shí),但是 Minor GC/Young GC 過(guò)于頻繁也會(huì)降低服務(wù)的整體性能,對(duì)于高并發(fā)服務(wù)也是需要關(guān)注的。
其中,「Full GC 過(guò)于頻繁」和「Minor GC/Young GC 耗時(shí)過(guò)長(zhǎng)」,這兩種情況屬于比較典型的 GC 問(wèn)題,大概率會(huì)對(duì)程序的服務(wù)質(zhì)量產(chǎn)生影響。剩余兩種情況的嚴(yán)重程度低一些,但是對(duì)于高并發(fā)或者高可用的程序也需要關(guān)注。
二、JVM性能調(diào)優(yōu)方法和步驟
對(duì) JVM 內(nèi)存的系統(tǒng)級(jí)的調(diào)優(yōu)策略主要是減少 GC 的頻率,尤其是 Full GC,從而減少 stop-the-world 的發(fā)生。
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í)行時(shí)間,分析是否進(jìn)行優(yōu)化。
系統(tǒng)崩潰前的一些現(xiàn)象:
- 每次垃圾回收的時(shí)間越來(lái)越長(zhǎng),由之前的 10ms 延長(zhǎng)到 50ms 左右,F(xiàn)ull GC 的時(shí)間也由之前的 0.5s 延長(zhǎng)到 4、5s。
- Full GC 的次數(shù)越來(lái)越多,最頻繁時(shí)隔不到 1 分鐘就進(jìn)行一次 Full GC。
- 老年代的內(nèi)存越來(lái)越大并且每次 Full GC 后老年代沒(méi)有內(nèi)存被釋放。
之后系統(tǒng)會(huì)無(wú)法響應(yīng)新的請(qǐng)求,逐漸逼近 OutOfMemoryError 的臨界值,這個(gè)時(shí)候就需要分析 JVM 內(nèi)存快照 dump。
2??生成堆的 dump 文件
通過(guò) JMX 的 MBean 生成當(dāng)前的 Heap 信息,大小為一個(gè) 3G(整個(gè)堆的大小)的 hprof 文件,如果沒(méi)有啟動(dòng) JMX 可以通過(guò) Java 的 jmap 命令來(lái)生成該文件。
3??分析 dump 文件
打開(kāi)這個(gè) 3G 的堆信息文件,顯然一般的 Window 系統(tǒng)沒(méi)有這么大的內(nèi)存,必須借助高配置的幾種 Linux 工具打開(kāi)該文件:
①Visual VM
②IBM HeapAnalyzer
③JDK 自帶的Hprof工具
④Mat(Eclipse專門的靜態(tài)內(nèi)存分析工具)推薦使用
說(shuō)明:文件太大,建議使用Eclipse專門的靜態(tài)內(nèi)存分析工具M(jìn)at打開(kāi)分析。
4??分析結(jié)果,判斷是否需要優(yōu)化
如果各項(xiàng)參數(shù)設(shè)置合理,系統(tǒng)沒(méi)有超時(shí)日志出現(xiàn),GC 頻率不高,GC 耗時(shí)不高,那么沒(méi)有必要進(jìn)行 GC 優(yōu)化。如果 GC 時(shí)間超過(guò) 1-3 秒,或者頻繁 GC,則必須優(yōu)化。
注意:如果滿足下面的指標(biāo),則一般不需要進(jìn)行 GC 優(yōu)化:
①M(fèi)inor GC 執(zhí)行時(shí)間不到 50ms。
②Minor GC 執(zhí)行不頻繁,約 10 秒一次。
③Full GC 執(zhí)行時(shí)間不到 1s。
④Full GC 執(zhí)行頻率不算頻繁,不低于 10 分鐘 1 次。
5??調(diào)整 GC 類型和內(nèi)存分配
如果內(nèi)存分配過(guò)大或過(guò)小,或者采用的 GC 收集器比較慢,則應(yīng)該優(yōu)先調(diào)整這些參數(shù),并且先找 1 臺(tái)或幾臺(tái)機(jī)器進(jìn)行 beta,然后比較優(yōu)化過(guò)的機(jī)器和沒(méi)有優(yōu)化的機(jī)器的性能對(duì)比,并有針對(duì)性的做出最后選擇。
6??不斷的分析和調(diào)整
通過(guò)不斷的試驗(yàn)和試錯(cuò),分析并找到最合適的參數(shù),如果找到了最合適的參數(shù),則將這些參數(shù)應(yīng)用到所有服務(wù)器。

三、JVM調(diào)優(yōu)參數(shù)參考
1??針對(duì) JVM 堆的設(shè)置,一般可以通過(guò) -Xms -Xmx 限定其最小、最大值,為了防止垃圾收集器在最小、最大之間收縮堆而產(chǎn)生額外的時(shí)間,通常把最大、最小設(shè)置為相同的值。
2??新生代和老年代將根據(jù)默認(rèn)的比例(1:2)分配堆內(nèi)存, 可以通過(guò)調(diào)整二者之間的比率來(lái)調(diào)整二者之間的大小,也可以針對(duì)回收代。
比如新生代,通過(guò)-XX:newSize -XX:MaxNewSize來(lái)設(shè)置其絕對(duì)大小。同樣,為了防止新生代的堆收縮,通常會(huì)把-XX:newSize -XX:MaxNewSize設(shè)置為同樣大小。
更大的新生代必然導(dǎo)致更小的老年代,大的新生代會(huì)延長(zhǎng) Minor GC/Young GC 的周期,但會(huì)增加每次的時(shí)間;小的老年代會(huì)導(dǎo)致更頻繁的 Full GC。
更小的新生代必然導(dǎo)致更大老年代,小的新生代會(huì)導(dǎo)致 Minor GC/Young GC 很頻繁,但每次的時(shí)間會(huì)更短;大的老年代會(huì)減少 Full GC 的頻率。
如何選擇應(yīng)該依賴應(yīng)用程序對(duì)象生命周期的分布情況:如果應(yīng)用存在大量的臨時(shí)對(duì)象,應(yīng)該選擇更大的新生代;如果存在相對(duì)較多的持久對(duì)象,老年代應(yīng)該適當(dāng)增大。但很多應(yīng)用都沒(méi)有這樣明顯的特性。
在抉擇時(shí)應(yīng)該根據(jù)以下兩點(diǎn):
- 本著 Full GC 盡量少的原則,讓老年代盡量緩存常用對(duì)象,JVM 的默認(rèn)比例(1:2)也是這個(gè)道理 。
- 觀察應(yīng)用一段時(shí)間,看其在峰值時(shí)老年代會(huì)占多少內(nèi)存,在不影響 Full GC 的前提下,根據(jù)實(shí)際情況加大新生代,比如可以把比例控制在(1:1)。但應(yīng)該給年老代至少預(yù)留 1/3 的增長(zhǎng)空間。
4??在配置較好的機(jī)器上(比如多核、大內(nèi)存),可以為老年代選擇并行收集算法:-XX:+UseParallelOldGC。
5??線程堆棧的設(shè)置:每個(gè)線程默認(rèn)會(huì)開(kāi)啟 1M 的堆棧,用于存放棧幀、調(diào)用參數(shù)、局部變量等,對(duì)大多數(shù)應(yīng)用而言這個(gè)默認(rèn)值太了,一般 256K 就夠用。理論上,在內(nèi)存不變的情況下,減少每個(gè)線程的堆棧,可以產(chǎn)生更多的線程,但這實(shí)際上還受限于操作系統(tǒng)。
四、排查Full GC問(wèn)題的實(shí)踐指南
1??清楚從程序角度,有哪些原因?qū)е?Full GC
- 大對(duì)象:系統(tǒng)一次性加載了過(guò)多數(shù)據(jù)到內(nèi)存中(比如 SQL 查詢未做分頁(yè)),導(dǎo)致大對(duì)象進(jìn)入了老年代。
- 內(nèi)存泄漏:頻繁創(chuàng)建了大量對(duì)象,但是無(wú)法被回收(比如 IO 對(duì)象使用完后未調(diào)用 close 方法釋放資源),先引發(fā) Full GC,最后導(dǎo)致 OOM)
- 程序頻繁生成一些長(zhǎng)生命周期的對(duì)象,當(dāng)這些對(duì)象的存活年齡超過(guò)分代年齡時(shí)便會(huì)進(jìn)入老年代,最后引發(fā) Full GC。
- 程序 BUG 導(dǎo)致動(dòng)態(tài)生成了很多新類,使得 Metaspace 不斷被占用,先引發(fā) Full GC,最后導(dǎo)致 OOM。
- 代碼中顯式調(diào)用了 gc 方法,包括自己的代碼甚至框架中的代碼。
- JVM 參數(shù)設(shè)置問(wèn)題:包括總內(nèi)存大小、新生代和老年代的大小、Eden 區(qū)和S區(qū)的大小、元空間大小、垃圾回收算法等等。
2??清楚排查問(wèn)題時(shí)能使用哪些工具
公司的監(jiān)控系統(tǒng):大部分公司都會(huì)有,可全方位監(jiān)控 JVM 的各項(xiàng)指標(biāo)。
JDK 的自帶工具,包括 jmap、jstat 等常用命令:
- 查看堆內(nèi)存各區(qū)域的使用率以及GC情況
jstat -gcutil -h20 pid 1000 - 查看堆內(nèi)存中的存活對(duì)象,并按空間排序
jmap -histo pid | head -n20 - dump堆內(nèi)存文件
jmap -dump:format=b,file=heap pid
- 可視化的堆內(nèi)存分析工具:JVisualVM、MAT等
3??排查指南
- 查看監(jiān)控,以了解出現(xiàn)問(wèn)題的時(shí)間點(diǎn)以及當(dāng)前 Full GC 的頻率(可對(duì)比正常情況看頻率是否正常)。
- 了解該時(shí)間點(diǎn)之前有沒(méi)有程序上線、基礎(chǔ)組件升級(jí)等情況。
- 了解 JVM 的參數(shù)設(shè)置,包括:堆空間各個(gè)區(qū)域的大小設(shè)置,新生代和老年代分別采用了哪些垃圾收集器,然后分析 JVM 參數(shù)設(shè)置是否合理。
- 再對(duì)可能原因做排除法,其中元空間被打滿、內(nèi)存泄漏、代碼顯式調(diào)用 gc 方法比較容易排查。
- 針對(duì)大對(duì)象或者長(zhǎng)生命周期對(duì)象導(dǎo)致的 Full GC,可通過(guò)
jmap -histo命令并結(jié)合 dump 堆內(nèi)存文件作進(jìn)一步分析,需要先定位到可疑對(duì)象。 - 通過(guò)可疑對(duì)象定位到具體代碼再次分析,這時(shí)候要結(jié)合 GC 原理和 JVM 參數(shù)設(shè)置,弄清楚可疑對(duì)象是否滿足了進(jìn)入到老年代的條件才能下結(jié)論。