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



    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 — весьма сговорчивая ОС, и охотно выложит нам все необходимые сведения, если правильно попросить.

    1. Сначала потребуется выяснить, где лежит библиотека виртуальной машины 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();
          }
      }
      

    2. Настал ключевой момент: откроем файл libjvm.so на чтение. При помощи нашего open-source ELF-парсера найдем в библиотеке символьную секцию, где среди символов присутствуют и все 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.

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

    Комментарии 15

      +19
      Хе-хе.
      Андрей, твои статьи нужно предварять дисклеймером:
      Известно, что C++ дает девелоперам полную свободу, по отстрелу ног. Но мы в Java ни чем не хуже — читайте свежую статью из цикла «1000 и 1 выстрел в ногу из Java.»
      :))))
        +4
        Ага. Зато есть, о чем потом на конференции рассказать :) Вот, на ближайшем Joker как раз доклад на тему, как разбирать последствия таких выстрелов :)
          +5
          То есть получается примерно так:
          «Дети, вот шкафчик с оружием. Код на замке: 42. А теперь я научу вас как по траектории разлета мозгов определить калибр.» ;)))))
        +5
        Как Java подхватывает такое «переключение» флага? Особенно если оно требует перераспределение памяти, дополнительную инициализацию, создание внутренних структур (флаги не просто так задаются при старте JVM).
          +1
          Разумеется, не все флаги получится менять на лету. Многие из них обрабатываются лишь один раз на старте JVM. В частности, все, что касается размеров Java Heap, выбора сборщика мусора и т.п. Эффект будет лишь от смены тех флагов, которые проверяются постоянно, например, различные опции трассировки. По-хорошему, все их надо сделать manageable, тогда и не придется плясать с бубном.
            +1
            Надо попробовать поменять флаг используемого компилятора (-server) в рантайм. Интересно посмотреть на «траекторию разлета мозгов»
              +1
              Не выйдет.
              -client / -server вообще не являются флагами JVM.
              До JDK 7 эти параметры разбирал launcher (иначе говоря, запускаемый бинарник) и выбирал соответствующую версию libjvm.

              Начиная с JDK 7 «серверная» библиотека libjvm включает в себя оба компилятора: C1 и C2. Есть возможность включить так называемую ступенчатую компиляцию ключиком -XX:+TieredCompilation. Но только из командной строки. Установка флага в runtime ни к чему не приведет, т.к. для поддержки ступенчатой компиляции требуется модифицированная версия интерпретатора байткода, которая генерируется лишь единожды при запуске JVM.
                0
                Да, я уже попробовал — сразу увидел что символа такого нет. Всё равно интересно получается и можно много чего сделать. Если интересно, код тут: github.com/voronaam/jflag
                  0
                  Попробовал менять «StackTraceInThrowable» в рантайме. Не получилось. А жаль, вот этим параметром как раз было бы отлично в райнтайме управлять.

                  Но то есть получилось, но при генерирование OutOfMemory exception прилетел SIGSEGV.

                  На этом я пока остановлюсь. Удалось сегфолт словить и достаточно.
                    +1
                    Проверил у себя — получилось успешно. На каком примере и на какой версии SIGSEGV валится?

                    С OutOfMemoryError и StackOverflowError есть особенность, что они преаллоцируются на старте JVM (иначе при попытке выполнения честного конструктора может не хватить памяти или стека). Поэтому, например, если запустили Java с -XX:-StackTraceInThrowable, а потом в рантайме выставили в true, то эти ошибки все равно вылетят без трейса.
                      0
                      Проверил сейчас: с Oracle JDK работает, на OpenJDK падает. Больше деталей тут.

                      На код примера я выше ссылку давал. Я не заметил что на вашем гитхабе уже лежит весь код, потому написал часть с нуля. Ну и поскольку я потратил на все эксперименты только один час код получился ужасный.

                      В детали сегфолта я не вдавался, но как мне показалось OpenJDK попыталась всё-таки раскрутить стэк эксепшена при печати его на экран. А сам throw — catch прошёл без нареканий.
            +3
            Опять же, никаких хаков, только чистая Java.

            Хорошая фраза в теле описания большущего хака. Повеселил.
            Забавно делать препарирование своего нативного окружения. Следующий шаг: «Чисто Java: Подмена нижележащей ОС (или хоста!) на лету.»

            Если не секрет, то, реально интересно, что послужило толчком к поиску именно такого, скажем прямо, нетрадиционного решения? Т.е. какой JVM флаг (не из набора manageable) нестерпимо захотелось менять во время выполнения программы без её рестарта?
              0
              Если не секрет, то, реально интересно, что послужило толчком к поиску именно такого, скажем прямо, нетрадиционного решения?

              Любознательность :) На самом деле, острой необходимости и не было. Пару раз возникало желание включить/выключить на лету TraceClassLoading, TraceClassUnloading и PrintCompilation. Или еще один раз проверить эффект от смены hashCode под нагрузкой. Но в итоге на production серверы все равно такой хак решили не внедрять.
                0
                А каким был эффект смены на лету hashCode под нагрузкой на тестовых?
                  +1
                  На сервере с бизнес-логикой — всего 1% прироста к производительности. На синтетических тестах на сериализацию — порядка 30%.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое