Вся сложность нативной рефлексии скрыта именно внутри JVM кода, чему посвящён целый класс на 1000+ строк кода. Здесь и ресолвинг метода, и преобразование типов, и всевозможные проверки, и перекладывание аргументов. И всё это интерпретируется каждый раз заново для каждого вызова, на что как раз и тратится уйма времени.
Попробуйте проделать это на любом из высокоуровневых языков — вы и не приблизитесь по производительности к С. Даже на Java, с помощью JIT, с параллельными вычислениями и хорошей моделью использования памяти в пространстве пользователя.
В своём недавнем выступлении на конференции Joker я как раз сравнивал Java и C на похожем примере. И там Java на малюсенькую долю секунды даже опередила C. Я заметил, что если число итераций цикла не константное, то GCC не делает loop unrolling даже с -O3. А JVM делает. И векторизировать по умолчанию умеет. А статистика, собранная в run-time, позволяет JVM выполнять спекулятивные оптимизации, на которые статические компиляторы не способны в принципе.
Я понимаю вашу задачу и ценю проделанную работу, но мне не очень нравится подход. Java разработчики привыкли, что с помощью Reflection и Unsafe можно сделать что угодно, и всячески это эксплуатируют. И я в том числе. Однако шумиха вокруг приватного API, Java 9 и модульности заставила меня всерьёз пересмотреть позицию. Если можно избегать хаков — лучше избегать. Тем более, что в вашем случае вроде как можно.
Если бы передо мной стояла подобная задача, я бы, пожалуй, сразу отказался от поддержки JRE-only окружения. Тогда через стандартный ToolProvider.getSystemToolClassLoader() мы получаем доступ к VirtualMachine и вызываем attach безо всяких хаков. Плюсы: 1) легальность; 2) публичный API; 3) только Java и никакой возни с нативными библиотеками.
Если же очень хочется на чистом JRE (впрочем, тех, кому фича действительно нужна, не затруднит и JDK поставить), то без натива не обойтись: будь то attach.dll, jattach или собственная JVMTI библиотека. Причём последний вариант самый простой, т. к. не требует знания PID и подключения агентов. Поэтому не понятно, чем вам JVMTI не угодил — нативная библиотека может точно так же применять любые переданные из Java трансформации. Собственно, Instrumentation API через него и работает. Кстати, если будут вопросы по jattach или JVMTI — всегда готов помочь.
А что смущает? Выглядит всё логично. nanoTime() быстрее, потому что это JVM intrinsic и выполняется в контексте Java кода без переключения в натив. Critical JNI ненамного быстрее, потому что в простом методе без аргументов ему экономить особо не на чем, разве что на лишних JNIEnv и jclass.
Кроме того, сравнивать gettimeofday с nanoTime не совсем корректно, т. к. nanoTime реализован по-другому. А вот currentTimeMillis реализован как раз через gettimeofday.
Кстати, не надо в JMH бенчмарках гонять циклы — рискуете попасться на все те грабли, с которыми JMH борется. Там внутри и так есть цикл. К тому же, теряется смысл Score. Если я измеряю одну операцию, то Score даёт интуитивно понятную оценку, типа nanoTime работает около 15 наносекунд, что соответствует примерно 30 тактам 2GHz процессора. А так 1179 — просто какое-то абстрактное число.
Если дело только в этом, можно и на Java лаунчер сделать. В JAR будет прописан Main-Class лаунчера, который запускает новый Java процесс с тем же JAR в classpath, но с другим Main классом.
Понятно. Что касается экспериментов, любопытно поисследовать, почему gc() + runFinalization() работал в Java 7, но перестал в Java 8. Как я уже писал в другой теме, сборки, вызванные System.gc() и дампом хипа, на уровне JVM принципиально не отличаются. Скорее всего, дамп помогал по другой причине, например, из-за таймингов. Не знаю, мне самому не удалось воспроизвести проблему: в JDK 8 System.gc() стабильно работал, и финализаторы вызывались. Вот, если бы в статье было про это — было бы интересно!
Всё верно. Со страшилками про Java 9 я переборщил :) В данном конкретном случае будет работать. ПОКА будет. Детали реализации, такие как приватные поля, могут измениться в любой момент, даже с минорным апдейтом.
Кое-что в класслоадерах уже изменили в Java 9. Например, системный ClassLoader больше не наследник URLClassLoader. Но я встречал проекты, где просто кастовали системный ClassLoader, чтобы взять у него URLClassPath. И фокус с usr_paths из той же серии.
А чем не подошёл стандартный в таких случаях подход, когда есть маленький лаунчер, который всё копирует, куда надо, потом запускает в отдельном процессе основную программу с нужными параметрами, дожидается её завершения, после чего всё подчищает?
Это ещё только первый уровень :)
Я как-то в похожей задаче столкнулся с проблемой, когда на сервере рассылок стала кончаться память. Не хип, а нативная память: при -Xmx4G Java процесс отъел больше 12GB! Причём безо всяких там Unsafe и JNI.
После тщетных исследований стал отлавливать все системные вызовы mmap и brk. И тогда обнаружил, что течёт zlib, потому как объекты java.util.zip.Inflater не успевают финализироваться. А объекты эти пришли из getResourceAsStream, который, как и в вашем случае, доставал из JAR картинки для писем.
Исправили очень просто: закэшировали один раз все ресурсы, чтобы не делать getResourceAsStream на каждую отправку письма.
Да, это проблема. Через Reflection можно даже это сделать, но будет всё равно ужасно. Лучше, наверное, вообще избегать необходимости удаления каталога с загруженной dll.
Впрочем, речь была о другом: при настройках по умолчанию System.gc точно запускает сборку; проблема в чём-то ином. Есть ещё несколько способов вызвать GC: через DiagnosticCommandMBean или через JVMTI. Но дамп хипа — это имхо перебор.
Принципиально сборки, вызванные через System.gc и через дамп хипа, не отличаются. Почему по-разному работает в Java 7 и Java 8 — не знаю. Может быть сотня причин; приведённого кода недостаточно, чтобы сказать, что именно. Могу сказать лишь одно — полагаться на вызов finalize точно не стоит. Очевидно, вы хотите закрыть ресурсы сразу при вызове unload — так и закройте их напрямую; может, даже явным вызовом finalize(), если другого способа нет.
Моя претензия вовсе не к использованию MAT, а к тому, что в статье напрочь отсутствует какой-либо анализ, а выводы основываются только на цифрах конкретного эксперимента. Это из той же серии, что написать самодельный бенчмарк, и на его основе утверждать, что Java в 500 раз медленнее C++.
Не стоит преувеличивать возможности Allocation Elimination. Обычно это работает только в простых случаях. Как минимум, метод анонимного класса для этого должен оказаться заинлайнен. Что очень маловероятно в реальных приложениях для мегаморфных callsite-ов вроде Collections.sort.
Посмотрите, сколько в хипдампе занимают объекты java.lang.Class по мнению MAT.
40 байт вместо реальных 96! Или ещё: java.lang.invoke.MemberName якобы занимает 32 байта, хотя на самом деле 56. И это пример не с потолка: я сталкивался с реальными утечками, связанными с MemberName: JDK-8152271.
А статья без какого-либо анализа основывается исключительно на инструменте, который в некоторых случаях врёт в 2 раза!
А почему вы думаете, что heap dump покажет вам реальное занимаемое место? Это же не дамп физической памяти, а очередной абстрактный формат, который разные тулы могут трактовать по-разному. Если уж и мерить размеры объектов, то с помощью правильных инструментов, см. JOL.
Для методов без аргументов искомый Method всегда окажется первым. Так уж LambdaMetafactory устроена. А, вот, для преобразования аргументов могут вызываться и другие методы. И тут уже без анализа байткода не обойтись. Строго говоря, и байткода-то может не быть. Всё это детали конкретной реализации.
Вся сложность нативной рефлексии скрыта именно внутри JVM кода, чему посвящён целый класс на 1000+ строк кода. Здесь и ресолвинг метода, и преобразование типов, и всевозможные проверки, и перекладывание аргументов. И всё это интерпретируется каждый раз заново для каждого вызова, на что как раз и тратится уйма времени.
Если бы передо мной стояла подобная задача, я бы, пожалуй, сразу отказался от поддержки JRE-only окружения. Тогда через стандартный ToolProvider.getSystemToolClassLoader() мы получаем доступ к VirtualMachine и вызываем attach безо всяких хаков. Плюсы: 1) легальность; 2) публичный API; 3) только Java и никакой возни с нативными библиотеками.
Если же очень хочется на чистом JRE (впрочем, тех, кому фича действительно нужна, не затруднит и JDK поставить), то без натива не обойтись: будь то attach.dll, jattach или собственная JVMTI библиотека. Причём последний вариант самый простой, т. к. не требует знания PID и подключения агентов. Поэтому не понятно, чем вам JVMTI не угодил — нативная библиотека может точно так же применять любые переданные из Java трансформации. Собственно, Instrumentation API через него и работает. Кстати, если будут вопросы по jattach или JVMTI — всегда готов помочь.
Кроме того, сравнивать gettimeofday с nanoTime не совсем корректно, т. к. nanoTime реализован по-другому. А вот currentTimeMillis реализован как раз через gettimeofday.
Кстати, не надо в JMH бенчмарках гонять циклы — рискуете попасться на все те грабли, с которыми JMH борется. Там внутри и так есть цикл. К тому же, теряется смысл Score. Если я измеряю одну операцию, то Score даёт интуитивно понятную оценку, типа nanoTime работает около 15 наносекунд, что соответствует примерно 30 тактам 2GHz процессора. А так 1179 — просто какое-то абстрактное число.
Кое-что в класслоадерах уже изменили в Java 9. Например, системный ClassLoader больше не наследник URLClassLoader. Но я встречал проекты, где просто кастовали системный ClassLoader, чтобы взять у него URLClassPath. И фокус с
usr_paths
из той же серии.С выходом Java 9 — сразу до свидания!
Не надо модифицировать
java.library.path
. Про System.load() слышали?Я как-то в похожей задаче столкнулся с проблемой, когда на сервере рассылок стала кончаться память. Не хип, а нативная память: при
-Xmx4G
Java процесс отъел больше 12GB! Причём безо всяких там Unsafe и JNI.После тщетных исследований стал отлавливать все системные вызовы
mmap
иbrk
. И тогда обнаружил, что течётzlib
, потому как объектыjava.util.zip.Inflater
не успевают финализироваться. А объекты эти пришли изgetResourceAsStream
, который, как и в вашем случае, доставал из JAR картинки для писем.Исправили очень просто: закэшировали один раз все ресурсы, чтобы не делать
getResourceAsStream
на каждую отправку письма.Впрочем, речь была о другом: при настройках по умолчанию System.gc точно запускает сборку; проблема в чём-то ином. Есть ещё несколько способов вызвать GC: через DiagnosticCommandMBean или через JVMTI. Но дамп хипа — это имхо перебор.
java.lang.Class
по мнению MAT.40 байт вместо реальных 96! Или ещё:
java.lang.invoke.MemberName
якобы занимает 32 байта, хотя на самом деле 56. И это пример не с потолка: я сталкивался с реальными утечками, связанными с MemberName: JDK-8152271.А статья без какого-либо анализа основывается исключительно на инструменте, который в некоторых случаях врёт в 2 раза!