Санкт-Петербургская группа тестирования JVM


« Краткий обзор Java... | Main | Странности финализац... »
20060502 вторник Май 02, 2006

Java и DTrace: новые возможности диагностики Java приложений

Мы начинаем новую серию постов, посвященных DTrace и Java HotSpot Virtual Machine (VM). В начале мы кратко расскажем что же такое DTrace, расскажем что было сделано в Java VM для поддержки DTrace и затем перейдем к наиболее интересной части—примерам, демонстрирующим анализ и диагностику Java приложений с помощью DTrace.

Что такое DTrace?

DTrace (Dynamic Tracing Framework) является частью операционной системы Solaris 10 и представляет собой мощный инструмент для диагностики как самой ОС так и пользовательских приложений в режиме реального времени. С помощью DTrace вы можете отслеживать узкие места производительности приложений, проводить настройку, анализ и диагностику системы. И все это DTrace делает динамически, не требуя остановки работы анализируемого приложения, в отличие от таких инструментов как truss, pstack, prstack и т.п.

Ниже мы приведем забавный пример такой динамической модификации—подмену информации, выводимой командой uname.

Как работает DTrace?

DTrace оперирует такими понятиями, как проба (probe) и провайдер (provider). Проба—это точка (или точка перехвата), в которой снимаются показатели системы. Пробы в свою очередь вводятся в систему специальными модулями ядра, именуемыми провайдерами. В ядро системы встроено больше десятка провайдеров (syscall, io, sched, ...) и более 30.000 проб. Каждый из провайдеров позволяет отлаживать определенную область операционной системы. Как следствие, посредством различных проб вы можете отслеживать любые процессы в Solaris 10, от пользовательских функций до системных вызовов и исполнения конкретных инструкций.

Важно отметить, что пока DTrace не начнет действовать, пробы находятся в пассивном состоянии, не сказываясь на производительности системы. Гарантируется также, что DTrace не нарушает функционирование систем, на которых он исполняется, что делает его еще более привлекательным для использования в уже работающих приложениях.

Как же использовать DTrace? Например, как посмотреть, откуда и сколько раз вызывается системный вызов write?

Инструкции для DTrace пишутся в виде скриптов на языке D. Этот язык очень простой и в то же время достаточно мощный. Он очень похож на C, но не пугайтесь, никаких указателей в D нет :)

Взаимодействие с DTrace происходит так:

Вот пример D-скрипта, с помощью которого вы можете узнать какими процессами или командами вызывается системный вызов write. Этому вызову соответствует проба syscall::write:entry.

% cat write.d #!/usr/sbin/dtrace -s

syscall::write:entry
{
    printf("pid: %d, execname: %s", pid, execname);
} %

Делаем файл write.d исполняемым (chmod +x write.d) и запускаем:

% ./write.d
CPU     ID   cc                 FUNCTION:NAME
  0    530                      write:entry pid: 4880, execname: sshd
  0    530                      write:entry pid: 5023, execname: mozilla-bin
  0    530                      write:entry pid: 4999, execname: mozilla
  0    530                      write:entry pid: 5024, execname: expr
  0    530                      write:entry pid: 5026, execname: cat
  0    530                      write:entry pid: 5028, execname: uname
  0    530                      write:entry pid: 5029, execname: pwd
  0    530                      write:entry pid: 5030, execname: domainname
  0    530                      write:entry pid: 5025, execname: which
  0    530                      write:entry pid: 5032, execname: whoami
... %

Много интересных примеров вы можете найти в блоге Глеба Рейса и в официальном руководстве по DTrace, а также в богатой коллекции примеров под названием DTrace ToolKit.

А вот пример D-скрипта, который модифицирует вывод команды uname:

% cat uname.d #!/usr/sbin/dtrace -s

#pragma D option quiet
#pragma D option destructive

syscall::uname:entry
{
    /* запоминаем значение переменной функции в локальной переменной потока self->addr */
    self->addr = arg0;
}

syscall::uname:return
{
    copyoutstr("Windows 3000", self->addr, 257);
    copyoutstr("mars-earth-gw.org", self->addr+257, 257);
    copyoutstr("911.911.911", self->addr+(257*2), 257);
    copyoutstr("zetasparc v42", self->addr+(257*3), 257);
    copyoutstr("impossible-improbability-driver", self->addr+(257*4), 257);
    /* обнуляем переменную, чтобы DTrace мог освободить память, выделенную под нее */ self->addr = 0; } %

