Статьи

Внутри Java 9 — схема версий, многоадресные JAR и многое другое

Два месяца назад я познакомил вас с Java 9 , изучая новые языковые функции и API. Но есть много вещей, которые мне пришлось опустить, так что вот два участника, чтобы это исправить. В этой первой части мы рассмотрим строки новой версии и синтаксис командной строки, многоадресные файлы JAR, улучшенное ведение журнала и многое другое.

JShell

В первой части мы пропустили Jigsaw, потому что все уже говорят об этом. В этой части мы сделаем то же самое с JShell, новым REPL для Java. (То есть, когда вы набираете код Java, и он сразу же оценивает его.) Если вы еще не знаете (много) об этом, но хотели бы, посмотрите это великолепное вступление, которое Роберт Филд дал в прошлом году в Devoxx Belgium. ( JEP 222 )

Строки новой версии

Чтобы облегчить это, давайте начнем с чего-то простого: строки версии.

Или так я думал, пока не попытался понять схему именования версий Java . Все началось с JDK 1.0 и 1.1 — пока что все просто, но дела идут вниз. По-видимому, версии 1.2–1.5 были в какой-то момент переименованы в Java 2. (Помните, J2SE ? Там 2 ). С JDK 1.5 стало ясно, что это не сработало, поэтому Sun начала называть его Java 5 . Когда-то вокруг Java 6 вся идея Java 2 была тихо похоронена, и с тех пор все немного прояснилось — мы просто говорим «Java X». (Знаете ли вы, что все версии Java вплоть до Java 7 включали в себя классные названия проектов, такие как Tiger и Mustang ?)

Строки версий, о которых сообщает JVM, остались нетронутыми — они всегда сообщали о 1.x... Теперь в JEP 223 строки версий и схема именования совпадают. Если вы проверите соответствующие системные свойства (см. Демонстрацию ), вы получите следующее:

 java.version: 9-ea java.runtime.version: 9-ea+138-jigsaw-nightly-h5561-20161003 java.vm.version: 9-ea+138-jigsaw-nightly-h5561-20161003 java.specification.version: 9 java.vm.specification.version: 9 

Это не слишком информативно, потому что оно запускается при ранней сборке доступа. В будущем java.version сообщит о таких строках, как 9.1.2 , которые следуют схеме $MAJOR.$MINOR.$SECURITY :

  • $MAJOR обозначает основную версию, которую Oracle планирует выпускать каждые два-три года.
  • $MINOR отмечает меньшие выпуски для исправлений ошибок и других деталей, которые регулярно следуют между ними — он сбрасывается в ноль, когда выпускается новая основная версия.
  • $SECURITY действительно интересен — он сталкивается с каждым выпуском, который «содержит критические исправления, в том числе необходимые для повышения безопасности», и не сбрасывается при увеличении $MINOR .

Чтобы сделать синтаксический анализ этой строки ненужным, мы получаем Version , симпатичный маленький класс, который делает это за нас.

 Version version = Runtime.version(); System.out.println("Reported by runtime: " + version); switch (version.major()) { case 9: System.out.println("Modularity!"); break; case 10: System.out.println("Value Types!"); break; } 

Параметры командной строки в стиле GNU

Инструменты Java имеют много специфических особенностей, когда дело доходит до синтаксиса их параметров командной строки:

  • некоторые используют одну черту для длинных версий ( -classpath ), другие используют две ( --gzip )
  • некоторые отдельные слова с тире ( --no-gzip ), другие — нет (опять же -classpath )
  • некоторые из них имеют однобуквенные формы ( -d ), другие — двухбуквенные ( -cp , серьезно, что не так с этим параметром ?!)
  • некоторые присваивают значения со знаком равенства ( -D<name>=<value> ), другие требуют пробела (я даже не буду…)

В Linux и других системах на основе GNU, напротив, почти все инструменты используют один и тот же синтаксис для своих опций:

  • две черты для длинных версий
  • слова разделены тире
  • сокращения используют одну черту и состоят из одной буквы

В безрассудно смелом шаге Java 9 изменяет все параметры командной строки, чтобы соответствовать этим правилам, тем самым нарушая все сценарии! Нет, просто шучу … 😎 Но JEP 293 установил руководство, отражающее и адаптирующее эти правила, и ожидается, что ему будут следовать новые варианты. В какой-то момент старые параметры могут быть перенесены в сторону более чистого синтаксиса, но это не является частью Java 9. JEP содержит много деталей и примеров — ознакомьтесь с ними.

Расширения и обновления

Java 9 поставляется с большим количеством JEP, которые расширяют или обновляют существующую функциональность. Здесь они, несколько упорядочены по темам, некоторые суммированы, некоторые более подробно.

Unicode

В то время как сама Java может быть написана в UTF-16 (да, ваш код может содержать смайлики), файлы свойств раньше были ограничены ISO-8859-1. Допустим, у вас есть файл config.properties как этот:

 money = € / \u20AC 

Затем доступ к этому файлу с помощью Java 8 дает:

 money = â▯¬ / € 

С JEP 226 эти времена наконец-то закончились и больше не требуется выходов из Юникода Запуск того же кода доступа на Java 9 показывает, что мы ожидали:

 money = € / € 

полном примере даже есть 😎, но наша подсветка кода не очень хорошо с этим работает.)

Обратите внимание, что существует несколько способов доступа к файлам свойств, и только один из них через PropertyResourceBundle был обновлен. Как именно определяется кодировка и как ее можно настроить, описано в примечании API JavaDoc . Однако значения по умолчанию разумны и делают API «просто работающим» для обычных случаев:

 try (InputStream propertyFile = new FileInputStream("config.properties")) { PropertyResourceBundle properties = new PropertyResourceBundle(propertyFile); properties.getKeys().asIterator().forEachRemaining(key -> { String value = properties.getString(key); System.out.println(key + " = " + value); }); } catch (IOException e) { e.printStackTrace(); } 

Вы найдете этот код и сопоставление с API Properties в демонстрационной версии . Если вы попытаетесь запустить его на Java 8 для сравнения, вы найдете изящное небольшое изменение API Java 9, сделанное для тех бедных разработчиков, которые все еще используют древний Enumeration .

В других новостях, связанных с Unicode, Java 9 поддерживает Unicode 8.0 . Ура! ( JEP 227 , JEP 267 )

Графика

Изображения TIFF теперь поддерживаются платформой ввода / вывода изображения (в javax.imageio ). В проекте Java Advanced Imaging (JAI) реализованы программы чтения и записи для этого формата, которые теперь стандартизированы и перемещены в javax.imageio.plugins.tiff . ( JEP 262 )

Экраны HiDPI в стиле Retina создают уникальные проблемы для настольных интерфейсов. Java уже имела дело с ними на Mac и теперь следует примеру Linux и Windows. При этом «компоненты Windows и GUI должны иметь соответствующий размер в соответствии с рекомендациями платформы, текст должен оставаться четким, несмотря на любое масштабирование по умолчанию, указанное в настройках HiDPI, а значки и изображения должны быть плавными и предпочтительно иметь детали, соответствующие плотности пикселей пикселя». дисплей ». ( JEP 263 )

В Linux триумвират рабочего стола Java (AWT, Swing и JavaFX) теперь может использовать GTK 3 . В настоящее время JVM по умолчанию использует GTK 2 и использует GTK3 только в том случае, если это указано новым системным свойством jdk.gtk.version или «GTK 3 требуется для взаимодействия, и это требование может быть обнаружено достаточно рано». ( JEP 283 )

JavaFX использовал устаревшую версию GStreamer , которая обновлена ​​до 1.4.4. Это должно улучшить стабильность и производительность воспроизведения медиафайлов с помощью JavaFX. ( JEP 257 )

HarfBuzz — это новый механизм разметки OpenType , который заменяет ICU , который больше не выпускается. ( JEP 258 )

Безопасность

Были реализованы алгоритмы хэширования SHA-3 SHA3-224, SHA3-256, SHA3-384 и SHA3-512. Их можно использовать через MessageDigest API . ( JEP 287 )

