- PVSM.RU - https://www.pvsm.ru -

Копаемся в памяти JVM. Манипуляции с флагами

Копаемся в памяти JVM. Манипуляции с флагами

HotSpot JVM имеет множество опций для отслеживания происходящего в виртуальной машине: PrintGC, PrintCompilation, TraceClassLoading и т.п. Как правило, они включаются параметрами командной строки, например, -XX:+PrintGCDetails. Однако порой возникает необходимость включить или выключить такой флаг непосредственно во время работы приложения, когда перезапуск JVM с другими параметрами невозможен. Этого можно добиться как штатным, так и хакерским способом, причем последний и мощнее, и интереснее. Впрочем, внимания заслуживают оба.

Из данной статьи вы узнаете:

  • где найти все флаги JVM, и на какие типы они делятся;
  • как прочитать или установить флаг программно, используя JMX [1];
  • как найти нужную область в памяти виртуальной машины и испортить модифицировать ее.

Какими бывают флаги в HotSpot JVM

Список всех флагов с пояснениями доступен в исходниках OpenJDK: основная часть в globals.hpp [2] наряду с дополнительными опциями архитектуры [3], компилятора [4] и G1 коллектора [5].

Как видно, флаги определяются разными макросами:

  • 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 [6]. Более того, при включенном Remote Management [7] это можно делать даже на удаленном сервере.

Прежде всего, необходимо получить экземпляр 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 [8]. Остается найти адрес JVM-флага. Задача непростая, поскольку от запуска к запуску адрес будет меняться по воле операционной системы. К счастью, Linux — весьма сговорчивая ОС, и охотно выложит нам все необходимые сведения, если правильно попросить.

  1. Сначала потребуется выяснить, где лежит библиотека виртуальной машины libjvm.so, и по какому адресу она загружена. В этом поможет виртуальная файловая система proc [9], в частности, файл /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();
        }
    }
    

  2. Настал ключевой момент: откроем файл libjvm.so на чтение. При помощи нашего open-source ELF-парсера [10] найдем в библиотеке символьную секцию, где среди символов присутствуют и все JVM флаги. Опять же, никаких хаков, только чистая Java.
    ElfReader elfReader = new ElfReader(jvmLibrary);
    ElfSymbolTable symtab = (ElfSymbolTable) elfReader.section(".symtab");
    

  3. Прибавим к адресу символа базовый адрес библиотеки, полученный в п.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 [11].

Если хотите узнать больше — приходите на конференцию по Java-технологиям Joker [12], которая состоится 15 октября в Санкт-Петербурге. От Одноклассников будет представлено три доклада, в том числе и по JVM.

Автор: apangin

Источник [13]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/44418

Ссылки в тексте:

[1] JMX: http://docs.oracle.com/javase/tutorial/jmx/index.html

[2] globals.hpp: http://hg.openjdk.java.net/hsx/hotspot-main/hotspot/file/tip/src/share/vm/runtime/globals.hpp

[3] архитектуры: http://hg.openjdk.java.net/hsx/hotspot-main/hotspot/file/tip/src/cpu/x86/vm/globals_x86.hpp

[4] компилятора: http://hg.openjdk.java.net/hsx/hotspot-main/hotspot/file/tip/src/share/vm/opto/c2_globals.hpp

[5] G1 коллектора: http://hg.openjdk.java.net/hsx/hotspot-main/hotspot/file/tip/src/share/vm/gc_implementation/g1/g1_globals.hpp

[6] Management API: http://docs.oracle.com/javase/7/docs/api/java/lang/management/package-summary.html

[7] Remote Management: http://docs.oracle.com/javase/tutorial/jmx/remote/jconsole.html

[8] Unsafe API: http://mishadoff.github.io/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/

[9] proc: http://man7.org/linux/man-pages/man5/proc.5.html

[10] open-source ELF-парсера: https://github.com/odnoklassniki/one-elf

[11] на GitHub: https://github.com/odnoklassniki/one-elf/tree/master/test/one/jvm

[12] Joker: http://jokerconf.com

[13] Источник: http://habrahabr.ru/post/195004/