Minimum Value Types (Shady Values v0.4)

Эта статья — перевод спецификации, посвященной описанию минимальной реализации типов-значений в Java, которую с нетерпением ждут уже несколько лет.  Изначально статья опубликована на Хабре. Добро пожаловать в MVT!

Замечания к переводу

Этот текст предназначен, в первую очередь, для разработчиков платформы Java. Мы даже немного поспорили с разработчиками: насколько может быть интересен такой текст широкой общественности? В нем слишком много посторонних, очень точных деталей про внутреннюю магию.

Чтобы было понятнее, речь пойдет в том числе о формате классфайла и наборе инструкций JVM, и эти веб-странички, вполне возможно, придется держать перед глазами, чтобы не потеряться в тексте. Лично мне помогал разбираться с этим Владимир Иванов и его доклады (именно для таких случаев и существуют дискуссионные зоны на конференции Joker).

Но давайте взглянем на это с другой стороны. В русскоязычном сообществе складывается тенденция рассматривать Java как некий идеал кровавого ынтерпрайза, неизменный, нерушимый, и находящийся в нерушим застое чуть ли не с самого своего появления. Легенда гласит, что все самое веселое происходит в JavaScript и около того, а на долю джавы приходится скучное ковыряние легаси. Настоящему хакеру совершенно не найти в этом ничего достойного приложения своих экстраординарных способностей.

Существует сообщество, некая общая культура, состоящая из опытных программистов и сетевых чародеев, история которой прослеживается вплоть до первых миникомпьютеров с разделением времени и самых ранних экспериментов с сетью ARPAnet. Члены этой культуры и дали рождение термину «хакер». Хакеры создали Интернет. Хакеры сделали операционную систему Unix тем, чем она является сегодня. Хакеры создали Java и JVM.

Надо четко понимать, что современный хакерский мир не ограничивается лишь маргинальными задачами на (еще/уже) неизвестных языках, и взломов сайта ФБР как в фильме Swordfish. Иногда изящные задачи, требующие много сил и таланта, появляются и в таких известных вещах как платформа Java — в вещах, о которых сегодня пишут заметки на Хабре, а завтра ими будет пользоваться половина мира. Иногда ты смотришь на кусок кода Java и поражаешься — какая неведомая логика привела к такому изощренному техническому решению. Ну вот она, эта логика, описана в нижеследующем тексте.

Это — статья для разработчиков Java. Но её можно и нужно читать сегодня, чтобы успеть ухватить момент. Потому что завтра всё изменится.

Заметка: оно уже обновляется, прямо сейчас! Горшочек, не вари! Обновления с учетом данных с JVM Language Summit и самых свежих наработок будут представлены в следующих статьях. Скорей всего, там же мы напишем некую обзорную статью, дающую верхнеуровневое, более практическое, понимание MVT. Если вам нужна такая статья, просьба написать об этом в комментариях.

Введение

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

Большая часть дискуссий концентрировалась на специализации дженериков как способа реализации полного параметрического полиморфизма в Java и JVM. Мы концентрировались на этом специально, и это принесло свои плоды: помогло найти такие ситуации, когда примитивы не сочетаются со ссылками, что, в свою очередь, заставило нас расширить модель байткода. Разобравшись с List<int>, перейти к List<Complex<int>> было уже куда проще.

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

Возвращаясь из прошлого в настоящее, на JVM Language Summit (2016), и на встрече Valhalla EG, нас много раз просили предоставить сборку с «ранним доступом», на которой можно было бы поиграть с векторизацией, GPU и Panama. Этот документ схематично показывает, какое подмножество экспериментальных возможностей JVM (и в меньшей степени — языка и библиотек) пригодно для первых экспериментов типами-значениями.

Оглядываясь назад, имеет смысл оценить масштаб задачи: тысячи инженеро-часов, посвященных проработке того светлого будущего, в котором мы живем сейчас. Сейчас — самое лучшее время чтобы использовать это видение и, наконец, выбрать первый вариант, что-то типа «hello world» для системы типов-значений.

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

  • Должно просто реализоваться в HotSpot JVM (в референсной реализации)
  • Не следует вносить существенных ограничений на последующую разработку языка Java или виртуальной машины
  • Продвинутые пользователи должны справиться с использованием в целях экспериментов и прототипирования
  • Формат класс-файлов должен как можно меньше меняться
  • Использование таких изменений должно быть жестко ограничено только экспериментальными зонами
  • Пользователи должны иметь возможность вести разработку с помощью стандартных тулчейнов

В дополнение к целям, мы точно не будем:

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

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

Функциональность

Конкретную функциональность для нашей минимальной (но все еще рабочей) поддержки типов-значений, можно сформулировать следующим образом:

  • Несколько специальных value-capable classes (сокращенно, VCC. Int128, и т.п.), каждый из которых виртуальная машина сможет связать с производным типом-значением
  • Синтаксис дескрипторов («Q-types») для описания новых типов-значений в класс-файлах
  • Обновление константного пула — возможность взаимодействовия с этими дескрипторами.
  • Небольшой набор инструкций байткода (vload, и т.п.) для перемещения между локальными переменными JVM и стеком
  • Ограниченная рефлексия для типов-значений (похожая на int.class)
  • Боксинг и анбоксинг, позволяющий выразить значения (как примитивы) в терминах джавовского универсального типа Object
  • Фабрики ссылок на методы, предоставляющие доступ к операциям над значением (доступ к членам, и т.п.)

Специальные value-capable классы, поддерживающие типы-значения, можно разрабатывать в современных тулчейнах как стандартные POJO. Тогда, обычный исходный код на Java, включая дженерик классы и методы, сможет обращаться к значениям только в их боксовой форме. Вместе с этим, ссылки на методы и специально сгенерированный байт код смогут работать со значениями в их изначальной, небоксовой форме.

Эта работа относится к JVM, а не языку. Поэтому мы не собираемся делать следующее:

  • Синтаксис для определения или использования типов-значений напрямую из Java кода
  • Специализированные дженерики в Java коде, которые смогут хранить небоксовые значения (или примитивы)
  • Библиотечные типы-значения или эволюционировавшие классы-значения типа java.util.Optional
  • Доступ до значений из произвольных модулей. (В нормальной ситуации, специальные value-capable классы не должны экспортироваться).

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

Мы ограничили объем этой работы специально, чтобы полезные эксперименты на продуктовой JVM можно было начать делать куда раньше, чем если бы мы выкатили сразу весь стек функциональности типов-значений.

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

Классы VCC

Класс может быть помечен специальной аннотацией @DeriveValueType (или, может быть, атрибутом). Класс, помеченный таким образом, называется value-capable class (сокращенно, VCC). Под этим имеется в виду, что кроме собственного типа, такому классу можно поставить в соответствие производный тип-значение (derived value type, или сокращенно, DVT).

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

Пример:

@jvm.internal.value.DeriveValueType
public final class DoubleComplex {
  public final double re, im;
  private DoubleComplex(double re, double im) {
    this.re = re; this.im = im;
  }
  ... // toString/equals/hashCode, accessors, math functions, etc.
}

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

Суперклассом этого VCC обязательно должен быть Object (похожее требование было и в полном наборе функциональности, в котором суперклассы были запрещены).

Класс, помеченный как VCC должен соответствовать требованиям, предъявляемым к value-based классам, поскольку его экземпляры будут использоваться как боксы для значений связанного типа-значения. В частности, класс и все его поля, должны быть помечены как final, а конструктор должен быть приватным.

Класс, помеченный как VCC обязан не использовать никаких методов, предоставляемых из Object на всех своих экземплярах, т.к. Подобное использование привело бы к неопределенным последствиям операций на боксовой версии. Методы equalshashCode и toString должны быть полностью заменены, не пытаясь позвать Object посредством super.

