Статьи

Отображение статистики пула памяти Java с помощью VisualVM

Компиляция Java Byte кода Just In Time (JIT) предоставляет окно возможностей в динамике времени выполнения. Продукт JIT, нативный код, хранится в пуле памяти, известном как кэш кода. Заполнение кеша кода приведет к прекращению работы JIT. Отключение JIT приведет к отказу в оптимизации наших приложений, которая может повысить производительность для соответствия или, в некоторых случаях, превысить эквивалентный код, написанный на C / C +. Когда это происходит, наши приложения потребляют гораздо больше ресурсов процессора и будут работать медленнее, чем должны.

Учитывая важную роль, которую играет кэш кода в производительности приложений, удивительно, как мало инструментов, позволяющих вам взглянуть на его внутреннюю работу. Но тогда не многие обращаются за такой инструментарием, так как не многие знают о важной роли, которую играет размер кеша кода в производительности приложений. Также удивительно, что JVM не обеспечивает большой видимости в этой таинственной части его внутренней работы. Это, однако, контролируется экземпляром java.lang.management.MemoryMXBean. Этот MXBean дает нам некоторое представление о том, какая часть этого пула памяти используется JIT. На рисунке 1 вы можете увидеть данные, предоставляемые браузером MBean, плагином VisualVM.

Рисунок 1. VisualVM MBean

Представление браузера MBean предоставляет простое текстовое представление атрибутов пула памяти. Вместо этого текстового представления я искал графическую временную шкалу использования Code Cache. С этой целью я написал свой собственный плагин VisualVM, который можно увидеть на рисунке 2. В оставшейся части этой части описываются шаги, предпринятые для создания этого нового плагина.

Рисунок 2. Визуализация пулов

памяти. Плагин VisualVM — это не что иное, как плагин NetBeans, и это не что иное, как специализированный JAR. Я использовал среду IDE NetBeans просто потому, что она делает его более удобным для создания и тестирования плагина VisualVM. Первое, что нужно сделать, это настроить NetBeans, добавив загруженную версию VisualVM (я использовал 1.3.3) в диспетчер платформы. Следующим шагом является создание нового проекта модуля NetBeans, который мы назовем MemoryPoolView. Вам нужно будет настроить проект для использования платформы VisualVM. Как только это будет завершено, мы можем приступить к задаче создания нового плагина.

Жизненный цикл модуля NetBeans

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 {

    @Override
    public void restored() {
        MemoryPoolViewProvider.initialize();
    }

    @Override
    public void uninstalled() {
        MemoryPoolViewProvider.unregister();
    }

}

Листинг 1. Установщик модуля Установщик

переопределяет два наиболее важных метода: restore () и uninstalled (). Эти методы вызываются при запуске и завершении работы NetBeans соответственно. Наша реализация restore () делает вызов для регистрации нашего MemoryPoolViewProvider с DataSourceViewsManager. DataSourceViewsManager управляет экземплярами DataSourceView. Таким образом, MemoryPoolViewProvider должен расширять этот класс.

Чтобы лучше понять, как все это работает, нам нужно знать о концепции источника данных в VisualVM. Как следует из названия, DataSource — это все, что будет снабжать VisualVM данными (в частности, производительностью). VisualVM поставляется с несколькими источниками данных, включая SnapShot, Host и представляющий интерес для нас, Application. Когда пользователь открывает DataSource, VisualVM запрашивает DataSourceViewsManager о любых зарегистрированных представлениях, которые могут отображать данные из этого источника данных. DataSourceViewsManager, в свою очередь, спрашивает каждое из представлений, можете ли вы отобразить данные из этого источника данных. Те провайдеры, которые ответят «да», получат источник данных и попросят посмотреть. VisualVM создаст новую панель с вкладками для этого представления. С этого момента, что происходит в представлении, зависит от поставщика представления.

В листинге 2 мы видим код, который регистрирует и отменяет регистрацию нашего провайдера представлений в ответ на события жизненного цикла. Методы supportViewFor () и createView () отвечают на запросы от DataSourceViewsManager. Поскольку supportViewFor может принимать только Application в качестве параметра, и мы хотим проверить кэш кода для всех приложений, этот метод просто возвращает true. В некоторых случаях вы можете захотеть провести более обширное тестирование, чтобы определить, хотите ли вы создать экземпляр представления.

class MemoryPoolViewProvider extends DataSourceViewProvider<Application> {
    
    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);
    }
}

Листинг 2. DataSourceViewProvider

Теперь, когда у нас есть MemoryPoolView, следующие шаги — построить представление и подключить его к Приложению. Для этого нам нужно рассмотреть две темы:

  • как построить представление, которое интегрируется в VisualVM
  • Логистика получения данных из Приложения для просмотра

Давайте начнем с рассмотрения того, как построить DataSourceView, который возвращается MemoryPoolViewProivder.createView ().

Анатомия представления VisualVM

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 {

   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));
    }

Листинг 3. MemoryPoolView

Получение данных

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() {
    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();
}

Перечисление 4, Получающее дескриптор на экземплярах MemoryMXBean

Мы получаем дескрипторы на MXBeans через MBeanServerConnection. Мы можем получить соединение с сервером из JmxModel, который, в свою очередь, получен из JmxModelFactory. Вся эта инфраструктура предоставляется для использования VisualVM. Все, что нам нужно знать, — это то, что мы ищем, а затем запросить соединение, чтобы найти его. Это достигается путем создания шаблона ObjectName. Из рисунка 1 мы видим, что ObjectName, используемое для привязки экземпляров MemoryPoolMXBean, имеет вид java.lang: type = MemoryPool, name = ”CMS Old Gen”. Изменение атрибута name с помощью подстановочного знака * позволяет нам найти все пулы памяти. Теперь все, что нам нужно сделать, это опросить каждый из бинов на предмет их значений и отобразить их.

Диаграммы в VisualVM

Возвращаясь к рисунку 2, мы видим, что данные для каждого MXBean-компонента отображаются на графике XY. Если вы запустите плагин, вы заметите, что график обновляется каждые 2 секунды. Как бы ни были сложны диаграммы, поставляемые VisualVM диаграммы удивительно просты в использовании.

Из трех различных типов диаграмм: Десятичная, Процентная и Байтная. MemoryPoolView использует байт. Мы строим диаграмму, используя предписанную вариацию модели строителя. Это продемонстрировано в конструкторе в листинге 5.

public class MemoryPoolPanel extends JPanel implements MemoryPoolModelListener {

    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);
    }
}

Перечисление 5 MemoryPoolPanel

Мы начинаем процесс с построения описания диаграммы, которую мы хотим. Начальные параметры указывают минимальное значение, максимальное значение, начальное поле Y, логическое значение, указывающее, можно ли скрыть диаграмму (ставит флажок в MasterView), и размер буфера для хранения точек данных. К этому описанию мы добавим заголовок и подробный вид для текущих, настроенных и занятых, а также меток осей X и Y. Наконец, мы добавляем строку для занятости и настроенного размера. Как только это будет завершено, мы готовы попросить ChartFactory создать диаграмму.

Последний шаг — подключение модели к представлению. Нам нужен таймер, чтобы мы могли регулярно обновлять графики. Еще раз, VisualVM оказывается полезным в том, что он обеспечивает CachedMBeanServerConnection. CachedMBeanServerConnection кэширует значения из MBeanServerConnection. Он также содержит таймер, который при пожаре вызывает очистку кеша. Очистка кеша приводит к обновлению кеша и последующему уведомлению всех MBeanCacheListeners о том, что кеш обновлен. Все, что нам нужно сделать, — это реализовать flush (), как указано в интерфейсе. Когда вызывается сброс (0), мы будем копаться в CompositeData, которая объединяет искомые значения данных. Эту реализацию можно найти в листинге 7.

class MemoryPoolModel implements MBeanCacheListener {

    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);
        }
    }

Перечисление 7 MemoryViewModel

Последний шаг должен обновить диаграмму. После обновления кэша модель сообщит представлению, что оно имеет новые значения данных. Затем представление может запросить модель для получения этих значений. Чтобы добавить значения, нам нужно поместить их в long [] и передать их на график. Обновление деталей происходит по той же схеме. Это продемонстрировано в листинге 8.

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);
}

Перечисление 8, Обновляющее диаграмму

Вывод

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.