# The JMH Gradle plugin turns a src/jmh directory into a runnable benchmark

> melix/jmh-gradle-plugin wires the JMH micro-benchmarking framework into Gradle, adding a jmh configuration, a chain of tasks that compile and generate benchmark code, and a configuration block that maps onto JMH options. It is Apache-2.0 licensed and the current release is 0.7.3.

**melix/jmh-gradle-plugin** — Integrates the JMH benchmarking framework with Gradle

- Repository: https://github.com/melix/jmh-gradle-plugin
- Stars: 724 · Forks: 86
- Language: Groovy
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/melix-jmh-gradle-plugin

## Benchmarks live in src/jmh, not in the test source set

The plugin's central decision is where benchmark code goes. The documentation is explicit that benchmark source files are expected in the `src/jmh` directory, which holds a `java` folder for benchmark sources and a `resources` folder for benchmark resources. That is a separate source set from the main one and from the test one, which is the point: a benchmark is not a test, it is not run by the test task, and it should not end up in the jar you ship.

The plugin integrates the JMH micro-benchmarking framework, the one that ships with the OpenJDK project, into Gradle. Applying it means adding a single plugin block to your build file, and the documentation writes the version as an attribute that resolves to 0.7.3 in this document:

```groovy
plugins {
  id "me.champeau.jmh" version "{plugin-version}"
}
```

Once applied, the project gains a `jmh` configuration and a set of tasks. The configuration is the place to declare the third party libraries your benchmarks need, and it is deliberately separate from the main `dependencies` block so that a library used only by a benchmark does not leak into your runtime classpath.

The plugin is written in Groovy, distributed under the Apache-2.0 licence, and its repository carries a `build-logic/` directory, `build.gradle.kts` and `settings.gradle.kts` build scripts, a `samples/` directory with a `simple-java` sample, and a `CHANGELOG.txt`.

## Five tasks, and the one you actually need

The task list shows the pipeline the plugin automates. `jmhClasses` compiles the raw benchmark code, `jmhRunBytecodeGenerator` runs the bytecode generator over that code and produces the real benchmarks, `jmhCompileGeneratedClasses` compiles the generated ones, `jmhJar` builds the JMH jar containing the JMH runtime together with your compiled benchmark classes, and `jmh` executes the benchmarks.

The `jmh` task depends on the others, so the documentation says it is generally sufficient to execute that single task:

```bash
gradle jmh
```

Two things are worth understanding about that chain. The generator step is not optional in JMH, and the plugin is packaging it as an explicit task, which means the annotation processing and the bytecode generation happen at build time inside Gradle rather than as a manual step you remember to run. And the jar step is what makes the run reproducible: the benchmarks execute from a jar that contains the JMH runtime and your compiled classes, which is how JMH expects to be launched with its own forking behaviour.

The output lands in `$buildDir/reports/jmh` by default, and the configuration block can redirect the human readable and machine readable outputs to explicit files, so a CI job can pick up a results file without parsing console output.

## The jmh configuration keeps benchmark dependencies separate

The `jmh` configuration is a normal Gradle configuration, so adding a dependency to it looks like adding one anywhere else. The documented example uses Apache Commons IO:

```groovy
dependencies {
    jmh 'commons-io:commons-io:2.7'
}
```

The same configuration is also the handle for changing the JMH version the plugin uses. The project states that the plugin uses JMH 1.37, and that you can upgrade the version by changing the version in the `dependencies` block:

```groovy
dependencies {
    jmh 'org.openjdk.jmh:jmh-core:0.9'
    jmh 'org.openjdk.jmh:jmh-generator-annprocess:0.9'
    jmh 'org.openjdk.jmh:jmh-generator-bytecode:0.9'
}
```

Those three coordinates are the parts that matter. `jmh-core` is the runtime, `jmh-generator-annprocess` is the annotation processor that turns your `@Benchmark` methods into generated sources, and `jmh-generator-bytecode` is the bytecode generator that the `jmhRunBytecodeGenerator` task runs. Declaring all three is what lets you pin a JMH version other than the default, and it also tells you why the plugin needs a compile step and a generation step before it can run anything.

## The configuration block mirrors the JMH option set

Beyond the dependency configuration there is a `jmh` block in the build file, and its properties map one to one onto JMH's own options. The documentation is clear that all the configuration variables apart from `includes` are unset by default, which means they fall back to the JMH defaults rather than to the plugin's own.

A representative slice looks like this:

```groovy
   includes = ['some regular expression'] // include pattern (regular expression) for benchmarks to be executed
   excludes = ['some regular expression'] // exclude pattern (regular expression) for benchmarks to be executed
   iterations = 10 // Number of measurement iterations to do.
   fork = 2 // How many times to forks a single benchmark. Use 0 to disable forking altogether
```

The list is long and covers the parts of JMH that change results: `benchmarkMode` with the throughput, average time, sample time and single shot modes, `batchSize`, `warmupIterations` and `warmupMode` for the warmup phase, `timeOnIteration` and `timeUnit` for the measurement window, `threads` and `threadGroups` for the concurrency shape, and `jvm`, `jvmArgs`, `jvmArgsAppend` and `jvmArgsPrepend` for the forked JVM. Profiling is covered by a `profilers` list, and results can be written as CSV, JSON, SCSV, TEXT or none at all through `resultFormat`.

