JMH для Java: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

JMH (Java Microbenchmark Harness): Полное руководство по микро-бенчмаркингу в Java

JMH (Java Microbenchmark Harness) — это библиотека для создания, выполнения и анализа микро-бенчмарков (микро-тестов производительности) на языке Java. Разработанная командой JVM Performance (включая таких экспертов, как Алексей Шипилёв), JMH является де-факто стандартом для измерения производительности небольших фрагментов кода в экосистеме Java.

1. Что это и зачем нужна?

Написание корректного теста производительности для Java — задача нетривиальная. JIT-компилятор, сборщик мусора, оптимизации на уровне процессора (кеширование, спекулятивное выполнение) и даже порядок выполнения операций могут полностью исказить результаты. Простой цикл for внутри main часто приводит к тому, что JIT-компилятор "выкидывает" тестируемый код как "мертвый" (Dead Code Elimination), и вы измеряете скорость пустого цикла.

JMH решает эти проблемы автоматически. Она управляет "прогревом" (Warmup) кода, чтобы JIT-компилятор стабилизировался, предотвращает оптимизацию "мертвого кода", управляет форками (Forks) для изоляции JVM, и предоставляет статистически значимые результаты (среднее, процентили, погрешность).

Зачем она нужна разработчику? Для принятия обоснованных решений при выборе алгоритма, структуры данных, способа сериализации, оптимизации критического участка кода или сравнения различных реализаций (например, StringBuilder vs String.concat). Без JMH вы рискуете "оптимизировать" то, что на самом деле не является узким местом, или получить результаты, которые не воспроизводятся в реальной эксплуатации.

2. Установка

JMH — это библиотека для Java, поэтому она не устанавливается через pip или npm. Наиболее удобный способ — использовать Maven или Gradle.

Способ 1: Maven (рекомендуется для быстрого старта)

Создайте новый Maven-проект и добавьте в pom.xml:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>jmh-example</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jmh</packaging>

    <dependencies>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.37</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.37</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Способ 2: Gradle

plugins {
    id 'java'
    id 'me.champeau.jmh' version '0.7.2'
}

dependencies {
    implementation 'org.openjdk.jmh:jmh-core:1.37'
    annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
}

jmh {
    jmhVersion = '1.37'
}

Способ 3: Скачать JAR (не рекомендуется)

Вы можете скачать fat-jar с GitHub, но это менее удобно для управления зависимостями.

3. Быстрый старт — минимальный рабочий пример

Создайте класс MyBenchmark.java:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime) // Измеряем среднее время выполнения
@OutputTimeUnit(TimeUnit.NANOSECONDS) // В наносекундах
@Warmup(iterations = 2, time = 1) // 2 итерации прогрева по 1 секунде
@Measurement(iterations = 3, time = 1) // 3 измерительные итерации по 1 секунде
@Fork(1) // Один форк (один запуск JVM)
@State(Scope.Thread) // Состояние бенчмарка — один экземпляр на поток
public class MyBenchmark {

    private String string1 = "Hello";
    private String string2 = "World";

    @Benchmark
    public String testStringConcat() {
        return string1 + " " + string2;
    }

    @Benchmark
    public String testStringBuilder() {
        return new StringBuilder()
                .append(string1)
                .append(" ")
                .append(string2)
                .toString();
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(MyBenchmark.class.getSimpleName())
                .build();
        new Runner(opt).run();
    }
}

Как запустить:

mvn clean package
java -jar target/benchmarks.jar

Вы увидите вывод, который покажет, какой метод быстрее (обычно testStringBuilder оказывается быстрее, хотя JIT может сильно оптимизировать конкатенацию).

4. Основные методы/функции/классы

4.1. @Benchmark

Сигнатура: @Benchmark public void methodName() или @Benchmark public ReturnType methodName()

Что делает: Аннотация, помечающая метод как бенчмарк. JMH будет вызывать этот метод многократно для сбора статистики. Метод может возвращать значение (чтобы JIT не удалил его как мертвый код) или быть void (если вы используете Blackhole).

Пример:

@Benchmark
public int sum() {
    return 1 + 2;
}

4.2. @BenchmarkMode

Сигнатура: @BenchmarkMode(Mode mode) или @BenchmarkMode({Mode.AverageTime, Mode.Throughput})