При использовании SecureRandom (в любой версии Java) вы получаете либо собственную реализацию, основанную на исходном источнике энтропии операционной системы, либо чистую версию Java . Последний «использует более старую реализацию RNG на основе SHA1, которая не так сильна, как алгоритмы, используемые утвержденными механизмами DRBG [Deterministic Random Bit Generator]». Поскольку более старые, но особенно встроенные системы полагаются на вариант Java, его безопасность была повышена за счет реализация механизмов DRBG, описанных в NIST 800-90Ar1 . Наряду с этим был усовершенствован API SecureRandom позволяющий передавать параметры в DRGB и будущие алгоритмы ( JEP 273 ):

 Instantiation instantiation = DrbgParameters.instantiation(128, RESEED_ONLY, null); SecureRandom random = SecureRandom.getInstance("DRBG", instantiation); byte[] bytes = new byte[20]; random.nextBytes(bytes); 

Новые возможности виртуальной машины Java

Машина делает всю работу за нас — интересно, встанет ли однажды? Он получил некоторую любовь в виде пары новых функций, поэтому, возможно, мы успокоили его еще на несколько лет. Если нет, то я приветствую наших новых повелителей насекомых .

Многофункциональные банки

Иногда вам может понадобиться написать код, который различает, какую версию Java вы используете — делайте это для Java 8 и для Java 9. До сих пор это было немного сложно, но Java 9 решает важную часть головоломки. Все соответствующие части платформы Java теперь создают и понимают многоразовые JAR-файлы, JAR-файлы, содержащие разные версии одних и тех же типов для разных версий Java.

Давайте посмотрим на пример :

  1. Я создал три класса: Main , VersionDependent для Java 8 и VersionDependent для Java 9, где первый выводит результат вызова второго, а второй просто возвращает «версию Java X», где X равно 8 или 9.

  2. Затем я скомпилировал Main и VersionDependent (для Java 8) в папку out-mr/java8 и VersionDependent (Java 9) в out-mr/java9 .

  3. Интересная часть — как их упаковать. Следующая команда создает mr.jar , VersionDependent.class дважды содержит VersionDependent.class (один раз из каждой папки out-mr/java-x ) и структурирован так, чтобы java выбирал нужные классы:

     jar9 --create --file out-mr/mr.jar -C out-mr/java-8 . \ --release 9 -C out-mr/java-9 . 
  4. Действительно, запуск его с помощью java -cp out-mr/mr.jar ...Main возвращает «версию Java 8» при запуске с 8 и «версию Java 9» при запуске с 9.

Вот как выглядит JAR изнутри:

 └ org └ codefx ... (moar folders) ├ Main.class └ VersionDependent.class └ META-INF └ versions └ 9 └ org └ codefx ... (moar folders) └ VersionDependent.class 

Таким образом, Java версии 8 и старше будут просто использовать классы в org но более новые версии могут перезаписывать некоторые из них содержимым из правой подпапки META-INF/versions version. Ухоженная.

Унифицированная регистрация

Отладка JVM, возможно, для объяснения сбоев приложений или для обнаружения утечек производительности, достаточно сложна. Отдельные параметры ведения журнала для разных подсистем не облегчали эту задачу. Благодаря JEP 158 и 271 , это скоро уйдет в прошлое. Он поставляется с новой опцией командной строки -Xlog (разве это не должно быть --log сейчас?), -Xlog может использоваться для определения регистрации до чрезвычайно подробного уровня. Вот некоторые из доступных настроек:

  • Каждое сообщение может иметь несколько тегов, в зависимости от подсистемы, которая его создала, и того, что оно делало в то время — например, gc , modules или os . Отдельные теги могут быть выбраны для применения к ним других настроек.
  • Конечно, сообщения имеют уровни ( error , warning , info , debug , trace , develop ) и могут быть отфильтрованы ими.
  • Выходные данные могут быть определены как stdout , stderr или как файл, который можно настроить для ротации файла журнала.
  • Затем есть украшения — полезная информация, прикрепленная к каждому сообщению (pid, uptime,…, технически тег и уровень также являются украшениями). Они могут быть включены в вывод.