Если вы запустите этот скрипт перед uname -a, вы обнаружите, что являетесь счастливым обладателем не Solaris, а ОС Windows 3000:

% ./uname.d &
% uname -a
Windows 3000 mars-earth.gw.org 911.911.911 zetasparc v42 impossible-improbability-driver
% mail -s 'UNBELIEVABLE!' everybody@sun.com
...but it worked!
^D

Поддержка DTrace в Java HotSpot VM

Все сказанное выше не может не радовать простого программиста, но использовать DTrace для отладки и диагностики Java приложения на уровне системных вызовов все же не так удобно. Гораздо удобнее было бы, например, иметь возможность отслеживать создание объектов определенных классов, создание потоков, захват и освобождение мониторов на уровне языка Java.

И новая версия Java HotSpot VM предоставляет такую возможность.

Как это сделано? Помимо встроенных провайдеров и проб DTrace позволяет разработчикам приложений создавать для приложения собственные провайдеры и пробы. Тем самым вы можете добавлять любые пробы в ваше приложение, да еще и в любом количестве! Такие провайдеры доступны с помощью провайдера pid.

В Java HotSpot Virtual Machine (VM), которая входит в состав Java SE 6.0 (Mustang), реализованы два провайдера: hotspot и hotspot_jni. Теперь вы можете узнать почти все (ну, или почти все) о том, что и в какой момент происходит в Java VM.

Провайдер hotspot

Этот провайдер предоставляет следующий список проб:

VM Lifecycle Probes: пробы, связанные с жизненным циклом VM

vm-init-begin

момент начала инициализации VM

vm-init-end

момент окончания инициализации VM

vm-shutdown

момент завершения VM

Thread Lifecycle Probes: пробы, связанные с жизненным циклом Java потоков

thread-start

момент запуска потока Java

thread-stop

момент остановки потока Java

Classloading Probes: пробы, связанные с дейтельностью загрузчика классов

class-loaded

момент завершения загрузки класса

class-unloaded

момент сигнализирующий о завершении выгрузки класса

GC Probes: пробы, связанные с работой сборщика мусора

gc-begin

момент начала работы сборщика мусора

gc-end

момент завершения работы сборщика мусора

mem-pool-gc-begin

момент начала сборки мусора или очистки данного коллектора

mem-pool-gc-end

момент окончания сборки мусора или очистки данного коллектора

Method Compilation Probes: пробы, связанные с компиляцией Java методов JIT компилятором

method-compile-begin

момент начала компиляции метода

method-compile-end

момент окончания компиляции метода

compiled-method-load

момент начала загрузки скомпилированного метода в VM

compiled-method-unload

момент окончания загрузки скомпилированного метода в VM

Monitor Probes: пробы, связанные с работой с Java мониторами

monitor-contended-enter

момент начала захвата монитора, уже захваченного другим потоком

monitor-contended-exit

момент освобождения потоком монитора, который в данный момент пытается захватить другой поток

monitor-wait

момент, когда поток входит в состояние ожидания в результате вызова Object.wait()

monitor-waited

момент завершения ожидания Object.wait() в силу истечения времени ожидания (timeout) или получения нотификации от другого потока с помощью Object.notify() или Object.notifyAll()

monitor-notify

момент вызова Object.notify()

monitor-notifyAll

момент вызова Object.notifyAll()

Application Tracking probes: пробы, связанные с работой приложения

method-entry

момент входа в Java метод

method-exit

момент выхода из Java метода

object-alloc

момент создания Java объекта


Более подробно о каждой группе проб, об аргументах, предаваемых в пробы мы расскажем в следующих постах. Пока же, приведем простой пример наблюдения за работой сборщика мусора.

Напишем D-скрипт gc1.d, в котором будем отслеживать пробы hotspot:::gc-begin и hotspot:::gc-end.

% cat gc1.d #!/usr/sbin/dtrace -s
#pragma D option quiet

hotspot$target:::gc-begin
{
    printf("GC started at %Y\n", walltimestamp);
}

hotspot$target:::gc-end
{
    printf("GC finished at %Y\n", walltimestamp);
} %

Поскольку hotspot провайдер определяется в рамках процесса, то полное имя провайдера на самом деле является hotspotPID, где PID - номер конкретного процесса. В данном примере используется макро переменная $target, которая указывает на pid Java процесса, который передается в командной строке DTrace-скрипту.

Теперь запусттим приложение Java, например, jconsole (начиная с версии JDK 6.0 jconsole входит в стандартный дистрибутив Java SE Development Kit).

% jconsole & %

Сделаем файл gc1.d исполняемым и запустим, передав в качестве аргумента идентификатор процесса (говоря по-русски, PID) jconsole:

% ./gc1.d -p 33322 GC started at 2006 Apr 26 20:44:56
GC finished at 2006 Apr 26 20:44:56
GC started at 2006 Apr 26 20:44:56
GC finished at 2006 Apr 26 20:44:56
GC started at 2006 Apr 26 20:44:59
GC finished at 2006 Apr 26 20:45:00
...
%

Интересно, почему так часто запускается GC и что происходит между его вызовами? Чтобы узнать это, можно усложнить скрипт, например, добавив пробы, информирующие о деятельности отдельных коллекторов или о создании новых Java объектов. Но об этом в следующих постах.

Провайдер hotspot_jni

Данный провайдер предоставляет DTrace набор пробов для входа и выхода для каждого JNI метода. То есть, для каждого JNI метода имеется пара проб: hotspot_jni$pid:::JNIMETHODNAME-entry и hotspot_jni:::JNIMETHODNAME-return. Поскольку методов в JNI более двухсот, то для иллюстрации приведем только несколько проб:

hotspot_jni:::AttachCurrentThread-entry hotspot_jni:::AttachCurrentThread-return hotspot_jni:::CallVoidMethod-entry hotspot_jni:::CallVoidMethod-return hotspot_jni:::GetStringLength-entry hotspot_jni:::GetStringLength-return ...

Аргументы, переданные в соответствующие методы JNI (за исключением Invocation API) являются одновременно и аргументами проб.

Продемонстрируем работу с hotspot_jni пробами на примере скрипта, считающего статистику вызовов методов JNI и по окончнию работы скрипта выводящий данные на терминал.

Сначала пишем D-скрипт hotspot_jni_1.d, и, как обычно, делаем его исполняемым.

#!/usr/sbin/dtrace -s #pragma D option quiet hotspot_jni$target:::*-entry { JNI_CALLS ++; @jni_calls[probename] = count(); } :::END { printa("%10@d %s\n", @jni_calls); printf("\n"); printf("Total number of JNI calls: %d\n", JNI_CALLS); }

Опять запускаем jconsole, а затем—hotspot_jni_1.d, передавая в качестве аргумента номер процесса jconsole: 

% jconsole & % ./hotspot_jni_1.d -p 69
         2 CallObjectMethod-entry
         2 GetStringCritical-entry
         2 NewObject-entry
         2 NewObjectArray-entry
         2 ReleaseStringCritical-entry
         2 SetIntField-entry
         2 SetObjectArrayElement-entry
         2 SetObjectField-entry
         4 NewStringUTF-entry
        26 GetObjectClass-entry
        28 CallStaticVoidMethod-entry
        74 GetByteArrayRegion-entry
        74 SetByteArrayRegion-entry
       148 GetObjectField-entry
       260 GetStringUTFLength-entry
       260 GetStringUTFRegion-entry
       262 GetStringLength-entry
       416 IsAssignableFrom-entry
       500 GetSuperclass-entry
       840 IsInstanceOf-entry
... %

Результат потрясающий, а главное—все так просто! :)

Полезные ссылки

Е.П. опубликовал vmrobot ( май 02 2006, 06:42:21 AM MSD ) Permalink Комментарии [1]

Trackback URL: http://blogs.sun.com/vmrobot/entry/java_%D0%B8_dtrace_%D0%BD%D0%BE%D0%B2%D1%8B%D0%B5_%D0%B2%D0%BE%D0%B7%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D0%B8
Комментарии:

"начиная с версии JDK 6.0 jconsole входит в стандартный дистрибутив Java SE Development Kit" -> "начиная с версии JDK 1.5.0..." "идентификатор процесса (говоря по-русски, PID)" :)))

опубликовал somebody Июнь 04, 2007 at 08:35 PM MSD #

Опубликовать комментарий:

Имя
E-Mail:
URL:

Ваш комментарий:

HTML Syntax: Отключен

Хиты страниц за сегодня: 93