java进程CPU异常问题排查
近期在进行日常巡检时发现,线上部分应用服务器的CPU突然比以往高出很多,经过登录机器排查确认是C2 CompilerThread线程始终长时间运行消耗了CPU。
一、现场环境
1.1. docker环境
查看docker进程:
docker ps -a
查看docker配置:
docker inspect a5777210b249
启动docker(已启动则忽略):
docker start a5777210b249
进入容器:
docker exec -it a5777210b249 /bin/bash
1.2. 操作系统
docker容器使用的操作系统是:debian(stretch )
root@zy:/opt# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
NAME="Debian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
现场jdk版本:
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (build 1.8.0_212-b04)
OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
1.3. 修改 APT 源列表为归档地址
由于现场环境没有外网,这里可以通过配置代理访问外网。
1.3.1. 配置代理
配置代理:
export http_proxy=http://ip:10809
export https_proxy=http://ip:10809
export socks_proxy=socks://ip:1080
export socks5_proxy=socks5://ip:1080
1.3.2. 镜像源
# 宿主机
vim /etc/apt/sources.list
deb https://mirrors.aliyun.com/debian-archive/debian stretch main contrib non-free
#deb https://mirrors.aliyun.com/debian-archive/debian stretch-proposed-updates main non-free contrib
#deb https://mirrors.aliyun.com/debian-archive/debian stretch-backports main non-free contrib
deb https://mirrors.aliyun.com/debian-archive/debian-security stretch/updates main contrib non-free
deb-src https://mirrors.aliyun.com/debian-archive/debian stretch main contrib non-free
#deb-src https://mirrors.aliyun.com/debian-archive/debian stretch-proposed-updates main contrib non-free
#deb-src https://mirrors.aliyun.com/debian-archive/debian stretch-backports main contrib non-free
deb-src https://mirrors.aliyun.com/debian-archive/debian-security stretch/updates main contrib non-free
参考:debian镜像_debian下载地址_debian安装教程-阿里巴巴开源镜像站;
包位于:debian-archive-debian-pool安装包下载_开源镜像站-阿里云。
1.3.3. 安装 debian-archive-keyring 包
手动下载并安装包含旧密钥的软件包:
wget http://archive.debian.org/debian/pool/main/d/debian-archive-keyring/debian-archive-keyring_2017.5+deb9u1_all.deb
dpkg -i debian-archive-keyring_2017.5+deb9u1_all.deb
1.3.4. 更新 APT 缓存
# 安装必要的HTTPS支持组件 docker容器缺少
wget https://mirrors.aliyun.com/debian-archive/debian/pool/main/a/apt/apt-transport-https_1.4.10_amd64.deb
dpkg -i apt-transport-https_1.4.10_amd64.deb
apt clean# 清理旧缓存
apt update # 更新仓库元数据
二、安装gdb
容器内安装gdb,这里考虑没有外网的场景如何安装gdb。
2.1.在线安装(有外网)
编译安装如果存在问题,尝试apt安装:
apt install gdb
gdb -version
apt install gcore
新建/usr/bin/pstack文件:
#!/bin/sh
if test $# -ne 1; then
echo "Usage: `basename $0 .sh` <process-id>" 1>&2
exit 1
fi
if test ! -r /proc/$1; then
echo "Process $1 not found." 1>&2
exit 1
fi
# GDB 路径需根据实际安装位置调整
GDB=${GDB:-/usr/bin/gdb}
# 检查 GDB 是否存在
if [ ! -x $GDB ]; then
echo "GDB not found. Install gdb first." 1>&2
exit 1
fi
# 运行 GDB 获取堆栈
$GDB --quiet -nx \
-ex "set height 0" \
-ex "attach $1" \
-ex "thread apply all bt" \
-ex "detach" \
-ex "quit"
2.2. 离线编译安装
以下两条命令均可以检查包是否已经安装:
dpkg -l | grep gdb
apt list --installed | grep gdb
2.2.1. 查看依赖
root@zy:/opt# apt-cache depends gdb
gdb
Depends: libbabeltrace-ctf1
Depends: libbabeltrace1
Depends: libc6
Depends: libexpat1
Depends: liblzma5
Depends: libncurses5
Depends: libpython3.5
Depends: libreadline7
Depends: libtinfo5
Depends: zlib1g
Conflicts: gdb
gdb-minimal
gdb-python2
Recommends: <libc-dbg>
libc6-dbg
Suggests: gdb-doc
Suggests: gdbserver
Replaces: gdb
gdb-minimal
gdb-python2
2.2.2 下载deb及其依赖包
在服务器通过命令在线下载;
mkdir gdb-dir
cd gdb-dir
apt-get download $(apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances --no-pre-depends gdb | grep "^\w")
ls
cdebconf_0.227_amd64.deblibcap-ng0_0.7.7-3+b1_amd64.deb libncursesw5_6.0+20161126-1+deb9u2_amd64.deb libsepol1_2.6-2_amd64.deb
debconf_1.5.61_all.deblibncurses5_6.0+20161126-1+deb9u2_amd64.deb libsemanage1_2.6-2_amd64.deb zlib1g_1%3a1.2.8.dfsg-5+deb9u1_amd64.deb
......
2.2.3. 使用dpkg命令离线安装
dpkg -i *.deb
三、安装arthas
3.1. 安装运行
mkdir arthas
cd arthas
wget https://kkgithub.com/alibaba/arthas/releases/download/arthas-all-4.0.5/arthas-bin.zip
java -jar arthas-boot.jar
3.2. 接口耗时
分析接口耗时:trace+类路径+方法名
trace 类全路径 方法名
分析CPU占用较高的5个线程:
thread -n 5
3.3. 火焰图
使用profiler生成火焰图:
profiler start # 启动性能分析
profiler stop --format html # 停止并生成火焰图
排查到卡死线程方法:

PhaseIterGVN 是 JVM 的 C2 编译器(JIT 优化编译器)中的一个关键阶段,负责全局值编号(Global Value Numbering)优化。LoadNode 是 JIT 编译器在优化过程中处理的节点,通常表示内存加载操作。当这些部分占用 CPU 达到 99%,说明 JIT 编译器在优化代码时可能遇到复杂逻辑或陷入性能瓶颈。
JVM 尝试通过 C2Compiler::compile_method 编译一个方法,但卡在那里了。
3.3.1. 检查JVM编译日志
如果服务可以重启,我们可以在应用启动时添加以下 JVM 参数,记录详细的编译过程:
-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=jit.log
关键日志字段:
● compile_id:编译任务 ID。
● time:编译耗时(单位:ms)。
● name:正在编译的方法名(格式:类名::方法名)。
在日志中搜索 COMPILE SKIPPED 或 COMPILE FAILED,观察是否有反复重试的编译任务。
3.3.2. 分析长时间编译的方法
过滤耗时异常的编译任务:
grep "compile_time=" jit.log | awk -F "compile_time=" '{print $2}' | sort -nr | head -n 10
观察编译队列:
grep "QueueFull" jit.log
如果日志中存在 QueueFull,说明编译任务堆积,JVM 来不及处理。
3.3.3. 结合 Arthas 反编译方法
如果已确定某个方法耗时较长(例如 com.example.MyClass::myMethod),使用 Arthas 的 jad 命令反编译其字节码:
jad com.example.MyClass myMethod
3.3.4. 解决方式
通过 JVM 参数排除动态类被 C2 编译:
-XX:CompileCommand=exclude,GeneratedClass::dynamicMethod
如果禁用后C2线程 CPU 恢复正常,说明该方法确实是根因。
四、gdb调试分析
在生产环境下,如果重启服务后CPU较高现象可能不再存在。为了定位C2Compiler::compile_method 编译卡死时的入参,我们可以使用 gdb 连接到 java 进程。
4.1. gdb 在线调试(意义不大,可跳过)
4.1.1. 附加线程
开始调试,注意会导致程序挂起:
gdb
attach 1
在地方设置断点C2Compiler::compile_method(ciEnv*, ciMethod*,int) :
(gdb) break C2Compiler::compile_method
# 查看断点
(gdb) info b
NumType Disp Enb Address What
1 breakpointkeep y 0x00007fed9c0d1ee4 <C2Compiler::compile_method(ciEnv*, ciMethod*, int)+4>
4.1.2. 继续运行
运行程序直到达到断点,忽略终止信号;
(gdb) continue
Continuing.
[New Thread 0x7fecbc0e2700 (LWP 179203)]
[New Thread 0x7febb7069700 (LWP 179204)]
Thread 81 "java" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fec234cb700 (LWP 140)]
0x00007fed9237df33 in ?? ()
(gdb) handle SIGSEGV nostop noprint pass
Signal Stop Print Pass to program Description
SIGSEGV No No YesSegmentation fault
(gdb) continue
Continuing.
[New Thread 0x7fed2c6e2700 (LWP 179205)]
[New Thread 0x7febb743d700 (LWP 179206)]
[Switching to Thread 0x7fed2ed7b700 (LWP 42)]
Thread 37 "java" hit Breakpoint 1, 0x00007fed9c0d1ee4 in C2Compiler::compile_method(ciEnv*, ciMethod*, int) () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
4.1.3. 运行到断点
一旦程序运行到断点处,你可以使用info args命令来查看当前函数的参数
(gdb) info args
No symbol table info available.
看起来 gdb 无法获取调试信息,所以它无法分辨方法参数存储在哪里。
我们不知道参数值,但我们可以假设参数存储在寄存器或堆栈中。我们可以把两者都转储出来,然后尝试找到类似ciMethod 东西。
(gdb) info registers
rax 0x7fed9cbb1d70 140658513485168
rbx 0x7fec0c3bc520 140651794253088
rcx 0xffffffff 4294967295
rdx 0x7fed281b2ae0 140656556845792
rsi 0x7fed2ed7a870 140656669862000
rdi 0x7fed944049d0 140658371217872
rbp 0x7fed2ed7a7d0 0x7fed2ed7a7d0
rsp 0x7fed2ed7a7d0 0x7fed2ed7a7d0
r80x1 1
r90x4009748 67147592
r10 0x7fed9441ff10 140658371329808
r11 0x2db9150a532590 12869873970652560
r12 0x7fed9441f000 140658371325952
r13 0x7fed9441f980 140658371328384
r14 0x7fed2ed7a870 140656669862000
r15 0x0 0
rip 0x7fed9c0d1ee4 0x7fed9c0d1ee4 <C2Compiler::compile_method(ciEnv*, ciMethod*, int)+4>
eflags 0x202 [ IF ]
cs0x3351
ss0x2b43
ds0x0 0
es0x0 0
fs0x0 0
gs0x0 0
k00x0 0
k10x0 0
k20x0 0
k30x0 0
k40x0 0
k50x0 0
k60x0 0
k70x0 0
(gdb) x/10xg $rsp
0x7fed2ed7a7d0: 0x00007fed2ed7ab80 0x00007fed9c180686
0x7fed2ed7a7e0: 0x0000000000000000 0x0000000000000000
0x7fed2ed7a7f0: 0x0000000000000001 0x00007fed2ed7a860
0x7fed2ed7a800: 0x0000000000000010 0x00007fecf80008f0
0x7fed2ed7a810: 0x00000000000003d8 0x00007fed9441f9d0
4.1.4. 恢复程序运行
(gdb) continue# 缩写 c,恢复所有线程运行
# 或
(gdb) detach # 退出调试但保持进程运行(适用于附加模式)
(gdb) quit # 退出
4.2. gdb 离线调试(重点)
虽然 GDB 可以在实时进程上使用,但离线模式下进行调查更有意义,要离线,我们需要收集核心转储(即应用程序内存的快照)。
4.2.1. Core dump 核⼼转储
在宿主机执行:
[root@zy debug]# nsenter -t 41962 --mount --uts --ipc --net --pid
root@zy:/# cd /opt/server/logs/gdb-dir/debug/
[root@zy debug]# gcore 1
[Thread debugging using libthread_db enabled]
.......
Saved corefile core.1
其中9626是进程id。然后我们将 core.1 导出到一个安全的位置。核心转储本身不足以进行分析,还需要原始二进制文件。
(gdb) gdb /usr/local/openjdk-8/bin/java core.1
4.2.2. 查看所有线程
我们需要找到一个线程并检查其变量:
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7fed9d843b40 (LWP 1) 0x00007fed9d4136dd in pthread_join () from /lib/x86_64-linux-gnu/libpthread.so.0
2 Thread 0x7fed9d842700 (LWP 7) 0x00007fed9d41817f in pthread_cond_wait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
3 Thread 0x7fed9a90a700 (LWP 8) 0x00007fed9d41817f in pthread_cond_wait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
4 Thread 0x7fed9a809700 (LWP 9) 0x00007fed9d41817f in pthread_cond_wait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
......
Id 列:GDB 分配的线程编号(后续操作使用此编号)。
LWP:操作系统级的线程ID(Lightweight Process ID,这个是容器内部的线程ID)。
线程36、40、43卡在 can_see_stored_value :
31 36 0x24
35 40 0x28
38 43 0x3B
4.2.3. 切换到目标线程
(gdb) thread 31
[Switching to thread 31 (Thread 0x7fed2f381700 (LWP 36))]
#0 0x00007fed9c51354c in MemNode::can_see_stored_value(Node*, PhaseTransform*) const () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
栈帧编号(如 #0, #1)表示调用层级,从当前执行点向上回溯。
(gdb) bt
#0 0x00007fed9c51354c in MemNode::can_see_stored_value(Node*, PhaseTransform*) const () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#1 0x00007fed9c5169b4 in LoadNode::Value(PhaseTransform*) const () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#2 0x00007fed9c5e97eb in PhaseIterGVN::transform_old(Node*) () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#3 0x00007fed9c5e713c in PhaseIterGVN::optimize() () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#4 0x00007fed9c1788db in Compile::Optimize() () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#5 0x00007fed9c17aae7 in Compile::Compile(ciEnv*, C2Compiler*, ciMethod*, int, bool, bool, bool) () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#6 0x00007fed9c0d20ec in C2Compiler::compile_method(ciEnv*, ciMethod*, int) () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#7 0x00007fed9c180686 in CompileBroker::invoke_compiler_on_method(CompileTask*) () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#8 0x00007fed9c183f77 in CompileBroker::compiler_thread_loop() () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#9 0x00007fed9c6e8481 in JavaThread::thread_main_inner() () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#10 0x00007fed9c6e85f0 in JavaThread::run() () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#11 0x00007fed9c5a4162 in java_start(Thread*) () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
#12 0x00007fed9d4124a4 in start_thread () from /lib/x86_64-linux-gnu/libpthread.so.0
#13 0x00007fed9cd39d0f in clone () from /lib/x86_64-linux-gnu/libc.so.6
4.2.4. 查看指定栈帧的局部变量
我们先从框架 #6 开始:
(gdb) frame 6
#6 0x00007fed9c0d20ec in C2Compiler::compile_method(ciEnv*, ciMethod*, int) () from /usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
(gdb) info args
No symbol table info available.
我们可以假设参数存储在寄存器或堆栈中。我们可以把两者都转储出来,然后尝试找到类似ciMethod 东西;
(gdb) info registers
rax 0x7fecf019b4f8 140655617225976
rbx 0x7fed2f380770 140656676177776
rcx 0x7fecf019b488 140655617225864
rdx 0x2638
rsi 0x130 304
rdi 0x7fed10d80e60 140656166571616
rbp 0x7fed2f3806d0 0x7fed2f3806d0
rsp 0x7fed2f37fc40 0x7fed2f37fc40
r80x7fed2f37fca0 140656676175008
r90x0 0
r10 0xfd253
r11 0xffffffffffffffff -1
r12 0x1 1
r13 0x7fed2f37fca0 140656676175008
r14 0x7fed9c7772e8 140658509050600
r15 0x7fed9cbd9a70 140658513648240
rip 0x7fed9c0d20ec 0x7fed9c0d20ec <C2Compiler::compile_method(ciEnv*, ciMethod*, int)+524>
eflags 0x293 [ CF AF SF IF ]
cs0x3351
ss0x2b43
ds0x0 0
es0x0 0
fs0x0 0
gs0x0 0
k00x0 0
k10x0 0
k20x0 0
k30x0 0
k40x0 0
k50x0 0
k60x0 0
k70x0 0
RIP: Instruction Pointer,指令指针寄存器。它包含了将要执行的下一条指令的内存地址,这里指向了C2Compiler::compile_method函数偏移524处;
这里我们将C2Compiler::compile_method函数整个帧空间数据dump出来(rsp-rbp),该空间存放的一般是函数局部变量:
其中rbp : 0x7fed2f3806d0 ,rsp 0x7fed2f37fc40 ;
(gdb) x/370xg $rsp
0x7fed2f37fc40: 0x0000000000000001 0x0000000000000001
0x7fed2f37fc50: 0x0000000000000000 0x00007fed2f37fd90
0x7fed2f37fc60: 0x00007fed2f37fe38 0x00007fed2f37fe68
0x7fed2f37fc70: 0x00007fed2f37ff08 0x00007fed2f3804b0
0x7fed2f37fc80: 0xffffffff00000001 0x00007fed101a0490(这个地址很重要)
0x7fed2f37fc90: 0x00007fed944049d0 0x01017fed9c5aaef1
0x7fed2f37fca0: 0x0000000000000000 0x00007fed2f37fca0
0x7fed2f37fcb0: 0x0101010000116e5e 0x00007fed101a0490
0x7fed2f37fcc0: 0x00007fedffffffff 0x00007fed10248bd0
0x7fed2f37fcd0: 0x00007fed10b14fc0 0x0000000000000000
0x7fed2f37fce0: 0x0000000000000000 0x0000000000000000
0x7fed2f37fcf0: 0x000000230000002b 0x0000000200000145
0x7fed2f37fd00: 0x0000000000036ee8 0x0000000000000000
0x7fed2f37fd10: 0x0101000000000005 0x0000000000000001
0x7fed2f37fd20: 0x0000001000000000 0x0000000000000000
0x7fed2f37fd30: 0x000000000000000e 0x0000000000000000
0x7fed2f37fd40: 0x0000000000000000 0x0000000000000000
0x7fed2f37fd50: 0x0000000000000000 0x0000000000000000
0x7fed2f37fd60: 0x0000000000000000 0x0000002100000000
0x7fed2f37fd70: 0x000054b02f380801 0x0000000000010001
0x7fed2f37fd80: 0x0100000000000003 0x000000022f380300
0x7fed2f37fd90: 0x00007fed00000006 0x00007fed103913c0
0x7fed2f37fda0: 0x00007fed0803bd40 0x00007fed08040560
0x7fed2f37fdb0: 0x00007fed08043d28 0x0000000000098158
0x7fed2f37fdc0: 0x00007fed2f380770 0x0000000000000000
0x7fed2f37fdd0: 0x0000000000000000 0x00007fed080c65c0
0x7fed2f37fde0: 0x00007fed10391710 0x00007fed10391770
0x7fed2f37fdf0: 0x00007fed10b196b0 0x00007fed10b19710
0x7fed2f37fe00: 0x0000000000000000 0x0000000000004f16
0x7fed2f37fe10: 0x00007fed9cbdca90 0x00007fed2f37fd90
0x7fed2f37fe20: 0x00007fed00000400 0x00007fed1077f4d0
......
0x7fed2f3805f0: 0x0000000000000000 0x00007fed9440a800
0x7fed2f380600: 0x0000000000000000 0x0000000000000064
0x7fed2f380610: 0x0000000000000000 0x0000000000000000
0x7fed2f380620: 0x0000000000000000 0x0000000000000000
0x7fed2f380630: 0x0000000000000000 0x00007fed9c1146f1
0x7fed2f380640: 0x00007fed9401c780 0x0000000800000000
0x7fed2f380650: 0x00007fed2f3808d0 0x0000000000000000
0x7fed2f380660: 0x0000000000000000 0x00007fed9cd46ba6
0x7fed2f380670: 0x00007fed2f380770 0x0000000000000000
0x7fed2f380680: 0x00007fed2f3806a0 0x00007f00ffffffff
0x7fed2f380690: 0x00060140b11027f8 0x0000000013da5bb7
0x7fed2f3806a0: 0x00007fed2f3806d0 0x00007febc405e060
0x7fed2f3806b0: 0x00007fed9440a800 0x00007fed9401c6f0
0x7fed2f3806c0: 0x00007fed2f380770 0x0000000000000000
0x7fed2f3806d0: 0x00007fed2f380a80 0x00007fed9c180686
4.2.5. 反汇编分析
可以到github下载openjdk源码(jdk8u-jdk8u202-b04),C2Compiler::compile_method函数调用了Compile::Compile构造函数;
void C2Compiler::compile_method(ciEnv* env, ciMethod* target, int entry_bci) {
assert(is_initialized(), "Compiler thread must be initialized");
bool subsume_loads = SubsumeLoads;
bool do_escape_analysis = DoEscapeAnalysis && !env->jvmti_can_access_local_variables();
bool eliminate_boxing = EliminateAutoBox;
while (!env->failing()) {
// Attempt to compile while subsuming loads into machine instructions.
Compile C(env, this, target, entry_bci, subsume_loads, do_escape_analysis, eliminate_boxing);
......
接下来对compile_method函数进行反编译,分析Compile C(....)入参。在frame 6中反汇编:
(gdb) disassemble /r
......
0x00007fed9c0d20ae <+462>: movzbl -0xa32(%rbp),%eax
0x00007fed9c0d20b5 <+469>: mov -0xa4c(%rbp),%r8d
0x00007fed9c0d20bc <+476>: mov $0x1,%r9d
0x00007fed9c0d20c2 <+482>: mov -0xa48(%rbp),%rcx
0x00007fed9c0d20c9 <+489>: mov -0xa40(%rbp),%rdx
0x00007fed9c0d20d0 <+496>: and %r12d,%r9d
0x00007fed9c0d20d3 <+499>: mov %rbx,%rsi
0x00007fed9c0d20d6 <+502>: mov %r13,%rdi
0x00007fed9c0d20d9 <+505>: mov %eax,0x8(%rsp)
0x00007fed9c0d20dd <+509>: movzbl -0xa31(%rbp),%eax
0x00007fed9c0d20e4 <+516>: mov %eax,(%rsp)
0x00007fed9c0d20e7 <+519>: callq 0x7fed9c179950 <_ZN7CompileC2EP5ciEnvP10C2CompilerP8ciMethodibbb>
0x00007fed9c0d20ec <+524>: 48 8b 85 00 f7 ff ff mov -0x900(%rbp),%rax
0x00007fed9c0d20f3 <+531>: 48 85 c0 test %rax,%rax
......
在x86_64上,Linux遵循System V AMD64 ABI,寄存器用途如下:
| 存器 | 作用 |
|---|---|
| rax | 存储函数调用的返回值 |
| rsp | 栈顶指针寄存器,指向栈顶 |
| rdi | 第一个参数 |
| rsi | 第二个参数 |
| rdx | 第三个参数 |
| rcx | 第四个参数 |
| r8 | 第五个参数 |
| r9 | 第六个参数 |
| rbx, rbp, r12-r15 | 数据存储,callee-save原则 |
| r10-r11 | 数据存储,caller-save原则 |
在C2Compiler::compile_method(ciEnv*, ciMethod*, int)函数524偏移处调用了Compile C(.....)构造函数;
Compile::Compile( ciEnv* ci_env, C2Compiler* compiler, ciMethod* target, int osr_bci,
bool subsume_loads, bool do_escape_analysis, bool eliminate_boxing )
: Phase(Compiler),
_env(ci_env),
......
参数设置详解;
1)rdi (第一个参数): ciEnv* env:
● mov %r13,%rdi
● 值来自 r13 寄存器
2)rsi (第二个参数): C2Compiler* compiler
● mov %rbx,%rsi
● 值来自 rbx 寄存器,rbx=0x7fed2f380770
3)rdx (第三个参数): ciMethod* target
● mov -0xa40(%rbp),%rdx
● 值来自 [rbp - 0xa40]
● 地址计算:$rbp - 0xa40 = 0x7fed2f3806d0 - 0xa40 = 0x7fed2f37fc90
4)rcx (第四个参数): int entry_bci
● mov -0xa48(%rbp),%rcx
● 值来自 [rbp - 0xa48]
● 地址计算:$rbp - 0xa48 = 0x7fed2f3806d0 - 0xa48 = 0x7fed2f37fc88
5)r8 (第五个参数): bool 类型参数
● mov -0xa4c(%rbp),%r8d
● 值来自 [rbp - 0xa4c] 的低32位
● 地址计算:$rbp - 0xa4c = 0x7fed2f3806d0 - 0xa4c = 0x7fed2f37fc84
6)r9 (第六个参数): bool 类型参数
● mov $0x1,%r9d 然后 and %r12d,%r9d
● 初始值设为1,然后与 r12d 做按位与操作
● 在您的寄存器信息中:r12 = 0x1
7)栈参数1 (第七个参数): bool 类型参数
● movzbl -0xa32(%rbp),%eax 然后 mov %eax,0x8(%rsp)
● 值来自 [rbp - 0xa32]
● 地址计算:$rbp - 0xa32 = 0x7fed2f3806d0 - 0xa32 = 0x7fed2f37fc9e
栈参数准备:
mov %eax, 0x8(%rsp); 将 eax 的值保存到 rsp+8
movzbl -0xa31(%rbp), %eax ; 零扩展加载一个字节到 eax
mov %eax, (%rsp); 将 eax 的值保存到栈顶 (rsp)
获取 C2Compiler::compile_method 的原始参数:
void C2Compiler::compile_method(ciEnv* env, ciMethod* target, int entry_bci)
1)ciEnv* env:直接来自 r13 寄存器,命令:
(gdb) p/x $r13
$1 = 0x7fed2f37fca0
2)ciMethod* target:来自 [rbp - 0xa40];
(gdb) x/gx $rbp - 0xa40
0x7fed2f37fc90: 0x00007fed944049d0
3)int entry_bci:来自 [rbp - 0xa48];
(gdb) x/gx $rbp - 0xa48 # 以十六进制格式显示从地址
0x7fed2f37fc88: 0x00007fed101a0490
结合后面HSDB调试器发现0x7fed2f37fc88地址实际存放的是ciMethod* target,并不是 entry_bci,具体原因尚不清楚。
4.2.6. HSDB调试器
给定一个内存地址,我们如何知道其对应的java代码呢?我们启动 HSDB 调试器。
/usr/local/openjdk-8/bin/java -classpath /usr/local/openjdk-8/lib/sa-jdi.jar sun.jvm.hotspot.CLHSDB
然后我们告诉它附加到我们的核心文件:
hsdb> attach /usr/local/openjdk-8/jre/bin/java /opt/server/logs/gdb-dir/debug/core.1
Opening core file, please wait...
通过 CLHSDB 检查核⼼,查看内存地址数据(这里地址来自上面的寄存器/堆栈中的地址):
hsdb> inspect 0x7fed10d80e60
Type is MemBarCPUOrderNode (size of 56)
Node Node::_in: Node* @ 0x00007fed10d80e98
Node Node::_out: Node* @ 0x00007fed10d80f00
node_idx_t Node::_cnt: 5
node_idx_t Node::_max: 5
node_idx_t Node::_outcnt: 2
node_idx_t Node::_outmax: 4
node_idx_t Node::_idx: 8111
jushort Node::_class_id: 17
jushort Node::_flags: 0
hsdb> inspect 0x7fecf019b4f8
hsdb> inspect 0x7fed2f380770
hsdb> inspect 0x7fecf019b488
Type is MergeMemNode (size of 48)
Node Node::_in: Node* @ 0x00007fecf019b4f8
Node Node::_out: Node* @ 0x00007fed103abe38
node_idx_t Node::_cnt: 40
node_idx_t Node::_max: 64
node_idx_t Node::_outcnt: 2
node_idx_t Node::_outmax: 8
node_idx_t Node::_idx: 8338
jushort Node::_class_id: 128
jushort Node::_flags: 0
hsdb> inspect 0x7fed2f3806d0
hsdb> inspect 0x7fed2f37fc40
hsdb> inspect 0x7fed2f37fca0
hsdb> inspect 0x7fed2f37fca0
hsdb> inspect 0x7fed9c7772e8
hsdb> inspect 0x7fed9cbd9a70
hsdb> inspect 0x7fed9c0d20ec
hsdb> inspect 0x7fed9c0d20ec
hsdb> inspect 0x7fed9c0d20ec
hsdb> inspect 0x7fed9c0d20ec
hsdb> inspect 0x7fed9c0d20ec
hsdb> inspect 0x00007fed101a0490
Type is ciMethod (size of 160)
uint ciBaseObject::_ident: 1680
Metadata* ciMetadata::_metadata: Metadata @ 0x00007fed2e207bc8
int ciMethod::_interpreter_invocation_count: 38658
int ciMethod::_interpreter_throwout_count: 0
int ciMethod::_instructions_size: -1
我们有一个 ciMethod(地址0x00007fed101a0490),但它看起来好像什么都不是。它的名字在哪里?
让我们检查 ciMethod 是否有相关字段来获取名称。
class ciMethod : public ciMetadata {
friend class CompileBroker;
CI_PACKAGE_ACCESS
friend class ciEnv;
friend class ciExceptionHandlerStream;
friend class ciBytecodeStream;
friend class ciMethodHandle;
friend class ciReplay;
private:
// General method information.
ciFlags_flags;
ciSymbol* _name; // 方法名
ciInstanceKlass* _holder; // 类名
ciSignature*_signature;
ciMethodData* _method_data;
ciMethodBlocks* _method_blocks;
// Code attributes.
int _code_size;
int _max_stack;
int _max_locals;
vmIntrinsics::ID _intrinsic_id;
int _handler_count;
int _interpreter_invocation_count;
int _interpreter_throwout_count;
int _instructions_size;
int _size_of_parameters;
bool _uses_monitors;
bool _balanced_monitors;
bool _is_c1_compilable;
bool _is_c2_compilable;
bool _can_be_statically_bound;
......
}
ciMethod 类型的对象,占用 160 字节内存。ciMethod 是 JVM 编译器接口(Compiler Interface)的一部分,表示一个即将被编译或已被编译的 Java 方法。
● _metadata:指向底层 JVM 元数据的指针,这个 Method 对象包含 Java 方法的原始元数据(字节码、访问标志等);
● _interpreter_invocation_count:JVM 使用此计数器触发 JIT 编译,值 38658 表示该方法已被解释执行超过 3.8 万次;
我们知道 0x00007fed101a0490 指向一个 ciMethod 实例,所以我们应该检查它的内容:
hsdb> examine 0x7fed101a0490/40
0x00007fed101a0490: 0x00007fed9cbb4530 0x0000000000000690 0x00007fed2e207bc8
0x00007fed101a04a8: 0x0000000081000001 0x00007fed101a0530 0x00007fed101a03e0
0x00007fed101a04c0: 0x00007fed101a0550 0x00007fed101a05d0 0x00007fed101a16d0
0x00007fed101a04d8: 0x0000000400000030 0x0000000000000002 0x0000970200000000
0x00007fed101a04f0: 0xffffffff00000000 0x0101010000000001 0x0000000000000000
0x00007fed101a0508: 0x00007fed101a18f0 0x00007fed101a1920 0x00007fed101a38a0
0x00007fed101a0520: 0x00007fed101a1650 0x0000000000000000 0x00007fed9cbb5ad0
0x00007fed101a0538: 0x0000000000000000 0x00007fed95064710 0x00007fed00000000
0x00007fed101a0550: 0x00007fed940a7750 0x00007fed101a03e0 0x00007fed101a0570
0x00007fed101a0568: 0x0000000000000000 0x0000000800000001 0x00007fed2f380778
0x00007fed101a0580: 0x000000040000000f 0x00007fed101a0590 0x00007fed940a8fb0
0x00007fed101a0598: 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00007fed101a05b0: 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00007fed101a05c8: 0x0000000000000000
然后我们逐个检查每个值:
hsdb> inspect 0x00007fed9cbb4530
hsdb> inspect 0x00007fed2e207bc8
Type is Method (size of 88)
ConstMethod* Method::_constMethod: ConstMethod @ 0x00007fed2e207b38
MethodData* Method::_method_data: MethodData @ 0x00007fecbf788ef8
MethodCounters* Method::_method_counters: MethodCounters @ 0x00007fed2c190588
AccessFlags Method::_access_flags: -2130706431
int Method::_vtable_index: 18
u2 Method::_method_size: 11
u1 Method::_intrinsic_id: 0
nmethod* Method::_code: nmethod @ null
address Method::_i2i_entry: address @ 0x00007fed2e207bf8
AdapterHandlerEntry* Method::_adapter: AdapterHandlerEntry @ 0x00007fed940baf30
address Method::_from_compiled_entry: address @ 0x00007fed2e207c08
address Method::_from_interpreted_entry: address @ 0x00007fed2e207c18
hsdb> inspect 0x00007fed101a0530
Type is ciSymbol (size of 32)
Symbol* ciSymbol::_symbol: Symbol @ 0x00007fed95064710
hsdb> inspect 0x00007fed101a03e0
Type is ciInstanceKlass (size of 136)
uint ciBaseObject::_ident: 1678
Metadata* ciMetadata::_metadata: Metadata @ 0x00000001001b3348
BasicType ciType::_basic_type: 12
ciSymbol* ciKlass::_name: ciSymbol @ 0x00007fed101a0470
InstanceKlass::ClassState ciInstanceKlass::_init_state: 4
bool ciInstanceKlass::_is_shared: 0
hsdb> inspect 0x00007fed101a0550
hsdb> inspect 0x00007fed101a05d0
Type is ciMethodData (size of 368)
uint ciBaseObject::_ident: 1682
Metadata* ciMetadata::_metadata: Metadata @ 0x00007fecbf788ef8
int ciMethodData::_data_size: 272
u_char ciMethodData::_state: 2
int ciMethodData::_extra_data_size: 72
intptr_t* ciMethodData::_data: intptr_t @ 0x00007fed101a0740
int ciMethodData::_hint_di: 256
intx ciMethodData::_eflags: 17
intx ciMethodData::_arg_local: 0
intx ciMethodData::_arg_stack: 0
intx ciMethodData::_arg_returned: 0
int ciMethodData::_current_mileage: 38658
MethodData ciMethodData::_orig: MethodData @ 0x00007fed101a0630
hsdb> inspect 0x00007fed101a16d0
hsdb> inspect 0x00007fed101a18f0
嗯,看起来 0x00007fed101a0530 指向方法名,而 0x00007fed101a03e0 指向类。来我们需要使用symbol 命令来检查符号:
# 方法名
hsdb> inspect 0x00007fed101a0530
Type is ciSymbol (size of 32)
Symbol* ciSymbol::_symbol: Symbol @ 0x00007fed95064710
hsdb>
hsdb> inspect 0x00007fed95064710
hsdb> symbol 0x00007fed95064710
#getParameterType
# 类名
hsdb> inspect 0x00007fed101a03e0
Type is ciInstanceKlass (size of 136)
uint ciBaseObject::_ident: 1678
Metadata* ciMetadata::_metadata: Metadata @ 0x00000001001b3348
BasicType ciType::_basic_type: 12
ciSymbol* ciKlass::_name: ciSymbol @ 0x00007fed101a0470
InstanceKlass::ClassState ciInstanceKlass::_init_state: 4
bool ciInstanceKlass::_is_shared: 0
hsdb> inspect 0x00007fed101a0470
Type is ciSymbol (size of 32)
Symbol* ciSymbol::_symbol: Symbol @ 0x00007fed94b29920
hsdb> symbol 0x00007fed94b29920
#org/springframework/core/MethodParameter
可以定位到org.springframework.core.MethodParameter类的getParameterType方法。
4.2.7. 同理
按照上面的步骤,切换到目标线程:
(gdb) thread 35
可以定位到org.springframework.core.ResolvableType类的forMethodParameter方法。
切换到目标线程:
(gdb) thread 38
可以定位到org.springframework.beans.factory.config.DependencyDescriptor类的getResolvableType方法。
五、方案
5.1. 条件C2编译线程数量
这里我们可以看下默认C2编译线程:
java -XX:+PrintFlagsFinal -version | grep CICompilerCount
我们可通过减少线程数,以降低资源抢占:
-XX:CICompilerCount=2
5.2. CodeCache
CodeCache 的核心作用:
- 存储 JIT 编译器生成的本地机器码;
- 存放:
- C1 编译的代码(基础优化);
- C2 编译的代码(高级优化);
- 运行时生成的适配器代码;
- 方法元数据。
考虑到CodeCache过小会对C2编译产生较大的影响,移除了运行参数ReservedCodeCacheSize配置项。
5.3. 启动参数配调整(排除指定方法)
通过4.2小节分析我们知道问题是由 C2 尝试编译以下方法导致的线程卡住:
● org/springframework/core/MethodParameter#getParameterType ;
● org/springframework/core/ResolvableType#forMethodParameter;
● org/springframework/beans/factory/config/DependencyDescriptor#getResolvableType;
该问题应该是jdk某些版本已知问题,可以参考文章:C2 Stuck in C2Compiler::compile_method with 100%CPU usage for one week。

