Skip to content
cruftex.net
Go back

The 6 Memory Metrics You Should Track in Your Java Benchmarks

Analysing or monitoring the memory consumption of a Java application is no easy task, but benchmarks require consistent results. Ironically, when I google for “benchmark java memory consumption” a Stack Overflow question of mine from 2015 comes up in first place. Well, two years later, after trying a lot of things that did not work, it is time for a summary.

This blog post touches different subjects, including:

The main focus will be on the first one.

The impatient reader can skip to the Conclusion. The rest of the article is organised as follows: After giving a motivation, we present relevant techniques to measure memory consumption and show their practical results based on an example benchmark. The benchmark and its setup are described at the end of the article.

Table of contents

Open Table of contents

Why Care?

While continuing to create different benchmarks for caching libraries and improving cache2k, we discovered that important information is missing. Let’s look at a benchmark result:

Whiskers show the 99.9 % confidence interval reported by JMH.cache2kCaffeineGuavaEhCache20500k1M1.5M2M2.5M3M3.5M4M4.5M5M5.5Moperations per secondcache2k · 4 threads, 1M entries, Zipfian factor 10: 4.99M ops/s ± 158.65k ops/sCaffeine · 4 threads, 1M entries, Zipfian factor 10: 2.26M ops/s ± 81.73k ops/sGuava · 4 threads, 1M entries, Zipfian factor 10: 1.56M ops/s ± 106.15k ops/sEhCache2 · 4 threads, 1M entries, Zipfian factor 10: 1.43M ops/s ± 17.2k ops/s

Whiskers show the 99.9 % confidence interval reported by JMH.

ZipfianSequenceLoadingBenchmark with G1 collector, 4 threads, 1M cache entries, Zipfian factor 10, throughput in operations per second. Raw data: ZipfianSequenceLoadingBenchmark.dat

The graph shows the throughput in operations per second for a capacity limit of 1M entries. The benchmark setup is described in more detail at the end of the article.

A cache library could “cheat” and store more entries than configured. Conversely, when one cache is effectively consuming more memory than the other, the cache library with the better memory footprint can obviously store more entries within the same amount of memory and yield a better throughput.

A cache is a typical example of the memory/time or space/time trade off. The throughput can be improved by increasing the memory. When benchmarking code with memory/time trade offs, keeping track of the consumed memory is essential when comparing the different implementations.

The Naive Approach

The first naive approach is asking the JVM how much memory is used. Here we have the first confusion. We can retrieve the total memory via Runtime.getRuntime().totalMemory() and the free memory via Runtime.getRuntime().freeMemory() but obviously not the used memory. Calculating just the difference isn’t always correct, since a GC may happen between the two method calls.

Retrieving and calculating the used memory will result in lots of different values between the used memory and the total memory. It just depends on when we make the call: After a GC, before the GC or somewhere in between.

We are looking for methods that produce reliable results with low variance, which don’t produce extra overhead during benchmarking and can be recorded without extra effort.

Metric: Object Graph Traversing Libraries

The memory size of an object can be determined by using the Java instrumentation functionality or via methods in the sun.misc.Unsafe class. Based on this, several solutions exist to estimate the memory consumption of an object graph. The popular ones are:

This method is used in the Caffeine memory overhead comparison.

Advantages:

Disadvantages:

The procedure can be useful for a library developer to get quick insights, but is less useful to determine a realistic value for memory consumption. When the total Java heap is traversed, the result is similar to the used heap space metric.

Metric: Used Memory after Forced GC

We can ask the JVM for the currently used memory:

    long getCurrentlyUsedMemory() {
      return
        ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed() +
        ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage().getUsed();
    }

With used memory we mean heap and non heap data. The non heap data includes memory areas such as thread stacks and class caches. We will also record the heap only data with a separate metric.

Since Java relies on garbage collection, the used memory floats between the minimum and maximum value. Measuring the amount of used memory will yield different results depending on when the last GC run happened. To get a reliable result about how much memory is used by the application, the logical idea is to force a GC run and then obtain the value for the used memory. The first try looks like this:

    long getCurrentlyUsedMemory() {
      return
        ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed() +
        ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage().getUsed();
    }
    long getPossiblyReallyUsedMemory() {
      System.gc();
      return getCurrentlyUsedMemory();
    }