И все это можно -Xlog в одну опцию -Xlog . Давайте начнем с простой регистрации пары тегов:

 java9 -cp out-mr/mr.jar -Xlog:os,modules,gc ...Main [0.002s][info][os] SafePoint Polling address: 0x00007feea4c96000 [0.002s][info][os] Memory Serialize Page address: 0x00007feea4c94000 [0.002s][info][os] HotSpot is running with glibc 2.22, NPTL 2.22 [0.009s][info][gc] Using G1 Java 9 version 

Мм, разве нет ничего для модулей? Давайте превратим этот тег в debug (если ничего не указано, уровень по умолчанию равен info ):

 java9 -cp out-mr/mr.jar -Xlog:os,modules=debug,gc org.codefx.demo.java9.internal.multi_release.Main [0.002s][info][os] SafePoint Polling address: 0x00007f3054a22000 [0.002s][info][os] Memory Serialize Page address: 0x00007f3054a20000 [0.002s][info][os] HotSpot is running with glibc 2.22, NPTL 2.22 [0.009s][info][gc] Using G1 [0.059s][debug][modules] set_bootloader_unnamed_module(): recording unnamed module for boot loader [0.063s][debug][modules] define_javabase_module(): Definition of module: java.base, version: 9-ea, location: jrt:/java.base, package #: 159 [... snip ... many, many more module messages ... ] 

Перебор. Но там мы можем увидеть это:

 [0.079s][info][modules,startuptime] Phase2 initialization, 0.0366552 secs 

Эй, это info ! Почему он не появился раньше ?! Поскольку он имеет два тега и, что удивительно, его недостаточно, если теги, определенные в командной строке, соответствуют одному из тегов сообщения — они должны соответствовать всем им. Мы можем расширить наш modules+startuptime сопоставления до modules+startuptime или использовать подстановочные знаки:

 java9 -cp out-mr/mr.jar -Xlog:os,modules*,gc* ...Main [0.002s][info][os] SafePoint Polling address: 0x00007f9c7f307000 [0.002s][info][os] Memory Serialize Page address: 0x00007f9c7f305000 [0.003s][info][os] HotSpot is running with glibc 2.22, NPTL 2.22 [0.007s][info][gc,heap] Heap region size: 1M [0.009s][info][gc ] Using G1 [0.009s][info][gc,heap,coops] Heap address: 0x00000006c6200000, size: 3998 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 [0.077s][info][modules,startuptime] Phase2 initialization, 0.0367418 secs Java 9 version [0.090s][info][gc,heap,exit ] Heap [0.090s][info][gc,heap,exit ] garbage-first heap total 256000K, used 2048K [0x00000006c6200000, 0x00000006c63007d0, 0x00000007c0000000) [0.090s][info][gc,heap,exit ] region size 1024K, 3 young (3072K), 0 survivors (0K) [0.090s][info][gc,heap,exit ] Metaspace used 4225K, capacity 4532K, committed 4864K, reserved 1056768K [0.090s][info][gc,heap,exit ] class space used 414K, capacity 428K, committed 512K, reserved 1048576K 

Послушайте, даже сборщику мусора было что сказать — в данном случае относительно выхода и кучи.

Это была только поверхность — есть еще много вариантов настройки. JEP довольно хорошо объясняет все это, включая примеры.

Я намекал на то, что это улучшение не все исправляет. Причина в том, что JEP сосредоточились на предоставлении инфраструктуры и перенаправлении некоторых (многие — определенно все сообщения GC) существующих вызовов через нее, но не обязательно всех. Хотя я не мог найти другие журналы, которые еще не используют новый механизм, вполне вероятно, что он там.

Проверка флага командной строки

Вот забавный факт: виртуальная машина Java 8 не проверяет значения, заданные для флагов командной строки, пока они на самом деле не нужны. К сожалению, это может быть довольно поздно в жизненном цикле приложения, и я вижу, как последующие сбои могут привести к разочарованию. Давайте возьмем этот пример:

 java -cp out-mr/mr.jar -XX:BlockLayoutMinDiamondPercentage=120 ...Main 