5.3.1. 修改启动参数
我们可以通过以下 Java 选项将该方法添加到排除列表中,当前进程运行启动参数如下:
java -Xmx192m -Xms128m -Xss328k -XX:MaxDirectMemorySize=32m \
-XX:CompileCommand=exclude,org.springframework.core.MethodParameter::getParameterType \
-XX:CompileCommand=exclude,org.springframework.core.ResolvableType::forMethodParameter \
-XX:CompileCommand=exclude,org.springframework.beans.factory.config.DependencyDescriptor::getResolvableType \
-jar server.jar
注意: CompileCommand=exclude 会完全排除该方法,因此该方法将被解释执行。这可能会影响应用程序的性能,因此 CompileCommand 应被视为一种解决方法。
解释执行和编译执行区别:
● 解释执行:逐条读取字节码,逐条翻译成机器码并执行。启动快,但执行速度相对较慢。
● 编译执行(JIT):将热点代码(被频繁执行的方法)整个编译成本地机器码,然后直接执行机器码。编译过程消耗时间,但编译后的执行效率高。
5.3.2. 验证配置生效
添加日志参数确认排除效果:
java \
-XX:NativeMemoryTracking=detail \
-XX:+PrintCodeCache \
-XX:+PrintCompilation \
-XX:+UnlockDiagnosticVMOptions \
-XX:+LogCompilation \
-XX:LogFile=/opt/server/gc/jit.log \
-XX:+PrintCodeCacheOnCompilation \
-XX:CompileCommand=exclude,org.springframework.core.MethodParameter::getParameterType \
-XX:CompileCommand=exclude,org.springframework.core.ResolvableType::forMethodParameter \
-XX:CompileCommand=exclude,org.springframework.beans.factory.config.DependencyDescriptor::getResolvableType \
-XX:+CITime -Xmx192m -Xms128m -Xss328k \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintHeapAtGC \
-Xloggc:/opt/server/gc/gc-%t.log \
-XX:+UseGCLogFileRotation \
-XX:NumberOfGCLogFiles=20 \
-XX:GCLogFileSize=100M \
-jar server.jar
在/opt/server/gc/jit.log日志中搜索:
CompilerOracle: exclude org/springframework/core/MethodParameter.getParameterType
CompilerOracle: exclude org/springframework/core/ResolvableType.forMethodParameter
CompilerOracle: exclude org/springframework/beans/factory/config/DependencyDescriptor.getResolvableType
### Excluding compile: org.springframework.core.MethodParameter::getParameterType
### Excluding compile: static org.springframework.core.ResolvableType::forMethodParameter
### Excluding compile: org.springframework.beans.factory.config.DependencyDescriptor::getResolvableType
同时可以查看日志中是否有这些方法的C2编译日志。
5.4. 启动参数调整(禁用C2)
java JIT 编译器将编译过程分为 5 个级别(0-4),每个级别提供不同的优化程度:
| 级别 | 名称 | 编译器 | 优化程度 | 特点 |
|---|---|---|---|---|
| 0 | 解释执行 | 解释器 | ⭐ | 不生成机器码,直接解释字节码 |
| 1 | 简单 C1 | C1 | ⭐⭐ | 基础编译,无性能分析 |
| 2 | 受限 C1 | C1 | ⭐⭐⭐ | 带基础性能分析 |
| 3 | 完全 C1 | C1 | ⭐⭐⭐⭐ | 带完整性能分析 |
| 4 | 完全 C2 | C2 | ⭐⭐⭐⭐⭐ | 高级优化(向量化、内联等) |
在现场环境最初只有一个C2编译线程卡住,运行一段时间后有两个C2编译线程卡住,最后出现三个C2编译线程卡住。
因此,因此后续还存在其它C2编译线程卡住的风险,我们可以直接禁用C2。
5.4.1. 修改启动参数
我们可以通过以下 Java 选项将C2禁用,当前进程运行启动参数如下:
java -Xmx192m -Xms128m -Xss328k -XX:MaxDirectMemorySize=32m \
-XX:TieredStopAtLevel=1 \
-jar server.jar
我们可以通过如下选项查看是否真正的禁用C2;
-XX:+PrintCodeCache \
-XX:+PrintCompilation \
-XX:+UnlockDiagnosticVMOptions \
-XX:+LogCompilation \
-XX:LogFile=/opt/server/gc/jit.log \