В качестве исключения, свободно можно использовать метод getClass; он ведет себя как если бы в VCC он заменялся на метод, возвращающий константу.

Как и все value-based классы, оставшиеся методы (clonefinalizewaitnotify и notifyAll) тоже не должны использоваться. Эту особенность мы возложили на пользователя, он должен добиться этого вручную. В полном варианте функциональности мы попытаемся найти способы добиться того же самого автоматически.
Резюмируя, JVM будет делать следующие структурные проверки по отношению к VCC:

  • Класс должен быть помечен как final
  • Класс должен быть правильным классом (не интерфейсом)
  • Суперкласс обязательно Object
  • Все нестатические поля помечены как final
  • Следующие методы Objectдолжны переопределяться: equalshashCodetoString
  • Следующие методы Object не должны переопределяться: clonefinalize

Эти структурные проверки выполняются, когда JVM создает DVT из VCC. Фазы этого процесса описываются ниже.

Помимо описанных выше ограничений, VCC могут делать все, что делают обычные value-based классы. Например, определение конструкторов, методов, полей и вложенных типов, реализацию интерфейсов, определение типовых переменных на себе или на методах. Нет никаких специальных ограничений на типы полей.

Как мы увидим позже, производные типы-значения содержат только поля. Они будут содержать тот же набор полей, что и VCC, из которого получены. Но JVM не станет добавлять им методы, конструкторы, вложенные типы или супер-типы. (В полном наборе функциональности, конечно, типы-значения будут «кодироваться как классы» и поддерживать все эти возможности).

Заметка: VCC, которые скомпилированы с использованием стандартного компилятора javac, не смогут определять вложенные поля (точнее, «inline sub-value»), которые сами по себе являются типами-значениями. Максимум что можно сделать — проставить поля с их связанными ссылочными типами («L-типами»). Обновленная версия javaс сможет объявлять такие под-значения сразу как настоящие «Q-типы». Эта версия javac даст разработчикам возможность работать с типами-значениями напрямую, не используя фокус с созданием отдельного производного типа-значения из VCC. Поэтому, если вы в VCC видите поле, которое самое по себе типизировано как VCC, это скорей всего, ошибка, так как приведет к непреднамеренному боксингу вложенного под-значения.

Вот чуть более подробный пример VCC, который описывает супербольшой long:

@DeriveValueType
final class Int128 extends Comparable<Int128> {
  private final long x0, x1;
  private Int128(long x0, long x1) { ... }
  public static Int128 zero() { ... }
  public static Int128 from(int x) { ... }
  public static Int128 from(long x) { ... }
  public static Int128 from(long hi, long lo) { ... }
  public static long high(Int128 i) { ... }
  public static long low(Int128 i) { ... }
  // possibly array input/output methods
  public static boolean equals(Int128 a, Int128 b) { ... }
  public static int hashCode(Int128 a) { ... }
  public static String toString(Int128 a) { ... }
  public static Int128 plus(Int128 a, Int128 b) { ... }
  public static Int128 minus(Int128 a, Int128 b) { ... }
  // more arithmetic ops, bit-shift ops
  public int compareTo(Int128 i) { ... }
  public boolean equals(Int128 i) { ... }
  public int hashCode() { ... }
  public boolean equals(Object x) { ... }
  public String toString() { ... }
}

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

Заметка: в первую очередь VCC появятся для расширения числовых типов, например, long. Поэтому они должны сразу следовать какому-то стандарту и иметь консистентный набор арифметических и битовых операций. Рано или поздно, понадобится создать набор интерфейсов, которые выразят общую структуру операций для числовых примитивов и числовых значений.

Тип-значение и тип-объект

Когда JVM загружает VCC, оно может или агрессивно создать производный тип-значение, или наоборот — выставить на классе флаг, который будет указывать на то, что тип-значение следует создавать по запросу. (К использованию рекомендуется именно второй способ).

Заметка: минимальный набор функциональности может не рассматривать этот порядок и оставить его неопределенным. Что касается полной версии, вопрос спорный, т.к. производные тип-значения и VCC в ней идентичны.

Сам по себе VCC совершенно не меняется на этапе загрузки. Он остается обычным POJO для value-based класса.

Соответствующий ему тип-значение создается как копия этого класса, но со следующими критическими изменениями:

  • Производный тип-значение помечается соответствующим образом (как «value-type»)
  • Производному типу-значению присваивается новое имя, полученное на основе изначального VCC
  • Все супер-типы изначального VCC стираются
  • Нестатические поля исходного VCC переносятся без изменений

Имя, данное DVT спрятано внутри реализации. Вместо этого, для обозначения обоих типов используется имя VCC, и утверждается, что всегда существует достаточно информации чтобы разрулить неопределенность. В дескрипторах байткода, буквы Q и L используется для отражения этой разницы, мы обозначаем VCC как Q-тип, а DVT — как L-тип.

Создание DVT должно происходить в какой-то момент после загрузки VCC, но перед первым созданием экземпляра DVT. Это контролируется семантикой тех конкретных инструкций, которые запускают инициализацию DVT ровно тем же способом, что и некогда ныне существующие инструкции (такие как getstatic или new) запускают инициализацию обычных классов. Детали будут описаны ниже.

Давайте снова начнем с примера класса DoubleComplex:

@jvm.internal.value.DeriveValueType
public final class DoubleComplex {
  public final double re, im;
  ...
  double realPart() { return re; }
}

Когда JVM решает синтезировать производный тип-значение для типа DobuleComplex, оно делает свежую копию, вырезая все члены класса кроме полей double. Важно отметить, что, для превращения синтетического класса в value-type, JVM использует не просто тип-объект, а особую внутреннюю магию.

Внутри JVM, результирующий тип-значение будет выглядеть как-то так:

@jvm.internal.value.DeriveValueType
public final class L-DoubleComplex {
  public final double re, im;
  ...
  double realPart() { return $value.re; }
}
public static __ByValue class Q-DoubleComplex {
  public final double re, im;
}

 

Гипотетическое ключевое слово __ByValue показывает, что вместо ссылок определяются значения. Пока мы серьезно не улучшим весь стек, такие вещи не могут быть напрямую записаны в исходном коде, но это разумно и полезно делать на этапе загрузки класса.

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

Заметка: подобный дизайн можно назвать «box-first», проектированием исходя из боксинга. Смысл в том, что JVM загружает только боксы, а тип-значение создается как побочный эффект загрузки. В конце концов, мы придем к такому дизайну, когда сами значения станут отправной точкой проектирования, но текущий box-first дизайн накладывает куда меньше ограничений на инструментарий, используемый для чтения и записи класс-файлов, включая JVM и javac. Поэтому, несмотря некоторую странность, этот box-first дизайн в данный момент является наилучшим решением.

Боксинг, анбоксинг и заимствования

Под капотом JVM, для конвертации между VCC и типом-значением, расставляются операции боксинга и анбоксинга. Семантика этих операций — простое побитовое копирование между ними. Оно, очевидно, хорошо определено, поэтому списки полей — идентичные.

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

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

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

Синтетическая операция анбоксинга также позволяет производному типу-значению использовать интерфейсы своего VCC, но не напрямую. Снова, если производный тип-значение передается куда-то, где нужен интерфейс, разработчик может просто забоксить его и передать ссылку на бокс. (В полной версии, мы хотим дать значению напрямую работать с интерфейсами, чтобы никакого боксинга разработчику видно не было. Это потребует аккуратной работы над взаимодействием значений и интерфейсов).

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

Дескрипторы значений

