JEP draft: Low-level Object layout introspection methods
Низкоуровневые методы интроспекции размещения объектов
| Ответственный | Aleksey Shipilev |
| Тип | Feature |
| Область | JDK |
| Статус | Draft |
| Компонент | core-libs / java.lang |
| Трудоёмкость | S |
| Длительность | S |
| Создан | 2020/07/10 17:23 |
| Обновлён | 2023/01/05 15:21 |
| Задача | 8249196 |
Summary
Предоставить методы для получения зависящих от реализации размеров объектов, смещений полей и адресов объектов, чтобы сделать возможной низкоуровневую интроспекцию поведения JVM.
Goals
Расширить общедоступные API интроспекции, чтобы уменьшить зависимость от Unsafe при интроспекции JVM только для чтения. Успешная реализация должна предоставить достаточно API, чтобы инструменты интроспекции могли полностью отказаться от Unsafe, не жертвуя при этом ни безопасностью, ни надёжностью. Амбициозная цель для успешной реализации — обеспечить более высокую надёжность и производительность по сравнению с аналогичными методами Unsafe и тем самым дать железный довод в пользу перехода.
Non-Goals
Цель не в том, чтобы предоставить чрезмерно глубокие API интроспекции JVM. Цель не в том, чтобы предоставить API, которыми можно злоупотребить, чтобы заставить JVM вести себя несовместимым образом. Цель не в том, чтобы изменить API Unsafe под новые методы.
Motivation
Хотя реализации Java намеренно обходят зависящие от реализации вопросы о размерах объектов, размещении объектов и раскладке полей, эти сведения всё же полезны при низкоуровневой работе над производительностью. Поэтому в экосистеме есть несколько библиотек, предоставляющих такого рода интроспекцию, и много других внутренних утилит, которые дают те же сведения.
Вот некоторые из них:
-
JOL (из OpenJDK Code Tools)
-
JAMM (активно используется в Apache Cassandra)
Все эти инструменты используют sun.misc.Unsafe для получения низкоуровневой информации о среде выполнения — как основной механизм или как запасной. Возможностей использовать Unsafe в современных JDK становится всё меньше, из-за чего поддержка этих библиотек превращается в постоянную головную боль с неясным будущим. Хотя используется Unsafe, данные, которые получают эти инструменты, обычно безопасны. Общий API для получения такого рода данных устраняет эту часть зависимости от Unsafe.
Есть несколько распространённых сценариев, в которых эти API полезны:
-
Размеры объектов. Знание точного размера объекта помогает эффективно подбирать размеры структур данных; основной пример — кэши в памяти. Например, JAMM регулярно используется вместе с Caffeine для взвешивания записей кэша (1, 2). Кроме того, это часто нужно для экспериментов с производительностью, требующих точного объёма занимаемой памяти. Поскольку размер объекта сильно зависит от реализации и от параметров развёртывания (разрядности JVM, заданного размера кучи, от которого зависит выбор режима compressed oops, ограничений на выравнивание объектов и т. д.), самый надёжный способ его узнать — измерение во время выполнения.
-
Раскладка полей. Знание точного расположения полей важно для низкоуровневой работы над производительностью. Один из основных сценариев — планирование с учётом доступных промежутков выравнивания и промежутков суперкласса, чтобы разместить в них новые поля без увеличения размера экземпляра. Вспомогательный сценарий — избегание ложного разделения (false sharing) за счёт группировки полей с помощью различных приёмов, зависящих от среды выполнения, с последующей проверкой того, что это работает во время выполнения, путём изучения смещений полей.
-
Информация об адресах. Знание точного адреса объекта тоже полезно для низкоуровневой работы над производительностью. Изучение того, как весь граф объектов размещается в памяти (различными сборщиками мусора), помогает согласовать шаблоны доступа в низкоуровневом коде или сообщать об ошибках размещения в GC. Объяснение поведения производительности через анализ относительных расстояний, которые проходят обращения к памяти, полезно для низкоуровневого бенчмаркинга. Проверка того, занимают ли объекты несколько кэш-линий или делят их с другими объектами, полезна для разбора аномалий производительности. Поиск общих частей двух графов объектов помогает понять, работает ли кэширование; в этом случае адрес объекта — удобный идентификатор объекта.
Существующие API
Для перечисленных выше целей уже существуют некоторые API.
Альтернативы для размеров объектов:
-
long Instrumentation.getObjectSize(Object)даёт оценку объёма памяти, необходимого для данного объекта. Для этого библиотеку нужно явно подключить как Java Agent. Многие библиотеки в итоге используют механизмы самоподключения, а потом обнаруживают, что в более поздних JDK для этого требуется явный параметр JDK. Для более удобного подключения агентов можно использовать атрибут манифеста Launcher-Agent-Class. -
JVMTI
long GetObjectSize(jobject)даёт такую же оценку объёма памяти. Опять же, библиотеку нужно подключить как Java Agent. -
Обходной путь: выделить много объектов одного типа и с помощью
ThreadMXBean::getThreadAllocatedBytesизмерить общий объём выделенной памяти, а значит, и средний размер объекта. Этот подход требует выделений памяти, которые могут быть непрактичны для и без того больших объектов; создание новых экземпляров того же типа может быть запрещено и т. д. -
Обходной путь: то же, что выше, но с помощью
Runtime.getFreeMemory. Помимо перечисленных выше проблем, этот способ учитывает ещё и все выделения памяти во всех остальных потоках. -
Не альтернатива: дамп кучи. Мало того что это непрактично для измерений в реальном времени, сам дамп кучи не содержит надёжной информации о размере экземпляров.
Альтернативы для смещений полей:
Unsafe.objectFieldOffsetиUnsafe.staticFieldOffset. Оба требуют повышенных привилегий для доступа кUnsafe. Кроме того, они возвращают не настоящие смещения, а cookie, который в текущей реализации выглядит как смещение. Поэтому за пределами использования сUnsafeрезультату в общем случае доверять нельзя. В довершение всегоUnsafeможет выбрасывать ошибки при попытке доступа к дополнительно защищённым классам вродеRecords.
Альтернативы для информации об адресах:
- Обходной путь: с помощью
Unsafeзаписать ссылку на объект в массивlong[]и прочитать её обратно какlong/int, в зависимости от конфигурации VM. Это довольно неуклюже, но на удивление безопасно с точки зрения общей корректности VM. Однако при неверном предположении о конфигурации VM этот способ может возвращать странные значения.
Description
Для целей обсуждения предположим, что методы помещаются в класс Runtime. Проверочную реализацию (proof of concept) можно найти в ветке «JDK-8249196-low-level-object» в репозитории jdk-sandbox:
$ git clone https://github.com/openjdk/jdk-sandbox
$ cd jdk-sandbox
$ git checkout --track origin/JDK-8249196-low-level-object
$ sh ./configure
$ make images
Разницу между базовой и изменённой средой выполнения можно увидеть так:
$ git diff master..JDK-8249196-low-level-object
Сравнение для этой ветки можно найти здесь, а ночные сборки этой ветки — здесь. Проще всего получить представление об API, используя jshell из ночных сборок:
$ curl https://builds.shipilev.net/openjdk-jdk-jep-8249196/openjdk-jdk-jep-8249196-latest-linux-x86_64-release.tar.xz | tar xJf -
$ jdk/bin/jshell
| Welcome to JShell -- Version 16-testing
| For an introduction type: /help intro
jshell> Runtime.sizeOf(new Object());
$1 ==> 16
Прототип используется, чтобы исследовать варианты API и получить базовые характеристики производительности для возможных реализаций.
Далее в этом разделе приводятся неформальная спецификация API, рекомендации по реализации и производительности, а также обсуждение безопасности.
Предлагаемые методы
sizeOf
Сначала метод для размера объекта:
/**
* Returns the runtime estimate of storage taken by a given object.
* ...
*/
public static long sizeOf(Object obj) { ... }
Этот метод возвращает размер экземпляра в байтах, примерно так же, как это сделал бы sizeof() в стиле C. Обратите внимание, что этот метод даёт лишь «поверхностный» sizeOf, и его результат не включает размеры объектов, на которые ссылается данный объект. Реализации вправе возвращать специальное значение (будет определено позже, например -1), если оценка размера недоступна или JVM отказывается её предоставить.
(Вопрос из разряда споров о мелочах: не следует ли назвать его shallowSizeOf?)
Пример, повторяющий представление «internals» в JOL, которое реализовано с помощью Unsafe:
$ java -jar jol-cli.jar internals java.util.ArrayList
java.util.ArrayList object internals:
OFFSET SIZE TYPE DESCRIPTION
0 4 (object header)
4 4 (object header)
8 4 (object header)
12 4 int AbstractList.modCount
16 4 int ArrayList.size
20 4 java.lang.Object[] ArrayList.elementData
Instance size: 24 bytes
$ jshell
jshell> Runtime.sizeOf(new ArrayList<>())
$1 ==> 24
Базовая реализация этого метода переходит в нативный код VM и выполняет obj->size(). Метод size() декодирует так называемый «layout helper» класса, который содержит полную информацию о размере для экземпляров и часть информации о размере для массивов (другая часть — сама длина массива). Основная часть затрат приходится на переход JNI в нативный метод JDK. Эта реализация занимает ~25 нс на вызов на современном настольном компьютере.
Интринсики C1 и C2 можно использовать, чтобы встроить декодирование layout helper и сэкономить затраты на переход JNI. Реализованные на данный момент интринсики C2 снижают затраты примерно до ~4 нс на вызов.
Самый большой объект, который можно выделить, — new long[Integer.MAX_VALUE-epsilon]; он занимает около 32 ГБ, поэтому для sizeOf требуется результат типа long. Это соответствует тому, что делает Instrumentation.getObjectSize().
deepSizeOf
Возникает соблазн предоставить метод deepSizeOf, который давал бы размер всего достижимого подграфа, начиная с данного объекта:
Пример, повторяющий GraphLayout из JOL, который делает это с помощью рефлексии и Unsafe:
$ jshell
jshell> Object o = List.of("1", "2", "3");
o ==> [1, 2, 3]
jshell> GraphLayout.parseInstance(o).toFootprint()
$2 ==> "java.util.ImmutableCollections$ListN@254989ffd footprint:
COUNT AVG SUM DESCRIPTION
3 24 72 [B
1 32 32 [Ljava.lang.Object;
3 24 72 java.lang.String
1 16 16 java.util.ImmutableCollections$ListN
8 192 (total)"
jshell> Runtime.deepSizeOf(o)
$1 ==> 192
При этом возникает ряд проблем реализации:
-
Подграф объектов может изменяться во время обхода. Эту проблему можно обойти оговоркой в спецификации о том, что при изменениях стабильность не гарантируется. Если это неприемлемо, JVM теоретически может перейти в режим stop-the-world на время обхода (как это делается при обходе кучи через JVMTI). Хотя для большинства подграфов обход занял бы довольно мало времени, потенциально неограниченное время, проведённое в таком safepoint, делает этот вариант непрактичным.
-
Поиск ссылочных полей в каждом классе требует определённой инженерной работы. На стороне VM это обычно сделать легко, поскольку там уже есть oop maps для работы GC. Сложнее сделать это на стороне JDK, где стандартным способом была бы рефлексия с сопутствующими накладными расходами.
-
Хранение фронта обхода потребовало бы поддерживать какую-либо структуру данных (битовую карту, хэш-множество и т. д.) для учёта уже посещённых объектов. На стороне Java это сделать легко (с помощью
IdentityHashMap), а на стороне VM возможно (с помощью нативных хэш-таблиц). Оба варианта повлекли бы неограниченные накладные расходы памяти на неограниченных подграфах объектов. Если хранить фронт на стороне VM, его структура данных на время обхода становится корнем GC, что может пагубно сказаться на паузах GC. -
Нужно продумать взаимодействие со сборкой мусора. Если всё делается на стороне VM, опросы safepoint пришлось бы вставлять вручную, иначе обход мог бы задерживать общий ход GC. Проверять GC, возможно, стоит в любом случае, потому что обходу может понадобиться учитывать перемещённые объекты, если он записывает адреса объектов, а не их Identity (идентичность объекта).
-
Есть особые классы JDK, которые создают множество взаимозависимых связей. Например,
java.lang.ref.Referenceсодержит ссылки на связанную с ним очередь ссылок и на «следующую» ссылку. ПоэтомуdeepSizeOf, наткнувшись на слабую ссылку, может обойти множество таких ссылок. Это можно смягчить фильтрацией, и это, вероятно, стоит делать, еслиdeepSizeOfреализован как метод JDK, а не как сторонняя (3rd party) библиотека. Нечто подобное может относиться кClass,ClassLoader,Threadи т. д.
Кроме того, есть возможность добавить в deepSizeOf функцию обратного вызова (callback), которая позволила бы пользователям отфильтровывать часть обходимых объектов. С этим связана серьёзная проблема: такой callback видел бы каждый объект, обойдённый в графе объектов, включая те, которые обычно достижимы только через поля private. Это можно отчасти смягчить, потребовав особых привилегий безопасности, но инкапсуляция всё равно серьёзно нарушается.
Текущий прототип выполняет большую часть работы на стороне JDK. Он хранит фронт обхода в IdentityHashSet, записывает текущую очередь объектов с помощью ArrayDeque и использует sizeOf (реализованный с помощью интринсиков, см. предыдущий раздел) для взвешивания объектов.
VM вызывается через нативный метод JDK, чтобы извлечь ссылки из данного объекта. Опять же, значительную часть затрат здесь составляет переход JNI. Сам метод вызывает API VM, которые глубоко затрагивают внутренности VM, поэтому сделать интринсиком весь метод «извлечения» может оказаться проблематично. Однако, как показывает текущий прототип, известный нативный метод JDK можно вызывать напрямую из кода, сгенерированного C1/C2.
Преимущество выполнения всей работы на стороне JDK — точность и скорость. Это точно, потому что JDK/JVM может обращаться ко всем полям (даже закрытым), и проверки доступа этому не мешают. Это быстро, потому что можно творить магию, недоступную сторонним (3rd party) библиотекам.
Если взять для примера взвешивание связного списка из 1K узлов, текущий прототип deepSizeOf на современном настольном компьютере тратит около 40 мкс и 30 КБ памяти кучи. Ближайший конкурент, JOL, делает то же самое примерно за 90 мкс и 300 КБ памяти кучи.
fieldOffsetOf
Метод для раскладки полей выглядит примерно так:
/**
* Returns the offset of the field within the object.
* ...
*/
public static long fieldOffsetOf(Field field) { ... }
Этот метод возвращает смещение поля в байтах внутри объявляющего его класса. Опять же, реализация может вернуть специальное значение «неизвестно», если смещение недоступно или JVM отказывается его предоставить.
Пример, повторяющий представление «internals» в JOL, которое реализовано с помощью Unsafe:
$ java -jar jol-cli.jar internals java.util.ArrayList
java.util.ArrayList object internals:
OFFSET SIZE TYPE DESCRIPTION
0 4 (object header)
4 4 (object header)
8 4 (object header)
12 4 int AbstractList.modCount
16 4 int ArrayList.size
20 4 java.lang.Object[] ArrayList.elementData
Instance size: 24 bytes
$ jshell
jshell> Runtime.fieldOffsetOf(ArrayList.class.getDeclaredField("size"))
$1 ==> 16
Неудобный момент: Field описывает и статические поля, что даёт любопытные смещения:
jshell> Runtime.fieldOffsetOf(Integer.class.getDeclaredField("MIN_VALUE"))
$3 ==> 144
Это смещение внутри зеркала java.lang.Class. Это можно принять с оговоркой в спецификации, что смещение отсчитывается от начала содержащего контейнера, а не обязательно от самого объекта.
С точки зрения реализации этот метод обращается к нативному коду VM, который определяет смещение поля. Эти данные в VM доступны всегда, потому что они нужны для связывания доступа к полям и генерации эффективного кода. Этот метод работает с рефлексивным Field, и результат можно использовать для всех экземпляров данного класса (если не происходит переопределение класса; см. обсуждение ниже). Поэтому этот метод не обязательно делать интринсиком.
fieldSizeOf
Метод для размера поля выглядит примерно так:
/**
* Returns the size of the field within the object.
* ...
*/
public static long fieldSizeOf(Field field) { ... }
Этот метод возвращает размер, который поле занимает в данном объекте. И снова реализация может вернуть специальное значение «неизвестно».
Пример, повторяющий представление «internals» в JOL, которое реализовано с помощью Unsafe:
$ java -jar jol-cli.jar internals java.util.ArrayList
java.util.ArrayList object internals:
OFFSET SIZE TYPE DESCRIPTION
0 4 (object header)
4 4 (object header)
8 4 (object header)
12 4 int AbstractList.modCount
16 4 int ArrayList.size
20 4 java.lang.Object[] ArrayList.elementData
Instance size: 24 bytes
$ jshell
jshell> Runtime.fieldSizeOf(ArrayList.class.getDeclaredField("elementData"))
$1 ==> 4
В текущих JVM возвращаемые значения довольно скучны: размеры полей примитивных типов почти всегда одинаковы во всех выпусках JDK, а размеры ссылочных полей зависят от разрядности JVM и режима сжатых ссылок. Всё становится гораздо интереснее с inline-классами, для которых рефлексия отвечает, что объявленное поле — это объектная проекция inline-типа. Этот метод позволяет JVM сообщить полный размер класса, содержащего inline-тип, включая все поля, к которым применён Flattening (плоское размещение). Подробнее см. в следующих разделах.
fieldOffsetOf и fieldSizeOf должно быть достаточно, чтобы понять, где в объектах находятся поля, и методом исключения — где находятся промежутки между полями.
addressOf
Метод определения адреса:
/**
* Returns the current memory address taken by a given object.
* ...
*/
public static long addressOf(Object obj) { ... }
Реализация всегда работает с несжатыми ссылками, поэтому результат не зависит от режима JVM.
Пример, повторяющий GraphLayout из JOL, который использует Unsafe для получения того же адреса:
$ jshell
jshell> Object o = new ArrayList<>()
o ==> []
jshell> GraphLayout.parseInstance(o).toPrintable()
$1 ==> "java.util.ArrayList@5910e440d object externals:
ADDRESS SIZE TYPE PATH VALUE
ff016ec8 16 [Ljava.lang.Object; .elementData []
ff016ed8 1113008 (something else) (somewhere else) (something else)
ff126a88 24 java.util.ArrayList (object)"
jshell> Long.toHexString(Runtime.addressOf(o))
$2 ==> "19c47dc08"
Этот метод может показаться небезопасным, поскольку возвращает что-то вроде указателя, но система типов Java ничего не знает об указателях, поэтому никакие операции, подобные операциям с указателями, с ним невозможны. С точки зрения программы на Java это просто примитивное значение long, и получить доступ к стоящему за ним объекту никак нельзя. Поэтому оно имеет лишь диагностическую ценность. Мы знаем, что это полезно, потому что представление «externals» в JOL, показывающее адреса объектов, используется на практике для диагностики производительности.
Если метод возвращает настоящий адрес объекта, то это значение можно использовать, передав его в уже небезопасные API:
-
Unsafe: пользователи могут передать адрес в операции Unsafe с «сырой» памятью. Но еслиUnsafeуже в руках пользователя, он и так может определить настоящий адрес объекта и работать с ним без каких-либо проверок. ПоэтомуaddressOfздесь не создаёт дополнительных проблем. Подробнее о взаимодействии сUnsafeсм. ниже. -
JNI: пользователи могут передать адрес в нативный код через JNI и таким образом получить доступ к внутреннему устройству объекта. Опять же, как только сделан вызов потенциально небезопасного нативного кода, есть гарантии, что он не будет копаться в «сырой» памяти. Например, можно разыменовать сам JNI-дескриптор
jobjectи получить тот же адрес объекта без каких-либо новых API (по сути, примерно это и делает текущая нативная реализацияaddressOf). ПоэтомуaddressOf, по-видимому, не создаёт здесь новой проблемы. -
Есть взаимодействие с Panama, который предоставляет ещё один способ доступа к внешней памяти, см. разделы ниже.
Такое злоупотребление можно дополнительно ограничить, смешивая значение со случайным cookie, своим для каждой JVM. Тогда сравнение адресов относительно друг друга останется полезным, а настоящий адрес не будет раскрыт для случайного или намеренного злоупотребления. Текущий прототип так и делает, поэтому результат addressOf отличается от результата, который возвращает JOL (см. пример выше).
Этот метод легко сделать интринсиком, поскольку он лишь выполняет «приведение» ссылки Java к «сырому» long и смешивает результат с cookie. Текущий прототип показывает характеристики производительности, аналогичные sizeOf: ~25 нс с вызовом JNI, ~1 нс с интринсиками C1/C2.
Взаимодействие с другими возможностями JVM/JDK
@Contended
Известно, что @Contended меняет стратегии раскладки полей, вставляя большие промежутки вокруг защищаемых блоков полей, что влияет на размеры экземпляров и смещения полей.
sizeOf возвращает настоящий размер экземпляра в том виде, в каком его видит JVM, включая все промежутки от @Contended, без какой-либо специальной обработки или изменений кода.
fieldOffsetOf и fieldSizeOf возвращают настоящие смещение/размер поля в том виде, в каком их видит JVM. Смещение обычно зависит от @Contended, а размер не зависит. Никакой специальной обработки не требуется.
addressOf не взаимодействует с @Contended.
Размещение на стеке
HotSpot пока не выполняет размещение объектов Java на стеке. Но будущее взаимодействие с ним выглядит безопасным.
sizeOf возвращает размер хранилища, не делая предположений о том, где это хранилище выделено. offsetOf возвращает смещение от начала объекта, которое одинаково как при размещении на стеке, так и в куче.
addressOf возвращает значение, которое не обязательно должно быть адресом в куче Java, а может быть и указателем внутрь стека потока; оба варианта безопасны, поскольку приводятся к «long».
fieldOffsetOf и fieldSizeOf не взаимодействуют с размещением на стеке.
Скалярная замена
HotSpot выполняет скалярную замену, то есть разбирает неубегающий объект на поля, а затем размещает их как обычные операнды. Это ставит интересный вопрос. Какой из этих путей выбрать?
-
Более простой вариант: вызов
sizeOf/addressOfпрекращает скалярную замену, и поэтому их семантика ясна. Именно этот путь выбран в текущем прототипе: встраивание прерывается с помощью@DontInline. -
Более сложный вариант:
sizeOf/addressOfотвечают «неизвестно», когда происходит скалярная замена. Для этого нужны дополнительные хуки в текущем C2. Хотя это усложняет реализацию, это может оказаться скрытым благом, поскольку эти методы могут служить слабыми датчиками того, что происходит скалярная замена. -
Безумный вариант: заставить
sizeOfотслеживать реально скаляризованные поля и измерять занимаемое ими хранилище. Для этого не только нужен очень сложный хук в механизм C2, но и возникают трудные вопросы вроде: «Следует ли считать, что поле, находящееся только в регистре, занимает хранилище?», «Следует ли учитывать хранилище поля после его последнего использования, когда это хранилище технически может быть переиспользовано?»
fieldOffsetOf и fieldSizeOf не взаимодействуют со скалярной заменой.
Размещение вне кучи
Размещение вне кучи по-прежнему подразумевает объекты, не являющиеся объектами Java (DirectByteBuffers находятся в куче, но их нативное содержимое — нет), поэтому к нему не применимы ни sizeOf, ни addressOf.
Даже если представить будущее, в котором объекты Java окажутся вне обычной кучи, новые API сами по себе ничего не делают. Они лишь запрашивают информацию у среды выполнения. Поэтому если среда выполнения допускает объекты Java вне кучи, она должна предоставлять на стороне VM средства доступа к их размерам и смещениям, и тогда новые API должны заработать как по волшебству.
Inline-типы
Появление inline-типов из Valhalla создаст проблемы с этими API, так же как они создают проблемы с Unsafe. К полям inline-типов может применяться Flattening в объекте-владельце, при этом они не отражаются как объявленные поля владельца.
sizeOf не создаёт проблем: он возвращает размер объекта в том виде, в каком его видит JVM, включая поля, к которым применён Flattening.
fieldOffsetOf не создаёт проблем, за исключением того, что мы не можем получить через рефлексию поле, к которому применён Flattening, и поэтому оно будет непрозрачным для инструментов интроспекции.
fieldSizeOf как раз обещает частично закрыть этот пробел. Посмотрите, например, что JOL выдаёт на текущем прототипе Valhalla:
public class Holder {
int myF;
Line line;
}
inline class Line {
Point p1, p2;
Line() { p1 = new Point(); p2 = new Point(); }
}
inline class Point {
int x, y, z;
Point() { x = 0; y = 0; z = 0; }
}
$ java -jar jol-cli.jar internals Holder -cp .
Holder object internals:
OFFSET SIZE TYPE DESCRIPTION
0 4 (object header)
4 4 (object header)
8 4 (object header)
12 4 Line Holder.line
16 20 (alignment/padding gap)
36 4 int Holder.myF
Instance size: 40 bytes
Space losses: 20 bytes internal + 0 bytes external = 20 bytes total
Поскольку рефлексия не видит тип, к которому применён Flattening (пока?), инструмент интроспекции считает, что ссылка на Line — это обычная ссылка Java размером 4 байта. Оставшиеся 20 байт считаются промежутком для выравнивания/заполнения. Но на самом деле их занимают поля Line и Point, к которым применён Flattening. В этом случае fieldOffsetOf может вернуть полный размер Line с учётом Flattening (24 байта), и инструменты узнают, что к нему применён Flattening.
addressOf обычным образом взаимодействует с inline-типами: попытка вызвать его на «ссылке» value-типа может в итоге привести к вызову на ссылочной проекции объекта из Value Objects (объекты-значения).
Unsafe
sizeOf не взаимодействует с Unsafe.
fieldOffsetOf выглядит как дублирование Unsafe.staticFieldOffset и Unsafe.objectFieldOffset с несколькими улучшениями. Во-первых, он доступен без привилегий безопасности, которые требуются для Unsafe. Во-вторых, он возвращает настоящее смещение, в отличие от Unsafe, который в текущей реализации возвращает cookie, похожий на смещение. Из-за этого смещение, полученное от offsetOf, может быть несовместимо с методами доступа Unsafe, такими как Unsafe::putInt(Object, int, int), которые ожидают cookie, выданный Unsafe.*FieldOffset, а не «голое» смещение. Это вполне укладывается в контракт Unsafe.
У addressOf нет прямых аналогов в Unsafe, но его можно эмулировать с его помощью. Есть методы Unsafe, с помощью которых можно писать в «сырую» память. Поскольку addressOf обычно возвращает некоторый адрес в куче Java и нет гарантии, что позже этот адрес всё ещё будет указывать на действительный объект, попытка записи с помощью Unsafe по адресу из addressOf может повредить кучу Java. Этот риск аналогичен тому, который возникает при наличии доступа к Unsafe в принципе.
Сборщики мусора
В конечном счёте за управление памятью кучи Java отвечают сборщики мусора. Это значит, что методы, которые исследуют объекты Java, не привлекая сборщик мусора напрямую, должны согласовывать свою работу с GC или гарантировать отсутствие негативных последствий.
sizeOf и offsetOf не взаимодействуют с GC напрямую. По крайней мере в HotSpot размер объекта доступен через klass word и связанный с ним layout helper, а смещения полей можно узнать, опросив класс и его карты полей. Это значит, что пока в эти методы передаётся корректная ссылка на объект Java и/или Field, никакой дополнительной обработки не требуется. Поскольку оба метода — это методы Java, ссылка на переданный объект всегда корректна: это обеспечивают обычные протоколы взаимодействия GC и мутатора.
addressOf не взаимодействует с GC напрямую, но возвращаемый им адрес может устареть очень рано, ещё до возврата из метода: например, когда STW GC срабатывает на опросе safepoint перед выходом из метода. Для корректности это не проблема, потому что пользователи ничего не могут сделать с адресом long, что позволило бы обращаться с ним как с указателем на объект.
Пытаться смягчить эту проблему, по-видимому, не имеет смысла, потому что: а) адрес может сделать недействительным любая другая возможность для STW GC (технически это возможно только в safepoint); б) конкурентный GC может в любой момент переместить объект, не уведомляя пользовательский код, который держит «голый адрес». В обоих случаях «решением» была бы блокировка GC до тех пор, пока пользователь не освободит «голый адрес». Для диагностического API это, по-видимому, не оправдано.
Переопределение классов
Текущий механизм переопределения классов позволяет изменять только тела методов, поэтому он не влияет на размещение объектов и полей.
Когда будет реализован JEP 159 («Enhanced Class Redefinition»), некоторые методы станут чувствительны к переопределениям: sizeOf и fieldOffSetOf могут меняться при добавлении или удалении полей. Чтобы эти методы оставались пригодными при развитии этой области, в спецификации, возможно, понадобится оговорка о том, что значения sizeOf и fieldSizeOf могут меняться во время выполнения. Если сделать оба метода интринсиками, затраты на получение актуальных размеров и смещений могут стать пренебрежимо малыми.
Panama
Panama, по-видимому, не взаимодействует с sizeOf и offsetOf.
Текущий код Panama даёт возможность обращаться к памяти всей нативной кучи, см. MemoryAccess и MemorySegment.ofNativeRestricted (сегмент «everything»). Эта куча включает собственную кучу Java виртуальной машины JVM, и технически результат addressOf можно использовать для доступа к внутреннему устройству объектов в обход проверок языка. Эта проблема в Panama уже существует, и сам Panama её смягчает: чтобы использовать сегмент «everything», нужно явно включить его специальным флагом времени выполнения. Со стороны API проблему можно смягчить, возвращая адреса со случайным смещением («солью»), чтобы указатели гарантированно не указывали в кучу Java.
Alternatives
Продолжать использовать Unsafe. Это противоречит цели JEP. Это по-прежнему осуществимо, но по мере изменения языка и среды выполнения требует изобретать всё новые трюки с Unsafe. Именно так сегодня поступают JOL и другие библиотеки. Хотя сейчас это работает, такой подход не так устойчив к будущим изменениям, как публичный API самого JDK.
Добавить новые методы в Unsafe. Это противоречит цели JEP. Новые упрощённые методы в Unsafe облегчили бы жизнь некоторым инструментам, поскольку им не пришлось бы прибегать к громоздким трюкам (например, к неудобным загрузкам и сохранениям с преобразованием типов для addressOf или к опросу смещений всех полей, чтобы вычислить sizeOf). Однако интереса к расширению sun.misc.Unsafe, по-видимому, нет, поскольку цель — свести к минимуму использование Unsafe за пределами JDK. В лучшем случае можно расширить jdk.internal.misc.Unsafe, но он специально защищён системой модулей и не доступен внешним инструментам напрямую.
Продолжать использовать Instrumentation, добавив туда методы. Сейчас Instrumentation покрывает только случай sizeOf, но его можно расширить и на другие случаи. Может показаться, что такие API защищены лучше, если они доступны только приложениям, которые явно согласились использовать агенты Java. Этот вариант неявно опирается на предположение, что агенты Java останутся доступны библиотекам, которым нужны эти возможности.
Продолжать использовать JVMTI, добавив туда методы. У этого варианта те же преимущества, что и у описанной выше альтернативы с Instrumentation, плюс один недостаток: опытным пользователям придётся поставлять платформенно-зависимый нативный код.
Целиком включить JOL в JDK. Если перенести весь JOL в JDK в виде модуля, он мог бы получить доступ к внутреннему устройству VM, не открывая его как публичный API. Однако JOL без изменений по-прежнему раскрывал бы те же данные, что и примитивы из этого JEP: размеры объектов, смещения полей, адреса объектов. При этом возникает серьёзный недостаток в сопровождении: процесс разработки JOL оказался бы привязан к процессу разработки JDK, а для поддержки предыдущих JDK потребовались бы отдельные бэкпорты и т. д. Развивать этот вариант, по-видимому, не стоит.
Расширение Panama. Поскольку Panama занимается взаимодействием с нативным кодом, возможно, имеет смысл включить новые API туда. Например, чтобы ObjectLayout располагался рядом с API MemoryLayout. Этот вариант может быть интересно изучить. Недостаток в том, что более или менее независимые примитивы окажутся зависимы от более крупного проекта.
Testing
Нужно обычное модульное тестирование, чтобы убедиться, что API делает то, что должен. Поскольку код даёт ответы, зависящие от реализации, для проверки того, что он работает как ожидается, потребуется тестирование на нескольких платформах. Мы не ожидаем, что для новой функциональности понадобится код, специфичный для ОС или архитектуры, но известно, что некоторый общий код ведёт себя по-разному на 32- и 64-битных путях выполнения. Чтобы проверить это, текущий прототип тестируется на x86_64 и x86_32.
Полноту API можно проверить с помощью специальных версий JAMM и JOL, использующих предлагаемые API.
Risks and Assumptions
Непредвиденное взаимодействие с новыми возможностями языка/VM. Может случиться так, что появится новая возможность языка или VM, которая окажется в конфликте с каким-либо из новых методов API. Этот риск смягчается тем, что в граничных случаях, когда VM не может что-то определить, API возвращают значения «не знаю, неважно».
Возможность реализации в других VM. Прототип показывает, что HotSpot может без особых проблем реализовать новые методы API, почти не опираясь на особенности HotSpot. Это слабое свидетельство того, что новые методы реализуемы и в других VM: им в любом случае приходится знать размеры объектов, адреса, смещения и размеры полей. Если какая-либо VM не может реализовать метод, ей следует разрешить возвращать значение «не знаю, неважно».
Dependencies
У этой работы нет зависимостей.