JVM 調(diào)優(yōu)概述
性能定義
- 吞吐量 - 指不考慮 GC 引起的停頓時間或內(nèi)存消耗,垃圾收集器能支撐應(yīng)用達到的最高性能指標(biāo)。
- 延遲 - 其度量標(biāo)準(zhǔn)是縮短由于垃圾啊收集引起的停頓時間或者完全消除因垃圾收集所引起的停頓,避免應(yīng)用運行時發(fā)生抖動。
- 內(nèi)存占用 - 垃圾收集器流暢運行所需要的內(nèi)存數(shù)量。
調(diào)優(yōu)原則
GC 優(yōu)化的兩個目標(biāo):
- 將進入老年代的對象數(shù)量降到最低
- 減少 Full GC 的執(zhí)行時間
GC 優(yōu)化的基本原則是:將不同的 GC 參數(shù)應(yīng)用到兩個及以上的服務(wù)器上然后比較它們的性能,然后將那些被證明可以提高性能或減少 GC 執(zhí)行時間的參數(shù)應(yīng)用于最終的工作服務(wù)器上。
將進入老年代的對象數(shù)量降到最低
除了可以在 JDK7 及更高版本中使用的 G1 收集器以外,其他分代 GC 都是由 Oracle JVM 提供的。關(guān)于分代 GC,就是對象在 Eden 區(qū)被創(chuàng)建,隨后被轉(zhuǎn)移到 Survivor 區(qū),在此之后剩余的對象會被轉(zhuǎn)入老年代。也有一些對象由于占用內(nèi)存過大,在 Eden 區(qū)被創(chuàng)建后會直接被傳入老年代。老年代 GC 相對來說會比新生代 GC 更耗時,因此,減少進入老年代的對象數(shù)量可以顯著降低 Full GC 的頻率。你可能會以為減少進入老年代的對象數(shù)量意味著把它們留在新生代,事實正好相反,新生代內(nèi)存的大小是可以調(diào)節(jié)的。
降低 Full GC 的時間
Full GC 的執(zhí)行時間比 Minor GC 要長很多,因此,如果在 Full GC 上花費過多的時間(超過 1s),將可能出現(xiàn)超時錯誤。
- 如果通過減小老年代內(nèi)存來減少 Full GC 時間,可能會引起 OutOfMemoryError 或者導(dǎo)致 Full GC 的頻率升高。
- 另外,如果通過增加老年代內(nèi)存來降低 Full GC 的頻率,F(xiàn)ull GC 的時間可能因此增加。
因此,你需要把老年代的大小設(shè)置成一個“合適”的值。
GC 優(yōu)化需要考慮的 JVM 參數(shù)
GC 優(yōu)化時最常用的參數(shù)是-Xms
,-Xmx
和-XX:NewRatio
。-Xms
和-Xmx
參數(shù)通常是必須的,所以NewRatio
的值將對 GC 性能產(chǎn)生重要的影響。
有些人可能會問如何設(shè)置永久代內(nèi)存大小,你可以用-XX:PermSize
和-XX:MaxPermSize
參數(shù)來進行設(shè)置,但是要記住,只有當(dāng)出現(xiàn)OutOfMemoryError
錯誤時你才需要去設(shè)置永久代內(nèi)存。
GC 優(yōu)化的過程
GC 優(yōu)化的過程和大多數(shù)常見的提升性能的過程相似,下面是筆者使用的流程:
1.監(jiān)控 GC 狀態(tài)
你需要監(jiān)控 GC 從而檢查系統(tǒng)中運行的 GC 的各種狀態(tài)。
2.分析監(jiān)控結(jié)果后決定是否需要優(yōu)化 GC
在檢查 GC 狀態(tài)后,你需要分析監(jiān)控結(jié)構(gòu)并決定是否需要進行 GC 優(yōu)化。如果分析結(jié)果顯示運行 GC 的時間只有 0.1-0.3 秒,那么就不需要把時間浪費在 GC 優(yōu)化上,但如果運行 GC 的時間達到 1-3 秒,甚至大于 10 秒,那么 GC 優(yōu)化將是很有必要的。
但是,如果你已經(jīng)分配了大約 10GB 內(nèi)存給 Java,并且這些內(nèi)存無法省下,那么就無法進行 GC 優(yōu)化了。在進行 GC 優(yōu)化之前,你需要考慮為什么你需要分配這么大的內(nèi)存空間,如果你分配了 1GB 或 2GB 大小的內(nèi)存并且出現(xiàn)了OutOfMemoryError
,那你就應(yīng)該執(zhí)行堆快照(heap dump)來消除導(dǎo)致異常的原因。
注意:
堆快照(heap dump)是一個用來檢查 Java 內(nèi)存中的對象和數(shù)據(jù)的內(nèi)存文件。該文件可以通過執(zhí)行 JDK 中的
jmap
命令來創(chuàng)建。在創(chuàng)建文件的過程中,所有 Java 程序都將暫停,因此,不要在系統(tǒng)執(zhí)行過程中創(chuàng)建該文件。
你可以在互聯(lián)網(wǎng)上搜索 heap dump 的詳細說明。
3.設(shè)置 GC 類型/內(nèi)存大小
如果你決定要進行 GC 優(yōu)化,那么你需要選擇一個 GC 類型并且為它設(shè)置內(nèi)存大小。此時如果你有多個服務(wù)器,請如上文提到的那樣,在每臺機器上設(shè)置不同的 GC 參數(shù)并分析它們的區(qū)別。
4.分析結(jié)果
在設(shè)置完 GC 參數(shù)后就可以開始收集數(shù)據(jù),請在收集至少 24 小時后再進行結(jié)果分析。如果你足夠幸運,你可能會找到系統(tǒng)的最佳 GC 參數(shù)。如若不然,你還需要分析輸出日志并檢查分配的內(nèi)存,然后需要通過不斷調(diào)整 GC 類型/內(nèi)存大小來找到系統(tǒng)的最佳參數(shù)。
5.如果結(jié)果令人滿意,將參數(shù)應(yīng)用到所有服務(wù)器上并結(jié)束 GC 優(yōu)化
如果 GC 優(yōu)化的結(jié)果令人滿意,就可以將參數(shù)應(yīng)用到所有服務(wù)器上,并停止 GC 優(yōu)化。
在下面的章節(jié)中,你將會看到上述每一步所做的具體工作。
命令
jmap
jmap 即 JVM Memory Map。
jmap 用于生成 heap dump 文件。
如果不使用這個命令,還可以使用 -XX:+HeapDumpOnOutOfMemoryError
參數(shù)來讓虛擬機出現(xiàn) OOM 的時候,自動生成 dump 文件。
jmap 不僅能生成 dump 文件,還可以查詢 finalize 執(zhí)行隊列、Java 堆和永久代的詳細信息,如當(dāng)前使用率、當(dāng)前使用的是哪種收集器等。
命令格式:
jmap [option] LVMID
option 參數(shù):
- dump - 生成堆轉(zhuǎn)儲快照
- finalizerinfo - 顯示在 F-Queue 隊列等待 Finalizer 線程執(zhí)行 finalizer 方法的對象
- heap - 顯示 Java 堆詳細信息
- histo - 顯示堆中對象的統(tǒng)計信息
- permstat - to print permanent generation statistics
- F - 當(dāng)-dump 沒有響應(yīng)時,強制生成 dump 快照
示例:jmap -dump PID 生成堆快照
dump 堆到文件,format 指定輸出格式,live 指明是活著的對象,file 指定文件名
$ jmap -dump:live,format=b,file=dump.hprof 28920 Dumping heap to /home/xxx/dump.hprof ... Heap dump file created
dump.hprof 這個后綴是為了后續(xù)可以直接用 MAT(Memory Anlysis Tool)打開。
示例:jmap -heap 查看指定進程的堆信息
注意:使用 CMS GC 情況下,jmap -heap 的執(zhí)行有可能會導(dǎo)致 java 進程掛起。
jmap -heap PID
[root@chances bin]# ./jmap -heap 12379
Attaching to process ID 12379, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 17.0-b16
using thread-local object allocation.
Parallel GC with 6 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 83886080 (80.0MB)
NewSize = 1310720 (1.25MB)
MaxNewSize = 17592186044415 MB
OldSize = 5439488 (5.1875MB)
NewRatio = 2
SurvivorRatio = 8
PermSize = 20971520 (20.0MB)
MaxPermSize = 88080384 (84.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 9306112 (8.875MB)
used = 5375360 (5.1263427734375MB)
free = 3930752 (3.7486572265625MB)
57.761608714788736% used
From Space:
capacity = 9306112 (8.875MB)
used = 3425240 (3.2665634155273438MB)
free = 5880872 (5.608436584472656MB)
36.80634834397007% used
To Space:
capacity = 9306112 (8.875MB)
used = 0 (0.0MB)
free = 9306112 (8.875MB)
0.0% used
PS Old Generation
capacity = 55967744 (53.375MB)
used = 48354640 (46.11457824707031MB)
free = 7613104 (7.2604217529296875MB)
86.39733629427693% used
PS Perm Generation
capacity = 62062592 (59.1875MB)
used = 60243112 (57.452308654785156MB)
free = 1819480 (1.7351913452148438MB)
97.06831451706046% used
jstack
jstack 用于生成 java 虛擬機當(dāng)前時刻的線程快照。
線程快照是當(dāng)前 java 虛擬機內(nèi)每一條線程正在執(zhí)行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現(xiàn)長時間停頓的原因,如線程間死鎖、死循環(huán)、請求外部資源導(dǎo)致的長時間等待等。
線程出現(xiàn)停頓的時候通過 jstack 來查看各個線程的調(diào)用堆棧,就可以知道沒有響應(yīng)的線程到底在后臺做什么事情,或者等待什么資源。 如果 java 程序崩潰生成 core 文件,jstack 工具可以用來獲得 core 文件的 java stack 和 native stack 的信息,從而可以輕松地知道 java 程序是如何崩潰和在程序何處發(fā)生問題。另外,jstack 工具還可以附屬到正在運行的 java 程序中,看到當(dāng)時運行的 java 程序的 java stack 和 native stack 的信息, 如果現(xiàn)在運行的 java 程序呈現(xiàn) hung 的狀態(tài),jstack 是非常有用的。
命令格式:
jstack [option] LVMID
option 參數(shù):
-
-F
- 當(dāng)正常輸出請求不被響應(yīng)時,強制輸出線程堆棧 -
-l
- 除堆棧外,顯示關(guān)于鎖的附加信息 -
-m
- 如果調(diào)用到本地方法的話,可以顯示 C/C++的堆棧
jps
jps(JVM Process Status Tool),顯示指定系統(tǒng)內(nèi)所有的 HotSpot 虛擬機進程。
命令格式:
jps [options] [hostid]
option 參數(shù):
-
-l
- 輸出主類全名或 jar 路徑 -
-q
- 只輸出 LVMID -
-m
- 輸出 JVM 啟動時傳遞給 main()的參數(shù) -
-v
- 輸出 JVM 啟動時顯示指定的 JVM 參數(shù)
其中[option]、[hostid]參數(shù)也可以不寫。
$ jps -l -m 28920 org.apache.catalina.startup.Bootstrap start 11589 org.apache.catalina.startup.Bootstrap start 25816 sun.tools.jps.Jps -l -m
jstat
jstat(JVM statistics Monitoring),是用于監(jiān)視虛擬機運行時狀態(tài)信息的命令,它可以顯示出虛擬機進程中的類裝載、內(nèi)存、垃圾收集、JIT 編譯等運行數(shù)據(jù)。
命令格式:
jstat [option] LVMID [interval] [count]
參數(shù):
- [option] - 操作參數(shù)
- LVMID - 本地虛擬機進程 ID
- [interval] - 連續(xù)輸出的時間間隔
- [count] - 連續(xù)輸出的次數(shù)
jhat
jhat(JVM Heap Analysis Tool),是與 jmap 搭配使用,用來分析 jmap 生成的 dump,jhat 內(nèi)置了一個微型的 HTTP/HTML 服務(wù)器,生成 dump 的分析結(jié)果后,可以在瀏覽器中查看。
注意:一般不會直接在服務(wù)器上進行分析,因為 jhat 是一個耗時并且耗費硬件資源的過程,一般把服務(wù)器生成的 dump 文件復(fù)制到本地或其他機器上進行分析。
命令格式:
jhat [dumpfile]
jinfo
jinfo(JVM Configuration info),用于實時查看和調(diào)整虛擬機運行參數(shù)。
之前的 jps -v 口令只能查看到顯示指定的參數(shù),如果想要查看未被顯示指定的參數(shù)的值就要使用 jinfo 口令
命令格式:
jinfo [option] [args] LVMID
option 參數(shù):
- -flag : 輸出指定 args 參數(shù)的值
- -flags : 不需要 args 參數(shù),輸出所有 JVM 參數(shù)的值
- -sysprops : 輸出系統(tǒng)屬性,等同于 System.getProperties()
HotSpot VM 參數(shù)
詳細參數(shù)說明請參考官方文檔:Java HotSpot VM Options,這里僅列舉常用參數(shù)。
JVM 內(nèi)存配置
GC 類型配置
輔助配置
典型配置
堆大小設(shè)置
年輕代的設(shè)置很關(guān)鍵。
JVM 中最大堆大小有三方面限制:
- 相關(guān)操作系統(tǒng)的數(shù)據(jù)模型(32-bt 還是 64-bit)限制;
- 系統(tǒng)的可用虛擬內(nèi)存限制;
- 系統(tǒng)的可用物理內(nèi)存限制。
整個堆大小 = 年輕代大小 + 年老代大小 + 持久代大小
- 持久代一般固定大小為 64m。使用
-XX:PermSize
設(shè)置。 - 官方推薦年輕代占整個堆的 3/8。使用
-Xmn
設(shè)置。
回收器選擇
JVM 給了三種選擇:串行收集器、并行收集器、并發(fā)收集器。
JVM 實戰(zhàn)
分析 GC 日志
獲取 GC 日志
獲取 GC 日志有兩種方式:
- 使用命令動態(tài)查看
- 在容器中設(shè)置相關(guān)參數(shù)打印 GC 日志
jstat -gc
統(tǒng)計垃圾回收堆的行為:
jstat -gc 1262 S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 26112.0 24064.0 6562.5 0.0 564224.0 76274.5 434176.0 388518.3 524288.0 42724.7 320 6.417 1 0.398 6.815
也可以設(shè)置間隔固定時間來打印:
$ jstat -gc 1262 2000 20
這個命令意思就是每隔 2000ms 輸出 1262 的 gc 情況,一共輸出 20 次
Tomcat 設(shè)置示例:
JAVA_OPTS="-server -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4 -verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log -Djava.awt.headless=true -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000 -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15"
-
-Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m
Xms,即為 jvm 啟動時得 JVM 初始堆大小,Xmx 為 jvm 的最大堆大小,xmn 為新生代的大小,permsize 為永久代的初始大小,MaxPermSize 為永久代的最大空間。 -
-XX:SurvivorRatio=4
SurvivorRatio 為新生代空間中的 Eden 區(qū)和救助空間 Survivor 區(qū)的大小比值,默認是 8,則兩個 Survivor 區(qū)與一個 Eden 區(qū)的比值為 2:8,一個 Survivor 區(qū)占整個年輕代的 1/10。調(diào)小這個參數(shù)將增大 survivor 區(qū),讓對象盡量在 survitor 區(qū)呆長一點,減少進入年老代的對象。去掉救助空間的想法是讓大部分不能馬上回收的數(shù)據(jù)盡快進入年老代,加快年老代的回收頻率,減少年老代暴漲的可能性,這個是通過將-XX:SurvivorRatio 設(shè)置成比較大的值(比如 65536)來做到。 -
-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log
將虛擬機每次垃圾回收的信息寫到日志文件中,文件名由 file 指定,文件格式是平文件,內(nèi)容和-verbose:gc 輸出內(nèi)容相同。 -
-Djava.awt.headless=true
Headless 模式是系統(tǒng)的一種配置模式。在該模式下,系統(tǒng)缺少了顯示設(shè)備、鍵盤或鼠標(biāo)。 -
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails
設(shè)置 gc 日志的格式 -
-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000
指定 rmi 調(diào)用時 gc 的時間間隔 -
-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15
采用并發(fā) gc 方式,經(jīng)過 15 次 minor gc 后進入年老代
如何分析 GC 日志
Young GC 回收日志:
2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs]
Full GC 回收日志:
2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]
通過上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen 屬于 Parallel 收集器。其中 PSYoungGen 表示 gc 回收前后年輕代的內(nèi)存變化;ParOldGen 表示 gc 回收前后老年代的內(nèi)存變化;PSPermGen 表示 gc 回收前后永久區(qū)的內(nèi)存變化。young gc 主要是針對年輕代進行內(nèi)存回收比較頻繁,耗時短;full gc 會對整個堆內(nèi)存進行回城,耗時長,因此一般盡量減少 full gc 的次數(shù)
通過兩張圖非常明顯看出 gc 日志構(gòu)成:
OutOfMemory(OOM)分析
OutOfMemory ,即內(nèi)存溢出,是一個常見的 JVM 問題。那么分析 OOM 的思路是什么呢?
首先,要知道有三種 OutOfMemoryError:
- OutOfMemoryError:Java heap space - 堆空間溢出
- OutOfMemoryError:PermGen space - 方法區(qū)和運行時常量池溢出
- OutOfMemoryError:unable to create new native thread - 線程過多
OutOfMemoryError:PermGen space
OutOfMemoryError:PermGen space 表示方法區(qū)和運行時常量池溢出。
原因:
Perm 區(qū)主要用于存放 Class 和 Meta 信息的,Class 在被 Loader 時就會被放到 PermGen space,這個區(qū)域稱為年老代。GC 在主程序運行期間不會對年老區(qū)進行清理,默認是 64M 大小。
當(dāng)程序程序中使用了大量的 jar 或 class,使 java 虛擬機裝載類的空間不夠,超過 64M 就會報這部分內(nèi)存溢出了,需要加大內(nèi)存分配,一般 128m 足夠。
解決方案:
(1)擴大永久代空間
- JDK7 以前使用
-XX:PermSize
和-XX:MaxPermSize
來控制永久代大小。 - JDK8 以后把原本放在永久代的字符串常量池移出, 放在 Java 堆中(元空間 Metaspace)中,元數(shù)據(jù)并不在虛擬機中,使用的是本地的內(nèi)存。使用
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
控制元空間大小。
注意:
-XX:PermSize
一般設(shè)為 64M
(2)清理應(yīng)用程序中 WEB-INF/lib
下的 jar,用不上的 jar 刪除掉,多個應(yīng)用公共的 jar 移動到 Tomcat 的 lib 目錄,減少重復(fù)加載。
OutOfMemoryError:Java heap space
OutOfMemoryError:Java heap space 表示堆空間溢出。
原因:JVM 分配給堆內(nèi)存的空間已經(jīng)用滿了。
問題定位
(1)使用 jmap 或 -XX:+HeapDumpOnOutOfMemoryError 獲取堆快照。 (2)使用內(nèi)存分析工具(visualvm、mat、jProfile 等)對堆快照文件進行分析。 (3)根據(jù)分析圖,重點是確認內(nèi)存中的對象是否是必要的,分清究竟是是內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)。
內(nèi)存泄露
內(nèi)存泄漏是指由于疏忽或錯誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存的情況。
內(nèi)存泄漏并非指內(nèi)存在物理上的消失,而是應(yīng)用程序分配某段內(nèi)存后,由于設(shè)計錯誤,失去了對該段內(nèi)存的控制,因而造成了內(nèi)存的浪費。
內(nèi)存泄漏隨著被執(zhí)行的次數(shù)越多-最終會導(dǎo)致內(nèi)存溢出。
而因程序死循環(huán)導(dǎo)致的不斷創(chuàng)建對象-只要被執(zhí)行到就會產(chǎn)生內(nèi)存溢出。
內(nèi)存泄漏常見幾個情況:
- 靜態(tài)集合類
- 聲明為靜態(tài)(static)的 HashMap、Vector 等集合
- 通俗來講 A 中有 B,當(dāng)前只把 B 設(shè)置為空,A 沒有設(shè)置為空,回收時 B 無法回收-因被 A 引用。
- 監(jiān)聽器
- 監(jiān)聽器被注冊后釋放對象時沒有刪除監(jiān)聽器
- 物理連接
- DataSource.getConnection()建立鏈接,必須通過 close()關(guān)閉鏈接
- 內(nèi)部類和外部模塊等的引用
- 發(fā)現(xiàn)它的方式同內(nèi)存溢出,可再加個實時觀察
- jstat -gcutil 7362 2500 70
重點關(guān)注:
- FGC — 從應(yīng)用程序啟動到采樣時發(fā)生 Full GC 的次數(shù)。
- FGCT — 從應(yīng)用程序啟動到采樣時 Full GC 所用的時間(單位秒)。
- FGC 次數(shù)越多,F(xiàn)GCT 所需時間越多-可非常有可能存在內(nèi)存泄漏。
解決方案
(1)檢查程序,看是否有死循環(huán)或不必要地重復(fù)創(chuàng)建大量對象。有則改之。
下面是一個重復(fù)創(chuàng)建內(nèi)存的示例:
public class OOM {
public static void main(String[] args) {
Integer sum1=300000;
Integer sum2=400000;
OOM oom = new OOM();
System.out.println("往ArrayList中加入30w內(nèi)容");
oom.javaHeapSpace(sum1);
oom.memoryTotal();
System.out.println("往ArrayList中加入40w內(nèi)容");
oom.javaHeapSpace(sum2);
oom.memoryTotal();
}
public void javaHeapSpace(Integer sum){
Random random = new Random();
ArrayList openList = new ArrayList();
for(int i=0;i<sum;i++){
String charOrNum = String.valueOf(random.nextInt(10));
openList.add(charOrNum);
}
}
public void memoryTotal(){
Runtime run = Runtime.getRuntime();
long max = run.maxMemory();
long total = run.totalMemory();
long free = run.freeMemory();
long usable = max - total + free;
System.out.println("最大內(nèi)存 = " + max);
System.out.println("已分配內(nèi)存 = " + total);
System.out.println("已分配內(nèi)存中的剩余空間 = " + free);
System.out.println("最大可用內(nèi)存 = " + usable);
}
}
執(zhí)行結(jié)果:
往ArrayList中加入30w內(nèi)容 最大內(nèi)存 = 20447232 已分配內(nèi)存 = 20447232 已分配內(nèi)存中的剩余空間 = 4032576 最大可用內(nèi)存 = 4032576 往ArrayList中加入40w內(nèi)容 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2245) at java.util.Arrays.copyOf(Arrays.java:2219) at java.util.ArrayList.grow(ArrayList.java:242) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208) at java.util.ArrayList.add(ArrayList.java:440) at pers.qingqian.study.seven.OOM.javaHeapSpace(OOM.java:36) at pers.qingqian.study.seven.OOM.main(OOM.java:26)
(2)擴大堆內(nèi)存空間
使用 -Xms
和 -Xmx
來控制堆內(nèi)存空間大小。
OutOfMemoryError: GC overhead limit exceeded
原因:JDK6 新增錯誤類型,當(dāng) GC 為釋放很小空間占用大量時間時拋出;一般是因為堆太小,導(dǎo)致異常的原因,沒有足夠的內(nèi)存。
解決方案:
查看系統(tǒng)是否有使用大內(nèi)存的代碼或死循環(huán); 通過添加 JVM 配置,來限制使用內(nèi)存:
<jvm-arg>-XX:-UseGCOverheadLimit</jvm-arg>
#### OutOfMemoryError:unable to create new native thread
原因:線程過多
那么能創(chuàng)建多少線程呢?這里有一個公式:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads MaxProcessMemory 指的是一個進程的最大內(nèi)存 JVMMemory JVM內(nèi)存 ReservedOsMemory 保留的操作系統(tǒng)內(nèi)存 ThreadStackSize 線程棧的大小
當(dāng)發(fā)起一個線程的創(chuàng)建時,虛擬機會在 JVM 內(nèi)存創(chuàng)建一個 Thread 對象同時創(chuàng)建一個操作系統(tǒng)線程,而這個系統(tǒng)線程的內(nèi)存用的不是 JVMMemory,而是系統(tǒng)中剩下的內(nèi)存: (MaxProcessMemory - JVMMemory - ReservedOsMemory) 結(jié)論:你給 JVM 內(nèi)存越多,那么你能用來創(chuàng)建的系統(tǒng)線程的內(nèi)存就會越少,越容易發(fā)生 java.lang.OutOfMemoryError: unable to create new native thread。
CPU 過高
定位步驟:
(1)執(zhí)行 top -c 命令,找到 cpu 最高的進程的 id
(2)jstack PID 導(dǎo)出 Java 應(yīng)用程序的線程堆棧信息。
示例:
`jstack 6795
"Low Memory Detector" daemon prio=10 tid=0x081465f8 nid=0x7 runnable [0x00000000..0x00000000]
"CompilerThread0" daemon prio=10 tid=0x08143c58 nid=0x6 waiting on condition [0x00000000..0xfb5fd798]
"Signal Dispatcher" daemon prio=10 tid=0x08142f08 nid=0x5 waiting on condition [0x00000000..0x00000000]
"Finalizer" daemon prio=10 tid=0x08137ca0 nid=0x4 in Object.wait() [0xfbeed000..0xfbeeddb8]
at java.lang.Object.wait(Native Method)
- waiting on <0xef600848> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116)
- locked <0xef600848> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)
"Reference Handler" daemon prio=10 tid=0x081370f0 nid=0x3 in Object.wait() [0xfbf4a000..0xfbf4aa38]
at java.lang.Object.wait(Native Method)
- waiting on <0xef600758> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:474)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)
- locked <0xef600758> (a java.lang.ref.Reference$Lock)
"VM Thread" prio=10 tid=0x08134878 nid=0x2 runnable
"VM Periodic Task Thread" prio=10 tid=0x08147768 nid=0x8 waiting on condition`
在打印的堆棧日志文件中,tid 和 nid 的含義:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word;">nid : 對應(yīng)的 Linux 操作系統(tǒng)下的 tid 線程號,也就是前面轉(zhuǎn)化的 16 進制數(shù)字 tid: 這個應(yīng)該是 jvm 的 jmm 內(nèi)存規(guī)范中的唯一地址定位
</pre>
在 CPU 過高的情況下,查找響應(yīng)的線程,一般定位都是用 nid 來定位的。而如果發(fā)生死鎖之類的問題,一般用 tid 來定位。
(3)定位 CPU 高的線程打印其 nid
查看線程下具體進程信息的命令如下:
top -H -p 6735
top - 14:20:09 up 611 days, 2:56, 1 user, load average: 13.19, 7.76, 7.82
Threads: 6991 total, 17 running, 6974 sleeping, 0 stopped, 0 zombie
%Cpu(s): 90.4 us, 2.1 sy, 0.0 ni, 7.0 id, 0.0 wa, 0.0 hi, 0.4 si, 0.0 st
KiB Mem: 32783044 total, 32505008 used, 278036 free, 120304 buffers
KiB Swap: 0 total, 0 used, 0 free. 4497428 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
6800 root 20 0 27.299g 0.021t 7172 S 54.7 70.1 187:55.61 java
6803 root 20 0 27.299g 0.021t 7172 S 54.4 70.1 187:52.59 java
6798 root 20 0 27.299g 0.021t 7172 S 53.7 70.1 187:55.08 java
6801 root 20 0 27.299g 0.021t 7172 S 53.7 70.1 187:55.25 java
6797 root 20 0 27.299g 0.021t 7172 S 53.1 70.1 187:52.78 java
6804 root 20 0 27.299g 0.021t 7172 S 53.1 70.1 187:55.76 java
6802 root 20 0 27.299g 0.021t 7172 S 52.1 70.1 187:54.79 java
6799 root 20 0 27.299g 0.021t 7172 S 51.8 70.1 187:53.36 java
6807 root 20 0 27.299g 0.021t 7172 S 13.6 70.1 48:58.60 java
11014 root 20 0 27.299g 0.021t 7172 R 8.4 70.1 8:00.32 java
10642 root 20 0 27.299g 0.021t 7172 R 6.5 70.1 6:32.06 java
6808 root 20 0 27.299g 0.021t 7172 S 6.1 70.1 159:08.40 java
11315 root 20 0 27.299g 0.021t 7172 S 3.9 70.1 5:54.10 java
12545 root 20 0 27.299g 0.021t 7172 S 3.9 70.1 6:55.48 java
23353 root 20 0 27.299g 0.021t 7172 S 3.9 70.1 2:20.55 java
24868 root 20 0 27.299g 0.021t 7172 S 3.9 70.1 2:12.46 java
9146 root 20 0 27.299g 0.021t 7172 S 3.6 70.1 7:42.72 java
由此可以看出占用 CPU 較高的線程,但是這些還不高,無法直接定位到具體的類。nid 是 16 進制的,所以我們要獲取線程的 16 進制 ID:
printf "%x\n" 6800
輸出結(jié)果:45cd
然后根據(jù)輸出結(jié)果到 jstack 打印的堆棧日志中查定位:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word;">`"catalina-exec-5692" daemon prio=10 tid=0x00007f3b05013800 nid=0x45cd waiting on condition [0x00007f3ae08e3000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000006a7800598> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2082)
at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:86)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
歡迎工作一到五年的Java工程師朋友們加入Java高并發(fā): 957734884,群內(nèi)提供免費的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構(gòu)資料)合理利用自己每一分每一秒的時間來學(xué)習(xí)提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!