JEP draft: Java Thread Sanitizer
Thread Sanitizer для Java
| Автор | jcbeyler |
| Ответственный | Jean Christophe Beyler |
| Тип | Feature |
| Область | JDK |
| Статус | Draft |
| Выпуск | tbd |
| Компонент | hotspot / runtime |
| Создан | 2018/07/30 16:26 |
| Обновлён | 2025/08/14 15:38 |
| Задача | 8208520 |
Примечание: это описание ещё не закончено :)
Аннотация
Реализовать в OpenJDK динамический детектор гонок данных для кода на Java и нативного кода JNI на основе ThreadSanitizer.
Цели
Дать способ обнаруживать гонки данных в программах на Java и в связанном с ними нативном коде JNI:
- Обнаруживает гонки данных в одном выполнении программы полно (т. е. находит все гонки данных в этом выполнении) и точно (т. е. не сообщает о ложных гонках данных)
- Включается флагом командной строки
- Выводит сведения о гонках данных в файл или в stderr
- Выдаёт трассировки стека на моменты чтения и записи, образующих гонку данных, со сведениями о потоках
Что не является целью
- Обнаруживает все гонки данных в исходном коде программы
- Дать способ обнаруживать гонки данных в самой JVM (HotSpot)
- Создать детектор гонок данных с низкими накладными расходами, пригодный для постоянной работы в продакшене
Мотивация
Гонка данных делает программу на Java некорректно синхронизированной, что приводит к ошибочному, недетерминированному и неожиданному поведению, которое обычно проявляется лишь изредка. Поскольку в Java есть JNI для взаимодействия нативного кода с кодом на Java, а поведение программ на C/C++ с гонками данных не определено, гонки данных в нативном коде могут вызывать неопределённое поведение программ на Java и нарушать гарантии безопасности памяти и типов, которые должна давать Java. По мере того как программы становятся всё сложнее, гонки данных остаются одним из главных препятствий на пути к надёжности, безопасности и отказоустойчивости программ на Java, но обнаружить, воспроизвести, исправить или устранить все гонки данных крайне сложно.
Поэтому важно, чтобы в OpenJDK был стабильный и поддерживаемый инструмент обнаружения гонок данных как для кода на Java, так и для нативного кода JNI. Такой инструмент — важный шаг вперёд: он поможет разработчикам отлаживать и исправлять гонки данных в больших и малых программах, уменьшая их число и повышая надёжность. В идеале инструмент должен быть динамически полным, чтобы обнаруживать во время выполнения все проявившиеся гонки данных, и точным, чтобы приносить пользу разработчикам, ведь разобраться в гонке данных и так непросто.
Вместо того чтобы полагаться на исследовательские инструменты, инструментирующие байт-код Java (например, FastTrack2 в RoadRunner), реализация полностью встраивает ThreadSanitizer (TSan) в OpenJDK. Так инструмент может обнаруживать гонки данных и в коде на Java, и в нативном коде JNI.
Описание TSAN для случая LLVM
TSan использует анализ отношения happens-before, чтобы динамически, полно и точно обнаруживать гонки данных в программах на C/C++. Реализация устроена так: компилятор LLVM встраивает в пользовательскую программу обратные вызовы на интересующие события, например захват и освобождение мьютекса и чтение и запись памяти. Среда выполнения TSan компонуется с исполняемым файлом. Когда поток выполняет обратный вызов TSan, среда выполнения TSan обновляет метаданные (для эффективности они хранятся по фиксированному смещению от адреса мьютекса или переменной) и проверяет их, чтобы определить, есть ли гонка данных. Если есть, в stderr выводится отчёт. Проделана значительная работа, чтобы обеспечить и низкие накладные расходы на производительность (обычно около ~4x), и практически нулевую долю ложных срабатываний: если TSan выдаёт отчёт о гонке данных, когда гонка не проявилась, это считается ошибкой в TSan.
В случае Java конечная цель TSan/Java — дать всестороннее обнаружение гонок данных в пользовательском коде на стыке языков. Реализация TSan/Java охватывает три основных направления: (1) добавление обратных вызовов TSan в JVM, (2) встраивание среды выполнения TSan так, чтобы она работала рядом с JVM, и (3) предоставление символизации, когда TSan выдаёт предупреждение.
Обратные вызовы TSan в JVM
Чтобы обнаруживать гонки данных в коде на Java, среда выполнения TSan должна отслеживать всю синхронизацию Java и все обращения к полям Java. Есть три источника синхронизации на уровне Java (мониторы на уровне языка, volatile-обращения, библиотеки java.util.concurrent) и два источника синхронизации, экспортируемой VM (raw-мониторы и raw-мониторы JVMTI). Обращения к полям Java включают чтение и запись скалярных и статических полей и массивов, а также обращения к полям через рефлексию. Реализация TSan/Java должна вставлять нужные обратные вызовы в среду выполнения TSan во всех перечисленных источниках синхронизации Java и обращений к полям.
Поскольку TSan хранит метаданные по смещениям от указателей, TSan должен точно знать, где в куче размещены объекты Java. TSan не может знать жизненный цикл объектов Java, поэтому инструментирование JVM должно сообщать TSan, когда объект Java перемещён или собран. В TSan есть обратный вызов для обработки перемещённого объекта, и реализация TSan/Java вставляет его в подходящих местах, например в сборщиках мусора.
Работа среды выполнения TSan в процессе JVM
Среда выполнения TSan была спроектирована для поддержания корректного состояния памяти и синхронизации в программе на C++. Поскольку JVM сама является сложной программой на C++, для мирного сосуществования им нужна тщательная координация. Главная проблема — активный перехват функций libc в TSan; TSan/Java обходит её так: TSan анализирует стек и хранит для каждого потока логический флаг «вызвано из JVM». Большинство функций libc при вызове из JVM перенаправляются в исходную реализацию libc. Заметное исключение — функции выделения памяти: они перенаправляются в собственный аллокатор TSan.
Символизация для отчётов TSan
Чтобы выдавать пользователям полезные трассировки стека, TSan поддерживает для каждого потока теневой стек, в котором сохраняются значения счётчика команд. Для TSan/Java передавать сырой «счётчик команд» бесполезно: символизатор C++ не может его декодировать, а блок кода позже может быть собран. Вместо этого мы используем механизм GTrace, чтобы передавать в среду выполнения TSan токены идентификаторов методов GTrace. Токены сохраняются, а позже TSan должен их декодировать. JVM предоставляет публичную функцию C++ (TsanJavaSymbol), через которую поток C++ или Java может обратиться к JVM и получить строку метода, связанную с токеном.
Пример использования
Разработчик конфигурирует и собирает JDK с флагом конфигурации --use-tsan. Затем при запуске java разработчик указывает -XX:+TSan, чтобы включить отслеживание TSan. Если обнаружена гонка, при завершении программы TSan/Java выдаст следующий отчёт:
WARNING: ThreadSanitizer: data race (pid=7616)
Read of size 4 at 0x7fd80270b328 by thread T26:
#0 C.Get()I (C.java:7)
#1 A.Get()I (A.java:6)
#2 (Generated Stub)
#3 GetValue (Native.cc:18) (Test_native+0x10e330f)
#4 Java_N_GetNative (Native.cc:23) (Test_native+0x10e330f)
...
Previous write of size 4 at 0x7fd80270b328 by thread T5:
#0 C.Set(I)V (C.java:4)
#1 A.Set(I)V (A.java:3)
#2 (Generated Stub)
#3 SetValue (Native.cc:7) (Test_native+0x10e31d8)
#4 Java_N_SetNative (Native.cc:12) (Test_native+0x10e31d8)
...
SUMMARY: ThreadSanitizer: data race in C.Get()I (C.java:7)
Флаг конфигурации и новый флаг
Должен быть флаг конфигурации --use-tsan для компиляции и компоновки со специфичными для TSan настройками. Также должен быть флаг JVM -XX:+TSan для включения TSan во время выполнения.
Прочие требования:
- Только 64-битные платформы (TSan поддерживает только 64-битные)
- Код JNI нужно компилировать компилятором LLVM с поддержкой TSan
Детали реализации
Текущие детали реализации:
- Реализация сделана только для интерпретатора, другие уровни многоуровневой компиляции и среды выполнения не поддерживаются
- Система обращается к среде выполнения LLVM Tsan, и среда выполнения Tsan сообщает пользователю о гонках; повторно реализовывать библиотеку Tsan внутри OpenJDK сейчас не планируется
Альтернативы
FastTrack и FastTrack2 — передовые алгоритмы динамического обнаружения гонок данных, одновременно полные и точные. Их прототипы реализованы во фреймворке RoadRunner с помощью инструментирования байт-кода. Поэтому они могут обнаруживать гонки данных в программах на Java, но не в нативном коде JNI.
Алгоритмы статического обнаружения гонок данных могут найти все гонки данных в исходном коде программы на Java, но обычно они также сообщают о большом числе ложных гонок. Кроме того, статическим алгоритмам трудно масштабироваться на большие объёмы исходного кода на Java и учитывать динамическую загрузку классов, рефлексию, код JNI и синхронизацию через volatile-поля и библиотеки java.util.concurrent.
Динамические детекторы гонок данных на основе алгоритма lockset или его вариантов могут обнаружить в исходном коде больше настоящих гонок данных, но не умеют учитывать все виды синхронизации, например обращения к volatile-полям и библиотеки java.util.concurrent. Поэтому они могут сообщать о ложных гонках данных.
Тестирование
С функциональной точки зрения, чтобы убедиться, что TSan не сломан, можно запускать существующие тесты JTREG с новым флагом. Кроме того, будет добавлено немного кода с состояниями гонки, чтобы явно тестировать состояния гонки и показать работоспособность и полезность системы TSan.
Например, должны проходить следующие модульные тесты:
- Симметричная гонка данных в двух потоках.
- Один поток обращается к данным без защиты, другой защищает своё обращение
- Оба потока корректно защищают свои обращения
- Использование кучи и глобальных статических переменных
- Использование synchronized-методов и synchronized-блоков
- Выход из synchronized-блока при обычном потоке управления, по исключению и по прерыванию потока
- Wait/notify и wait/interrupt
- Рекурсивные мониторы
- Синхронизация через volatile
- Синхронизация через java.util.concurrent
Риски и допущения
Когда функция выключена, потерь производительности и рисков нет. Пользователь, который не включает систему, не заметит разницы в производительности.
Однако ожидается, что при включении TSan накладные расходы будут большими. Это сложная система, которая добавляет логику почти к каждому чтению и записи памяти; предполагается, что TSan не будет включаться в продакшен-задачах.