26-生產環境如何排除和優化 JVM?

通過前面幾個課時的學習,相信你對 JVM 的理論及實踐等相關知識有了一個大體的印象。而本課時將重點講解 JVM 的排查與優化,這樣就會對 JVM 的知識點有一個完整的認識,從而可以更好地應用於實際工作或者面試了。

我們本課時的面試題是,生產環境如何排查問題?

典型回答

如果是在生產環境中直接排查 JVM 的話,最簡單的做法就是使用 JDK 自帶的 6 個非常實用的命令行工具來排查。它們分別是:jps、jstat、jinfo、jmap、jhat 和 jstack,它們都位於 JDK 的 bin 目錄下,可以使用命令行工具直接運行,其目錄如下圖所示:
在這裏插入圖片描述
接下來我們來看看這些工具的具體使用。

1. jps(虛擬機進程狀況工具)

jps(JVM Process Status tool,虛擬機進程狀況工具)它的功能和 Linux 中的 ps 命令比較類似,用於列出正在運行的 JVM 的 LVMID(Local Virtual Machine IDentifier,本地虛擬機唯一 ID),以及 JVM 的執行主類、JVM 啓動參數等信息。語法如下:

jps [options] [hostid]

常用的 options 選項:

  • -l:用於輸出運行主類的全名,如果是 jar 包,則輸出 jar 包的路徑;
  • -q:用於輸出 LVMID(Local Virtual Machine Identifier,虛擬機唯一 ID);
  • -m:用於輸出虛擬機啓動時傳遞給主類 main() 方法的參數;
  • -v:用於輸出啓動時的 JVM 參數。

使用實例:

➜  jps -l
68848
40085 org.jetbrains.jps.cmdline.Launcher
40086 com.example.optimize.NativeOptimize
40109 jdk.jcmd/sun.tools.jps.Jps
68879 org.jetbrains.idea.maven.server.RemoteMavenServer36
➜  jps -q
40368
68848
40085
40086
68879
➜  jps -m
40400 Jps -m
68848
40085 Launcher /Applications/IntelliJ IDEA2.app/Contents/lib/idea_rt.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/oro-2.0.8.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/resources_en.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/maven-model-3.6.1.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/qdox-2.0-M10.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/plexus-component-annotations-1.7.1.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/httpcore-4.4.13.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/maven-resolver-api-1.3.3.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/netty-common-4.1.47.Final.jar:/Applications/IntelliJ IDEA2.app/Contents/plugins/java/lib/maven-resolver-connector-basic-1.3.3.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/maven-artifact-3.6.1.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/plexus-utils-3.2.0.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/netty-resolver-4.1.47.Final.jar:/Applications/IntelliJ IDEA2.app/Contents/lib/guava-28.2-
40086 NativeOptimize
68879 RemoteMavenServer36
➜  jps -v
68848  -Xms128m -Xmx2048m -XX:ReservedCodeCacheSize=240m -XX:+UseCompressedOops -Dfile.encoding=UTF-8 -XX:+UseConcMarkSweepGC -XX:SoftRefLRUPolicyMSPerMB=50 -ea -XX:CICompilerCount=2 -Dsun.io.useCanonPrefixCache=false -Djava.net.preferIPv4Stack=true -Djdk.http.auth.tunneling.disabledSchemes="" -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Djdk.attach.allowAttachSelf -Dkotlinx.coroutines.debug=off -Djdk.module.illegalAccess.silent=true -Xverify:none -XX:ErrorFile=/Users/admin/java_error_in_idea_%p.log -XX:HeapDumpPath=/Users/admin/java_error_in_idea.hprof -javaagent:/Users/admin/.jetbrains/jetbrains-agent-v3.2.0.de72.619 -Djb.vmOptionsFile=/Users/admin/Library/Application Support/JetBrains/IntelliJIdea2020.1/idea.vmoptions -Didea.paths.selector=IntelliJIdea2020.1 -Didea.executable=idea -Didea.home.path=/Applications/IntelliJ IDEA2.app/Contents -Didea.vendor.name=JetBrains
40085 Launcher -Xmx700m -Djava.awt.headless=true -Djava.endorsed.dirs="" -Djdt.compiler.useSingleThread=true -Dpreload.project.path=/Users/admin/github/blog-example/blog-example -Dpreload.config.path=/Users/admin/Library/Application Support/JetBrains/IntelliJIdea2020.1/options -Dcompile.parallel=false -Drebuild.on.dependency.change=true -Djava.net.preferIPv4Stack=true -Dio.netty.initialSeedUniquifier=1366842080359982660 -Dfile.encoding=UTF-8 -Duser.language=zh -Duser.country=CN -Didea.paths.selector=IntelliJIdea2020.1 -Didea.home.path=/Applications/IntelliJ IDEA2.app/Contents -Didea.config.path=/Users/admin/Library/Application Support/JetBrains/IntelliJIdea2020.1 -Didea.plugins.path=/Users/admin/Library/Application Support/JetBrains/IntelliJIdea2020.1/plugins -Djps.log.dir=/Users/admin/Library/Logs/JetBrains/IntelliJIdea2020.1/build-log -Djps.fallback.jdk.home=/Applications/IntelliJ IDEA2.app/Contents/jbr/Contents/Home -Djps.fallback.jdk.version=11.0.6 -Dio.netty.noUnsafe=true -Djava.io.tmpdir=/Users/admin/Library/Caches/Je
40086 NativeOptimize -Dfile.encoding=UTF-8
40425 Jps -Dapplication.home=/Users/admin/Library/Java/JavaVirtualMachines/openjdk-14/Contents/Home -Xms8m -Djdk.module.main=jdk.jcmd
68879 RemoteMavenServer36 -Djava.awt.headless=true -Dmaven.defaultProjectBuilder.disableGlobalModelCache=true -Xmx768m -Didea.maven.embedder.version=3.6.1 -Dmaven.ext.class.path=/Applications/IntelliJ IDEA2.app/Contents/plugins/maven/lib/maven-event-listener.jar -Dfile.encoding=UTF-8

