HotSpot JVM имеет множество опций для отслеживания происходящего в виртуальной машине: PrintGC
, PrintCompilation
, TraceClassLoading
и т.п. Как правило, они включаются параметрами командной строки, например, -XX:+PrintGCDetails
. Однако порой возникает необходимость включить или выключить такой флаг непосредственно во время работы приложения, когда перезапуск JVM с другими параметрами невозможен. Этого можно добиться как штатным, так и хакерским способом, причем последний и мощнее, и интереснее. Впрочем, внимания заслуживают оба.
Из данной статьи вы узнаете:
- где найти все флаги JVM, и на какие типы они делятся;
- как прочитать или установить флаг программно, используя JMX;
- как найти нужную область в памяти виртуальной машины и
испортитьмодифицировать ее.
Какими бывают флаги в HotSpot JVM
Список всех флагов с пояснениями доступен в исходниках OpenJDK: основная часть в globals.hpp наряду с дополнительными опциями архитектуры, компилятора и G1 коллектора.
Как видно, флаги определяются разными макросами:
- product и product_rw флаги можно задавать в командной строке ключиком -XX;
- develop и notproduct неинтересны, поскольку в официальных релизах JDK являются константами;
- manageable флаги позволено изменять в run-time через JMX;
- experimental официально не поддерживаются (в частности, по причине недостаточной протестированности), но могут быть включены на свой страх и риск. Для модификации этих флагов требуется добавить ключ командной строки
UnlockExperimentalVMOptions
, например,-XX:+UnlockExperimentalVMOptions -XX:+TrustFinalNonStaticFields
- diagnostic не предназначены для использования, кроме как в целях расследования проблем виртуальной машины. Включить их можно только совместно с
UnlockDiagnosticVMOptions
, например,-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
Чтобы вывести все флаги, доступные в вашей версии JVM, вместе с их актуальными значениями, следует запустить JVM с параметром PrintFlagsFinal
:
java -XX:+PrintFlagsFinal
Работа с флагами через JMX
HotSpot позволяет программно прочитать или установить значения некоторых флагов посредством Management API. Более того, при включенном Remote Management это можно делать даже на удаленном сервере.
Прежде всего, необходимо получить экземпляр MXBean с именем com.sun.management:type=HotSpotDiagnostic
.
import com.sun.management.HotSpotDiagnosticMXBean;
import java.lang.management.ManagementFactory;
import javax.management.MBeanServer;
...
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
HotSpotDiagnosticMXBean bean = ManagementFactory.newPlatformMXBeanProxy(
server,
"com.sun.management:type=HotSpotDiagnostic",
HotSpotDiagnosticMXBean.class);
Метод bean.getVMOption(String option)
позволит узнать текущее значение JVM-опции,
а bean.setVMOption(String option, String newValue)
— задать новое.
Если прочитать можно любой флаг, то изменению поддаются только manageable
.
Метод bean.getDiagnosticOptions()
вернет список всех manageable
опций.
Пример:
// Включение флага JVM, отвечающего за вывод ReentrantLock и т.п. в thread dump
bean.setVMOption("PrintConcurrentLocks", "true");
Прямой доступ к памяти JVM
К сожалению, набор опций, изменяемых посредством JMX, невелик. Но ведь флаги JVM — это лишь обычные переменные, размещенные в адресном пространстве процесса. Если знать адрес переменной, по нему можно записать новое значение через Unsafe API. Остается найти адрес JVM-флага. Задача непростая, поскольку от запуска к запуску адрес будет меняться по воле операционной системы. К счастью, Linux — весьма сговорчивая ОС, и охотно выложит нам все необходимые сведения, если правильно попросить.
- Сначала потребуется выяснить, где лежит библиотека виртуальной машины
libjvm.so
, и по какому адресу она загружена. В этом поможет виртуальная файловая система proc, в частности, файл/proc/self/maps
, где перечислены все регионы виртуального адресного пространства текущего процесса. Найдем в нем строчку, оканчивающуюся на/libjvm.so
.2b6707956000-2b67084b8000 r-xp 00000000 68:02 1823284 /usr/java/jdk1.7.0_40/jre/lib/amd64/server/libjvm.so
Первое число (0x2b6707956000) и будет базовым адресом, по которому загружена библиотека.
Заметьте, все это можно проделать на чистой Java!
private String findJvmMaps() throws IOException { BufferedReader reader = new BufferedReader(new FileReader("/proc/self/maps")); try { for (String s; (s = reader.readLine()) != null; ) { if (s.endsWith("/libjvm.so")) { return s; } } throw new IOException("libjvm.so not found"); } finally { reader.close(); } }
- Настал ключевой момент: откроем файл
libjvm.so
на чтение. При помощи нашего open-source ELF-парсера найдем в библиотеке символьную секцию, где среди символов присутствуют и все JVM флаги. Опять же, никаких хаков, только чистая Java.ElfReader elfReader = new ElfReader(jvmLibrary); ElfSymbolTable symtab = (ElfSymbolTable) elfReader.section(".symtab");
- Прибавим к адресу символа базовый адрес библиотеки, полученный в п.1, и получим искомое место в памяти, где хранится значения флага. Теперь его запросто поменяем через
Unsafe.putInt
:private ElfSymbol findSymbol(String name) { for (ElfSymbol symbol : symtab) { if (name.equals(symbol.name()) && symbol.type() == ElfSymbol.STT_OBJECT) { return symbol; } } throw new NoSuchElementException("Symbol not found: " + name); } public void setIntFlag(String name, int value) { ElfSymbol symbol = findSymbol(name); unsafe.putInt(baseAddress + symbol.value(), value); } public void setBooleanFlag(String name, boolean value) { setIntFlag(name, value ? 1 : 0); }
Заключение
Как видите, в Java без единой строчки нативного кода можно управлять runtime окружением, в том числе и самой виртуальной машиной. Однако помните, что использование недокументированных методов сопряжено с риском, и мы ни в коем случае не рекомендуем их к применению в production.
Полный исходный код эксперимента вы найдете на GitHub.
Если хотите узнать больше — приходите на конференцию по Java-технологиям Joker, которая состоится 15 октября в Санкт-Петербурге. От Одноклассников будет представлено три доклада, в том числе и по JVM.
Автор: apangin