bpftrace 探测 Java 运行时栈-实践
开发Java应用的时候, 有时候我们想知道某个函数到底在哪里被调用的. 我们可以采取的方法有:
- 若是本地开发, 可以在函数体上加断点, 每当函数被调用, 都会暂停.
- 若是我们可以改动的代码, 我们可以加日志打印栈, 这样就能发现在哪里被调用.
- 不论是不是我们自己的Java代码, 我们都可以通过
Btrace
进行注入脚本, 在脚本打印运行时栈.
但是有时候, 我们想知道我们的 Java 代码是哪里调用了系统的 native 代码, 比如某些系统调用(syscall), 那么该如何获取这些栈呢?
这时候, 使用Java 提供的这些方法, 都无法达到目的, 所以, 我们要借用系统层级的 tracing 方式. 如今最流行且最简单的方式就是使用 bpftrace
. 本文接下来将用一个例子来说明, 如何使用 bpftrace
来查找我们的Java应用是如何调用 recvfrom
这个系统调用的.
Java 代码
下面是我们用来演示的代码, 它的意图就是不断的循环去获得某个网页. 不断循环的目的是为了给我们的手工操作留有足够的时间. 这段代码有网络操作, 所以会调用系统调用 recvfrom
来拿到响应(response).(感谢 chatGPT 给我们演示代码)
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class URLTest {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 1000; i++) {
try {
URL url = new URL("http://www.tianxiaohui.com");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
System.out.println(i + "Response code: " + con.getResponseCode());
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String line;
StringBuilder response = new StringBuilder();
while ((line = in.readLine()) != null) {
response.append(line);
}
in.close();
//System.out.println(response.toString());
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
Thread.sleep(2000);
}
}
}
本地编译并运行
$ javac URLTest.java
$ java -XX:+PreserveFramePointer URLTest
Response code: 301
安装编译 perf-map-agent
下载
可以直接克隆这个 git repo:
$ git clone https://github.com/jvm-profiling-tools/perf-map-agent.git
或者直接下载最新的代码
$ curl https://github.com/jvm-profiling-tools/perf-map-agent/archive/refs/heads/master.zip --output perf-map-agent.zip
$ unzip perf-map-agent.zip
编译
$ cd perf-map-agent
$ cmake .
$ make
生成JIT编译后符号表
# 获取 java 进程号
$ jcmd
3408161 jdk.jcmd/sun.tools.jcmd.JCmd
3406489 URLTest
# 设置 JAVA_HOME
$ export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
# 生成符号表
$ bin/create-java-perf-map.sh 3406489
# 查看符号表
ls -lah /tmp/perf-3406489.map
-rw-rw-r-- 1 root root 42K Nov 15 05:08 /tmp/perf-3406489.map
bpftrace 获取调用栈
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_recvfrom /pid==3406489/{ @[ustack(20)] = count(); }'
Attaching 1 probe...
^C
@[
recvfrom+116
]: 2
@[
__GI___recv+110
Java_java_net_SocketInputStream_socketRead0+434
Interpreter+28336
Interpreter+4352
Interpreter+4352
Interpreter+4352
Interpreter+4352
Interpreter+5875
Interpreter+4352
Interpreter+4352
Interpreter+3728
Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0+3828
call_stub+138
JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+883
jni_invoke_static(JNIEnv_*, JavaValue*, _jobject*, JNICallType, _jmethodID*, JNI_ArgumentPusher*, Thread*) [clone .constprop.1]+682
jni_CallStaticVoidMethod+352
JavaMain+3441
ThreadJavaMain+13
start_thread+755
]: 3
把翻译代码变成编译代码
java -XX:+PreserveFramePointer -XX:-TieredCompilation -XX:CompileThreshold=1 URLTest
__GI___recv+110
Java_java_net_SocketInputStream_socketRead0+434
Ljava/net/SocketInputStream;::socketRead0+244
Ljava/net/SocketInputStream;::read+224
Ljava/io/BufferedInputStream;::fill+784
Ljava/io/BufferedInputStream;::read1+176
Ljava/io/BufferedInputStream;::read+252
Lsun/net/www/http/HttpClient;::parseHTTPHeader+444
Lsun/net/www/http/HttpClient;::parseHTTP+1004
Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0+1420
Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream+196
Ljava/net/HttpURLConnection;::getResponseCode+96
调大code cache
在Java中,您可以通过设置-XX:ReservedCodeCacheSize标志来调整JIT代码缓存的大小。该标志控制JIT编译器可用来存储生成的代码的本机内存的最大数量。默认情况下,代码高速缓存的大小由平台确定 - 32位JVM通常为240MB,而64位JVM通常为480MB。
要调整JIT代码缓存区的大小,可以将-XX:ReservedCodeCacheSize标志设置为自定义值。该值应以字节为单位指定,并且可以是2的幂次方或2048(2KB)的倍数。以下是将标志设置为512MB的示例:
java -XX:ReservedCodeCacheSize=536870912 <your_program>
这将将JIT代码缓存的最大大小设置为512MB。
请注意,增加JIT代码缓存的大小可以通过允许JIT编译器在内存中存储更多的生成代码来提高性能。但是,这也会增加应用程序的内存使用量。您应仔细调整代码缓存的大小,并监视应用程序的内存使用情况,以确保它不超过系统上的可用内存。