Displaying Java’s Memory Pool Statistics with VisualVM
Join the DZone community and get the full member experience.
Join For FreeThe "Just In Time" (JIT) compilation of Java Byte code provides a window of opportunity within the dynamics of a run time. The product of the JIT, native code, is stored in a memory pool known as the Code Cache. Filling the Code Cache will cause the JIT to stop working. Turning off the JIT will deny our applications key optimizations that could boost performance to match or, in some cases, exceed equivalent code written in C/C+. When this happens, our applications consume much more CPU and will execute slower than they should.
Given the important role that the Code Cache has in application performance, it’s surprising how little tooling there is out there to let you peek into it’s inner workings. But then, not many are asking for such tooling as not many are aware of the important role that code cache sizing plays in application performance. It’s also surprising that the JVM doesn’t provide much visibility into this mysterious part of it’s inner workings. It is, however, monitored by an instance of java.lang.management.MemoryMXBean. This MXBean does give us some insight into how much of this memory pool is being utilized by the JIT. In figure 1, you can see the data being exposed by the MBean brower, VisualVM plugin.
Figure 1. VisualVM MBean
The MBean browser view provides a simple textual view of the memory pool’s attributes. Instead of this textual view, I was looking for a graphical time line of Code Cache utilization. To this end, I wrote my own VisualVM plugin which can be seen in Figure 2. The rest of this piece describes the steps taken to create this new plugin.
Figure 2. Memory pools visualization
A VisualVM plugin is nothing more than a NetBeans plugin and that is nothing more than a specialized JAR. I’ve used the NetBeans IDE here simply because it makes it more convenient to build and test the VisualVM plugin. The first thing to do is to configure NetBeans by adding a downloaded version of VisualVM (I used 1.3.3) to the platform manager. The next step is to create a new NetBeans module project which we’ll call MemoryPoolView. You’ll need to configure the project to use the VisualVM platform. Once this is complete, we can get on with the task of building the new plugin.
NetBeans Module Lifecycle
NetBeans modules have a life cycle that is supported by the class ModuleInstaller. MemoryPoolView requires it’s own installer so that it can manage it’s own life-cycle. Installers can be generated using the NetBeans menu item New | Module Installer. The generated code should look something like that found in listing 1.
public class Installer extends ModuleInstall {Listing 1. Module Installer
@Override
public void restored() {
MemoryPoolViewProvider.initialize();
}
@Override
public void uninstalled() {
MemoryPoolViewProvider.unregister();
}
}
The Installer overrides the two most important methods, restored() and uninstalled(). These methods are called when NetBeans starts up and shutdowns respectively. Our implementation of restored() makes a call to register our MemoryPoolViewProvider with the DataSourceViewsManager. The DataSourceViewsManager manages instances of DataSourceView. Thus MemoryPoolViewProvider must extend this class.
To have a better understanding of how all of this works we need to know about the concept of a data source in VisualVM. As the name suggests, a DataSource is anything that will feed a VisualVM with (more specifically, performance) data. VisualVM comes with a number of data sources including SnapShot, Host, and of interest to us, Application. When the user opens a DataSource, VisualVM asks the DataSourceViewsManager for any views that are registered that can display data from that data source. The DataSourceViewsManager in turn asks each of the views, can you display data from this DataSource. Those view providers that answer yes will be given the data source and asked for a view. VisualVM will create a new tabbed pane for that view. From that point forward, what happens in the view is up to the view provider.
In listing 2, we can see the code that registers and unregisters our view provider in response to lifecycle events. The supportsViewFor() and createView() methods respond to the requests from the DataSourceViewsManager. Because supportsViewFor can only accept an Application as a parameter and we want to inspect the code cache for all applications, this method simply returns true. In some cases you might want to put in more extensive testing to determine if you want to instantiate a view.
class MemoryPoolViewProvider extends DataSourceViewProvider<Application> {Listing 2. DataSourceViewProvider
private static DataSourceViewProvider<Application> instance = new MemoryPoolViewProvider();
@Override
public boolean supportsViewFor(final Application application) {
return true;
}
@Override
public synchronized DataSourceView createView(final Application application) {
return new MemoryPoolView(application);
}
static void initialize() {
DataSourceViewsManager.sharedInstance().addViewProvider(instance, Application.class);
}
static void unregister() {
DataSourceViewsManager.sharedInstance().removeViewProvider(instance);
}
}
Now that we have MemoryPoolView, the next steps are to build the view and connect it to the Application. To do this, we need to consider two topics:
- how to build a view that integrates into VisualVM
- the logistics of acquiring the data from the Application for the view
Let's start by looking at how to build the DataSourceView that is returned by MemoryPoolViewProivder.createView().
The Anatomy of a VisualVM View
If you look back at figure 2, you can see that each individual view is contained in it’s own tabbed pane. Each tabbed pane has a title and an icon. After that, it may seem that the layout of each view is somewhat random. While it is true that each view is customized to best display what it needs to show, there is a wee bit of structure in there. The panel’s inner space is broken down into a master area on top and 4 display areas down below. The master area is generally used to provide course grained controls, while each of the 4 display areas present data. These display areas are arranged in a 4 quadrant grid. This layout is visible in MemoryPoolView as each quadrant contains a view of one memory pool. A time line of the Code Cache counters is displayed in the lower right hand quadrant. You can also clearly see this grid layout in the monitor view.
In our master view, there are 4 check boxes. You may have noticed these (optionally displayed) check boxes in other views. For example, the Threads plugin has two check boxes, one for each of the two views it maintains. These check boxes will hide or reveal a corresponding view.
Getting back to the code, we will provide the tabbed pane’s title and icon as arguments in our MemoryPoolView constructor. Note that MemoryPool view must extends DataSourceView as that is what is returned by the view providers createView method. The MasterView and DetailsView will be constructed in a call to createComponent() as shown in listing 3.
The last visual detail to consider here is positioning. In the constructor one argument is the magic number 60. VisualVM orders the tabs using preferences suggested by the user. In this case the suggestion is this tab should be in position 60. Anything smaller will be on the left and anything bigger will be on the right. Adding a DetailsView requires that you specify a quadrant and a slot. DataViewComponent defines constants for TOP_LEFT, BOTTOM_RIGHT and so on. The position is used when two DetailsView are placed in the same quadrant. Looking back at figure 2 you can see that the young generational spaces where both placed in TOP_LEFT with Eden in position 10 and Survivors in position 20.
We configure all of this in the class MemoryPoolView as shown in listing 3.
class MemoryPoolView extends DataSourceView {Listing 3. MemoryPoolView
public MemoryPoolView(Application application) {
super(application,"Memory Pools",
new ImageIcon(ImageUtilities.loadImage(IMAGE_PATH, true)).getImage(),
60,
false);
}
@Override
protected DataViewComponent createComponent() {
//Data area for master view:
JEditorPane generalDataArea = new JEditorPane();
generalDataArea.setBorder(BorderFactory.createEmptyBorder(7, 8, 7, 8));
//Master view:
DataViewComponent.MasterView masterView =
new DataViewComponent.MasterView("Memory Pools", "View of Memory Pools", generalDataArea);
//Configuration of master view:
DataViewComponent.MasterViewConfiguration masterConfiguration =
new DataViewComponent.MasterViewConfiguration(false);
//Add the master view and configuration view to the component:
dvc = new DataViewComponent(masterView, masterConfiguration);
// the magic that gets a handle on all instances of MemoryPoolMXBean
findMemoryPools();
MemoryPoolPanel panel;
for ( MemoryPoolModel model : models) {
panel = new MemoryPoolPanel(model.getName());
model.registerView(panel);
Point position = calculatePosition(model.getName());
dvc.addDetailsView(new DataViewComponent.DetailsView(
model.getName(), "memory pool metrics", position.y, panel, null), position.x);
}
return dvc;
}
private Point calculatePosition(String name) {
return positions.get(name);
}
static {
positions = new HashMap<String,Point>();
positions.put( "Par Eden Space", new Point(DataViewComponent.TOP_LEFT,10));
positions.put( "PS Eden Space", new Point(DataViewComponent.TOP_LEFT,10));
positions.put( "Eden Space", new Point(DataViewComponent.TOP_LEFT,10));
positions.put( "G1 Eden", new Point(DataViewComponent.TOP_LEFT,10));
positions.put( "Par Survivor Space", new Point(DataViewComponent.TOP_LEFT,20));
positions.put( "PS Survivor Space", new Point(DataViewComponent.TOP_LEFT,20));
positions.put( "Survivor Space", new Point(DataViewComponent.TOP_LEFT,20));
positions.put( "G1 Survivor", new Point(DataViewComponent.TOP_LEFT,20));
positions.put( "CMS Old Gen", new Point(DataViewComponent.TOP_RIGHT,10));
positions.put( "PS Old Gen", new Point(DataViewComponent.TOP_RIGHT,10));
positions.put( "Tenured Gen", new Point(DataViewComponent.TOP_RIGHT,10));
positions.put( "G1 Old Gen", new Point(DataViewComponent.TOP_RIGHT,10));
positions.put( "CMS Perm Gen", new Point(DataViewComponent.BOTTOM_LEFT,10));
positions.put( "Perm Gen", new Point(DataViewComponent.BOTTOM_LEFT,10));
positions.put( "PS Perm Gen", new Point(DataViewComponent.BOTTOM_LEFT,10));
positions.put( "G1 Perm Gen", new Point(DataViewComponent.BOTTOM_LEFT,10));
positions.put( "Code Cache", new Point(DataViewComponent.BOTTOM_RIGHT,10));
}
Getting the Data
Now that we’ve covered the basics of laying out a VisualVM view it’s time to move on to acquiring the data to feed the views. As was the case in view construction, VisualVM provides a lot built useful functionality for data acquisition and handling. In this case we want a handle on all the instances of MemoryPoolMXBean. To get to them we will make use of VisualVM facilities that eliminate our need to write complex JMX client code. Lets take the magic out of findMemoryPools() as we continue to build out MemoryPoolView in listing 4.
protected void findMemoryPools() {Listing 4 Obtaining a handle on MemoryMXBean instances
try {
MBeanServerConnection conn = getMBeanServerConnection();
ObjectName pattern = new ObjectName("java.lang:type=MemoryPool,name=*");
for (ObjectName name : conn.queryNames(pattern, null)) {
initializeModel(name, conn);
}
} catch (Exception e) {
LOGGER.throwing(MemoryPoolView.class.getName(), "Exception: ", e);
}
}
private MBeanServerConnection getMBeanServerConnection() {
JmxModel jmx = JmxModelFactory.getJmxModelFor((Application)super.getDataSource());
return jmx == null ? null : jmx.getMBeanServerConnection();
}
We obtain handles on MXBeans through a MBeanServerConnection. We can get the server connection from a JmxModel which in turn is acquired from the JmxModelFactory. All of this infrastructure is provided to use by VisualVM. All we have to know is what we are looking for and then query the connection to find it. This is achieved by creating an ObjectName pattern. From figure 1 we can see that the ObjectName used to bind instances of MemoryPoolMXBean is something like java.lang:type=MemoryPool,name=”CMS Old Gen”. Changing the name attribute to with wildcard * allows us to find all of the memory pools. Now all we have to do is interrogate each of the beans for their values and display them.
Charting in VisualVM
Referring back to figure 2 we can see that the data for each MXBean displayed in an XY plot. If you run the plugin you’’ notice that the graph is updated every 2 seconds. As complex as the charts look, VisualVM’s supplied charts are remarkably easy to use.
Of the three different types of chart, Decimal, Precent and Byte. MemoryPoolView uses Byte. We build the chart using a prescribed variation of the builder pattern. This is demonstrated in the constructor in listing 5.
public class MemoryPoolPanel extends JPanel implements MemoryPoolModelListener {Listing 5 MemoryPoolPanel
private SimpleXYChartSupport chart;
public MemoryPoolPanel(String name) {
setLayout(new BorderLayout());
SimpleXYChartDescriptor description = SimpleXYChartDescriptor.bytes(0, 20, 100, false, 1000);
description.setChartTitle(name);
description.setDetailsItems(new String[3]);
description.addLineItems("Configured");
description.addLineItems("Used");
description.setDetailsItems(new String[]{"Size current",
"configured",
"occupancy"});
description.setXAxisDescription("<html>Time</html>");
description.setYAxisDescription("<html>Memory Pool (K)</html>");
chart = ChartFactory.createSimpleXYChart(description);
add(chart.getChart(),BorderLayout.CENTER);
}
@Override
public void memoryPoolUpdated(MemoryPoolModel model) {
long[] dataPoints = new long[2];
dataPoints[0] = model.getCommitted();
dataPoints[1] = model.getUsed();
chart.addValues(System.currentTimeMillis(), dataPoints);
String[] details = new String[3];
details[0] = Long.toString(model.getCommitted());
details[1] = Long.toString(model.getMax());
details[2] = Long.toString(model.getUsed());
chart.updateDetails(details);
}
}
We start the process by building a description of the chart that we want. The initial parameters indicate minimal value, maximum value, initial Y margin, a boolean to indicate if the chart can be hidden (puts checkbox in MasterView), and the size of a buffer to hold data points. To this description we’ll add a title and a detail view for current, configured, and occupancy, and X and Y axis labels, . Finally we add a line for occupancy and currently configured size. Once this is complete, we are ready to ask the ChartFactory to create the chart.
The final step is to connect the model to the view. We need a timer so that we can update the charts at a regular rate. Once again, VisualVM proves useful in that it provides a CachedMBeanServerConnection. A CachedMBeanServerConnection caches the values from a MBeanServerConnection. It also contains a timer that when fires causes the cache to be flushed. Flushing the cache causes the cache to be refreshed and then notifying all MBeanCacheListeners that the cache has been updated. All we have to do is implement the flushed() as specified by the interface. When flushed(0 is called we will dig through the CompositeData that bundles the data values we are looking for. This implementation can be found in Listing 7.
class MemoryPoolModel implements MBeanCacheListener {Listing 7 MemoryViewModel
public MemoryPoolModel(final ObjectName mbeanName, final JmxModel model,
final MBeanServerConnection mbeanServerConnection) throws Exception {
this.mbeanName = mbeanName;
this.mbeanServerConnection = mbeanServerConnection;
CachedMBeanServerConnectionFactory.getCachedMBeanServerConnection(model,
2000).addMBeanCacheListener(this);
name = mbeanServerConnection.getAttribute(mbeanName, "Name").toString();
type = mbeanServerConnection.getAttribute(mbeanName, "Type").toString();
}
@Override
public void flushed() {
try {
CompositeData poolStatistics = (CompositeData)mbeanServerConnection.getAttribute(mbeanName, "Usage");
if ( poolStatistics != null) {
CompositeType compositeType = poolStatistics.getCompositeType();
if ( compositeType != null) {
Collection keys = compositeType.keySet();
for ( String key : compositeType.keySet()) {
if ( key.equals("committed"))
this.committed = (Long)poolStatistics.get("committed");
else if ( key.equals("init"))
this.initial = (Long)poolStatistics.get("init");
else if ( key.equals("max"))
this.max = (Long)poolStatistics.get("max");
else if ( key.equals("used"))
this.used = (Long)poolStatistics.get("used");
else
LOGGER.warning("Unknown key: " + key);
}
tickleListeners();
}
}
} catch (Throwable t) {
LOGGER.throwing(MemoryPoolModel.class.getName(), "Exception recovering data from MemoryPoolMXBean ", t);
}
}
The final step is to update the chart. Once the cache has been refreshed, the model will let the view know that it has new data values. The view can then query the model to obtain those values. To add values we need to put them into a long[] and pass them to the chart. Updating the details follows the same pattern. This is demonstrated in listing 8.
public void memoryPoolUpdated(MemoryPoolModel model) {Listing 8 Updating a chart
long[] dataPoints = new long[2];
dataPoints[0] = model.getCommitted();
dataPoints[1] = model.getUsed();
chart.addValues(System.currentTimeMillis(), dataPoints);
String[] details = new String[3];
details[0] = Long.toString(model.getCommitted());
details[1] = Long.toString(model.getMax());
details[2] = Long.toString(model.getUsed());
chart.updateDetails(details);
}
Conclusion
As was demonstrated here, there is quite a bit of support to aid in the visualization of performance data in VisualVM. We can use this support to do the heavy lifting that would normally take considerable amounts of boiler plate code. This demonstration only focuses only a fraction of the support that is available. For example, we could easily build in snapshot capabilities that would take advantage of the support for that feature.
Finally, MemoryPoolView has now been released as an open source project @ http://java.net/projects/memorypoolview.
Opinions expressed by DZone contributors are their own.
Comments