The problem with it is that System.gc() triggers a concurrent garbage collection. When requesting the usage values immediately after the System.gc() call the garbage collection is not finished yet. To wait for the GC run to complete we can use counter values exposed by the JVM (BTW: I found that trick in the JMH sources):

  long getGcCount() {
    long sum = 0;
    for (GarbageCollectorMXBean b : ManagementFactory.getGarbageCollectorMXBeans()) {
      long count = b.getCollectionCount();
      if (count != -1) { sum +=  count; }
    }
    return sum;
  }
  long getReallyUsedMemory() {
    long before = getGcCount();
    System.gc();
    while (getGcCount() == before);
    return getCurrentlyUsedMemory();
  }

Here is a debug example using this technique, with the classic CMS collector:

gcCount=25
36.692: [GC (System.gc()) [PSYoungGen: 855764K->38944K(1925120K)] 966467K->149647K(2325504K), 0.0670620 secs] [Times: user=0.27 sys=0.00, real=0.06 secs] 
36.759: [Full GC (System.gc()) [PSYoungGen: 38944K->0K(1925120K)] [ParOldGen: 110703K->111316K(400384K)] 149647K->111316K(2325504K), [Metaspace: 11212K->11212K(1058816K)], 0.4352339 secs] [Times: user=1.50 sys=0.00, real=0.44 secs] 
usedHeap=169860016, usedNonHeap=18995232, totalUsed=188855248, gcCount=27

We can see that actually two GC runs are reported for one call to System.gc().

The result of this metric can be found in the graph below under the name usedMem_fin.

One result value is the average of 15 benchmark runs. We spawn five JVM instances and do three benchmark runs inside the same process on the identical cache instance. The memory usage is determined after stopping the workload. As we can see in the graph below, the confidence interval is slightly bigger and the value slightly higher than in usedMem_settled, especially for the CMS collector. Possible reasons for the variance:

An alternative approach to wait for the GC to complete is to check whether a finalizer is called on a freshly allocated object, see the Stack Overflow question mentioned at the beginning.

Metric: Used Memory after Forced GC and Settling

To get rid of the variance and improve the accuracy we use the previous metric but run multiple full GC cycles until the value stabilises. The code for that looks like this:

    long getSettledUsedMemory() {
      long m;
      long m2 = getReallyUsedMemory();
      do {
        Thread.sleep(567)
        m = m2;
        m2 = getUsedMemory();
      } while (m2 < getReallyUsedMemory());
      return m;
    }

The result of this metric can be found in the graph below under the name usedMem_settled.

Metric: Used Heap Memory

Various methods exist to obtain a heap dump or heap histogram of live objects (VisualVM, jmap, jhat). This can be used to determine the space occupied by all objects on the Java heap.

In our benchmarks we programmatically obtain a heap histogram via the JVM Attach API, specifically sun.tools.attach.HotSpotVirtualMachine.heapHisto("-live"). After the benchmark run the procedure waits until the used memory values stabilise, then prints the top objects from the heap dump, extracts the used size of all live objects and stores it in the JMH results. The code to do this can be found in the ForcedGcMemoryProfiler.

Advantages:

Disadvantages:

The result of this metric can be found in the graph below under the name usedHeap_settled. Using the heap histogram produces values with less variance than using ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed() since it reliably counts only live objects. After settling, with nothing happening in the JVM, the values don’t differ much.

Metric: Maximum Used Memory via GC Notification

Another way to obtain a metric of the used memory is to register for notifications of the garbage collector activity. The details are explained in the JavaDoc of GarbageCollectionNotificationInfo. In the information we can find the used and total size for all memory pools right after the end of the garbage collection run.

The metric we record in our benchmarks is the peak value of the used memory that gets reported during a benchmark run. In the graphs it can be found under the name usedMem_max.

We could also use the GC notification to extract the value of the used heap memory. The problem is that the method enumerates all memory pools and the memory pool names differ by the selected GC type. Code summing up only the heap pools would be fragile.

The values of the GC notification correspond to the values of the management beans.

Showtime: Used Memory Graphs

Now it’s finally time for the first graph. Let’s see how the metrics look for the tested libraries:

Whiskers show the 99.9 % confidence interval reported by JMH.usedHeap_settledusedMem_settledusedMem_finusedMem_maxcache2kCaffeineEhCache2Guava050 MB100 MB150 MB200 MB250 MB300 MB350 MB400 MBmemoryusedHeap_settled · cache2k: 134 MB ± 0 MBusedMem_settled · cache2k: 151.5 MB ± 1.7 MBusedMem_fin · cache2k: 152.1 MB ± 1.9 MBusedMem_max · cache2k: 321.6 MB ± 2.1 MBusedHeap_settled · Caffeine: 114.1 MB ± 0.1 MBusedMem_settled · Caffeine: 133.6 MB ± 2.1 MBusedMem_fin · Caffeine: 135.4 MB ± 2.7 MBusedMem_max · Caffeine: 231.5 MB ± 10.5 MBusedHeap_settled · EhCache2: 163 MB ± 0 MBusedMem_settled · EhCache2: 182.8 MB ± 1.5 MBusedMem_fin · EhCache2: 183.4 MB ± 1.8 MBusedMem_max · EhCache2: 377.1 MB ± 0.9 MBusedHeap_settled · Guava: 106.3 MB ± 0 MBusedMem_settled · Guava: 122.1 MB ± 1.4 MBusedMem_fin · Guava: 123.1 MB ± 1.7 MBusedMem_max · Guava: 351.1 MB ± 1.1 MB

Whiskers show the 99.9 % confidence interval reported by JMH.

ZipfianSequenceLoadingBenchmark with CMS collector, 4 threads, 1M cache entries, Zipfian factor 10, used memory. Raw data: ZipfianSequenceLoadingBenchmarkMemoryUsed4-1M-10.dat

As we can see usedMem_fin has low confidence. After one or more additional full GC runs the reported memory becomes a little smaller with no variance, as we can see in usedMem_settled. Guava seems to be the most memory efficient, but looking at the usedMem_max metric it falls behind Caffeine and cache2k. This means that Guava either needs more memory during operation or it is less friendly with minor garbage collections. A detail that may be worth some more investigation.

Doing the same with the G1 garbage collector (VM option -XX:+UseG1GC) shows another picture:

Whiskers show the 99.9 % confidence interval reported by JMH.usedHeap_settledusedMem_settledusedMem_finusedMem_maxcache2kCaffeineEhCache2Guava0100 MB200 MB300 MB400 MB500 MB600 MB700 MB800 MBmemoryusedHeap_settled · cache2k: 134 MB ± 0 MBusedMem_settled · cache2k: 149.4 MB ± 0.3 MBusedMem_fin · cache2k: 149.5 MB ± 0.3 MBusedMem_max · cache2k: 367.3 MB ± 7.2 MBusedHeap_settled · Caffeine: 114.2 MB ± 0.1 MBusedMem_settled · Caffeine: 132.5 MB ± 0.2 MBusedMem_fin · Caffeine: 132.5 MB ± 0.2 MBusedMem_max · Caffeine: 283.2 MB ± 6.2 MBusedHeap_settled · EhCache2: 163 MB ± 0 MBusedMem_settled · EhCache2: 180.8 MB ± 0.4 MBusedMem_fin · EhCache2: 180.8 MB ± 0.4 MBusedMem_max · EhCache2: 759.7 MB ± 18.4 MBusedHeap_settled · Guava: 106.3 MB ± 0 MBusedMem_settled · Guava: 120.8 MB ± 0.2 MBusedMem_fin · Guava: 120.8 MB ± 0.2 MBusedMem_max · Guava: 516.2 MB ± 6 MB

Whiskers show the 99.9 % confidence interval reported by JMH.

ZipfianSequenceLoadingBenchmark with G1 collector, 4 threads, 1M cache entries, Zipfian factor 10, used memory. Raw data: ZipfianSequenceLoadingBenchmarkMemoryUsed4-1M-10.dat

The slight difference between usedMem_fin and usedMem_settled is gone. That means G1 is capable of collecting garbage more reliably on the first full GC run. Looking at the usedMem_max metric we have an indicator of how the cache implementations are affected by the different collectors.

Metric: Total Memory

The used memory after a GC isn’t all the memory that is occupied by the JVM. The JVM reserves more memory to have “breathing space” for garbage collection. We call this the total memory or committed memory. The total memory is requested with:

    long getTotalMemory() {
      return
        ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getCommitted() +
        ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage().getCommitted();
    }

To have a more compact naming we use the term total instead of committed. We extract two metrics. totalMem_settled is the total memory after the benchmark run and multiple full GCs. totalMem_max is the maximum total memory that was reported by the GC notification.

Metric: Process VmRSS and VmHWM reported by Linux