При использовании value-capable модулей (VCM), язык описания класс-файлов расширяется Q-типами, которые напрямую обозначают типы-значения (без боксинга). Синтаксис дескриптора: QBinaryName;, где BinaryName — это внутренняя форма имени VCC (внутренняя форма отличается тем, что в ней все слеши заменены на точки). По факту, имя класса получается из имени соответствующего VCC.

Для сравнения, стандартный дескриптор ссылочного типа называется L-типом. Если класс C является VCC, у него есть и Q-тип, и L-тип. Заметьте, что использование L-типов никак не коррелирует с использованием Q-типов. Например, они могут быть одновременно представлены в типах методов, причем, в любых сочетаниях.

Дескриптор Q-типа может быть использован как тип поля класса, определенного в VCM. Но тот же дескриптор не может встречаться в ссылке на поле (CONSTANT_Fieldref) для этого поля (даже в VCM!), если эта ссылка используется хотя бы в одной из четырех групп инструкций getfield.

(Заметка: фабрики ссылок на методы, описанные ниже, будут поддерживать загрузку и обновление полей, как для значений, так и для объектов. В нашем прототипе, сами инструкции полей мы менять не станем.)

Дескриптор Q-типа может быть типом элемента массива в классе, принадлежащем VCM (и снова, это относится только к VCM, и скорей всего, только к специальной экспериментальной версии класс-файлов. Давайте перестанем это повторять снова и снова, поскольку это ограничение описано в самом начале). Нет никаких байткодов для создания, чтения или записи таких массивов, но в нашем прототипе для таких функций будут доступны ссылки на методы.

Поле массива Q-типа инициализируется значением по-умолчанию для этого типа-значения, а не null. Это значение по-умолчанию (по крайней мере, прямо сейчас) определяется как комбинация значений по-умолчанию для отдельных элементов. Такое умолчательное значение можно получить из подходящей ссылки на метод, такой как комбинатор MethodHandles.empty.

(Другими словами, значения по умолчанию строятся просто комбинированием типо-специфичных умолчаний для nullfalse\0 и 0.0. Все переменные в куче Java инициализируются этими нулевыми данными, включая и значения. Появление возможности вручную указывать умолчания маловероятно, ну или по крайней мере, это появится только в будущем).

Дескриптор Q-типа может быть параметром или типом возвращаемого значения для метода в файле класса. Как описано ниже, верификатор заставляет возвращать для таких параметров значения, совпадающие с Q-типом (но не соответствующим L-типом или любым другим типом).

Любая ссылка на метод (константа CONSTANT_Methodref или CONSTANT_InterfaceMethodRef) может указывать Q-тип в дескрипторе. После разрешения такой константы, определение такого метода уже не может быть нативным, и должно использовать байткоды, которые напрямую работают со значениями Q-типа.

Более того, константа CONSTANT_Fieldrefможет использовать Q-тип в своем собственном дескрипторе.

Заметьте, что язык Java не предоставляет никакого непосредственного способа создавать Q-типы в файлах классов. Тем не менее, генераторы байткода могут использовать такие типы и работать с ними. Скорее всего, проект Valhalla приведет к созданию экспериментальных возможностей языка, которые позволят работать с Q-типами напрямую из исходного кода.

 

Инструкции и константный пул

Несмотря на то, что наши типы-значения уже получили имена и члены вроде ссылочных типов, они все еще отличаются от любых ссылочных типов, поэтому для внедрения Q-типов, нам нужно расширить структуры константного пула.

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

Между тем, некоторые типы в константном пуле используют «сырые» имена классов, без обычных для дескрипторов букв (L в начале и; в конце). В частности, константа CONSTANT__Class напрямую привязывается (после разрешения) к загруженному файлу класса, так что соответствующее отражение совершенно точно является основным отражением класса. Но что делать, если классфайлу нужно использовать второе отражение?

При использовании байткодов ldc или ldc_w, или на статическом аргументе, CONSTANT_Class, начинающийся с экранированного дескриптора, разрешается в главное отражение Class для разрешенного класса. Если нужно, вторичное отражение может быть получено, используя CONSTANT_Dynamic (из отдельного документа JDK-8177279).

При использовании в качестве компонента класса в константах CONSTANT_Methodref или CONSTANT_FieldrefCONSTANT_Class всегда будет указывать на сам классфайл, без «привязки» к какому-то конкретному «представлению». Байткод, который использует поле или ссылку на метод, будет определять, является ли получатель Q или L типом. Об этой «привязке» байткода можно говорить как о неком режиме, таком что стандартная инструкция getfieldстановится инструкцией L-режима, а vgetfield — инструкцией Q-режима.

(Заметка: подобный дизайн «универсальных» ссылок на поля и методы подразумевает, что JVM собирается закэшировать достаточно ресурсов в элементе константного пула для CONSTANT_Fieldref, чтобы хватило и на getfield и на vgetfield, что означает либо дублирование памяти, либо строгое выравнивание между раскладкой в памяти забоксенных L-значений и буферизованных Q-значений одного и того же класса. То же самое про ссылки на методы. Выравнивание может иметь важное значение в реализации самой JVM, можно сделать значения Q-типов и L-типов внутренне очень похожими, за исключением конкретного способа хранения и идентификации. Это очень хорошо, особо если мы когда-нибудь добавим еще и U-типы).

В случае API MethodHandles.Lookup, разница между Q-типами и L-типами (и соответственно, режимами вызова) будет отмечена в отражении Class, переданном первым аргументом в вызове API, такого как Lookup.findGetter. Если в findGetter передано вторичное отражение для Q-типа, оно вернет геттер поля из получателя по значению. Если же передано первичное отражение (L-тип), тогда, конечно, оно вернет получателю геттер поля по ссылке, в точности как это и происходит сейчас.

 

Ограничения на Q-режим и вызовы методов

При выполнении вызовов методов в Q-режиме (с получателем Q-типа), нельзя использовать никакие методы из java.lang.Object; JVM или рантайм ссылок на методы возможно, даже будет строго следить за выполнением этого правила. Другими словами, Q-типы не наследуются от Object. Вместо этого, они либо определяют свои собственные методы, которые заменят методы из Object, по тем же правилам, что и value-based классы, либо просто не будут пользоваться методами из Object.

Как исключение, метод Object.getClass разрешен к использованию, но он должен возвращать первичное отражение Class, соответствующее VCC.

Заметка: идея в том, что getClass просто сообщает класс-отражение загруженного файла, чтобы установить тип объекта. Эта идея может измениться.

Эти ограничения применяются ко ссылкам на методы, полученным из Q-типов и для инструкций vinvoke (если они поддерживаются).

Изменения в JVM, необходимые для поддержки Q-типов

