JMeter can happily chug along with a few hundred threads, but push it to a few thousand, and suddenly you’re staring down the barrel of an OutOfMemoryError (OOM) in the Java heap. This isn’t about JMeter itself being inefficient; it’s about Java’s garbage collector (GC) struggling to keep up with the memory churn generated by thousands of concurrent virtual users, each with their own request/response data, timers, assertions, and listener results.

Let’s see JMeter in action. Imagine we’re running a simple HTTP Request sampler against a local web server, with 10,000 threads, a ramp-up of 60 seconds, and a loop count of 1.

// JMeter Test Plan Structure (simplified for illustration)
// Thread Group
//   - Number of Threads (constant): 10000
//   - Ramp-up Period (seconds): 60
//   - Loop Count: 1
//   - HTTP Request Sampler
//     - Server Name or IP: localhost
//     - Port: 8080
//     - Path: /
//   - View Results Tree Listener (disabled for actual high-load runs)
//   - Simple Data Writer Listener (to save results)

When you run this, the JVM starts allocating memory for each thread’s context, the sampler’s state, and any data collected by listeners. At 10,000 threads, this can easily exceed the default heap size.

The core problem is that each JMeter thread, while a lightweight Java thread, still consumes memory. When you have thousands of them, coupled with the data JMeter collects (especially if you’re using listeners like "View Results Tree" which stores all responses in memory), the Java heap can fill up. The garbage collector tries to reclaim memory, but if new objects are being created faster than the GC can clean them up, or if the GC needs to perform a full, stop-the-world collection on a massive heap, the JVM can run out of space, leading to an OOM.

Common Causes and Fixes

Here’s a breakdown of why you’re hitting OOMs and how to fix them, ordered by likelihood.

  1. Insufficient Heap Size: This is the most common culprit. The default JVM heap size is often too small for large-scale JMeter tests.

    • Diagnosis: Check your jmeter.log file for java.lang.OutOfMemoryError: Java heap space. Also, monitor your JVM’s heap usage during the test using tools like jstat -gcutil <pid> 1000 or VisualVM.
    • Fix: Increase the maximum heap size (-Xmx) by editing the jmeter.bat (Windows) or jmeter (Linux/macOS) script in your JMeter bin directory. Look for the HEAP setting. For a 10k thread test, you might need -Xmx8g (8 gigabytes) or even -Xmx12g.
      # Example modification in jmeter (Linux/macOS)
      # Find this line:
      # HEAP="-Xms1g -Xmx1g"
      # Change it to:
      HEAP="-Xms4g -Xmx8g"
      
    • Why it works: This directly gives the JVM more memory to work with, allowing it to hold more thread contexts and collected data before needing to trigger a GC cycle or before the GC can’t keep up.
  2. Excessive Listener Usage (Especially "View Results Tree"): Listeners like "View Results Tree" or "View Results in Table" store all sampler results in memory. With 10,000 threads each performing multiple iterations, this data quickly explodes.

    • Diagnosis: Observe memory usage in VisualVM during the test. If memory climbs steadily and never drops significantly after GC cycles, a listener is likely the issue.
    • Fix: Disable "View Results Tree" and "View Results in Table" for actual load tests. Use "Simple Data Writer" or "Summary Report" which aggregate results or write to disk, rather than holding everything in RAM. Configure "Simple Data Writer" to save to a file.
      // In Simple Data Writer configuration:
      // Filename: /path/to/your/results.csv
      // Configure other options as needed (e.g., Save as XML, Save Field Names)
      
    • Why it works: By not storing every single request and response in memory, you dramatically reduce the amount of data the JVM needs to manage, freeing up heap space.
  3. High GC Pause Times: Even with enough heap, if the GC takes too long to run (a "long pause"), JMeter threads might time out or the JVM might appear unresponsive, potentially leading to OOMs if the pause is so long that it prevents memory from being reclaimed before it’s needed again.

    • Diagnosis: Use GC logging (-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps) to analyze GC pause times. Tools like GCViewer can help visualize this log. Look for pauses exceeding several hundred milliseconds.
    • Fix: Switch to a more modern GC algorithm like G1GC (Garbage-First) or Shenandoah/ZGC if your Java version supports it and you’re on Linux. G1GC is often a good default for server-side applications. Add these JVM arguments to your jmeter or jmeter.bat script:
      # Example for G1GC:
      HEAP="-Xms4g -Xmx8g -XX:+UseG1GC"
      
    • Why it works: G1GC is designed to provide more predictable pause times by dividing the heap into regions and performing concurrent garbage collection, reducing the impact of "stop-the-world" pauses.
  4. Excessive JVM Thread Stack Size: Each Java thread has its own stack. While default sizes are usually fine, if you have extremely deep call stacks within your JMeter scripts (e.g., complex BeanShell or JSR223 scripts with many function calls), this can contribute to memory usage.

    • Diagnosis: Check the thread dump (jstack <pid>) for unusually large stack traces within your JMeter threads. Monitor the overall memory footprint per thread.
    • Fix: Reduce the thread stack size if it’s unnecessarily large. The default is typically 1MB. You can set it using -Xss. For example, -Xss512k reduces it to 512 kilobytes. Be cautious: setting this too low can cause StackOverflowError.
      # Example for reduced stack size:
      HEAP="-Xms4g -Xmx8g -XX:+UseG1GC -Xss512k"
      
    • Why it works: This reduces the memory allocated for each thread’s individual call stack, lowering the overall JVM memory footprint when you have thousands of threads.
  5. Memory Leaks in Custom Code or Plugins: If you’ve developed custom JMeter plugins or are using complex JSR223 scripts with Groovy/Java, you might inadvertently be creating memory leaks where objects are held onto longer than necessary.

    • Diagnosis: Use a Java profiler (like YourKit, JProfiler, or even VisualVM with its profiling tools) attached to the JMeter JVM. Analyze heap dumps taken at different points in the test to identify objects that are accumulating unexpectedly. Look for leaked ThreadLocal variables or unclosed resources.
    • Fix: Identify the leaking code and ensure resources are properly closed, ThreadLocal variables are cleaned up (.remove()), and objects are no longer referenced when they are no longer needed. This is highly specific to the problematic code.
    • Why it works: Eliminating actual memory leaks ensures that memory is correctly returned to the JVM for garbage collection, preventing the heap from growing indefinitely.
  6. Large Request/Response Payloads: If your JMeter test is sending or receiving very large payloads (e.g., large file uploads, large JSON/XML responses), this data itself consumes significant heap space, especially if stored by listeners or processed by samplers.

    • Diagnosis: Monitor the size of requests and responses in your test. Check the metrics from "Simple Data Writer" or "Summary Report" for average/max response data size.
    • Fix: If possible, reduce the size of payloads. If not, ensure you are not storing large payloads unnecessarily (see point 2). For very large file uploads/downloads, consider using JMeter’s built-in capabilities for streaming or handling them efficiently, and avoid loading entire large files into memory within scripts.
    • Why it works: Reducing the volume of data that needs to be held in memory directly reduces heap pressure.

After addressing these points, the next error you might encounter isn’t an OOM, but rather a performance bottleneck in your test execution itself, such as CPU saturation on the JMeter machine or network saturation, indicating that your JMeter instance is now the limiting factor in your test, not the JVM’s memory.

Want structured learning?

Take the full Jmeter course →