程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

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

[4] C2 CompilerThread9 长时间占用CPU解决方案

posted @ 2025-06-27 11:06  大奥特曼打小怪兽  阅读(235)  评论(0)    收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步