Q-типы, как и другие типы дескрипторов типов, могут использоваться во множестве мест. Вот основные:

  • определения методов и полей (UTF8 ссылки в структурах method_info и field_info)
  • символьные ссылкина методы и поля (UTF8 компонент в CONSTANT_NameAndType)
  • Имена типов (UTF8 ссылки в константах CONSTANT_Class)
  • Типы элемента массива (после левой скобочки [) в любом дескрипторе
  • Типы в stack map верификатора (с помощью нового кода для Q-типов)
  • Операнд (CONSTANT_Class) какого-то байткода (описано ниже)

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

Минимальная модель требует обработку Q-типов в элементах массивов и полях объектов (или значений), как минимум, достаточно специализированную, чтобы инициализировать такие поля их значениями по-умолчанию, которые не являются (и не могут быть) обычным null L-типа.

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

Если тип-значение содержит только примитивные поля, то необходимо реализовать создание массивов такого типа-значения, и в этих массивах данные должны храниться максимально плоско.

(Заметка: этот документ вводит так называемые «плоские массивы», элементами которых с начала и до конца являются структуры. Укороченная версия реализации может выбросить поддержку каких-то конкретных, или вообще всех подобных массивов, предназначенных для хранения значений. Например, даже если поля типов-значений будут содержать одновременно и примитивы, и ссылки, и/или под-значения, то массивы, которые смогут реализовывать такие смешанные наборы типов — реализовать будет куда как сложнее, чем просто массивы, содержащие значения только примитивных полей. Такие массивы можно выбросить из самых ранних реализаций. Те вызовы API, которые ожидают подобные сложные массивы, временно могут бросать ошибки и не возвращать никаких значений. Мы предупреждали!)

Плоские массивы, когда их таки реализуют, должны будут иметь компонентный тип, в свою очередь тоже являющийся Q-типом. Они будут отличаться от массивов соответствующих L-типов, по тому же принципу, как Integer[].class отличается от int[].class. Точно так же, супер-типы численных массивов будут только Object (как и в int[]), а не каким-то отдельным типом массива. Такие массивы не будут преобразовываться ни в какие другие типы массива, и работать с ними следует только с помощью напрямую полученных ссылок на методы.

В современной JVM, когда для класса создается первая реализация с помощью new, запускается его инициализация (за исключением случая, когда она уже произошла с помощью какой-то другой инструкции типа getstatic). Инициализация рекурсивно запускает инициализацию супер-класса, примерно так же, как раньше загрузка класса запускала загрузку супер-класса. Когда тип-значение внедряется в файл класса, он работает примерно как супертип (и при этом уважает фазы загрузки и инициализации класса).

В частности, из того что в объекте (назовем его «контейнером») поле Q-типа обладает корректным значением значением Q-типа (значением по-умолчанию) с первых моментов существования объекта-контейнера, напрямую следует, что класс этого поля должен быть инициализирован заранее, до того, как будет создан контейнер. Это требование довольно похоже на требование инициализации суперкласса, и их можно считать проявлением одного и того принципа: Когда класс иницилизируется (и соответственно, загружается), вначале нужно инициализировать (и загрузить) его зависимости, и «зависимостями» могут являться и суперклассы, и классы любых значений, встроенные в структуру подклассов. (Как если бы суперкласс в подклассе захватывал большое анонимное поле с типом-значением). Гарантируя правильное время инициализации классов полей, мы можем установить инвариант, под которым, методы класса могут работать на экземплярах класса (и на чистых значениях, и на объектах), пока не запустится инициализация класса.

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

Заметьте, что инструкция vunbox, в реальности должна несколько ослабить свои требования, поскольку уже существует живой экземпляр VCC, и единственное, что еще осталось сделать — создать DVT и увидеть, что его инициализация ничего не делает, потому что не может содержать никакого кода.

Байткоды значений

Были добавлены следующие байткоды инструкций:

  • vload кладет значение (Q-типа) из локала на стек
  • vstore берет значение (Q-типа) со стека в локал
  • vreturn берет значение (Q-типа) со стека и возвращает из текущего метода
  • vbox и vunbox преобразуют между соответствующими Q-типом и L-типом
  • vaload и vastore организуют доступ к «плоским» массивам Q-типа
  • vdefault кладет на стек значение по-умолчанию, уникальное для каждого конкретного Q-типа
  • vgetfield забирает Q-тип и кладет поле, выбранное из Q-типа
  • vwithfield забирает Q-тип и выбранное поле, и кладет обновленный Q-тип

Значения хранятся в отдельных локалах, а не в группах локалов, как это происходит с long и double(конкретнее, они используют их пары).

Формат этих инструкций — все еще следует обсуждать. Некоторые из них должны иметь поле операнда, которые описывает тип значения, над которым производится манипуляция. Инструкции манипуляции над полями требуют CONSTANT_Fieldref. Определенно, vboxvunbox и vdefault требуют иметь явное поле типа операнда.

JVM может использовать разрешение Q-типов для получения информации о размере Q-типа и требований на выравнивание, чтобы правильно «упаковать» их во фрейм стека интерпретатора. Или может быть, JVM может просто использовать забоксенные или буферизованные представления (соответствующие value-capable L-типы, или какой-то внутренний тип кучи или стека) и проигнорировать всю информацию о размере.

Похоже, что для для инструкций, посвященных перемещению данных, мы можем выбросить операнды типов. Если мы вдруг увидим, что интерпретатор JVM обязан использовать универсальный с точки зрения внутренней реализации «тип-носитель» для всех типов-значений на стеке, то можно просто потребовать, чтобы этот «носитель» был само-описываемым, и тогда в инструкциях перемещения нам не нужно будет пере-подтверждать точный тип значения.

Так как инструкции invokevirtualinvokespecial и invokeinterface являются инструкциями Q-режима, они не смогут вызывать методы на Q-значениях. Ссылки на методы, invokestatic и invokedynamic всегда будут позволять вызывать методы на Q-типах, и для начала этого вполне достаточно. Такие ссылки на методы могут, на самом деле, внутренне боксить Q-тип и запускать метод соответствующего L-типа, но эту тактику можно улучшать и оптимизировать в библиотеках поддержки Java, без постоянных провалов в интерпретатор.

(Заметка: совершенно очевидно, что в полной версии у нас будет специальная инструкция для вызовов в Q-режиме, т.е. vinvoke, и она позволит делать все эти вызовы без боксинга. Для U-типов, инструкция uninvoke, работающая соответственно в U-режиме, точно таким же образом будет работать на динамически помеченном значении-получателе, которое будет иметь либо Q-тип, либо L-тип. Этот расширенный режим может показаться несколько экстравагантным, но, по-видимому, он действительно нужен для алгоритмах на дженериках, которые возникают при использовании интерфейсов и типов переменных, которые могут работать на диапазонах как чистых значений, так и ссылок на объекты.)

Взаимодействие с верификатором

При определении состояния на входе в метод, если Q-тип встречается в дескрипторах аргументов метода, верификатор смотрит, существует ли этот Q-тип (не L-тип!) в соответствующем локале на момент входа.

При возврате из метода, если тип возвращаемого значения является Q-типом, этот же Q-тип должен находиться на вершине стека.

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

Как и в случае с примитивными типами intи float, Q-тип сам по себе не преобразуется ни в какие другие типы верификатора, или в супер-типы oneWord и top. Это влияет на соответствующие значения при вызове методов, и на точки слияния в потоке управления. Q-типы не конвертируются в L-типы, и даже в боксы или супер-типы (Object, интерфейсы) своих L-типов тоже не конвертируются.

Кроме очевидных, vloadvstorevreturn и семейства инструкций invoke, единственные байткоды, которые гарантированно производят или потребляют операнды Q-типов — это poppop2swap, и семейство инструкций dup. Со временем будут добавляться новые байткоды. Верификатор проверяет правильное использование Q-типов.

Инструкции vaload и vastore работают по образу и подобию существующих инструкций для массивов. Если у нас есть универсальный «тип-носитель», нет никакой необходимости для этих инструкций пере-подтверждать Q-тип, над которым они работают. Его всегда можно получить из самого массива.

Инструкция vgetfield имеет уровень доступа, аналогичный существующей getfield. Если в каком-то типе-значении есть публичное поле, любой класс может прочитать это это поле из значения соответствующего типа.

Но вот vwithfield уже имеет жесткий контроль доступа, вне зависимости от доступности поля. Только класс с приватным доступом до типа-значения может выполнять замену поля. Это ограничение аналогично инструкцииputfield на final-поле, которая разрешена только классу, в котором это поле объявлено, и на самом деле, только в конструкторах этого класса. Поскольку VCC и DVT являются двумя сторонами одного и того же логического типа, JVM должна позволять VCC выполнять vwithfield и на его DVT. И наоборот, с помощью ссылок на методы, разве что VCC каким-то будет содержать встроенные инструкции vwithfield.

(Заметка: инструкции обновления полей-значений примерно соответствуют инструкциям обновления полей-объектов, в которых эти поля — final. Правила отличаются в деталях. JVM проверяет, чтобы putfield выполнился только на final полях, и только в конструкторах. Язык Java усиливает ограничения, гарантируя, что каждое такое поле должно устанавливаться в точности один раз, по всем путям исполнения до нормального выхода из конструктора. Точно так же, язык будет проверять правильную инициализацию полей-значений, но здесь уже JVM не играет никакой конкретной роли, разве что ограничивает использование vwithfield приватным кодом. Это ограничение не идентично своему варианту для final полей, поскольку vwithfield можно легально использовать и вне конструкторов значений.)

Рефлексивный Lookup API позволит VCC и DVT предоставить доступ к приватным членам и возможностям друг друга, как сейчас это происходит в nestmates. Включая разрешение использовать инструкцию vwithfield. Поскольку DVT не содержит никаких методов, этот обмен правами ассиметричен, но в любом случае, он двунаправленный по самой идее.

(Заметка: будущие версии JVM могут реализовать явные nestmates на уровне VM, которые будут испить доступ к приватным полям и методам друг друга. В этих версиях JVM, инструкция vwithfieldбудет доступна для всех nestmates каждого конкретного типа-значения. Другими словами, vwithfieldдоступна внутри некоей «капсулы», в которой доступны все приватные методы).

Инструкция vdefault, похоже, очень «приватная» для каждого конкретного типа-значения, поскольку позволяет создание значения без использования конструктора. Но значения по-умолчанию имеют в JVM очень особый статус, поскольку любой массив определенного типа всегда пред-заполнен этими значениями по-умолчанию. Поэтому, на самом деле, нет ничего «приватного» в vdefault. Любой класс когда угодно может вычислить значение по-умолчанию для любого Q-типа.

Q-типы и байткоды

Другие байткоды тоже могут взаимодействовать с Q-типами, по крайней мере эти:

  • Все байткоды вызова: любой аргумент или возвращаемое значение может быть Q-типом. Получатель (компонент класс в Methodref) может и не уметь этого, даже для статических членов
  • ldc и ldc_w (Q-типа, или возможно, динамически сгенерированной константы)
  • anrearraymultianewarray работает на массивах с элементами Q-типа

Следующие инструкции могут не поддерживаться в минимальной реализации:

  • getfieldputfieldgetstaticputstatic (для значения Q-типа)

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

(Заметьте, что такие файлы классов будут создаваться прямой раскруткой байткода, или расширенной нестандартной версией javac. VCC не будут содержать дескрипторов Q-типа, поскольку стандартная версия javac никогда их не генерирует).

Рефлексия типов-значений

В мире типов-значений, есть фундаментальное отличие, которое заключается в роли рефлексивного типа-отражения класса, java.lang.Class. До появления типов-значений, между файлами классов, и их Class-отражениями существовало отношение «один к одному». Единственное исключение было — отражения примитивных типов вроде int.class, им не соответствовало ни одного файла класса. После появления типов-значений, один файл класса может соответствовать более чем одному Class-отражению. В особенности, загрузка типа-значения сразу же делает доступными два отражения, одно из которых Q-типа, и другое — L-типа. Такое отношение похоже на уже существующее между int.class и Integer.class, но в данном случае они означают один и тот же класс с разных точек зрения или разных проекций. (Еще будет U-тип, который объединит их оба. Специализируемые классы смогут создавать неограниченное число производных специализаций).

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

( Заметка: если посмотреть на это хорошенько прищурившись, можно заметить, что Integer.classработает как основное зеркало для вторичного зеркала int.class. Возможно, в будущем их можно будет как-то объединить, ибо на самом деле они говорят об одном и том же общем классе).

Для этой минимальной реализации типов-значений, мы идем в светлое будущее, выделяя Class-отражения для DVT и VCC, и работая с VCC как с основным отражением, а DVT — вторичным.

В изначальном прототипе, публичный класс jdk.experimental.value.ValueType (во внутреннем модуле) будет содержать все методы runtime-реализации этих значений.

ValueType будет содержать следующие публичные методы для рефлексии Q-типов:

public class ValueType<T> {
  static boolean classHasValueType(Class<T> x);
  static ValueType<T> forClass(Class<T> x);
  Class<T> valueClass();  // DVT, secondary mirror
  Class<T> boxClass();  // VCC, principal mirror
  ...
}

Предикат classHasValueType является истинным, если аргумент, представляет собой Q-тип или VCC (его L-тип). Фабрика forClass возвращает дескриптор Q-типа для любого типа, производного от VCC. (В случае любого другого типа оно кидает IllegalArgumentException. Если хочется избежать исключения, можно заранее выполнить проверку classHasValueType).

Два акцессора valueClass и boxClass возвращают, соответственно, либо в точности объекты java.lang.Class этого Q-типа, либо изначальные (VCC) L-типы.

В этом случаеValueType.forClass(vt.valueClass()) совпадает с vt, то же самое для boxClass. Поэтому, любой Class типа-значения может быть использован для получения своего дескриптора ValueType.

Легаси метод Class.forName продолжит возвращать boxClass, из соображений совместимости. Это поведение может или сохраниться, или измениться в будущем. (В будущем, конструкция языка T.class будет отражать в исходном коде что-то более подходящее для типа, присвоенного T, чтобы соответствовать слогану «работает как int».

Вторичное отражение Q-типа не сможет работать с наиболее осмысленными рефлексивными запросами, такими как getDeclaredMethods. Причина заключается в том, что DVT, полученное из VCC, должно быть как можно более легким. Скорей всего, рефлексивные запросы сидят только поля, и всё.

В любом случае, для проверки членов типа-значения, пользователям следует прибегать к использованию VCC (boxClass). (Такой расклад может поменять, если вдруг оба класса сольются). Это вполне нормально работает, потому что VCC загружается и обслуживается как обычный POJO, а DVT из него получается без всяких изменений.

Несобственные классы для Q-типов могут возникать при использовании рефлексивного API примерно там же, где возникают примитивные псевдо-типы (вроде int.class). Эти API включают как самые основы рефлексии (Class и типы в java.lang.reflect), так и новые API в java.lang.invoke, такие как MethodType и MethodHandles.Lookup. Константы константного пула, которые работают с этими типами, могут обращаться с Q-типами точно так же, как с L-типами, а все различия отражены верным выбором объектов Class.

(Несобственные классы иногда называются крассами, созвучно английскому слову «crass» — «грубый». Этот каламбур имеет целью подчеркнуть, что такой красс существует исключительно для материализации подобного разделения в рантайме. Основной класс — это нечто, возвращаемое методом Class.forName, что предназначена представлять класс в отношении один к одному. С свою очередь, «красс» — это что угодно другое, но все еще имеющее тип java.lang.ClassБолее строгий подход к рефлексии использует «отражения классов» в достаточной степени материализованной иерархии типов интерфейсов).

Как всегда, API ссылок на методы можно использовать для манипуляции с массивами, загрузки и выгрузки полей, для вызова методов и получения ссылок на методы. Отражения Q-типа будут, в общем случае, работать примерно как существующие отражения примитивов, сообщая JVM о необходимости передавать данные как чистое значение, а не ссылку на его бокс.

Трансформации, которые могут изменять типы (такие как asType) будут делать боксинг и анбоксинг типов-значений точно так же, как они делают это с боксингом и анбоксингом примитивов. Поэтому, следующий код создает ссылку на метод, отбоксит значение типа DobuleComplex в отдельный объект:

Class<DoubleComplex> srcType = DoubleComplex.class;
Class<DoubleComplex> qt = ValueType.forClass(srcType).valueClass();
MethodHandle mh = identity(qt).asType(methodType(Object.class, qt));

Конечно, преобразующий методы MethodHandle.invoke даст возможность пользователям работать со ссылками на объекты для Q-типов: или в терминах боксовых типов, как это прямо сейчас уже реализовано в языке Java, или (используя соответствующие байткоды) более непосредственно, в терминах Q-типов.

Боксовые значения

Боксинг полезен для обеспечения взаимодействия между Q-типами и API, которые все еще используют ссылки на Object для выполнения каких-то обобщенных действий. Многие инструменты (такие как отладчики, логгеры, или какой-нибудь println), подразумевают на вход стандартный формат Object, это позволяет отображать им произвольнее данные. VCC L-типа или Q-типа (короче, какой бы механизм в конце концов не прикрутили в качестве контейнера Q-типов) играют важное значение не только в текущем прототипе, но и, скорей всего, останутся в полностью завершенной системе.

Как мы отмечали раньше, экземпляры VCC (которые имеют L-тип), в первую очередь используются как боксы для значений соответствующего Q-типа. API ссылок на методы позволит реализовать такие операторы преобразований боксинга-анбоксинга, которые будут выглядеть как ссылки на методы, или даже смогут неявно применяться к аргументам.

VCC L-типа также позволят сделать красивую спецификацию для части возможностей Q-типов, например toString — можно будет написать их как обычные Java методы с использованием L-типов. Рантайм поиска ссылок на методы будет следить за различиями между боксовым и небоксовым получателем (this), и по ресурсам это будет почти ничего не стоить (HotSpot JVM уже обладает достаточным набором оптимизаций скаляризации, которые позволят выбросить этап боксинга).

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

Так как VCC являются value-based, не следует делать синхронизацию на боксах, делать какие-то решения на основе идентичности ссылок, пытаться изменять их поля, или пытаться использовать null для проверки боксов.

(Заметка: эти ограничения, скорей всего, останутся в силе, даже если в будущем сами боксы поменяются).

Будущие версии JVM могут помочь в определении (или даже подавлении) некоторых из этих ошибок, а также они могут предоставить дополнительные оптимизации, если вдруг заметят такие боксы (и это не потребует полного escape analysis).

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

Фабрики значений

Получив возможность выполнять ссылки на методы, которые работают с Q-типами, все остальные возможности (временно) могут быть использованы исключительно посредством ссылок на методы. Включая следующее:

  • Преобразующие операции (такие как боксинг)
  • Получение значений по-умолчанию для Q-типов
  • Создание Q-типов
  • Сравнение Q-типов
  • Вызов методов, определенных для Q-типов
  • Чтение полей Q-типов
  • Обновление полей Q-типов
  • Чтение и запись полей (или элементов массивов), имеющих Q-тип
  • Конструирование, чтение и запись массивов Q-типов

MethodHandles.Lookup и API MethodHandles сможет работать над Q-типами (представленными как Class-объекты), и предоставят методы, которые смогут выполнять практически все эти функции.

(Заметка: иногда полезно думать об этих операциях как о недостающих байткодах. Они могут использоваться вместе с invokedynamic, чтобы эмулировать те байткоды, которые потребуются экспериментальным транслятором. Рано или поздно, некоторые из множества таких байткодов «осядут» прямо в основном наборе байткодов JVM, и тогда использование invokedynamic понемногу станет уменьшаться).

Существующее API ссылок на методы должно быть улучшено следующим способом:

  • Фабрики MethodType должны принимать объекты Class, представляющие Q-типы, точно так же, как сейчас это делается для примитивов.
  • invokeasType и explicitCastArguments должны работать с парами Q-типам/L-тип так же, как они работают с парами примитив/обертка.
  • Lookup.in позволит свободно делать преобразования между парами Q-тип/L-тип (без потери привилегированных режимов)
  • Нестатический поиск по Q-типам будет возвращать ссылки на методы, первый параметр которых будет именно Q-типом (но не L-типом)
  • Lookup.findVirtual предоставит все доступные нестатические методы Q-типа, если ему передали класс Q-типа.
  • Lookup.findConstructor предоставит все доступные конструкторы изначального VCC, как для Q-типа, так и для легаси L-типа. Тип возвращаемого значения ссылки на метод, возвращенный из findConstructor будет идентично искомому классу, даже если он имеет Q-тип.
  • Методы findVirtual и findConstructor могут также производить ad-hoc pattern matching (требует обсуждения!) на методах с модификаторами private static, принадлежащих изначальному L-типу, и это можно считать конвенцией для добавления методов только к Q-типу. (Это полезно в случае, когда эти методы сложно или неправильно делать виртуальными методами L-типа).
  • Фабрика identity должна поддерживать Q-типы
  • Фабрика empty должна поддерживать Q-типы, производя такую ссылку на метод, которая вернет значение по-умолчанию для данного типа.
  • Все фабрики, занимающиеся обработкой массивов, должны поддерживать Q-типы, производя методы для создания, чтения и записи массивов Q-типа. (Включая arrayConstructorarrayLengtharrayElementGetter, and arrayElementSetter, плюс варианты с var-handle. Следует отметить, что некоторые из этих фабрик могут бросать исключения, если вдруг соответствующие типы массивов не поддерживаются).
  • Все преобразователи должны принимать ссылки на методы, которые работают с Q-типами, точно так же, как это сейчас происходит с примитивными типами.

(Заметка: да, метод типа-значения, получается с помощью findVirtual, несмотря на то, что виртуальность не существует для final классов. Более слабый вариант — подключить к этому findSpecial, или использовать новое API findDirect, для того, чтобы подчеркнуть разницу. Но поскольку Java уже нормально живет с «final virtual» методами, мы все-таки будем использовать существующие инструменты.)

Поиск ссылки на метод на L-типах, скорее всего, сможет дать дополнительные результаты, несмотря на то, что (в зависимости от модели экспериментов пользователя) некоторые методы на на L-типах могут быть скрыты, по причине того что боксовые типы будут не такими выразительными, по сравнению со своими типами-значениями. Конечно, виртуальные метод L-типа, представленный как ссылка на метод, будет принимать параметр получателя L-типа, а не Q-типа.

С другой стороны, как сказано выше, мы можем придумать специализированные конвенции для связи private static методов L-типа, с помощью ссылок на методы, как если бы они были конструкторами или нестатическими методами Q-типа. Это станет дополнительной договоренностью между стратегией трансляции и рантаймом ссылок на методы, о котором JVM ничего не узнает. Статические методы, видимые только для рантайма ссылок на методы, будут содержать некие куски логики, предназначенные только для использования Q-типом. Ну и конечно, мы превратим все такие штуки в байткоды и задвинем под капот JVM.

Также как и value-based классы, VCC необходимо переопределись все соответствующие методы из Object. Производные Q-типы не наследуют, и не отвечают на стандартные методы Object. Они отвечают только на те методы, для которых самостоятельно реализовали точно такую же сигнатуру (например, можно реализовать свой toString).

Дополнительные функции, о которых дальше идет речь, (на данный момент) не укладываются в API MethodHandle, и поэтому скомпонованы в класс поддержки jdk.experimental.value.ValueType.

ValueType будет содержать следующие методы:

public class ValueType<T> {
  ...
  MethodHandle defaultValueConstant();
  MethodHandle substitutabilityTest();
  MethodHandle substitutabilityHashCode();
  MethodHandle findWither(Lookup lookup, Class<?> refc,
                          String name, Class<?> type);
}

Метод defaultValueConstant возвращает ссылку на метод, которая не принимает аргументов, и возвращает значение по-умолчанию для этого Q-типа. Это эквивалент (скорей всего, куда более эффективный) для создания массива из одного элемента этого типа-значения с последующей загрузкой результата.

Метод substitutabilityTest возвращает ссылку на метод, которая сравнивает два операнда переданного Q-типа и выносит решение о возможности их взаимозаменяемости (взаимоподстановке). В частности, поля сравниваются парами на предмет взаимозаменяемости, и результат является логической конъюнкцией всех этих сравнений. Примитивы и ссылки являются взаимозаменяемыми тогда и только тогда, когда они равны при использовании соответствующей версии оператора == языка Java, исключая специальный случай с float и double, которые вначале конвертируются в «сырые биты», и только потом сравниваются.

Так же, метод substitutabilityHashCode возвращает ссылку на метод, который возвращает ссылку на метод, который принимает один операнд Q-типа, и возвращает хэш-код, который гарантированно равен для двух значений этого типа, если они взаимозаменяемы, и скорей всего разный хэш-код в остальных случаях.

(Открытый вопрос в том, следует ли расширить размер этого хэш-кода до 64 бит. Скорей всего, в самом начале он будет представлен как 32-битная композиция хэш-кодов полей типа-значения, используя легаси значения хэш-кодов. Композиция под-кодов, вероятно, будет в самом начале использовать base-31 полином, хотя мы и понимаем, что такая технология композиции чрезвычайно неоптимальна).

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

Чтобы ограничить использование таких «wither» примитивов, параметр refc и класс будут проверяться относительно соответствия Q-типу этого ValueType. Если они не относятся к одному и тому же Q-типу, доступ будет ограничен. Это ограничение доступа в будущем может расшириться. Типы-значения могут, конечно, определить специальные winther-методы с собственными именами, которые инкапсулируют всю подобную логику. В том числе, можно сделать байткод для withfield, тогда он будет напрямую выражать операцию обновления поля, но в этом случае придется решать всё те же проблемы ограничения доступа.

(Имя метода wither не означает «отравлять» или «портить» что-либо, как это видно из словаря английского языка. Это отсылка к конвенции наименования для методов, которые занимаются функциональным обновлением значений записи. Запросив у комплексного числа c.withRe(0), мы получим в результате новое, полностью мнимое число. В сравнении с этим, c.setRe(0), т.е. вызов сеттера, изменит само комплексное число, удалив из него любую непутевую реальную часть. Методы-сеттеры хорошо подходят для изменяемых объектов, в то время как wither-методы больше подходят для значений. Заметьте, что метод, на самом деле, может быть и гетерам, и сеттером, и wither’ом, даже если он не начинается ни с одного из этих стандартных слов (get, set, wither). Поэтому однажды конвенции типов-значений могут просто поменяться, и формы типа withRe(0) заменятся просто на re(0)).

Скорей всего, все эти методы из ValueType однажды станут виртуальными методами класса Lookup, либо статическими методами MethodHandles, в зависимости от ведущего аргумента.

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

Ниже приведена таблица, резюмирующая новые ссылки на методы и гипотетические байткоды для их операций над типами-значениями. Третья колонка показывает и общий эффект гипотетического байткода, и тип реальной ссылки на метод. В этой таблице, почти каждый тип может стать Q-типом, мы даже делаем ударение этих Q в начале. Тип QC, в частности, означает тип-значение, над которым производятся операции. Сложное обозначение VT<QC> обозначает экземпляр ValueType, производного от этого Q-типа.

Тип RC в самом конце списка акцессоров полей Q-типа в обычных объектах, является обычным L-типом. Заметьте, что Q-типы («работающие как int!») могут читаться и писаться во все поля и элементы массивов.

Эта таблица не покрывает множество ссылок на методы, которые всего лишь копируют значения Q-типа, загружают или сохраняют их в нормальные объекты и массивы. Такие операции могут встречаться во множестве мест, включая findStaticGetterfindStaticSetterfindVirtualfindStaticfindStaticSetterarrayElementSetteridentityconstant, итд, итп.

ВСЁ ЭТО СКОРО ИЗМЕНИТСЯ

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

Дальнейшая работа

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

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

Q-типы в исходном коде на языке Java

Если брать самый минимум, то для работы с Q-типами язык менять не обязательно. Комбинация паков в JVM (VCC), трансформации классфайлов с помощью аннотаций и прямая генерация байткода — достаточны чтобы поупражняться с интересными микро-бенчмарками. Ссылки на методы дают вполне рабочую альтернативу прямой генерации байткода, и предполагается сделать их полностью совместимыми с работой над Q-типами (об этом ниже).

Тем не менее, здесь нет ничего похожего на поддержку в языке. Скорей всего, первые же эксперименты с javac позволят простым образом ссылаться на Q-типы и создавать для них переменные, прямо в Java коде (с учетом очевидных по смыслу ограничений).

В частности, конструкторы объектов в байткоде будут сильно отличаться по виду от как будто бы эквивалентных конструкторов типов-значений. (Пока все поля — final, синтаксис для конструкторов объектов отлично ложится и на конструкторы типов-значений). Разумно будет сделать так, чтобы javac взял на себя ответственность за компиляцию байткода для обоих версий конструктора VCC).

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

Q-подстановки в VCC

VCC, собранный из исходника на Java, может использовать дополнительные аннотации (или может быть, атрибуты) на своих полях и методах, которые приводят к созданию Q-типов одновременно с тем, как трансформируется байткод, для которого загружаются и компилируются VCC.

Явно пригодится пара трансформаций, которые называются Q-подстановка и Q-перегрузка. Первая удаляет L-типы и заменяет их соответствующими Q-типами. Вторая просто копирует методы, заменяя некоторые (или вообще все) из тех, что имеют L-типы в дескрипторе, соответствующими Q-типами. Набор идей по этому поводу отслеживается в JDK-8164889.

В качестве альтернативы для Q-подстановки на основе аннотаций, можно использовать экспериментальную возможность в языке, которая позволила бы напрямую использовать Q-типы в исходном коде на языке Java. Такие эксперименты, скорей всего, будут происходить как честь проекта Valhalla, и если повезет — они произойдут достаточно скоро, чтобы трансформации можно было не реализовывать.

Еще больше байткодов

  • Библиотечную ссылку на метод defaultValueConstant можно заменить новым байткодом vdefault, или байткодом aconst_null с префиксом.
  • Библиотечную ссылку substitutabilityTest можно заменить на новый байт код vcmp, или if_acmpeqс префиксом.
  • Ссылку findWither можно поменять на новый байт код vwithfieldЙВ.
  • findGetter можно заменить на улучшенный соответствующим образом байткод getfield
  • arrayConstructor отлично заменяется на улучшенный байткод anewarray либо на multianewarray.
  • arrayElementGetter заменяется на новый байткод vaload, или aaload с префиксом
  • arrayElementSetter заменяется на новый байткод vastore или aastore с префиксом.
  • arrayLength заменяется на улучшенный байткод arraylength

Можно внезапно обнаружить, что у этого дизайна есть отличное свойство: он основан на едином наборе универсальных байткодов (или макро-байткодов с префиксом), которые работают симметрично для всех ссылок, значений и примитивов, позволяя им использовать любой дескриптор типа, на е только Q-типы. Такие «универсальные байткоды» заслуживают собственной конвенции наименований, типа uloadustoreuinvokeucmpu2u, итп. Если же байткоды работают только над значениями, мы можем использовать конвенцию начальной буквой v.

Как и в дескрипторах типа I, используемого в JVM для работы с int, short, boolean, char и byte, дескриптор типа L (с классом) используется для хранения любых L-типов, так же как дескриптор типа Q(с классом) используется для хранения любых K-типов. В результате, вне зависимости от того, что I-типов всего четыре, а L-типов может быть неограниченное количество, в JVM существует всего три типа-носителя, которые на самом деле и выполняют всю эту работу. (Еще существуют мономорфные типы-носители для long, float и double). Очевидно, что носитель L-типа — это одно машинное слово, указывающее на кучу, а вот носитель Q-типа более сложный, это не только структура данных, которая обозначающая «прицеп» с каким-то значением, но и еще и описание его размера и разметки в памяти (как минимум, это нужно для GC). В действительности, он должен еще и описывать класс значения. В конце концов, носитель Q-типа — просто такой вид типизированного локатора для буфера, который может находиться в любом месте (куча Java, куча C, стек треда).

Больше структур данных

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

  • Разрешить типам-значениям объявлять поля непримитивных типов (Q-типов и L-типов)
  • Реализовать плоские массивы на всех доступных типах-значениях
  • Добавить поддержку атомарного доступа до значений (например, volatile полей)
  • Предоставить аннотации для сверх-выравнивания значений (в смысле, сверх того ограниченного выравнивания, которое позволяет JVM, особенно для 64-битных данных)
  • Предоставить низкоуровневую («unsafe») рефлексию по внутренней организации типов-значений внутри JVM
  • Научить типы-значения представлять не только обычные, но и внешние типы (например, unsignedдля C) или какие-то безопасные указатели (например, адрес плюс + тип + область действия)
  • Оптимизировать производительность и плотность хранения данных всего вышеописанного.
  • Что-нибудь сделать для обнаружения и предупреждения вопиюще безграмотного использования value-based типов, таких как Integer и боксы Q-типов. Например, кидать ошибку при попытке синхронизоваться на них.
  • Добавить поддержку методов, вызываемых напрямую из типов-значений.
  • Добавить поддержку реализации интерфейсов непосредственно для типа-значения, и в особенности, выполнение default-методов этих интерфейсов.
  • Предоставить тип, могущий напрямую представлять как значения Q-типа, так и L-типа.
  • Оформить видение стандартных способов взаимодействия со значениями, включая методы для печати, сравнения и хэширования.
  • Добавить специальные дженерики, поддерживающие и Q-типы, и примитивы. (Обратить на это особое внимание!)

Если типы-значения полностью будут интегрированы с интерфейсами, Q-типы должны наследовать default-методы из супертипов. Это — ключевая точка взаимодействия между обобщенными алгоритмами и структурами данных (такими как сортировки и TreeMap). В минимальной версии, нужно боксить значения и запускать default-методы из боксов, а это может привести к печальным последствиям для производительности. В полной реализации, исполнение методов по умолчанию должно оптмизироваться для каждого конкретного типа-значения. Кроме того, следует создать Фреймворк, который будет проверять, что эти default-методы сами по себе хорошо работают с типами-значениями (в частности, никаких null, синхронизации, органиченный ==, тип). Придется провести огромную работу на эту тему.

Похоже, довольно сложно создать массив со значениями с типом QT[], который будет подтипом массива-интерфейса I[], даже если интерфейс I является супертипом для типа-значения QT. Дальнейшая работа над структурой типов JVM, возможно, позволит делать и так. Типы интерфейсов Iточно относятся к лагерю L-типов, в данный момент, а интерфейсы массивов — это массивы ссылок, ибо являются подтипами Object[]. Но массив типов-значений QT не может быть прозрачно обработан в качестве массива ссылок.

Brige-o-matic

В некоторых случаях, добавить поддержку методов API, которые поддерживали бы Q-типы можно всего лишь создав соответствующие переходники. Трансформаторы байткода, или генераторы, могут вообще не генерировать тела таких методов-переходников, если переходники (вместо байткодов) имеют специально организованные бутстрап-методы. Такой способ думать об этом ведет к множеству следствий и полезных использований, включая автогенерацию стандартных методов equalshashCodeи toString. Работа над этим направлением ведется в рамках JDK-8164891.

Настоящие (JVM-native) классфайлы типов-значений

Минимальная функциональность предлагает использовать загрузку, построенную от боксов, т.е. нужно загружать POJO и потом производить из них значения и боксы к ним. Вначале предполоагалось, что боксы будут просто POJO, а типы значения внутри будут получаться простым копированием полей из них. Даже методы, которые предназначены исключительно для работы с типами-значениями, для начала должны быть методами в POJO.

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

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

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

  • Формат файлов классов, который может напрямую учитывать типы-значения (например бит ACC_VALUE в заголовках)
  • Использование Q-типов в исходном коде на изначальном языке программирования
  • Байткоды для прямой загрузки полей Q-типов, и в поля внутри Q-типа
  • Байткоды для прямого вызова методов Q-типа
  • Полная интеграция типов-значений с массивами, интерфейсами, тип

Гейзенбоксы

Как было предложено выше, L-типы для значений являются value-based, и в какой-то версии JVM может попытаться проконтролировать это:

  • Синхронизация бокосового Q-типа может бросать исключение, например IllegalMonitorStateException
  • Ссылочные проверки (оператор == в Java, или инструкция acmp) могут ответить true для двух эквивалентных боксовых значений Q-типов, даже если сами ссылки как таковые и не равны. Конечно, это потребует отдельной проработки логики постановок для Q-типов. Два бокса, для которых хотя бы однажды было установлена эквивалентность, далее всегда будут считаться взимозаменяемыми.
  • Попытки писать значения с помощью рефлексии прямо в поля Q-типа желательно блокировать, даже если кто-то позоветsetAccessible.
  • Попытки с помощью рефлексии звать конструктор бокса тоже следует заворачивать, даже если кто-то установит setAccessible.

Боксы, идентичность которых друг другу меняется от случая к случаю, называется «гейзенбоксами». Чтобы провести аналогию, ссылочная эквивалентность (==acmp) двух гейзенбоксов коллапсирует их в один и тот же объект, поскольку это мгновенно означает их полную взаимозаменяемость по отношению ко внешнему коду, и следовательно, их Q-значения тоже эквивалентны. Эти две копии когда-нибудь в будущем могут потерять когерентность, проверки эквивалентности будут завершаться неудачей, но их внешняя взаимозаменяемость от этого не сломается. Что случится с предикатом эквивалентности в этом случае, можно понять, засунув их в бокс с котом Шредингера, и наблюдая множество загадочных и печальных последствий… Все это обсуждается в рамках JDK-8163133.

Авторы

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

Брайан Гёц — Senior Java Language Architect в Oracle, работает над развитием языка и платформы Java. C поглощением Sun Oracle-ом работает над проработкой наиболее перспективных проектов, включая Value Types. В прошлом известен как человек, опубликовавший под сотню статей по внутреннему устройству платформы Java, практикам разработки под нее, параллельному программированию. Именно он является главным автором мирового бестселлера Java Concurrency in Practice. Также Брайан участвует в конференциях, и обычно рассказывает о модели памяти Java, сборке мусора, перфомансе, и так далее.

Переводчики

Олег Чирухин — на момент написания этого текста, работает архитектором в компании «Сбербанк-Технологии», занимается разработкой архитектуры систем автоматизированного управления бизнес-процессами. До перехода в Сбербанк-Технологии, принимал участие в разработке нескольких государственных информационных систем, включая Госуслуги и Электронную Медицинскую Карту, а также в разработке онлайн-игр. Спикер на конференциях JUG.ru (JPoint, JBreak). Текущие исследовательские интересы включают виртуальные машины, компиляторы и языки программирования.

Благодарности

Большое спасибо Тагиру Валееву и Владимиру Иванову, давшим необычайно полезные комментарии по сути вопроса. Ваш скептицизм и критика были неоценимы, и все-таки я дописал эту статью 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *