Getting L2 Cache Statistics using Mbeans

15 Apr 2021

This is a follow-up to my previous notes about cacheing with Hibernate, Ehcache and the JCache API - specifically looking at how to access cache statistics.

This is not about using the jconsole GUI tool (which you certainly can do, as an alternative approach), but instead this is about programmatically accessing cache statistics from within your application.

The JCache API makes cache statistics available via the CacheStatisticsMXBean interface.

(And for Ehcache, there is a corresponding implementation class.)

To access any specific MBean you need to know its name.

Before using the MBean’s name, you first need to create an MBeanServer instance:

Java
1
final MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();

And then you can use the beanServer.queryMBeans(name, query) method to retrieve a set of related MBeans - one or more of which can be cache MBeans.

The query method takes two parmeters:

Here I will only use the ObjectName, by using a string to represent the MBeans I want to retrieve - and I will leave the query expression set to null.

However, to use this query method I first need to know the names of the beans I want to retrieve. MBean names are generally implemented based on a set of best practices guidelines - but, even so, the chosen naming approach may not be documented.

The general structure of an MBean name is: domain:key-property-list, for example:

com.sun.someapp:type=Whatsit,name=25

You may have no idea in advance what the domain or key-property values are for a bean you want to select. Fortunately, MBeans are supposed to be self-documenting, so we can retrieve these details as follows:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import java.lang.management.ManagementFactory;
import javax.management.MBeanServer;
import javax.management.ObjectName;

...

final MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();
Set<ObjectName> objNames = beanServer.queryNames(ObjectName.WILDCARD, null);
for (ObjectName objName : objNames) {
    String name = objName.getCanonicalName();
}

You can run this once to dump the name details for all of the MBeans available in your Java application.

Most of the MBeans used by a standard Java application are for the virtual machine (VM) in which the application is running, to assist in reporting on VM-related statistics such as memory usage. They can be ignored, for our purposes.

The JavaDoc for CacheStatisticsMXBean tells us that it has the following type:

javax.cache:type=CacheStatistics

So that is what we will look for in the resulting list of names and properties. For one of our L2 cache MBeans, we may see results like the following:

javax.cache:Cache=org.me.myapp.MyClass,CacheManager=file./C./path/to/HibernateOrmNative/target/classes/ehcache.xml,type=CacheStatistics

When we split this into its constituent parts, we have the following:

javax.cache:
Cache=org.me.myapp.MyClass,
CacheManager=file./C./path/to/HibernateOrmNative/target/classes/ehcache.xml,
type=CacheStatistics

From this, we can see the following keys:

  • type - it has the expected value of CacheStatistics
  • Cache - the name of the Ehcache alias in ehcache.xml
  • CacheManager - the path to the ehcache.xml file

For our purposes, we can simplify this string with the use of some * wildcards, which will then represent all the possible L2 caches we may want to track:

 "javax.cache:type=CacheStatistics,CacheManager=*,Cache=*"

We can now use this string as follows:

Java
1
2
3
4
5
6
7
import javax.management.ObjectName;
import javax.management.ObjectInstance;

...

final Set<ObjectInstance> cacheBeans = beanServer.queryMBeans(ObjectName
        .getInstance("javax.cache:type=CacheStatistics,CacheManager=*,Cache=*"), null);

This is a Set of results because we may have defined multiple different caches in our Ehcache configuration.

When we iterate over the above set, we now have access to the statistics we want:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import javax.management.ObjectName;
import javax.management.ObjectInstance;
import javax.cache.management.CacheStatisticsMXBean;
import javax.management.MBeanServer;
import javax.management.MBeanServerInvocationHandler;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectInstance;
import javax.management.ObjectName;

...

final Set<ObjectInstance> cacheBeans = beanServer.queryMBeans(ObjectName
            .getInstance("javax.cache:type=CacheStatistics,CacheManager=*,Cache=*"), null);

    cacheBeans.stream().map(cacheBean -> {
        final CacheStatisticsMXBean cacheStatisticsMXBean
                = MBeanServerInvocationHandler.newProxyInstance(beanServer,
                        cacheBean.getObjectName(), CacheStatisticsMXBean.class, false);
        // stats are cumulative over the lives of each cache:
        Map<String, Object> cacheStatisticsMap = new HashMap<>();
        cacheStatisticsMap.put("Name", cacheBean.getObjectName().getKeyProperty("Cache"));
        cacheStatisticsMap.put("Gets", cacheStatisticsMXBean.getCacheGets());
        cacheStatisticsMap.put("Hits", cacheStatisticsMXBean.getCacheHits());
        cacheStatisticsMap.put("Misses", cacheStatisticsMXBean.getCacheMisses());
        return cacheStatisticsMap;
    }).forEachOrdered(cacheStatisticsMap -> {
        cacheStatisticsList.add(cacheStatisticsMap);
    });

} catch (NullPointerException | MalformedObjectNameException e) {
    System.err.print(e);
    return cacheStatisticsList;
}

This is not a comprehensive list of statistics. Check the CacheStatisticsMXBean documentation for a complete list.

Now, I can use this code to report on the state of the L2 caches used by my application.