При создании большого количества классов ByteBuddy нужно ли мне приобретать блокировки любого типа?

Я создаю несколько классов ByteBuddy (используя DynamicTypeBuilder) и загружаю их. Создание этих классов и их загрузка происходит в одном потоке (главном потоке; я сам не создаю никаких потоков и ничего не отправляю в ExecutorService) в относительно простой последовательности.

Я заметил, что выполнение этого в модульном тесте несколько раз подряд дает разные результаты. Иногда классы создаются и загружаются нормально. В других случаях я получаю ошибки из сгенерированного байт-кода, когда он впоследствии используется (часто в общей области, где я использую withArgumentArrayElements, если это важно; ArrayIndexOutOfBoundsErrors и тому подобное; опять же, в других случаях все это работает нормально (с теми же входами)).

Это похоже на состояние гонки, но, как я уже сказал, я не создаю никаких тем. Поскольку я не использую потоки, их может использовать только ByteBuddy (или JDK). Я не уверен, где это будет. Есть ли механизм синхронизации ByteBuddy, который я должен использовать при создании и загрузке классов с DynamicTypeBuilder.make() и getLoaded()? Возможно, какое-то разрешение класса происходит (или не происходит!) в фоновом потоке или что-то в make() время, и я случайно каким-то образом препятствую его завершению? Возможно, если я собираюсь использовать эти классы немедленно (я), мне нужно предоставить другой TypeResolutionStrategy? Я сбит с толку, и это должно быть ясно, и я не могу понять, почему однопоточная программа с одними и теми же входными данными должна создавать сгенерированные классы, которые ведут себя по-разному от запуска к запуску.

Мой шаблон для загрузки этих классов:

  1. Попробуйте загрузить (обычно несуществующий) класс, используя Class#forName(name, true, Thread.currentThread().getContextClassLoader()).
  2. Если (когда) это не удается, создайте класс, сгенерированный ByteBuddy, и загрузите его, используя обычные рецепты ByteBuddy.
  3. Если это не удастся, то только потому, что какой-то другой поток мог уже создать класс. В этом модульном тесте нет другого потока. В любом случае, если здесь произойдет сбой, я повторяю шаг 1, а затем выдаю исключение, если загрузка не удалась.

Есть ли какие-либо шаги, специфичные для ByteBuddy, которые я должен предпринять в дополнение к этим или вместо них?


person Laird Nelson    schedule 08.09.2020    source источник
comment
Интересный вопрос и хорошее объяснение, хотя воспроизведение MCVE было бы еще лучше. Поскольку вы упомянули модульные тесты, просто позвольте мне перепроверить, возможно, вы запускаете несколько модульных тестов параллельно в одной и той же JVM, может быть, через Maven Surefire или что-то в этом роде. Или если другие тестовые методы или классы запускаются в той же JVM до запуска вашего теста. Тогда он может быть чувствителен к порядку выполнения, потому что речь идет о загрузке классов.   -  person kriegaex    schedule 09.09.2020
comment
Хорошая мысль, и я определенно совершил эту ошибку много лет назад. Но не в этот раз. Я не часто бываю в полном тупике, но эта проблема определенно заставляет меня сомневаться!   -  person Laird Nelson    schedule 09.09.2020
comment
Похоже на дело Рафаэля. Если бы вы могли дать ему что-нибудь для работы, вероятно, он смог бы быстро найти основную причину.   -  person kriegaex    schedule 09.09.2020
comment
Я собираюсь изучить семантику javadoc.io/static/net.bytebuddy/byte-buddy/1.10.14/net/ (где я уже нашел ошибку в 1.10.14). Предполагать, что что-то не так, когда размер массива равен 0, но как это приведет к NPE с одним запуском и ArrayIndexOutOfBoundsException с другим, в настоящее время мне не понятно.   -  person Laird Nelson    schedule 09.09.2020


Ответы (2)


Фу! Я думаю, что могу списать это на ошибку в моем коде (слава богу). Вкратце то, что выглядело как проблемы параллелизма, было (скорее всего) проблемой со случайно общими именами классов и HashMap порядком итерации: когда один конкретный подкласс был создан, а затем загружен, другой просто загружался (не создавался) и наоборот. Чистый эффект был похож на эффект состояния гонки.

person Laird Nelson    schedule 11.09.2020

Byte Buddy полностью потокобезопасен. Но он пытается создать класс каждый раз, когда вы вызываете load, что является довольно дорогой операцией. Чтобы избежать этого, Byte Buddy предлагает TypeCache механизм, позволяющий реализовать эффективный кеш.

Обратите внимание, что такие библиотеки, как cglib, предлагают автоматическое кэширование. Byte Buddy этого не делает, поскольку кеш использует все входные данные как ключи и ссылается на них статически, что может легко привести к утечке памяти. Кроме того, ключи довольно неэффективны, поэтому Byte Buddy выбрал этот подход.

person Rafael Winterhalter    schedule 11.09.2020
comment
Спасибо за эту информацию. Когда класс загружен, каковы варианты его использования для кэширования? Как вы хорошо знаете, ClassLoader#loadClass() кэширует загрузки. Какие варианты использования вы имели в виду для TypeCache? - person Laird Nelson; 12.09.2020
comment
Например, если вы хотите создать прокси по требованию. Вы создаете класс для каждой прокси-базы при первом запросе и возвращаете предыдущее значение при каждой последующей попытке. - person Rafael Winterhalter; 12.09.2020
comment
Но разве этот класс по запросу не будет загружен загрузчиком классов и, следовательно, никогда не будет сгенерирован повторно? Я имею в виду: если вызывающая сторона сначала пытается выполнить loadClass(proxyClass), и она терпит неудачу, потому что класс не существует, то вы генерируете его с помощью ByteBuddy, затем снова loadClass(proxyClass) — зачем здесь нужен еще один кеш? Последующие вызовы loadClass не будут восстанавливать прокси. (Кажется, я что-то упускаю.) - person Laird Nelson; 12.09.2020
comment
По умолчанию Byte Buddy генерирует случайные имена, чтобы такое поведение не было неявным. И даже при одинаковых именах Byte Buddy каждый раз генерирует новый загрузчик классов. Только если вы используете стратегию внедрения, разрешайте уже существующие классы и определяйте одно и то же имя каждый раз, когда это будет результатом. - person Rafael Winterhalter; 13.09.2020
comment
Ага! Верно; это была часть случайного имени, которую я забыл (в моем случае я использую детерминированные имена и стратегию внедрения). Спасибо! - person Laird Nelson; 13.09.2020