2. jstat(虛擬機統計信息監視工具)

jstat(JVM Statistics Monitoring Tool,虛擬機統計信息監視工具)用於監控虛擬機的運行狀態信息。

例如,我們用它來查詢某個 Java 進程的垃圾收集情況,示例如下:

➜  jstat -gc 43704
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT    CGC    CGCT     GCT
10752.0 10752.0  0.0    0.0   65536.0   5243.4   175104.0     0.0     4480.0 774.0  384.0   75.8       0    0.000   0      0.000   -          -    0.000

參數說明如下表所示:
在這裏插入圖片描述

注意:年輕代的 Edem 區滿了會觸發 young gc,老年代滿了會觸發 old gc。full gc 指的是清除整個堆,包括 young區 和 old 區。

jstat 常用的查詢參數有:

  • -class,查詢類加載器信息;
  • -compiler,JIT 相關信息;
  • -gc,GC 堆狀態;
  • -gcnew,新生代統計信息;
  • -gcutil,GC 堆統計彙總信息。

3. jinfo(查詢虛擬機參數配置工具)

jinfo(Configuration Info for Java)用於查看和調整虛擬機各項參數。語法如下:

jinfo
查看 JVM 參數示例如下:

➜ jinfo -flags 45129 VM Flags:
-XX:CICompilerCount=3 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=1431306240
-XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496 -XX:+UseCompressedClassPointers
-XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC

其中 45129 是使用 jps 查詢的 LVMID。
我們可以通過 jinfo -flag [+/-]name 來修改虛擬機的參數值,比如下面的示例:

➜  jinfo -flag PrintGC 45129 # 查詢是否開啓 GC 打印
-XX:-PrintGC
➜  jinfo -flag +PrintGC 45129 # 開啓 GC 打印
➜  jinfo -flag PrintGC 45129 # 查詢是否開啓 GC 打印
-XX:+PrintGC
➜  jinfo -flag -PrintGC 45129 # 關閉 GC 打印
➜  jinfo -flag PrintGC 45129 # 查詢是否開啓 GC 打印
-XX:-PrintGC

4. jmap(堆快照生成工具)

jmap(Memory Map for Java)用於查詢堆的快照信息。

查詢堆信息示例如下:

