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

参考