Two conveniences are worth calling out. `listBenchmarks`, `listProfilers`, `listProfilersDetails` and `listResultFormats` let you print the available values and exit, which is the fast way to find out what a mode is called. And `jmhOptions` passes arbitrary command line options straight through to the JMH runner, with null or empty elements ignored, for the options the block does not model.

## Plugin id and Gradle version both changed over time

Two compatibility facts are documented and both matter when you adopt a plugin that has been around for years.

The first is the plugin id. The documentation carries a warning that versions prior to 0.6.0 used the `me.champeau.gradle.jmh` plugin id, so the id in your build file is a reliable signal of which era a build script came from. The second is the Gradle version floor: 0.6 and later require Gradle 6.8 or above, and the project keeps a table mapping Gradle versions to the oldest plugin version that works with them. Gradle 8.0 needs plugin 0.7.0 or newer, Gradle 7.0 needs 0.5.3, Gradle 5.5 needs 0.5.0, and the older rows go down to 0.4.4 for Gradle 4.1 and 4.2. The table even notes why one row differs, since 0.4.7 for Gradle 4.9 is the version that brings the lazy tasks API.

The release history is thin but regular. Version 0.7.1 came in April 2023, 0.7.2 in November 2023 and 0.7.3 in April 2025. The repository is not archived and the last push was on 2026-09-23, so the branch is being worked on between releases, which is the normal shape for a build plugin that has to keep pace with Gradle's own release cycle.

## Where the plugin is the wrong tool

The clearest case is a project that already has a working JMH setup through Maven. The plugin exists to make benchmarks part of a Gradle build, and if your build is Maven then it solves a problem you do not have while adding a plugin id, a source set and a task chain to maintain.

The second case is a project that wants a benchmark without the machinery. A JMH benchmark is not just a method with an annotation. It needs a harness, a warmup, a fork, and a measurement window, and a number written on a laptop is not a benchmark. The plugin exists precisely to supply that machinery, so using it is not overhead, it is the feature. The overhead would come from the opposite choice, and the plugin is the better answer there.

The third is a project that cannot accept the extra build steps. Every benchmark run now includes compiling the raw sources, generating the benchmark sources and compiling those, and generating a jar. On a large project that is a noticeable cost on every iteration, and the plugin gives you no documented way to skip the generation step, because the generation step is what produces the benchmarks.

One documentation gap is worth flagging. The list of options in the `jmh` configuration block ends in the middle of a comment about the `jmhArgs` option, so the full set of pass-through options is not visible in the project documentation and has to be read from the JMH side.

## Conclusion

jmh-gradle-plugin fits a JVM team that wants JMH benchmarks living next to the code they measure, with the same Gradle build, the same dependency resolution and one command to run them. It does not fit a project that already drives JMH through Maven, or one that wants benchmarks to run without the annotation processor and bytecode generation steps the plugin adds. Check three things before adopting it: that the plugin id you use matches your plugin version, because releases before 0.6.0 used the id me.champeau.gradle.jmh, that your Gradle version is new enough, since 0.6 and later need Gradle 6.8 or above, and that the JMH version the plugin brings in is acceptable to you, since the project targets JMH 1.37 while the release tag 0.7.3 dates from 2025-04-16 and the last push was on 2026-09-23.

## FAQ

### How do I add the JMH Gradle plugin to a build?

Add the plugin block to your build file, using the id me.champeau.jmh and the version you want. Versions before 0.6.0 used the id me.champeau.gradle.jmh, so the id in an existing build script tells you which generation of the plugin it targets.

### Where do I put my JMH benchmark sources?

In the src/jmh directory, with Java sources under src/jmh/java and resources under src/jmh/resources. The plugin treats that as a separate source set from main and test code.

### Which Gradle version do I need?

Plugin versions 0.6 and later require Gradle 6.8 or newer. The project documents a table of minimum plugin versions per Gradle release, from Gradle 4.1 needing 0.4.4 up to Gradle 8.0 needing 0.7.0.

### How do I add a third party library to my benchmarks only?

Declare it in the jmh configuration, for example jmh 'commons-io:commons-io:2.7' inside the dependencies block. The same configuration is where you change the JMH version itself.

### Which command runs the benchmarks?

The jmh task, so gradle jmh. It depends on the other tasks, which compile the raw benchmark code, run the bytecode generator, compile the generated benchmarks and build the JMH jar before the run.

## Sources

- [Issues](https://github.com/melix/jmh-gradle-plugin/issues)
- [License: Apache-2.0](https://github.com/melix/jmh-gradle-plugin/blob/master/LICENSE)
- [melix/jmh-gradle-plugin on GitHub](https://github.com/melix/jmh-gradle-plugin)
- [README](https://github.com/melix/jmh-gradle-plugin/blob/master/README.md)
- [Releases](https://github.com/melix/jmh-gradle-plugin/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/melix-jmh-gradle-plugin
