Главная » Хабрахабр » [Перевод] Хватит кормить логгеры! Даешь больше модификаторов! Lazy Static Final Fields. Черновой набросок фичи

[Перевод] Хватит кормить логгеры! Даешь больше модификаторов! Lazy Static Final Fields. Черновой набросок фичи

Джон Роуз спешит на помощь! Достало, что в Java логгеры инициализируются в момент инициализации класса, отчего замусоривают весь запуск?

Вот как это может выглядеть:

lazy private final static Logger LOGGER = Logger.getLogger("com.foo.Bar");

Поведение существующих механизмов ленивого вычисления предлагается улучшить, изменив гранулярность: теперь она будет не с точностью до класса, а с точностью до конкретной переменной. Этот документ расширяет поведение final-переменных, позволяя по желанию поддерживать ленивое выполнение — как в самом языке, так и в JVM.

Почти каждая операция линковки может дергать ленивый код. В Java глубоко встроены ленивые вычисления. Например, выполнение метода <clinit> (байткод инициализатора класса) или использование bootstrap-метода (для invokedynamic call site или констант CONSTANT_Dynamic).

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

В AOT-компиляторах, это делает особо сложным оптимизацию статических ссылок на поля, даже для полей с легко анализируемым константным значением. Если затронуть одно поле, тем самым ты затронешь их все. Похожая проблема проявляется с ранее предложенными механизмами реализации свертки констант (во время работы javac) для константных полей со сложными инициализаторами. Стоит среди полей затесаться хотя бы одному переусложнённому статическому полю, и совершенно все поля этого класса становится невозможно анализировать.

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

private final static Logger LOGGER = Logger.getLogger("com.foo.Bar");

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

Ленивые переменные также хорошо себя зарекомендовали. Final-переменные очень полезны, они являются основным механизмом Java API для того, чтобы указать на константность значений. JIT может оптимизировать и финальные, и «stable» — переменные значительно лучше, чем просто какие-то переменные. Начиная с Java 7, они начали играть всё более важную роль во внутренностях JDK, будучи отмеченными аннотацией @Stable. Наконец, использование lazy final переменных позволит библиотеками, таким как JDK, уменьшить зависимость на код <clinit>, что, в свою очередь, должно уменьшить время запуска и повысить качество AOT-оптимизаций. Добавление ленивых финальных переменных позволит этому полезному паттерну использования стать более распространенным, даст возможность использоваться в большем количестве мест.

Такое поле называется ленивым (lazy field), и обязано также иметь модификаторы static и final. Поле сможет быть объявлено с новым модификатором lazy, который является контекстным ключевым словом, воспринимаемым исключительно как модификатор.

Компилятор и рантайм договариваются с целью запустить инициализатор в точности при первом использовании переменной, а не при инициализации класса, которому принадлежит это поле. Ленивое поле должно иметь инициализатор.

Поскольку элементы константного пула и сами по себе вычисляются лениво, достаточно просто назначить правильно подобранное значение для каждой static lazy final переменной, связанной с этим элементом. Каждое lazy static final поле связывается в момент компиляции с элементом константного пула, который представляет его значение. Разрешены только те приведения, которые уже используются в MethodHandle.invoke. (К одному элементу можно привязать более одной ленивой переменной, но вряд ли это является полезной или осмысленной фичей.) Имя атрибута — LazyValue, и он должен относиться к элементу константного пола, который можно ldc-шнуть в значение, которое конвертируемо в тип ленивого поля.

Инструменты вроде компиляторов могут каким-то своим образом попытаться использовать это поле. Таким образом, ленивое статическое поле можно рассматривать как именованный псевдоним на элемент константного пула внутри класса, который объявил это поле.