Что делает: Определяет, что именно измерять. Mode.AverageTime — среднее время операции. Mode.Throughput — количество операций в секунду. Mode.SampleTime — распределение времени (с процентилями). Mode.SingleShotTime — время одного запуска (для холодного старта). Mode.All — все режимы сразу.

Пример:

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public class ThroughputBenchmark { ... }

4.3. @State

Сигнатура: @State(Scope scope)

Что делает: Определяет область видимости состояния бенчмарка. Scope.Thread — каждый поток получает свой экземпляр. Scope.Benchmark — один экземпляр на весь бенчмарк (разделяется между потоками). Scope.Group — один экземпляр на группу потоков.

Пример:

@State(Scope.Thread)
public class MyState {
    public int x = 42;
    public int y = 100;
}

@Benchmark
public int measure(MyState state) {
    return state.x + state.y;
}

4.4. @Setup и @TearDown

Сигнатура: @Setup(Level level) или @TearDown(Level level)

Что делает: Методы инициализации и очистки. Level.Trial — один раз перед/после всего бенчмарка. Level.Iteration — перед/после каждой итерации. Level.Invocation — перед/после каждого вызова метода (используйте с осторожностью, это дорого).

Пример:

@State(Scope.Thread)
public class SetupExample {
    private List<String> data;

    @Setup(Level.Iteration)
    public void init() {
        data = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            data.add("item" + i);
        }
    }

    @TearDown(Level.Iteration)
    public void cleanup() {
        data = null; // Помогаем GC
    }

    @Benchmark
    public int size() {
        return data.size();
    }
}

4.5. Blackhole

Сигнатура: public void consume(Object obj) и public long consume(long value)

Что делает: Класс, который "поглощает" значения, предотвращая Dead Code Elimination. Если ваш бенчмарк что-то вычисляет, но не возвращает результат, JIT может решить, что вычисления не нужны. Blackhole.consume() заставляет JIT думать, что результат используется.

Пример:

@Benchmark
public void measureWithBlackhole(Blackhole bh) {
    int sum = 0;
    for (int i = 0; i < 100; i++) {
        sum += i;
    }
    bh.consume(sum); // Без этого JIT может удалить цикл
}

4.6. @Param

Сигнатура: @Param({"value1", "value2"}) или @Param({"10", "100", "1000"})

Что делает: Позволяет параметризовать бенчмарк. JMH автоматически запустит бенчмарк для каждого значения параметра. Это удобно для сравнения производительности при разных размерах данных.

Пример:

@State(Scope.Benchmark)
public class ParamBenchmark {
    @Param({"10", "100", "1000"})
    public int size;

    private List<Integer> list;

    @Setup
    public void init() {
        list = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            list.add(i);
        }
    }

    @Benchmark
    public int sum() {
        return list.stream().mapToInt(Integer::intValue).sum();
    }
}

4.7. @Fork

Сигнатура: @Fork(int value) или @Fork(value = 3, warmups = 1)

Что делает: Запускает бенчмарк в отдельном процессе JVM (форке). Это изолирует бенчмарк от влияния предыдущих тестов (например, от профилирующей информации JIT). Рекомендуется @Fork(1) для быстрых тестов и @Fork(3) для более надежных результатов.

Пример:

@Fork(value = 3, warmups = 1) // 3 форка, 1 прогревочный
public class ForkExample { ... }

4.8. @Warmup и @Measurement

Сигнатура: @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)

Что делает: @Warmup — количество итераций "прогрева" (результаты не учитываются). @Measurement — количество итераций, результаты которых учитываются. time — время одной итерации. Прогрев нужен, чтобы JIT-компилятор успел оптимизировать код до начала измерений.

Пример:

@Warmup(iterations = 3, time = 2)
@Measurement(iterations = 5, time = 1)
public class WarmupExample { ... }

4.9. @CompilerControl

Сигнатура: @CompilerControl(Mode mode)

Что делает: Позволяет управлять поведением JIT-компилятора для конкретного метода. Mode.DONT_INLINE — запретить инлайнинг. Mode.EXCLUDE — исключить метод из компиляции (запускать в интерпретаторе). Mode.INLINE — принудительно инлайнить. Используется для анализа влияния JIT-оптимизаций.

Пример:

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
@Benchmark
public void noInlineMethod() {
    // Этот метод не будет встроен JIT-компилятором
}

5. Реальный практический пример (100-200 строк кода)

Сравним производительность различных способов фильтрации списка

Рекомендаций