➜  jmap -heap 45129
Attaching to process ID 45129, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.101-b13
using thread-local object allocation.
Parallel GC with 6 thread(s)
Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4294967296 (4096.0MB)
   NewSize                  = 89128960 (85.0MB)
   MaxNewSize               = 1431306240 (1365.0MB)
   OldSize                  = 179306496 (171.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
   capacity = 67108864 (64.0MB)
   used     = 5369232 (5.1204986572265625MB)
   free     = 61739632 (58.87950134277344MB)
   8.000779151916504% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 179306496 (171.0MB)
   used     = 0 (0.0MB)
   free     = 179306496 (171.0MB)
   0.0% used
   2158 interned Strings occupying 152472 bytes.

我們也可以直接生成堆快照文件,示例如下:

➜ jmap -dump:format=b,file=/Users/admin/Documents/2020.dump 47380 Dumping heap to /Users/admin/Documents/2020.dump … Heap dump file created

5. jhat(堆快照分析功能)

jhat(JVM Heap Analysis Tool,堆快照分析工具)和 jmap 搭配使用,用於啓動一個 web 站點來分析 jmap 生成的快照文件。

執行示例如下:

jhat /Users/admin/Documents/2020.dump Reading from /Users/admin/Documents/2020.dump… Dump file created Tue May 26
16:12:41 CST 2020 Snapshot read, resolving… Resolving 17797 objects… Chasing references, expect 3 dots… Eliminating duplicate references… Snapshot resolved. Started HTTP server on port 7000 Server is ready.

上述信息表示 jhat 啓動了一個 http 的服務器端口爲 7000 的站點來展示信息,此時我們在瀏覽器中輸入:http://localhost:7000/,會看到如下圖所示的信息:
在這裏插入圖片描述

6. jstack(查詢虛擬機當前的線程快照信息)

jstack(Stack Trace for Java)用於查看當前虛擬機的線程快照,用它可以排查線程的執行狀況,例如排查死鎖、死循環等問題。

比如,我們先寫一段死鎖的代碼:

public class NativeOptimize {
    private static Object obj1 = new Object();
    private static Object obj2 = new Object();
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj2) {
                    System.out.println(Thread.currentThread().getName() + "鎖住 obj2");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj1) {
                        // 執行不到這裏
                        System.out.println("1秒鐘後," + Thread.currentThread().getName()
                                + "鎖住 obj1");
                    }
                }
            }
        }).start();
        synchronized (obj1) {
            System.out.println(Thread.currentThread().getName() + "鎖住 obj1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj2) {
                // 執行不到這裏
                System.out.println("1秒鐘後," + Thread.currentThread().getName()
                        + "鎖住 obj2");
            }
        }
    }
}

以上程序的執行結果如下:

main:鎖住 obj1
Thread-0:鎖住 obj2

此時我們使用 jstack 工具打印一下當前線程的快照信息,結果如下:

➜  bin jstack -l 50016
2020-05-26 18:01:41
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.101-b13 mixed mode):
"Attach Listener" #10 daemon prio=9 os_prio=31 tid=0x00007f8c00840800 nid=0x3c03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   Locked ownable synchronizers:
	- None
"Thread-0" #9 prio=5 os_prio=31 tid=0x00007f8c00840000 nid=0x3e03 waiting for monitor entry [0x00007000100c8000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.example.optimize.NativeOptimize$1.run(NativeOptimize.java:25)
	- waiting to lock <0x000000076abb62d0> (a java.lang.Object)
	- locked <0x000000076abb62e0> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:745)
   Locked ownable synchronizers:
	- None
"Service Thread" #8 daemon prio=9 os_prio=31 tid=0x00007f8c01814800 nid=0x4103 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   Locked ownable synchronizers:
	- None
"C1 CompilerThread2" #7 daemon prio=9 os_prio=31 tid=0x00007f8c0283c800 nid=0x4303 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   Locked ownable synchronizers:
	- None
"C2 CompilerThread1" #6 daemon prio=9 os_prio=31 tid=0x00007f8c0300a800 nid=0x4403 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   Locked ownable synchronizers:
	- None
"C2 CompilerThread0" #5 daemon prio=9 os_prio=31 tid=0x00007f8c0283c000 nid=0x3603 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   Locked ownable synchronizers:
	- None
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007f8c0283b000 nid=0x4603 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   Locked ownable synchronizers:
	- None
"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007f8c03001000 nid=0x5003 in Object.wait() [0x000070000f8ad000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076ab08ee0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
	- locked <0x000000076ab08ee0> (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)
   Locked ownable synchronizers:
	- None
"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007f8c03000000 nid=0x2f03 in Object.wait() [0x000070000f7aa000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076ab06b50> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:502)
	at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
	- locked <0x000000076ab06b50> (a java.lang.ref.Reference$Lock)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
   Locked ownable synchronizers:
	- None
"main" #1 prio=5 os_prio=31 tid=0x00007f8c00802800 nid=0x1003 waiting for monitor entry [0x000070000ef92000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.example.optimize.NativeOptimize.main(NativeOptimize.java:41)
	- waiting to lock <0x000000076abb62e0> (a java.lang.Object)
	- locked <0x000000076abb62d0> (a java.lang.Object)
   Locked ownable synchronizers:
	- None
"VM Thread" os_prio=31 tid=0x00007f8c01008800 nid=0x2e03 runnable
"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007f8c00803000 nid=0x2007 runnable

"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007f8c00006800 nid=0x2403 runnable

"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007f8c01800800 nid=0x2303 runnable
"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007f8c01801800 nid=0x2a03 runnable
"GC task thread#4 (ParallelGC)" os_prio=31 tid=0x00007f8c01802000 nid=0x5403 runnable
"GC task thread#5 (ParallelGC)" os_prio=31 tid=0x00007f8c01006800 nid=0x2d03 runnable
"VM Periodic Task Thread" os_prio=31 tid=0x00007f8c00010800 nid=0x3803 waiting on condition
JNI global references: 6
Found one Java-level deadlock:
=============================
"Thread-0":
  waiting to lock monitor 0x00007f8c000102a8 (object 0x000000076abb62d0, a java.lang.Object),
  which is held by "main"