5.4.2. 验证性能
下面使用jmeter进行压测,以检验token接口为例,设置如下参数:
● 线程数:1,模拟单个用户;
● 循环次数:设置循环次数为3000;
服务器:16核心(Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz),内存32GB。
(1) 启动参数如下:
java -Xmx192m -Xms128m -Xss328k -XX:MaxDirectMemorySize=32m \
-XX:TieredStopAtLevel=1 \
-jar server.jar
使用Aggragate Report统计响应信息并提供请求数,平均值,最大,最小值,错误率,大约吞吐量(以请求数/秒为单位)和以kb/s为单位的吞吐量;

请求响应时间在96ms以内的请求占比为99%。
(2) 启动参数如下:
java -Xmx192m -Xms128m -Xss328k -XX:MaxDirectMemorySize=32m \
-jar server.jar
使用Aggragate Report统计响应信息并提供请求数,平均值,最大,最小值,错误率,大约吞吐量(以请求数/秒为单位)和以kb/s为单位的吞吐量;

请求响应时间在95ms以内的请求占比为99%。
(3) 通过对比可以发现,在单用户场景,开启-XX:TieredStopAtLevel=1对服务性能影响微乎其微。
对性能的影响在1%内。【1%之内可以考虑降级为C1,从而避免这个的影响】。
5.5. 结论
最终服务启动参数:
java -Xmx192m -Xms128m -Xss328k -XX:MaxDirectMemorySize=32m \
-XX:TieredStopAtLevel=1 \
-jar server.jar
参考文章
[1 如何调试 JVM 运行时数据?HSDB(Hotspot Debugger)从入门到实战 | 二哥的Java进阶之路
[2] C2 Stuck in C2Compiler::compile_method with 100%CPU usage for one week
[3] https://medium.com/netcracker/analyzing-a-stuck-hotspot-c2-compilation-85e0ca230744

浙公网安备 33010602011771号