12. Ленивое поле никогда не является константной переменной (в том смысле JLS 4. 28). 4) и явным образом исключено из участия в константных выражения (в смысле JLS 15. Вместо этого, ленивое поле захватывает новый вид атрибута классфайла под названием LazyValue, с которым JVM сверяется при линковке ссылки на это конкретное поле. Поэтому, оно никогда не захватывает атрибут ConstantValue, даже если его инициализатор является константным выражением. Формат этого нового атрибута похож на предыдущий, поскольку он также указывает на элемент константного пула, в данном случае — тот, который разрешается в значение поля.

Вместо этого, любой метод <clinit> объявляющего класса инициализируется по правилам, определенным в JVMS 5. Когда линкуется ленивое статическое поле, обычный процесс исполнения инициализаторов класса не должен исчезать. Другими словами, байткод getstatic для ленивого статического поля выполняет всё ту же линковку, что и для любого статического поля. 5. После инициализации (или в ходе уже-запущенной инициализации текущего треда), JVM разрешает элементы константного пула, связанные с полем, и сохраняет полученные из константного пула значения в это самое поле.

Поскольку lazy static final не могут быть пустыми, им нельзя присваивать никаких значений — даже в том небольшом количестве контекстов, где это работает для пустых финальных переменных.

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

Field. Ленивые поля могут быть обнаружены с помощью reflection API, используя два новых метода API в java.lang.reflect. Новый метод isAssigned возвращает false тогда и только тогда, когда поле является ленивым и всё еще не проинициализировано на момент запуска isAssigned. Новый метод isLazy возвращает true тогда и только тогда, когда поле имеет модификатор lazy. Не существует никаких способов узнать, проинициализировано ли поле, кроме как с помощью isAssigned. (Оно может вернуть true чуть ли не на следующем вызове в этом же самом треде, в зависимости от наличия гонок).

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

То есть ленивое поле ссылки не должно инициализироваться в null, а числовые типы не должны иметь нулевое значение. Есть одно необычное ограничение на ленивые финальные поля: они никогда не должны инициализироваться в свои дефолтные значения. Если инициализатор ленивого статического поля возвращает своё значение по умолчанию, линковка этого поля упадёт с соответствующей ошибкой. Ленивое булево значение может быть инициализировано всего одним значением — true, поскольку false является его значением по умолчанию.

чтобы позволить реализациям JVM резервировать значения по умолчанию как внутреннее сторожевое значение, отмечающее состояние неинициализированного поля. Это ограничение введено для того. 4. Значение по умолчанию уже задано в изначальном значении любого поля, установлено в момент подготовки (это описано в JLS 5. Так что это значение естественным образом уже существует в начале жизненного цикла любого поля, и поэтому является логичным выбором для использования в качестве сторожевого значения, отслеживающего состояние этого поля. 2). Для этого JVM может, например, реализовать ленивое поле как неизменяемую ссылку на соответствующий элемент константного пула. Используя эти правила, из ленивого статического поля никогда нельзя получить изначальное значение по умолчанию.

Нулевое число можно обернуть в ненулевую ссылку на Integer. Ограничения на значения по умолчанию можно обойти, оборачивая значения (которые, возможно, равны дефолтным) в боксы или контейнеры какого-то удобного вида. Непримитивные типы можно обернуть в Optional, который становится empty в случае попадания на null.

Если JVM может доказать, что ленивая статическая переменная может быть проинициализирована без наблюдаемых внешних эффектов, она может сделать эту инициализацию в любое время. Чтобы поддержать свободу в способах реализации фичи, требования на метод isAssigned специально занижены. На isAssigned накладывается лишь то требование, что если уж оно вернуло false, то никакие из побочные эффекты от инициализации переменной не должны наблюдаться в текущем треде. В этом случае, isAssigned будет возвращать true даже если getfield никогда не вызывалось. Такой контракт позволяет компилятором подменить ldc на getstatic для собственных полей, что позволяет JVM не заниматься отслеживанием детализированных состояний финальных переменных, имеющих общие или вырожденные элементы в константном пуле. А если он вернул true, то тогда текущей тред может в будущем наблюдать побочные эффекты инициализации.

Как это уже происходит с CONSTANT_Dynamic, JVM выбирает произвольного победителя этой гонки и предоставляет значение этого победителя во все участвующие в гонке треды, и записывает его для всех последующих попыток получить значение. Несколько тредов могут прийти в состояние гонки за инициализацией ленивого финального поля. Чтобы обойти гонки, конкретные реализации JVM могут попробовать использовать CAS операции, если платформа их поддерживает — победитель гонки увидит предыдущее значение по умолчанию, а побеждённые увидят недефолтное значение, выигравшее в гонке.

Таким образом, существующие правила единственного присваивания финальных переменных продолжают работать и теперь захватывают все сложности ленивых вычислений.

Та же самая логика применима к безопасной публикации с помощью финальных полей — она одинакова как для ленивых, так и для неленивых полей.

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

Можно использовать вложенные классы как контейнеры для ленивых переменных.

Можно определить что-то вроде библиотечного API для управления ленивыми значениями или (в более общем смысле) любыми монотонными данными.

Отрефакторить то, что собирались сделать ленивыми статическими переменными так, чтобы они превратились в нульарные статические методы и их тела публиковались с помощью ldc CONSTANT_Dynamic констант, каким-то способом.

Приведённые выше обходные пути не предоставляют бинарно-совместимого способа эволюционно отвязать существующие статические константы от их завязки на <clinit>) (Замечание.

Константный пул не сможет быть хранилищем для нестатических полей, но он всё ещё может держать bootstrap-методы (в зависимости от текущего экземпляра). Если говорить о предоставлении большей функциональности, можно разрешить ленивым полям быть нестатичными или нефинальными, сохраняя текущие соответствия и аналогии между поведением статических и нестатических полей. Такие исследования являются хорошей основой для будущих проектов, построенных на основе этого документа. Frozen arrays (если их реализуют) могут получить ленивый вариант. И кстати, такие возможности делают ещё более осмысленным наше решение запретить значения по умолчанию.

Иногда это кажется очень неприятным ограничением, которое отбрасывает нас назад во времена изобретения пустых финальных переменных. Ленивые переменные должны инициализироваться с помощью собственных инициализирующих выражений. В будущем, можно будет попытаться применить те же возможности и к ленивым финальным переменным. Вспомните, что эти пустые финальные переменные могут инициализироваться произвольными блоками кода, включая логику try-finally, и они могут инициализироваться группами, а не одновременно. Архитектура такой фичи может стать более ясна после появления деконструкторов, поскольку решаемые ими задачи в каком-то смысле пересекаются. Возможно, одна или больше ленивых переменных может быть связана с приватным блоком инициализирующего кода, задача которого заключается в том, чтобы назначить каждую переменную ровно один раз, как это происходит с инициализатором класса или конструктором объекта.

Совсем скоро пройдёт конференция Joker 2018, на которой будет множество видных специалистов по Java и JVM. Минутка рекламы. Посмотреть полный список спикеров и докладов можно на официальном сайте.

Ведущий инженер Da Vinci Machine Project (часть OpenJDK). Джон Роуз — инженер и архитектор JVM в Oracle. Раньше работал над inner classes, делал изначальный порт HotSpot на SPARC, Unsafe API, а также разрабатывал множество динамических, параллельных и гибридных языков, включая Common Lisp, Scheme («esh»), динамические биндинги для C++. Ведущий инженер JSR 292 (Supporting Dynamically Typed Languages on the Java Platform), занимается спецификацией динамических вызовов и связанных вопросов, таких как профилирование типов и улучшенные компиляторные оптимизации.

До перехода в JRG принимал участие в разработке банковских и государственных информационных систем, экосистемы самописных языков программирования, онлайн-игр. Олег Чирухин — на момент написания этого текста работает комьюнити-менеджером в компании JUG.ru Group, занимается популяризацией Java-платформы. Текущие исследовательские интересы включают виртуальные машины, компиляторы и языки программирования.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Из песочницы] От var b до собеседования

Вы почти закончили универ или колледж? Вас пригласили на собеседования, но вы идете туда без подготовки? У вас нет образования (высшего), но хотите работать программистом или в сфере IT? Речь пойдёт по большей степени о поиске работы, я буду говорить ...

OpenSceneGraph: Основы работы с геометрией сцены

OpenGL, являющийся бэкэндом для OpenSceneGraph, использует геометрические примитивы (такие как точки, линии, треугольники и полигональные грани) для построения всех объектов трехмерного мира. Эти данные хранятся в специальных массивах. Эти примитивы задаются данными об их вершинах, в которые входят координаты вершин, ...