So far we have only utilised JVM internal metrics. But is this the amount of memory that is really used? Let’s ask the operating system! Linux provides the metric resident set size (RSS) which is the amount of physical memory that a process is using. Very conveniently for our benchmarking purposes, Linux provides another metric called high water mark (“HWM”) which is the peak value of the resident set size. The metrics VmRSS and VmHWM are obtained via the proc interface in /proc/<pid>/status. See man proc for more details. The metric VmHWM isn’t exposed by the usual Linux tools.

Showtime: Total/Committed Memory Graphs

Let’s take a look at the results for the CMS collector:

Whiskers show the 99.9 % confidence interval reported by JMH.totalMem_settledtotalMem_maxVmRSSVmHWMcache2kCaffeineEhCache2Guava0500 MB1 GB1.5 GB2 GB2.5 GB3 GB3.5 GB4 GB4.5 GB5 GBmemorytotalMem_settled · cache2k: 3.12 GB ± 404.5 MBtotalMem_max · cache2k: 3.14 GB ± 411.5 MBVmRSS · cache2k: 2.43 GB ± 151.5 MBVmHWM · cache2k: 2.44 GB ± 150.8 MBtotalMem_settled · Caffeine: 4.41 GB ± 392.8 MBtotalMem_max · Caffeine: 4.41 GB ± 391.4 MBVmRSS · Caffeine: 3.69 GB ± 90.9 MBVmHWM · Caffeine: 3.69 GB ± 91 MBtotalMem_settled · EhCache2: 3.41 GB ± 603.6 MBtotalMem_max · EhCache2: 3.39 GB ± 579.7 MBVmRSS · EhCache2: 2.85 GB ± 492.3 MBVmHWM · EhCache2: 2.85 GB ± 488.6 MBtotalMem_settled · Guava: 3.75 GB ± 365.7 MBtotalMem_max · Guava: 3.73 GB ± 383.4 MBVmRSS · Guava: 3.18 GB ± 60.8 MBVmHWM · Guava: 3.18 GB ± 60.8 MB

Whiskers show the 99.9 % confidence interval reported by JMH.

ZipfianSequenceLoadingBenchmark with CMS collector, 4 threads, 1M cache entries, Zipfian factor 10, total memory. Raw data: ZipfianSequenceLoadingBenchmarkMemory4-1M-10-total.dat

Again we also did a benchmark run with the G1 collector:

Whiskers show the 99.9 % confidence interval reported by JMH.totalMem_settledtotalMem_maxVmRSSVmHWMcache2kCaffeineEhCache2Guava01 GB2 GB3 GB4 GB5 GB6 GB7 GB8 GB9 GBmemorytotalMem_settled · cache2k: 464 MB ± 1.1 MBtotalMem_max · cache2k: 4.95 GB ± 680.2 MBVmRSS · cache2k: 564.4 MB ± 2.2 MBVmHWM · cache2k: 2.01 GB ± 254 MBtotalMem_settled · Caffeine: 401.5 MB ± 0.2 MBtotalMem_max · Caffeine: 6.48 GB ± 452 MBVmRSS · Caffeine: 509.2 MB ± 5.9 MBVmHWM · Caffeine: 3.73 GB ± 34 MBtotalMem_settled · EhCache2: 564.6 MB ± 0.2 MBtotalMem_max · EhCache2: 6.5 GB ± 806 MBVmRSS · EhCache2: 684.7 MB ± 6.4 MBVmHWM · EhCache2: 4.13 GB ± 217.9 MBtotalMem_settled · Guava: 370.1 MB ± 0.1 MBtotalMem_max · Guava: 8.04 GB ± 286.7 MBVmRSS · Guava: 505.7 MB ± 5.6 MBVmHWM · Guava: 2.75 GB ± 165.3 MB

Whiskers show the 99.9 % confidence interval reported by JMH.

ZipfianSequenceLoadingBenchmark with G1 collector, 4 threads, 1M cache entries, Zipfian factor 10, total memory. Raw data: ZipfianSequenceLoadingBenchmarkMemory4-1M-10-total.dat

We can see that the total memory reported by the JVM is more than the memory reported by the operating system. The reason for this is that the RSS metric only reports memory that is really used. The process may request more memory from the OS and it is only claimed on the first write to that memory section. This technique is called memory overcommitment.

Looking at the values for totalMem_max, the G1 collector seems to request a high amount of memory which is never used. We can also see that the G1 collector releases memory back to the OS after a full GC, while CMS does not.

Metric: Allocation Rate

The values for the used memory reported by the JVM after a full GC and the value reported by the OS differ a lot. To find out a possible cause, we can take a look at the allocation rate. This tells us how many objects (and garbage) a program is creating.