"main":
  waiting to lock monitor 0x00007f8c0000ed58 (object 0x000000076abb62e0, a java.lang.Object),
  which is held by "Thread-0"

Java stack information for the threads listed above:
===================================================
"Thread-0":
	at com.example.optimize.NativeOptimize$1.run(NativeOptimize.java:25)
	- waiting to lock <0x000000076abb62d0> (a java.lang.Object)
	- locked <0x000000076abb62e0> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:745)
"main":
	at com.example.optimize.NativeOptimize.main(NativeOptimize.java:41)
	- waiting to lock <0x000000076abb62e0> (a java.lang.Object)
	- locked <0x000000076abb62d0> (a java.lang.Object)

Found 1 deadlock.

從上述信息可以看出使用 jstack ,可以很方便地排查出代碼中出現“deadlock”(死鎖)的問題。

考點分析

Java 虛擬機的排查工具是一個合格程序員必備的技能,使用它我們可以很方便地定位出問題的所在,尤其在團隊合作的今天,每個人各守一攤很容易出現隱藏的 bug(缺陷)。因此使用這些排查功能可以幫我們快速地定位並解決問題,所以它也是面試中常問的問題之一。

和此知識點相關的面試題還有以下這些:

  • 除了比較實用的命令行工具之外,有沒有方便一點的排查工具?
  • JVM 常見的調優手段有哪些?

知識擴展

可視化排查工具

JVM 除了上面的 6 個基礎命令行工具之外,還有兩個重要的視圖調試工具,即 JConsole 和 JVisualVM,它們相比於命令行工具使用更方便、操作更簡單、結果展現也更直觀。

JConsole 和 JVisualVM 都位於 JDK 的 bin 目錄下,JConsole(Java Monitoring and Management Console)是最早期的視圖調試工具,其啓動頁面如下圖所示:

在這裏插入圖片描述
可以看出我們可以用它來連接遠程的服務器,或者是直接調試本機,這樣就可以在不消耗生產環境的性能下,從本機啓動 JConsole 來連接服務器。選擇了調試的進程之後,運行界面如下圖所示:
在這裏插入圖片描述
從上圖可以看出,使用 JConsole 可以監控線程、CPU、類、堆以及 VM 的相關信息,同樣我們可以通過線程這一頁的信息,發現之前我們故意寫的死鎖問題,如下圖所示:
在這裏插入圖片描述
可以看到 main(主線程)和 Thread-0 線程處於死鎖狀態。

JVisualVM 的啓動圖如下圖所示:
在這裏插入圖片描述
由上圖可知,JVisualVM 既可以調試本地也可以調試遠程服務器,當我們選擇了相關的進程之後,運行如下圖所示:
在這裏插入圖片描述
可以看出 JVisualVM 除了包含了 JConsole 的信息之外,還有更多的詳細信息,並且更加智能。例如,線程死鎖檢查的這頁內容如下圖所示:

在這裏插入圖片描述
可以看出 JVisualVM 會直接給你一個死鎖的提示,而 JConsole 則需要程序員自己分析。

JVM 調優

JVM 調優主要是根據實際的硬件配置信息重新設置 JVM 參數來進行調優的,例如,硬件的內存配置很高,但 JVM 因爲是默認參數,所以最大內存和初始化堆內存很小,這樣就不能更好地利用本地的硬件優勢了。因此,需要調整這些參數,讓 JVM 在固定的配置下發揮最大的價值。

JVM 常見調優參數包含以下這些:

  • -Xmx,設置最大堆內存大小;
  • -Xms,設置初始堆內存大小;
  • -XX:MaxNewSize,設置新生代的最大內存;
  • -XX:MaxTenuringThreshold,設置新生代對象經過一定的次數晉升到老生代;
  • -XX:PretrnureSizeThreshold,設置大對象的值,超過這個值的對象會直接進入老生代;
  • -XX:NewRatio,設置分代垃圾回收器新生代和老生代內存佔比;
  • -XX:SurvivorRatio,設置新生代 Eden、Form Survivor、To Survivor 佔比。

我們要根據自己的業務場景和硬件配置來設置這些值。例如,當我們的業務場景會有很多大的臨時對象產生時,因爲這些大對象只有很短的生命週期,因此需要把“-XX:MaxNewSize”的值設置的儘量大一些,否則就會造成大量短生命週期的大對象進入老生代,從而很快消耗掉了老生代的內存,這樣就會頻繁地觸發 full gc,從而影響了業務的正常運行。

小結

本課時我們講了 JVM 排查的 6 個基本命令行工具:jps、jstat、jinfo、jmap、jhat、jstack,以及 2 個視圖排查工具:JConsole 和 JVisualVM;同時還講了 JVM 的常見調優參數,希望本課時的內容可以切實的幫助到你。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章