乐趣区

关于后端:生产环境如何排查和优化JVM

前言

通过后面的介绍,对 JVM 的实践及实际等相干常识有了一个大体的印象。Java 虚拟机的排查工具是一个合格程序员必备的技能,应用它咱们能够很不便地定位出问题的所在,尤其在团队单干的明天,每个人各守一摊很容易呈现暗藏的 bug(缺点)。因而应用这些排查性能能够帮咱们疾速地定位并解决问题。本文时将重点解说 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

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

参数阐明如下所示:

  • S0C 年老代中第一个存活区的大小
  • S1C 年老代中第二个存活区的大小
  • S0U 年老代中第一个存活区已应用的空间(字节)
  • S1U 年老代中第二个存活区已应用的空间(字节)
  • EC Eden 区大小
  • EU 年老代中 Eden 区已应用的空间(字节)
  • OC 老年代大小
  • OU 老年代已应用的空间(字节)
  • YGC 从应用程序启动到采样时 young gc 的次数
  • YGCT 从应用程序启动到采样时 young gc 的所用的工夫(s)
  • FGC 从应用程序启动到采样时 full gc 的次数
  • FGCT 从应用程序启动到采样时 full gc 的所用的工夫
  • GCT 从应用程序启动到采样时整个 gc 所用的工夫

留神:年老代的 Eden 区满了会触发 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 <option> <pid>

查看 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 打印
➜  jinfo -flag +PrintGC 45129 # 开启 GC 打印
➜  jinfo -flag PrintGC 45129 # 查问是否开启 GC 打印
➜  jinfo -flag -PrintGC 45129 # 敞开 GC 打印
➜  jinfo -flag PrintGC 45129 # 查问是否开启 GC 打印 

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");
            }
        }
    }
}

以上程序的执行后果如下:
`text
main:锁住 obj1
Thread-0:锁住 obj2
`

此时咱们应用 jstack 工具打印一下以后线程的快照信息,后果如下:

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”(死锁)的问题。

常识扩大

可视化排查工具

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 的常见调优参数,心愿本文的内容能够切实的帮忙到你。

本文由 mdnice 多平台公布

退出移动版