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.mapbpftrace 获取调用栈
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编译器在内存中存储更多的生成代码来提高性能。但是,这也会增加应用程序的内存使用量。您应仔细调整代码缓存的大小,并监视应用程序的内存使用情况,以确保它不超过系统上的可用内存。