【Java】【JVM】性能监控与故障诊断

JVM性能监控与故障诊断完全指南

掌握JVM监控工具是Java后端开发者的核心生存技能,能在生产环境快速定位性能瓶颈和致命错误。以下是从命令行到可视化、从监控到日志解读的完整技术栈。


一、命令行监控工具详解

1.1 jstat - JVM统计信息监控工具

定位 :实时监控JVM各内存区域和GC性能数据,生产环境首选(轻量无侵入)。

核心命令

bash 复制代码
# 通用格式:jstat -<option> <pid> <interval> <count>
jstat -gcutil 12345 1000 5  # 每1秒采样1次,共5次

关键选项

选项 说明 使用频率
-gcutil 垃圾回收统计(百分比) ⭐⭐⭐⭐⭐
-gc 详细GC堆信息(容量/用量) ⭐⭐⭐⭐
-class 类加载统计 ⭐⭐⭐
-compiler JIT编译统计 ⭐⭐
-printcompilation 已编译方法

-gcutil输出解读生产必看):

bash 复制代码
$ jstat -gcutil 12345 1000 3
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00  97.38  27.50  42.30  94.20  92.10   1234   15.234     5    2.345   17.579
  0.00  97.38  45.20  42.30  94.20  92.10   1234   15.234     5    2.345   17.579
  0.00  97.38  68.90  42.30  94.20  92.10   1235   15.267     5    2.345   17.612

