Java Performance Testing: JMeter, Gatling & JVM Profiling Guide
Performance testing is the discipline of measuring how a Java application behaves under load — not just whether it works, but how fast it works, how much memory it uses, and where it breaks down. This guide covers the full spectrum: micro-benchmarking individual methods with JMH, load testing APIs with JMeter and Gatling, and profiling the JVM to find real bottlenecks.
Types of Performance Tests
- Load testing: Apply expected production load and measure response times and throughput.
- Stress testing: Push beyond normal load to find the breaking point.
- Spike testing: Simulate sudden bursts of traffic (e.g., flash sale events).
- Soak/endurance testing: Run at normal load for hours to detect memory leaks or degradation over time.
- Micro-benchmarking: Measure the performance of a specific method or algorithm in isolation.
Micro-Benchmarking with JMH
JMH (Java Microbenchmark Harness) is the correct tool for benchmarking individual Java methods. Never use System.currentTimeMillis() for micro-benchmarks — the JVM's JIT compiler will optimise away code that has no observable effects, making your results meaningless.
Add JMH to your Maven project:
<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>
Write a benchmark:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
@Fork(2)
public class StringConcatBenchmark {
private static final int N = 1000;
@Benchmark
public String usingStringBuilder() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < N; i++) sb.append(i);
return sb.toString();
}
@Benchmark
public String usingStringConcat() {
String result = "";
for (int i = 0; i < N; i++) result += i;
return result;
}
}
Run with: java -jar target/benchmarks.jar
Blackhole to consume return values if you are not returning them — otherwise the JIT may eliminate the code as dead.
Load Testing with Apache JMeter
JMeter is the most widely used open-source load testing tool for Java applications. It simulates concurrent users hitting your API endpoints.
Basic JMeter test plan structure:
- Thread Group — defines number of users, ramp-up time, loop count
- HTTP Request Sampler — defines which endpoint to hit
- Listeners — View Results Tree, Summary Report, Aggregate Report
- Assertions — response code checks, response body checks
Running JMeter from the command line (preferred for CI/CD):
jmeter -n -t my-test-plan.jmx -l results.jtl -e -o report-output/
Key JMeter metrics to watch:
- Throughput (TPS): Requests per second your app can sustain
- Average response time: Mean time per request
- 90th/95th/99th percentile: More useful than average — tells you what the slowest users experience
- Error %: Should be 0% under normal load
Load Testing with Gatling
Gatling is a Scala-based load testing tool popular in Java projects because it integrates with Maven/Gradle and produces beautiful HTML reports. Simulations are written as code, so they can be version-controlled.
class OrderApiSimulation extends Simulation {
val httpProtocol = http
.baseUrl("http://localhost:8080")
.acceptHeader("application/json")
val createOrder = scenario("Create Order")
.exec(
http("POST /api/orders")
.post("/api/orders")
.body(StringBody("""{"customerId":"c1","items":[{"sku":"SKU-1","qty":2}]}"""))
.contentTypeHeader("application/json")
.check(status.is(201))
)
setUp(
createOrder.inject(
rampUsers(100).during(30.seconds),
constantUsersPerSec(50).during(60.seconds)
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile(95).lt(500),
global.failedRequests.percent.lt(1)
)
}
Run with Maven: mvn gatling:test
JVM Profiling: Finding Real Bottlenecks
Load tests tell you that you have a problem. Profiling tells you where. Key JVM profiling tools:
- VisualVM — free, bundled with JDK. CPU sampling, heap analysis, thread monitoring.
- JProfiler / YourKit — commercial, more accurate CPU profiling with call trees.
- async-profiler — open-source, low overhead, generates flame graphs. Best for production profiling.
- Java Flight Recorder (JFR) — built into JDK 11+, near-zero overhead, ideal for always-on monitoring.
Enable JFR for a running application:
jcmd <PID> JFR.start duration=60s filename=recording.jfr
jcmd <PID> JFR.dump filename=recording.jfr
Analyse with JDK Mission Control (JMC) to view CPU hot spots, garbage collection events, and lock contention.
Common Java Performance Anti-Patterns
- N+1 query problem: Fetching a list of 100 orders then making 100 separate DB calls for each customer. Use JOIN FETCH or batch loading.
- Excessive object creation in loops: Creating
new SimpleDateFormat()in every loop iteration. Use thread-locals orDateTimeFormatter(immutable). - Synchronised on wrong level: Over-synchronising causes thread contention. Use
ConcurrentHashMapinstead ofCollections.synchronizedMap(). - Large heap with old GC: Tune GC with
-XX:+UseG1GCor-XX:+UseZGCfor low-latency requirements. - Missing database indexes: A missing index on a frequently queried column is the most common real-world performance problem.
Performance Testing in CI/CD
Integrate Gatling with your Maven build so performance regressions are caught automatically:
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>4.9.0</version>
<configuration>
<simulationClass>simulations.OrderApiSimulation</simulationClass>
<failOnError>true</failOnError>
</configuration>
</plugin>
Run as a nightly job against a staging environment — not on every commit, as load tests are slow and affect shared infrastructure.
Conclusion
Effective Java performance testing combines micro-benchmarking (JMH) for method-level insight, load testing (JMeter or Gatling) for system-level validation, and JVM profiling (async-profiler, JFR) for root-cause analysis. Start with a realistic load test that mirrors production traffic patterns, let the profiler guide your optimisation, and measure again to confirm the improvement.
Related: Java Design Patterns | Java Microservices Architecture