JVM 調優概述
性能定義
吞吐量 - 指不考慮 GC 引起的停頓時間或內存消耗,垃圾收集器能支撐應用達到的最高性能指標。延遲 - 其度量標準是縮短由于垃圾啊收集引起的停頓時間或者完全消除因垃圾收集所引起的停頓,避免應用運行時發生抖動。內存占用 - 垃圾收集器流暢運行所需要的內存數量。
調優原則
GC 優化的兩個目標:
將進入老年代的對象數量降到最低減少 Full GC 的執行時間
GC 優化的基本原則是:將不同的 GC 參數應用到兩個及以上的服務器上然后比較它們的性能,然后將那些被證明可以提高性能或減少 GC 執行時間的參數應用于最終的工作服務器上。
將進入老年代的對象數量降到最低
除了可以在 JDK7 及更高版本中使用的 G1 收集器以外,其他分代 GC 都是由 Oracle JVM 提供的。關于分代 GC,就是對象在 Eden 區被創建,隨后被轉移到 Survivor 區,在此之后剩余的對象會被轉入老年代。也有一些對象由于占用內存過大,在 Eden 區被創建后會直接被傳入老年代。老年代 GC 相對來說會比新生代 GC 更耗時,因此,減少進入老年代的對象數量可以顯著降低 Full GC 的頻率。你可能會以為減少進入老年代的對象數量意味著把它們留在新生代,事實正好相反,新生代內存的大小是可以調節的。
降低 Full GC 的時間
Full GC 的執行時間比 Minor GC 要長很多,因此,如果在 Full GC 上花費過多的時間(超過 1s),將可能出現超時錯誤。
如果通過減小老年代內存來減少 Full GC 時間,可能會引起 OutOfMemoryError 或者導致 Full GC 的頻率升高。另外,如果通過增加老年代內存來降低 Full GC 的頻率,Full GC 的時間可能因此增加。
因此,你需要把老年代的大小設置成一個“合適”的值。
GC 優化需要考慮的 JVM 參數
類型參數描述堆內存大小-Xms啟動 JVM 時堆內存的大小-Xmx堆內存最大限制新生代空間大小-XX:NewRatio新生代和老年代的內存比-XX:NewSize新生代內存大小-XX:SurvivorRatioEden 區和 Survivor 區的內存比
GC 優化時最常用的參數是-Xms,-Xmx和-XX:NewRatio。-Xms和-Xmx參數通常是必須的,所以NewRatio的值將對 GC 性能產生重要的影響。
有些人可能會問如何設置永久代內存大小,你可以用-XX:PermSize和-XX:MaxPermSize參數來進行設置,但是要記住,只有當出現OutOfMemoryError錯誤時你才需要去設置永久代內存。
GC 優化的過程
GC 優化的過程和大多數常見的提升性能的過程相似,下面是筆者使用的流程:
1.監控 GC 狀態
你需要監控 GC 從而檢查系統中運行的 GC 的各種狀態。
2.分析監控結果后決定是否需要優化 GC
在檢查 GC 狀態后,你需要分析監控結構并決定是否需要進行 GC 優化。如果分析結果顯示運行 GC 的時間只有 0.1-0.3 秒,那么就不需要把時間浪費在 GC 優化上,但如果運行 GC 的時間達到 1-3 秒,甚至大于 10 秒,那么 GC 優化將是很有必要的。
但是,如果你已經分配了大約 10GB 內存給 Java,并且這些內存無法省下,那么就無法進行 GC 優化了。在進行 GC 優化之前,你需要考慮為什么你需要分配這么大的內存空間,如果你分配了 1GB 或 2GB 大小的內存并且出現了OutOfMemoryError,那你就應該執行**堆快照(heap dump)**來消除導致異常的原因。
注意:
**堆快照(heap dump)**是一個用來檢查 Java 內存中的對象和數據的內存文件。該文件可以通過執行 JDK 中的jmap命令來創建。在創建文件的過程中,所有 Java 程序都將暫停,因此,不要在系統執行過程中創建該文件。
你可以在互聯網上搜索 heap dump 的詳細說明。
3.設置 GC 類型/內存大小
如果你決定要進行 GC 優化,那么你需要選擇一個 GC 類型并且為它設置內存大小。此時如果你有多個服務器,請如上文提到的那樣,在每臺機器上設置不同的 GC 參數并分析它們的區別。
4.分析結果
在設置完 GC 參數后就可以開始收集數據,請在收集至少 24 小時后再進行結果分析。如果你足夠幸運,你可能會找到系統的最佳 GC 參數。如若不然,你還需要分析輸出日志并檢查分配的內存,然后需要通過不斷調整 GC 類型/內存大小來找到系統的最佳參數。
5.如果結果令人滿意,將參數應用到所有服務器上并結束 GC 優化
如果 GC 優化的結果令人滿意,就可以將參數應用到所有服務器上,并停止 GC 優化。
在下面的章節中,你將會看到上述每一步所做的具體工作。
命令
jmap
jmap 即 JVM Memory Map。
jmap 用于生成 heap dump 文件。
如果不使用這個命令,還可以使用 -XX:+HeapDumpOnOutOfMemoryError 參數來讓虛擬機出現 OOM 的時候,自動生成 dump 文件。
jmap 不僅能生成 dump 文件,還可以查詢 finalize 執行隊列、Java 堆和永久代的詳細信息,如當前使用率、當前使用的是哪種收集器等。
命令格式:
jmap [option] LVMID
option 參數:
dump - 生成堆轉儲快照finalizerinfo - 顯示在 F-Queue 隊列等待 Finalizer 線程執行 finalizer 方法的對象heap - 顯示 Java 堆詳細信息histo - 顯示堆中對象的統計信息permstat - to print permanent generation statisticsF - 當-dump 沒有響應時,強制生成 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 這個后綴是為了后續可以直接用 MAT(Memory Anlysis Tool)打開。
示例:jmap -heap 查看指定進程的堆信息
注意:使用 CMS GC 情況下,jmap -heap 的執行有可能會導致 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 虛擬機當前時刻的線程快照。
線程快照是當前 java 虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等。
線程出現停頓的時候通過 jstack 來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在后臺做什么事情,或者等待什么資源。 如果 java 程序崩潰生成 core 文件,jstack 工具可以用來獲得 core 文件的 java stack 和 native stack 的信息,從而可以輕松地知道 java 程序是如何崩潰和在程序何處發生問題。另外,jstack 工具還可以附屬到正在運行的 java 程序中,看到當時運行的 java 程序的 java stack 和 native stack 的信息, 如果現在運行的 java 程序呈現 hung 的狀態,jstack 是非常有用的。
命令格式:
jstack [option] LVMID
option 參數:
-F - 當正常輸出請求不被響應時,強制輸出線程堆棧-l - 除堆棧外,顯示關于鎖的附加信息-m - 如果調用到本地方法的話,可以顯示 C/C++的堆棧
jps
jps(JVM Process Status Tool),顯示指定系統內所有的 HotSpot 虛擬機進程。
命令格式:
jps [options] [hostid]
option 參數:
-l - 輸出主類全名或 jar 路徑-q - 只輸出 LVMID-m - 輸出 JVM 啟動時傳遞給 main()的參數-v - 輸出 JVM 啟動時顯示指定的 JVM 參數
其中[option]、[hostid]參數也可以不寫。
$ 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),是用于監視虛擬機運行時狀態信息的命令,它可以顯示出虛擬機進程中的類裝載、內存、垃圾收集、JIT 編譯等運行數據。
命令格式:
jstat [option] LVMID [interval] [count]
參數:
[option] - 操作參數LVMID - 本地虛擬機進程 ID[interval] - 連續輸出的時間間隔[count] - 連續輸出的次數
jhat
jhat(JVM Heap Analysis Tool),是與 jmap 搭配使用,用來分析 jmap 生成的 dump,jhat 內置了一個微型的 HTTP/HTML 服務器,生成 dump 的分析結果后,可以在瀏覽器中查看。
注意:一般不會直接在服務器上進行分析,因為 jhat 是一個耗時并且耗費硬件資源的過程,一般把服務器生成的 dump 文件復制到本地或其他機器上進行分析。
命令格式:
jhat [dumpfile]
jinfo
jinfo(JVM Configuration info),用于實時查看和調整虛擬機運行參數。
之前的 jps -v 口令只能查看到顯示指定的參數,如果想要查看未被顯示指定的參數的值就要使用 jinfo 口令
命令格式:
jinfo [option] [args] LVMID
option 參數:
-flag : 輸出指定 args 參數的值-flags : 不需要 args 參數,輸出所有 JVM 參數的值-sysprops : 輸出系統屬性,等同于 System.getProperties()
HotSpot VM 參數
詳細參數說明請參考官方文檔:Java HotSpot VM Options,這里僅列舉常用參數。
JVM 內存配置
配置描述-Xms堆空間初始值。-Xmx堆空間最大值。-XX:NewSize新生代空間初始值。-XX:MaxNewSize新生代空間最大值。-Xmn新生代空間大小。-XX:PermSize永久代空間的初始值。-XX:MaxPermSize永久代空間的最大值。
GC 類型配置
配置描述-XX:+UseSerialGC串行垃圾回收器-XX:+UseParallelGC并行垃圾回收器-XX:+UseParNewGC使用 ParNew + Serial Old 垃圾回收器組合-XX:+UseConcMarkSweepGC并發標記掃描垃圾回收器-XX:ParallelCMSThreads=并發標記掃描垃圾回收器 = 為使用的線程數量-XX:+UseG1GCG1 垃圾回收器
輔助配置
配置描述-XX:+PrintGCDetails打印 GC 日志-Xloggc:<filename>指定 GC 日志文件名-XX:+HeapDumpOnOutOfMemoryError內存溢出時輸出堆快照文件
典型配置
堆大小設置
年輕代的設置很關鍵。
JVM 中最大堆大小有三方面限制:
相關操作系統的數據模型(32-bt 還是 64-bit)限制;系統的可用虛擬內存限制;系統的可用物理內存限制。
整個堆大小 = 年輕代大小 + 年老代大小 + 持久代大小
持久代一般固定大小為 64m。使用 -XX:PermSize 設置。官方推薦年輕代占整個堆的 3/8。使用 -Xmn 設置。
回收器選擇
JVM 給了三種選擇:串行收集器、并行收集器、并發收集器。
JVM 實戰
分析 GC 日志
獲取 GC 日志
獲取 GC 日志有兩種方式:
使用命令動態查看在容器中設置相關參數打印 GC 日志
jstat -gc 統計垃圾回收堆的行為:
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
也可以設置間隔固定時間來打印:
$ jstat -gc 1262 2000 20
這個命令意思就是每隔 2000ms 輸出 1262 的 gc 情況,一共輸出 20 次
Tomcat 設置示例:
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 區和救助空間 Survivor 區的大小比值,默認是 8,則兩個 Survivor 區與一個 Eden 區的比值為 2:8,一個 Survivor 區占整個年輕代的 1/10。調小這個參數將增大 survivor 區,讓對象盡量在 survitor 區呆長一點,減少進入年老代的對象。去掉救助空間的想法是讓大部分不能馬上回收的數據盡快進入年老代,加快年老代的回收頻率,減少年老代暴漲的可能性,這個是通過將-XX:SurvivorRatio 設置成比較大的值(比如 65536)來做到。-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log 將虛擬機每次垃圾回收的信息寫到日志文件中,文件名由 file 指定,文件格式是平文件,內容和-verbose:gc 輸出內容相同。-Djava.awt.headless=true Headless 模式是系統的一種配置模式。在該模式下,系統缺少了顯示設備、鍵盤或鼠標。-XX:+PrintGCTimeStamps -XX:+PrintGCDetails 設置 gc 日志的格式-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000 指定 rmi 調用時 gc 的時間間隔-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15 采用并發 gc 方式,經過 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 回收前后年輕代的內存變化;ParOldGen 表示 gc 回收前后老年代的內存變化;PSPermGen 表示 gc 回收前后永久區的內存變化。young gc 主要是針對年輕代進行內存回收比較頻繁,耗時短;full gc 會對整個堆內存進行回城,耗時長,因此一般盡量減少 full gc 的次數
通過兩張圖非常明顯看出 gc 日志構成:
OutOfMemory(OOM)分析
OutOfMemory ,即內存溢出,是一個常見的 JVM 問題。那么分析 OOM 的思路是什么呢?
首先,要知道有三種 OutOfMemoryError:
OutOfMemoryError:Java heap space - 堆空間溢出OutOfMemoryError:PermGen space - 方法區和運行時常量池溢出OutOfMemoryError:unable to create new native thread - 線程過多
OutOfMemoryError:PermGen space
OutOfMemoryError:PermGen space 表示方法區和運行時常量池溢出。
原因:
Perm 區主要用于存放 Class 和 Meta 信息的,Class 在被 Loader 時就會被放到 PermGen space,這個區域稱為年老代。GC 在主程序運行期間不會對年老區進行清理,默認是 64M 大小。
當程序程序中使用了大量的 jar 或 class,使 java 虛擬機裝載類的空間不夠,超過 64M 就會報這部分內存溢出了,需要加大內存分配,一般 128m 足夠。
解決方案:
(1)擴大永久代空間
JDK7 以前使用 -XX:PermSize 和 -XX:MaxPermSize 來控制永久代大小。JDK8 以后把原本放在永久代的字符串常量池移出, 放在 Java 堆中(元空間 Metaspace)中,元數據并不在虛擬機中,使用的是本地的內存。使用 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 控制元空間大小。
注意:-XX:PermSize 一般設為 64M
(2)清理應用程序中 WEB-INF/lib 下的 jar,用不上的 jar 刪除掉,多個應用公共的 jar 移動到 Tomcat 的 lib 目錄,減少重復加載。
OutOfMemoryError:Java heap space
OutOfMemoryError:Java heap space 表示堆空間溢出。
原因:JVM 分配給堆內存的空間已經用滿了。
問題定位
(1)使用 jmap 或 -XX:+HeapDumpOnOutOfMemoryError 獲取堆快照。 (2)使用內存分析工具(visualvm、mat、jProfile 等)對堆快照文件進行分析。 (3)根據分析圖,重點是確認內存中的對象是否是必要的,分清究竟是是內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)。
內存泄露
內存泄漏是指由于疏忽或錯誤造成程序未能釋放已經不再使用的內存的情況。
內存泄漏并非指內存在物理上的消失,而是應用程序分配某段內存后,由于設計錯誤,失去了對該段內存的控制,因而造成了內存的浪費。
內存泄漏隨著被執行的次數越多-最終會導致內存溢出。
而因程序死循環導致的不斷創建對象-只要被執行到就會產生內存溢出。
內存泄漏常見幾個情況:
靜態集合類聲明為靜態(static)的 HashMap、Vector 等集合通俗來講 A 中有 B,當前只把 B 設置為空,A 沒有設置為空,回收時 B 無法回收-因被 A 引用。
監聽器監聽器被注冊后釋放對象時沒有刪除監聽器
物理連接DataSource.getConnection()建立鏈接,必須通過 close()關閉鏈接
內部類和外部模塊等的引用發現它的方式同內存溢出,可再加個實時觀察jstat -gcutil 7362 2500 70
重點關注:
FGC — 從應用程序啟動到采樣時發生 Full GC 的次數。FGCT — 從應用程序啟動到采樣時 Full GC 所用的時間(單位秒)。FGC 次數越多,FGCT 所需時間越多-可非常有可能存在內存泄漏。
解決方案
(1)檢查程序,看是否有死循環或不必要地重復創建大量對象。有則改之。
下面是一個重復創建內存的示例:
public class OOM {
public static void main(String[] args) {
Integer sum1=300000;
Integer sum2=400000;
OOM oom = new OOM();
System.out.println("往ArrayList中加入30w內容");
oom.javaHeapSpace(sum1);
oom.memoryTotal();
System.out.println("往ArrayList中加入40w內容");
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("最大內存 = " + max);
System.out.println("已分配內存 = " + total);
System.out.println("已分配內存中的剩余空間 = " + free);
System.out.println("最大可用內存 = " + usable);
}
}
執行結果:
往ArrayList中加入30w內容
最大內存 = 20447232
已分配內存 = 20447232
已分配內存中的剩余空間 = 4032576
最大可用內存 = 4032576
往ArrayList中加入40w內容
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)擴大堆內存空間
使用 -Xms 和 -Xmx 來控制堆內存空間大小。
OutOfMemoryError: GC overhead limit exceeded
原因:JDK6 新增錯誤類型,當 GC 為釋放很小空間占用大量時間時拋出;一般是因為堆太小,導致異常的原因,沒有足夠的內存。
解決方案:
查看系統是否有使用大內存的代碼或死循環; 通過添加 JVM 配置,來限制使用內存:
<jvm-arg>-XX:-UseGCOverheadLimit</jvm-arg>
OutOfMemoryError:unable to create new native thread
原因:線程過多
那么能創建多少線程呢?這里有一個公式:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
MaxProcessMemory 指的是一個進程的最大內存
JVMMemory JVM內存
ReservedOsMemory 保留的操作系統內存
ThreadStackSize 線程棧的大小
當發起一個線程的創建時,虛擬機會在 JVM 內存創建一個 Thread 對象同時創建一個操作系統線程,而這個系統線程的內存用的不是 JVMMemory,而是系統中剩下的內存: (MaxProcessMemory - JVMMemory - ReservedOsMemory) 結論:你給 JVM 內存越多,那么你能用來創建的系統線程的內存就會越少,越容易發生 java.lang.OutOfMemoryError: unable to create new native thread。
CPU 過高
定位步驟:
(1)執行 top -c 命令,找到 cpu 最高的進程的 id
(2)jstack PID 導出 Java 應用程序的線程堆棧信息。
示例:
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 的含義:
nid : 對應的 Linux 操作系統下的 tid 線程號,也就是前面轉化的 16 進制數字
tid: 這個應該是 jvm 的 jmm 內存規范中的唯一地址定位
在 CPU 過高的情況下,查找響應的線程,一般定位都是用 nid 來定位的。而如果發生死鎖之類的問題,一般用 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
輸出結果:45cd
然后根據輸出結果到 jstack 打印的堆棧日志中查定位:
"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)