Хотя я понятия не имею, что делает BlockLayoutMinDiamondPercentage (и ему было лень его искать), на самом деле не похоже, что 120 — это действительный процент. Java 8 не потревожена этим и успешно выполняет наш JAR, выполняя весь код успешно — очевидно, значение не требуется при выполнении этой программы. Или, может быть, 120 является действительным значением в конце концов? Java 9 так не считает:

 java9 -cp out-mr/mr.jar -XX:BlockLayoutMinDiamondPercentage=120 ...Main intx BlockLayoutMinDiamondPercentage=120 is outside the allowed range [ 0 ... 100 ] Improperly specified VM option 'BlockLayoutMinDiamondPercentage=120' Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit. 

Лучше … Потому что благодаря JEP 245 Java 9 имеет возможность проверять все входные флаги во время запуска. И, как видно выше, он также печатает пояснительные сообщения об ошибках.

Зарезервированные области стека

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

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

Резервирование кадров для критических разделов

JEP 270 добавил новую аннотацию @ReservedStackAccess . С его помощью методы могут идентифицировать себя как критические разделы, которые необходимо завершить, если система не должна оказаться в поврежденном состоянии. Поведение JVM в отношении стека выполнения было соответствующим образом изменено. Он резервирует некоторое дополнительное пространство стека и всякий раз, когда возникает @ReservedStackAccess StackOverflowError он проверяет, есть ли в стеке метод @ReservedStackAccess аннотированный. Если это так, это пространство становится доступным для него, чтобы он мог выполнять дополнительную работу. Как только это сделано, исключение тем не менее выдается.

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

Сколько места зарезервировано в стеке? На данный момент по умолчанию используется одна страница памяти, и «эксперименты показали, что этого достаточно для покрытия критических разделов блокировок java.util.concurrent , которые были аннотированы до сих пор».

пример

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

 int depth; @ReservedStackAccess private void determineDepthWithReservedStack() { determineDepth(); } private void determineDepth() { depth = 0; try { recurseToDetermineMaxDepth(); } catch (StackOverflowError err) { } System.out.printf("Depth: %d%n", depth); } private void recurseToDetermineMaxDepth() { depth++; recurseToDetermineMaxDepth(); } 

Идя снизу вверх, вы видите метод, бесконечно рекурсивный и увеличивающий поле depth , другой метод, обертывающий его, перехватывающий исключение и печатающий полученную глубину, и, наконец, третий метод, запрашивающий доступ к зарезервированной области стека. Я не пользуюсь зарезервированной областью стека, поскольку я просто исчерпываю ее с помощью более рекурсивных вызовов. Это означает, что вызовы defineDepth или determineDepthWithReservedStack конечном итоге приведет к переполнению стека, что приведет к печати количества вызовов. Но вывод должен отличаться и в самом деле:

 DEPTH USING REGULAR STACK Depth: 21392 DEPTH USING RESERVED STACK Java HotSpot(TM) 64-Bit Server VM warning: Potentially dangerous stack overflow in ReservedStackAccess annotated method org.codefx.demo.java9.internal.stack.ReservingStackAreas.determineDepthWithReservedStack()V [1] Depth: 59544 

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

Внутренность!

В случае, если вы устали, имейте в виду, что это внутренний API. Чтобы убедить компилятор предоставить вам доступ к аннотации, вам нужно взломать базовый модуль с помощью --add-exports java.base/jdk.internal.vm.annotation=<module> , где <module> — это либо модуль вашего модуля. имя (если вы уже делаете это) или ALL-UNNAMED . Но JVM все равно будет игнорировать аннотацию, если вы не отключите ограничение для привилегированного кода с помощью -XX:-RestrictReservedStack .

Посмотрите, как это происходит вместе в compile.sh и run.sh в демонстрационном проекте .

Но подождите, еще больше!

Да, есть еще кое-что, и на этот раз вам не придется ждать два месяца — обещаю. Вторая часть должна быть сделана на следующей неделе и будет посвящена повышению производительности и улучшению компилятора. Я уверен, что в новостной рассылке будет краткий обзор, так что если вам интересно подписаться (до вечера пятницы по UTC) или подписаться на нас в Medium, либо я, либо SitePoint (или оба), где он появится на следующей неделе.

Если вам понравилась эта статья, помогите нам распространить информацию и расскажите об этом другу или коллеге!