Javaアプリのメモリリークを調べる
JavaアプリのメモリリークをPrometheusとJDK Flight Recorderを使って調べるときのメモ。
準備
ソースコード
メモリリークがあるSpring Bootのデモアプリ。
build.gradle
plugins { id 'org.springframework.boot' version '2.6.7' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() }
application.properties
Prometheusでメトリックスを見るために、Spring Boot ActuatorのPrometheusエンドポイントを有効にする。
management.endpoints.web.exposure.include=prometheus
DemoApplication.java
メモリリークを発生させるために、1MBのオブジェクトを1秒毎に生成して破棄されないようにListに追加する。
@EnableScheduling @SpringBootApplication public class DemoApplication { private final List<Demo> list = new ArrayList<>(); public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Scheduled(fixedRate = 1000) public void run() { list.add(new Demo()); } private static class Demo { private final byte[] buff = new byte[1_000_000]; } }
Prometheus
prometheus.yml
今回はDocker Desktop for MacでPrometheusを使うので、コンテナからホストにアクセスするためにhost.docker.internal
を使う。
scrape_configs: - job_name: spring-boot scrape_interval: 5s metrics_path: /actuator/prometheus static_configs: - targets: ['host.docker.internal:8080']
Demo
prometheus.ymlをDockerでファイル共有できるディレクトリに置いてから、Prometheusを起動する。
% cp prometheus.yml /tmp % docker run -p 9090:9090 -v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus
今回はTemurin 17.0.3を使う。
% sdk use java 17.0.3-tem
検証用にヒープサイズを256MBにして、デモアプリを起動する。
% java -Xms256m -Xmx256m -jar build/libs/demo-0.0.1-SNAPSHOT.jar
Prometheus
localhost:9090
のPrometheusにアクセスしてヒープの使用量を見ると、old領域が徐々に増えているのが確認できる。
(最終的には256MBに到達してOOMが発生する)
JDK Flight Recorder
JFRファイルに記録する。
# 起動時から記録する場合 % java -Xms256m -Xmx256m -XX:StartFlightRecording -jar build/libs/demo-0.0.1-SNAPSHOT.jar # 起動後に記録する場合 % jcmd <pid> JFR.start # ファイルに保存する % jcmd <pid> JFR.dump filename=recording.jfr path-to-gc-roots=true
JDK Mission ControlでJFRファイルを開くと、メモリリークしているオブジェクトが確認できる。
デバッグ用にstack traceも記録したい場合は、settings=profile
を指定する。
% java -Xms256m -Xmx256m -XX:StartFlightRecording:settings=profile -jar build/libs/demo-0.0.1-SNAPSHOT.jar or % jcmd <pid> JFR.start settings=profile
参考
- https://docs.docker.com/desktop/mac/networking/
- Prometheus, Micrometer
- https://prometheus.io/docs/prometheus/latest/installation/#using-docker
- https://micrometer.io/docs/ref/jvm
- https://github.com/micrometer-metrics/micrometer/blob/main/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemoryMetrics.java
- https://docs.oracle.com/en/java/javase/17/docs/api/java.management/java/lang/management/MemoryPoolMXBean.html
- JFR