字段详解

  • S0/S1:Survivor0/Survivor1区使用率(%)
  • E :Eden区使用率(%)→ >90%触发Young GC
  • O :Old区使用率(%)→ >70%需警惕Mixed GC或Full GC (G1默认45%触发)
  • M :Metaspace使用率(%)→ >85%可能OOM (需调MaxMetaspaceSize
  • YGC :Young GC次数 → 每秒>5次说明Young区过小
  • YGCT :Young GC总耗时(秒)→ 单次>100ms需调优
  • FGC :Full GC次数 → >0次必须立即排查 (停顿秒级)
  • FGCT:Full GC总耗时(秒)
  • GCT:GC总耗时(秒)

生产告警阈值

bash 复制代码
# YGC频率 > 5次/秒
if [ $(jstat -gcutil $PID | tail -1 | awk '{print $7}' | awk -F '.' '{print $1}') > 5 ]; then
    echo "ALERT: High YGC frequency!"
fi

# Old区 > 70%
if [ $(jstat -gcutil $PID | tail -1 | awk '{print $4}' | cut -d'.' -f1) > 70 ]; then
    echo "ALERT: Old gen > 70%!"
fi

1.2 jmap - 内存映射工具

定位:生成堆快照(Heap Dump),分析内存泄漏、大对象。

核心命令

bash 复制代码
# 1. 查看堆概况(快速)
jmap -heap 12345

# 2. 查看对象统计(直方图)
jmap -histo 12345 | head -20  # 显示前20个占用最多的类

# 3. 生成Heap Dump(生产慎用,会STW)
jmap -dump:format=b,file=/tmp/heap.hprof 12345

# 4. 查看类加载器统计
jmap -clstats 12345
-heap输出解读
bash 复制代码
$ jmap -heap 12345
Attaching to process ID 12345, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.281-b09

using thread-local object allocation.
Garbage-First (G1) GC with 8 thread(s)

Heap Configuration:  ← 堆配置
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 4294967296 (4096.0MB)  ← -Xmx4g
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 2576351232 (2457.0MB)
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB  ← -XX:MaxMetaspaceSize未设置,危险!

Heap Usage:  ← 堆使用
G1 Heap:
   regions  = 2048       ← 4GB堆/2MB Region = 2048个Region
   capacity = 4294967296 (4096.0MB)
   used     = 2147483648 (2048.0MB)  ← 已使用50%
   free     = 2147483648 (2048.0MB)
   50.0% used

G1 Young Generation:
Eden Space:
   regions  = 341
   capacity = 1358954496 (1296.0MB)
   used     = 714709504 (681.7MB)
   free     = 644244992 (614.3MB)
   52.59444444444444% used
Survivor Space:
   regions  = 24
   capacity = 50331648 (48.0MB)
   used     = 50331648 (48.0MB)
   free     = 0 (0.0MB)
   100.0% used  ← Survivor满,下次Young GC会晋升到Old

G1 Old Generation:
   regions  = 768
   capacity = 2873092096 (2736.0MB)
   used     = 1342076928 (1280.0MB)
   free     = 1531015168 (1456.0MB)
   46.78362573099415% used  ← 接近G1 Mixed GC阈值45%

关键信息提取

  • MaxMetaspaceSize未设置:存在Metaspace OOM风险
  • Survivor使用率100%:下次Young GC大量对象晋升Old,可能触发Mixed GC
  • Old区使用率46.8%:接近触发Mixed GC,需关注后续增长
-histo对象直方图
bash 复制代码
$ jmap -histo 12345 | head -20
 num     #instances         #bytes  class name
----------------------------------------------
   1:       1234567     98765432  [C  ← char[] (String底层)
   2:        789012     47340720  java.lang.String
   3:        345678     27654240  java.util.LinkedHashMap$Entry
   4:        234567     18765360  com.example.Order
   5:        123456      9876480  java.math.BigDecimal
   6:         89012      7120960  [Ljava.lang.Object;
   7:         56789      4543120  [B  ← byte[]
   8:         23456      3752960  java.util.ArrayList
   9:         12345      2966400  java.util.HashMap$Node
  10:          6789      2715600  java.util.LinkedHashMap
...
Total:      5678901    456789012  ← 总实例数和内存

# 快速识别内存泄漏
# 1. 多次采样对比:jmap -histo 12345 > 1.txt,等待5分钟,再次采样
# 2. diff 1.txt 2.txt,看哪个类实例数持续增长

常见内存泄漏嫌疑

  • [C :String堆积(日志未限流、缓存未清理)
  • [B :byte[]堆积(文件读取未关闭、网络流缓存)
  • com.example.Order :业务对象未释放(静态集合持有)
Heap Dump分析
bash 复制代码
# 生产环境谨慎使用,会STW 5-30秒
jmap -dump:live,format=b,file=/tmp/heap.hprof 12345

# 后续使用MAT或VisualVM分析

MAT分析步骤

  1. Histogram :查看com.example.Order引用链
  2. Dominators Tree:找GC Root路径
  3. Leak Suspects:自动报告泄漏嫌疑

1.3 jstack - 线程堆栈分析

定位 :分析线程死锁、CPU飙高、阻塞问题,生产环境救命工具

核心命令

bash 复制代码
# 1. 查看所有线程堆栈
jstack 12345

# 2. 检测死锁(自动识别)
jstack -l 12345 | grep "deadlock"

# 3. 查看Java/F JVM线程
jstack -m 12345  # mixed mode,包含C++ native栈

# 4. 输出到文件
jstack 12345 > /tmp/thread.dump
死锁分析实战
bash 复制代码
$ jstack -l 12345

"Thread-1" #56 prio=5 os_prio=0 tid=0x00007f8b9c008000 nid=0x1e8b waiting for monitor entry [0x00007f8b8e8f0000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.example.DeadlockDemo.method1(DeadlockDemo.java:20)
        - waiting to lock <0x00000000f6a9f108> (a java.lang.Object)
        - locked <0x00000000f6a9f110> (a java.lang.Object)
        at com.example.DeadlockDemo$$Lambda$19/0x00000008400a1040.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Thread-2" #57 prio=5 os_prio=0 tid=0x00007f8b9c009800 nid=0x1e8c waiting for monitor entry [0x00007f8b8e7f0000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.example.DeadlockDemo.method2(DeadlockDemo.java:32)
        - waiting to lock <0x00000000f6a9f110> (a java.lang.Object)
        - locked <0x00000000f6a9f108> (a java.lang.Object)
        at com.example.DeadlockDemo$$Lambda$20/0x00000008400a1440.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f8b8c0052d8 (object 0x00000000f6a9f108, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x00007f8b8c0053a8 (object 0x00000000f6a9f110, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.example.DeadlockDemo.method1(DeadlockDemo.java:20)
        - waiting to lock <0x00000000f6a9f108>
        - locked <0x00000000f6a9f110>
"Thread-2":
        at com.example.DeadlockDemo.method2(DeadlockDemo.java:32)
        - waiting to lock <0x00000000f6a9f110>
        - locked <0x00000000f6a9f108>

Found 1 deadlock.

解读

  • Thread-1 :持有0x110,等待0x108
  • Thread-2 :持有0x108,等待0x110
  • 根本原因:锁获取顺序相反
CPU飙高分析
bash 复制代码
# 1. 找到CPU最高的线程ID(top命令)
top -Hp 12345
# 输出:PID 12375 占CPU 99.9%

# 2. 线程ID转16进制
printf "%x\n" 12375  # 得到 306f

# 3. jstack定位线程
jstack 12345 | grep -A 20 "306f"

"Thread-123" #123 prio=5 os_prio=0 tid=0x00007f8b9c123000 nid=0x306f runnable [0x00007f8b8c7f0000]
   java.lang.Thread.State: RUNNABLE
        at com.example.InfiniteLoop.badLoop(InfiniteLoop.java:15)  ← 定位到死循环代码
        at com.example.InfiniteLoop.lambda$main$0(InfiniteLoop.java:8)
        at java.lang.Thread.run(Thread.java:748)

状态说明

  • RUNNABLE :线程在运行或等待CPU(正常)
  • BLOCKED :等待锁(waiting for monitor entry
  • WAITINGObject.wait()LockSupport.park()
  • TIMED_WAITINGThread.sleep()或带超时wait()

1.4 jcmd - 多功能诊断工具(JDK 7+)

定位 :瑞士军刀,整合jmapjstack等功能,推荐替代旧工具

核心命令

bash 复制代码
# 查看所有命令
jcmd 12345 help

# 1. 生成Heap Dump(推荐替代jmap)
jcmd 12345 GC.heap_dump /tmp/heap.hprof

# 2. 线程Dump(推荐替代jstack)
jcmd 12345 Thread.print > /tmp/thread.dump

# 3. 查看系统属性
jcmd 12345 VM.system_properties

# 4. 查看JVM启动参数
jcmd 12345 VM.flags

# 5. 查看命令行
jcmd 12345 VM.command_line

# 6. 查看GC信息
jcmd 12345 GC.class_histogram  # 替代jmap -histo

# 7. 手动触发GC
jcmd 12345 GC.run

# 8. 查看ClassLoader统计
jcmd 12345 GC.class_stats

# 9. 查看JIT编译队列
jcmd 12345 Compiler.queue

# 10. 查看已编译方法
jcmd 12345 Compiler.codelist

优势

  • 轻量级(无STW)
  • 命令统一,学习成本低
  • 功能覆盖全面

二、可视化监控工具

2.1 jconsole - JMX监控控制台

启动方式

bash 复制代码
# 本地连接
jconsole 12345

# 远程连接(需JVM参数)
java -Dcom.sun.management.jmxremote \
     -Dcom.sun.management.jmxremote.port=9999 \
     -Dcom.sun.management.jmxremote.authenticate=false \
     -Dcom.sun.management.jmxremote.ssl=false \
     -jar app.jar

核心功能页签

  • 概述:堆内存、线程、类、CPU使用率趋势图
  • 内存:Eden/Survivor/Old/Metaspace实时用量,手动触发GC
  • 线程:所有线程状态,检测死锁
  • :已加载类数量,类加载器统计
  • VM摘要:JVM版本、参数、系统属性
  • MBean:操作JMX暴露的MBean(如Tomcat线程池)

实战场景:快速查看堆内存趋势,识别内存泄漏(Old区持续上涨)。


2.2 jvisualvm - 全能分析工具

启动jvisualvm(JDK自带,功能比jconsole强大10倍)

核心插件(工具 → 插件):

  • Visual GC:实时显示各内存区域及GC事件(强烈推荐)
  • BTrace:动态追踪(无需重启)
  • MBeans:JMX操作
  • Threads Inspector:线程深度分析

实用功能

  1. Sampler:CPU/内存采样,定位热点方法
  2. Profiler:CPU/内存精准分析(带调用树)
  3. Heap Dump分析:内置MAT-lite功能
  4. Thread Dump:一键生成并分析
  5. 远程连接:JMX/JSTATd连接生产环境

生产连接

bash 复制代码
# 在被监控JVM启动jstatd
jstatd -J-Djava.security.policy=jstatd.all.policy -p 1099

# policy文件内容(jstatd.all.policy)
grant codebase "file:${java.home}/../lib/tools.jar" {
    permission java.security.AllPermission;
};

然后在VisualVM中添加远程主机,选择jstatd连接。


三、致命错误日志:hs_err_pid.log深度解读

3.1 日志生成场景

当JVM崩溃(Segmentation Fault、内存不足、断言失败)时自动生成,文件名为hs_err_pid<pid>.log

触发原因

  • Native代码崩溃:JNI、JNA调用C/C++库
  • JVM Bug:JIT编译错误、GC崩溃
  • 内存不足:无法分配内存、栈溢出
  • 系统信号:SIGSEGV (11)、SIGBUS (7)

3.2 日志结构解析

头部信息

复制代码
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007f8b9c123456, pid=12345, tid=0x00007f8b8c7f0000
#
# JRE version: Java(TM) SE Runtime Environment (8.0_281-b09) (build 1.8.0_281-b09)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.281-b09 mixed mode linux-amd64 compressed oops)
# Problematic frame:
# C  [libmysql.so+0x123456]  mysql_query+0x123
#
# Core dump written. Default location: /tmp/core or core.12345
#

关键字段

  • SIGSEGV:段错误,访问非法内存地址
  • pc=0x...:崩溃的指令地址
  • pid/tid:进程ID和线程ID
  • Problematic frame:崩溃的库和函数

线程栈

复制代码
Current thread (0x00007f8b9c008000):  JavaThread "main" [_thread_in_native, id=12345, stack(0x00007f8b8c6f0000,0x00007f8b8c7f0000)]

Stack: [0x00007f8b8c6f0000,0x00007f8b8c7f0000],  sp=0x00007f8b8c7ef000,  free space=1019k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C  [libmysql.so+0x123456]  mysql_query+0x123
j  com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal()V+567
j  com.mysql.cj.jdbc.ClientPreparedStatement.execute()V+89
j  com.example.UserDAO.getUser(J)Lcom/example/User;+123
J 9145 C2 com.example.UserService.process(J)V (567 bytes) @ 0x00007f8b9a123456 [0x00007f8b9a123400+0x56]
v  ~StubRoutines::call_stub
V  [libjvm.so+0x1234567]  JavaCalls::call_helper(JavaValue*, methodHandle*, JavaCallArguments*, Thread*)+0x567
V  [libjvm.so+0x1234568]  jni_invoke_static(JNIEnv_*, JavaValue*, _jobject*, JNICallType, _jmethodID*, JNI_ArgumentPusher*, Thread*)+0x234

解读

  • _thread_in_native :线程正在执行Native代码(JNI)
  • 栈帧类型C=Native代码, j=解释执行, J=JIT编译, V=JVM内部
  • 崩溃位置libmysql.somysql_query函数

进程内存映射

复制代码
Memory: 4k page, physical 16384k(4096k free), swap 0k(0k free)

vm_info: Java HotSpot(TM) 64-Bit Server VM (25.281-b09) for linux-amd64 JRE (1.8.0_281-b09), built on Dec  4 2020 00:00:00 by "java_re" with gcc 4.8.2

# 活跃内存区域
[0x00007f8b8c6f0000 - 0x00007f8b8c7f0000] stack of thread 0x00007f8b9c008000 (main)
[0x00000000f6a00000 - 0x00000000f7b00000] Java heap (Eden)
[0x00000000f7b00000 - 0x00000000f7c00000] Java heap (Survivor)
[0x00000000f7c00000 - 0x00000000f8c00000] Java heap (Old)

JVM参数

复制代码
VM Arguments:
jvm_args: -Xmx4g -Xms4g -XX:+UseG1GC -Dfile.encoding=UTF-8 
java_command: com.example.MyApp
Launcher Type: SUN_STANDARD

系统信息

复制代码
OS:Linux (4.18.0-193.el8.x86_64)
CPU:16 total, 16 cores
Memory: 64G total, 32G free

3.3 真实案例解读

案例1:JNI崩溃(最常见)
复制代码
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007f8b9c123456, pid=12345, tid=0x00007f8b8c7f0000
#
# Problematic frame:
# C  [libmyjni.so+0x123456]  process_data+0x123
#
# Stack:
# C  [libmyjni.so+0x123456]  process_data+0x123
# j  com.example.JNIWrapper.nativeProcess([B)V+0
# j  com.example.DataProcessor.process(Ljava/nio/ByteBuffer;)V+123

# 分析
1. 崩溃在libmyjni.so的process_data函数
2. 传入的ByteBuffer可能为null或已释放
3. 检查JNI代码:未检查IsDirectByteBuffer或GetDirectBufferAddress返回NULL

解决方案

c 复制代码
// 修复前
JNIEXPORT void JNICALL Java_com_example_JNIWrapper_nativeProcess(JNIEnv *env, jobject obj, jobject buffer) {
    char* data = (*env)->GetDirectBufferAddress(env, buffer);  // 可能返回NULL
    process(data);  // SIGSEGV
}

// 修复后
JNIEXPORT void JNICALL Java_com_example_JNIWrapper_nativeProcess(JNIEnv *env, jobject obj, jobject buffer) {
    if (buffer == NULL) {
        jclass exc = (*env)->FindClass(env, "java/lang/NullPointerException");
        (*env)->ThrowNew(env, exc, "ByteBuffer is null");
        return;
    }
    char* data = (*env)->GetDirectBufferAddress(env, buffer);
    if (data == NULL) {
        jclass exc = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
        (*env)->ThrowNew(env, exc, "Not a direct ByteBuffer");
        return;
    }
    process(data);
}
案例2:JIT编译Bug
复制代码
# Problematic frame:
# J 9145 C2 com.example.UserService.process(J)V (567 bytes) @ 0x00007f8b9a123456 [0x00007f8b9a123400+0x56]
#
# 分析
1. J表示JIT编译代码
2. C2编译器优化的方法崩溃
3. 可能原因:C2激进优化导致内存访问错误

# 临时解决方案
-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation
-XX:CompileCommand=exclude,com/example/UserService::process  # 排除该方法编译

# 长期方案
升级JDK版本(Oracle在后续版本修复)
案例3:Metaspace OOM导致崩溃
复制代码
#
# java.lang.OutOfMemoryError: Metaspace
#
# Memory: 4k page, physical 16384k(0k free), swap 0k(0k free)
#
# 分析
1. Metaspace未设置MaxMetaspaceSize,无限增长
2. 动态生成类(CGLIB、JSP)未卸载

# 解决方案
-XX:MaxMetaspaceSize=512m -XX:+UseCompressedOops
-XX:+CMSClassUnloadingEnabled (JDK 8)
-XX:+ClassUnloadingWithConcurrentMark (G1)

3.4 日志分析工具

CrashReport分析(JDK自带):

bash 复制代码
# 使用jhsdb分析core dump
jhsdb clhsdb --core /tmp/core.12345 --exe /usr/lib/jvm/java-8/bin/java

# 或启动GUI分析
jhsdb hsdb --core /tmp/core.12345

生产环境应急

  1. 保留现场:立即备份hs_err_pid.log和core dump
  2. 快速恢复:启动备用实例,流量切换
  3. 根因分析:根据日志定位到代码/库/JDK版本
  4. 长期修复
    • JNI代码:增加健壮性检查
    • JVM Bug:升级JDK版本
    • 内存不足:增加堆或优化代码

四、监控实战总结

生产环境监控脚本

bash 复制代码
#!/bin/bash
PID=$(pgrep -f myapp.jar)

# 1. 监控GC(每秒采样)
jstat -gcutil $PID 1000 > /tmp/gc.log &

# 2. 监控堆(每小时生成Dump)
while true; do
    jmap -dump:format=b,file=/tmp/heap-$(date +%H).hprof $PID
    sleep 3600
done &

# 3. 监控线程(检测到死锁时告警)
while true; do
    if jstack $PID | grep -q "Found.*deadlock"; then
        echo "DEADLOCK DETECTED!" | mail -s "ALERT" admin@example.com
    fi
    sleep 60
done &

JVM_exporter + Prometheus + Grafana监控大盘

yaml 复制代码
# docker-compose.yml
  jvm-exporter:
    image: sscaling/jmx-prometheus-exporter
    ports:
      - "5556:5556"
    environment:
      JVM_OPTS: "-javaagent:/jmx_prometheus_javaagent.jar=5556:/config.yml"
    volumes:
      - ./jmx-exporter-config.yml:/config.yml

# config.yml
rules:
  - pattern: ".*"

Grafana大盘关键面板

  1. GC停顿时间avg(jvm_gc_pause_seconds_sum / jvm_gc_pause_seconds_count)
  2. 堆使用率(jvm_memory_bytes_used / jvm_memory_bytes_max) * 100
  3. 线程状态sum(jvm_threads_current) by (state)
  4. 类加载数jvm_classes_loaded_total

总结:从监控到故障闭环

监控 → 发现 → 分析 → 解决 → 预防

场景 首选工具 辅助工具 关键指标
GC性能 jstat -gcutil Visual GC插件 YGC/YGCT, Old使用率
内存泄漏 jmap -histo + MAT jcmd GC.class_histogram 对象实例增长
死锁 jstack -l jconsole线程页 BLOCKED线程
CPU飙高 jstack + top Profiler采样 RUNNABLE热点方法
JVM崩溃 hs_err_pid.log jhsdb分析core Problematic frame
综合监控 Prometheus + Grafana jvisualvm 全指标大盘

黄金法则

  1. 无监控不上线:所有生产JVM必须暴露JMX/actuator
  2. 保留现场:OOM/core dump/死锁时立即备份日志
  3. 压测验证:调优参数必须在压测环境验证
  4. 文档沉淀:每次故障后更新Runbook

掌握这些工具,你就是生产环境的"JVM医生"。

相关推荐
华如锦9 小时前
四:从零搭建一个RAG
java·开发语言·人工智能·python·机器学习·spring cloud·计算机视觉
Tony_yitao9 小时前
22.华为OD机试真题:数组拼接(Java实现,100分通关)
java·算法·华为od·algorithm
JavaGuru_LiuYu9 小时前
Spring Boot 整合 SSE(Server-Sent Events)
java·spring boot·后端·sse
爬山算法9 小时前
Hibernate(26)什么是Hibernate的透明持久化?
java·后端·hibernate
彭于晏Yan9 小时前
Springboot实现数据脱敏
java·spring boot·后端
每天吃饭的羊9 小时前
媒体查询
开发语言·前端·javascript
luming-029 小时前
java报错解决:sun.net.utils不存
java·经验分享·bug·.net·intellij-idea
北海有初拥9 小时前
Python基础语法万字详解
java·开发语言·python
alonewolf_999 小时前
Spring IOC容器扩展点全景:深入探索与实践演练
java·后端·spring
super_lzb9 小时前
springboot打war包时将外部配置文件打入到war包内
java·spring boot·后端·maven