Whiskers show the 99.9 % confidence interval reported by JMH.cache2kCaffeineEhCache2Guava050 MB/s100 MB/s150 MB/s200 MB/s250 MB/s300 MB/s350 MB/s400 MB/sallocation rate in bytes per secondallocRate(byte/s) · cache2k: 126.3 MB/s ± 0.9 MB/sallocRate(byte/s) · Caffeine: 351.7 MB/s ± 13.7 MB/sallocRate(byte/s) · EhCache2: 200.2 MB/s ± 0.7 MB/sallocRate(byte/s) · Guava: 237.2 MB/s ± 1.3 MB/s

Whiskers show the 99.9 % confidence interval reported by JMH.

ZipfianSequenceLoadingBenchmark with CMS collector, 4 threads, 1M cache entries, Zipfian factor 10, allocation rate. Raw data: ZipfianSequenceLoadingBenchmarkMemory4-1M-10-allocRate.dat

The result with the G1 collector:

Whiskers show the 99.9 % confidence interval reported by JMH.cache2kCaffeineEhCache2Guava020 MB/s40 MB/s60 MB/s80 MB/s100 MB/s120 MB/s140 MB/s160 MB/s180 MB/s200 MB/sallocation rate in bytes per secondallocRate(byte/s) · cache2k: 112.9 MB/s ± 3.4 MB/sallocRate(byte/s) · Caffeine: 180.6 MB/s ± 6 MB/sallocRate(byte/s) · EhCache2: 188.4 MB/s ± 2.3 MB/sallocRate(byte/s) · Guava: 97.4 MB/s ± 6.7 MB/s

Whiskers show the 99.9 % confidence interval reported by JMH.

ZipfianSequenceLoadingBenchmark with G1 collector, 4 threads, 1M cache entries, Zipfian factor 10, allocation rate. Raw data: ZipfianSequenceLoadingBenchmarkMemory4-1M-10-allocRate.dat

However, looking at the absolute values does not make sense, since the throughput is very different. We can normalise the allocation rate and calculate the allocated bytes per operation:

Whiskers show the 99.9 % confidence interval reported by JMH.cache2kCaffeineEhCache2Guava020 B40 B60 B80 B100 B120 B140 B160 Ballocated bytes per operationallocRate(byte/op) · cache2k: 24.6 bytes/op ± 0 bytes/opallocRate(byte/op) · Caffeine: 86.2 bytes/op ± 0.3 bytes/opallocRate(byte/op) · EhCache2: 141.9 bytes/op ± 0.2 bytes/opallocRate(byte/op) · Guava: 67.5 bytes/op ± 0.1 bytes/op

Whiskers show the 99.9 % confidence interval reported by JMH.

ZipfianSequenceLoadingBenchmark with G1 collector, 4 threads, 1M cache entries, Zipfian factor 10, normed allocation rate. Raw data: ZipfianSequenceLoadingBenchmarkMemory4-1M-10-allocRatePerOp.dat

This graph is identical for the CMS and G1 collector.

The allocation rate is determined by JMH via the GcProfiler which uses ThreadMXBean.getThreadAllocatedBytes.

Conclusion

To my knowledge this is the first systematic evaluation of different Java memory consumption metrics for their usage in benchmarking. We created the metric usedMem_max and discovered the hidden gem VmHWM that proves useful for our purposes.

The major findings are:

Highlighting the last point, let’s take a look at the best performers according to usedMem_settled:

Whiskers show the 99.9 % confidence interval reported by JMH.GuavaCaffeinecache2kEhCache2020 MB40 MB60 MB80 MB100 MB120 MB140 MB160 MB180 MBusedHeap_settledusedHeap_settled · Guava: 106.3 MB ± 0 MBusedHeap_settled · Caffeine: 114.2 MB ± 0.1 MBusedHeap_settled · cache2k: 134 MB ± 0 MBusedHeap_settled · EhCache2: 163 MB ± 0 MB

Whiskers show the 99.9 % confidence interval reported by JMH.

ZipfianSequenceLoadingBenchmark with G1 collector, 4 threads, 1M cache entries, Zipfian factor 10, used heap memory, sorted by best performance. Raw data: ZipfianSequenceLoadingBenchmarkMemory4-1M-10-usedHeap-sorted.dat

In contrast, here are the best performers according to VmHWM:

Whiskers show the 99.9 % confidence interval reported by JMH.cache2kGuavaCaffeineEhCache20500 MB1 GB1.5 GB2 GB2.5 GB3 GB3.5 GB4 GB4.5 GBVmHWMVmHWM · cache2k: 2.01 GB ± 254 MBVmHWM · Guava: 2.75 GB ± 165.3 MBVmHWM · Caffeine: 3.73 GB ± 34 MBVmHWM · EhCache2: 4.13 GB ± 217.9 MB

Whiskers show the 99.9 % confidence interval reported by JMH.

ZipfianSequenceLoadingBenchmark with G1 collector, 4 threads, 1M cache entries, Zipfian factor 10, peak memory usage reported by the operating system (VmHWM), sorted by best performance. Raw data: ZipfianSequenceLoadingBenchmarkMemory4-1M-10-VmHWM-sorted.dat

Going back to our initial motivation, we found that cache2k has superior throughput and consumes less real memory (according to the VmHWM metric) than other cache products for the tested benchmark scenario.

In our benchmark scenario the internal data structures of the cache dominate the used memory. This is intentional. The cache libraries will not impact the overall memory consumption of real applications as much as shown here, because the payload data has a bigger size. It must also be noted that the differences between VmHWM and usedMem_settled will look more or less dramatic when different benchmark parameters are used (thread count, hit rate, etc.).

Future work: Looking at the results, there are a lot of phenomena that deserve a closer look. Now that we have the tooling in place, we can use it for more detailed benchmarks. Tools and benchmarks can also be used for a deeper analysis of the effects of the G1 garbage collector which will become the default for Java 9. Finally, it’s the plan (or my wish) to integrate the memory consumption metrics into JMH to make them easily and widely available. Meaningful benchmarking of code that has memory/time trade offs and relies on garbage collection is an interesting topic and is worth some more thought. Instead of letting the memory expand unbounded, the benchmark could force identical memory limits for all benchmark targets.

Based on the feedback I get, I will update this article and/or provide another article that is more hands-on and includes the JMH options.

Addendum: Benchmark Setup Description

For reproduction, here is the setup. The benchmark can be run with:

git clone https://github.com/cache2k/cache2k-benchmark.git
git checkout c9ce981eafb8bce5fd1d719fa618011129732dee
cd cache2k-benchmark
# compiles the benchmark and produces: jmh-suite/target/benchmarks.jar
mvn -DskipTests package
# runs the benchmarks, results are written to: /var/run/shm/jmh-result
bash jmh-run.sh
# generate the graphs
bash processJmhResults.sh --dir /var/run/shm/jmh-result process

The code to obtain the metrics usedHeap_settled and usedMem_settled is in ForcedGcMemoryProfiler.java. The code to determine usedMem_max is in GcProfiler.java.

Benchmark

The benchmark source is in ZipfianSequenceLoadingBenchmark.java. The benchmark is selected from a variety of cache benchmarks in the cache2k benchmarks suite. The cache is operated in a read through configuration, with a size limit of 1M entries. The access pattern is a generated Zipfian distribution of 10M different numbers. Zipfian factor 10 means we generate a number space 10 times the cache size. We will use different factors in the upcoming benchmarks to facilitate different hit rates. In this setup the hit rate turns out to be between 77% and 82%. A cache miss gets a penalty via JMH’s blackhole: Blackhole.consumeCPU(1000).

It is important to note that the payload data (keys and values) consists only of integer objects. When storing bigger objects in the cache the differences in memory consumption between cache implementations would be less noticeable.

The benchmark will be described in more detail in a following blog post.

Environment

Compared Caches and Versions

We use these “cache” implementations:

JMH Setup

The JMH command line arguments are -gc true -f 5 -wi 2 -w 20s -i 3 -r 20s, which results in:

Threads and Cores

The benchmarks are run with four threads on four available CPU cores. The number of cores is set to four via the Linux CPU hotplug feature.

Plots With Confidence

In the plots, units are in SI (base 10), meaning 1MB is 10^6 bytes. This way the unit prefixes for operations/s and bytes/s are identical.

Every bar is plotted with a confidence interval. The interval does not represent the upper and lower bounds of a measured value, instead it is much more sensitive. The confidence interval is calculated by JMH with a level of 99.9%. This means the likelihood that the value is within the shown interval is 99.9%.

Updates

Update from 2017-04-03

Note

Update 2026: Typo and wording fixes, switch to UK English.



Previous Post
Java Caching Benchmarks 2016 - Part 2
Next Post
Java Caching Benchmarks Part 3 - The Finals