Spec-Zone.ru › Java Language Specification 8

Глава 12. Выполнение

Содержание

12.1. Запуск виртуальной машины Java
12.1.1. Загрузка класса Test
12.1.2. Связывание Test: Проверка, Подготовка, (Необязательное) Разрешение
12.1.3. Инициализация теста: Выполнение инициализаторов
12.1.4. Вызов Test.main
12.2. Загрузка классов и интерфейсов
12.2.1. Процесс загрузки
12.3. Связывание классов и интерфейсов
12.3.1. Проверка двоичного представления
12.3.2. Подготовка типа класса или интерфейса
12.3.3. Разрешение символических ссылок
12.4. Инициализация классов и интерфейсов
12.4.1. Когда происходит инициализация
12.4.2. Подробная процедура инициализации
12.5. Создание новых экземпляров класса
12.6. Заключительная обработка экземпляров класса
12.6.1. Реализация заключительной обработки
12.6.2. Взаимодействие с моделью памяти
12.7. Выгрузка классов и интерфейсов
12.8. Выход из программы

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

Виртуальная машина Java начинает работу с загрузки указанного класса и затем вызывает метод main в этом указанном классе. Раздел §12.1 описывает этапы загрузки, связывания и инициализации, участвующие в выполнении main, в качестве введения к концепциям в этой главе. Более подробные разделы описывают детали загрузки (§12.2), связывания (§12.3) и инициализации (§12.4).

Глава продолжает описание процедур создания новых экземпляров класса (§12.5); и заключительной обработки экземпляров класса (§12.6). Она завершается описанием выгрузки классов (§12.7) и процедурой, выполняемой при выходе программы (§12.8).

12.1. Запуск виртуальной машины Java

Выполнение виртуальной машины Java начинается с вызова метода main некоторого указанного класса, передавая ему один аргумент — массив строк. В примерах в этом спецификации этот первый класс обычно называется Test.

Точные семантики запуска виртуальной машины Java приведены в главе 5 спецификации «The Java Virtual Machine Specification, Java SE 8 Edition». Здесь мы представляем общий обзор процесса с точки зрения языка программирования Java.

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

Например, в реализации UNIX команда:

java Test reboot Bob Dot Enzo

обычно запускает виртуальную машину Java, вызывая метод main класса Test (класс в безымянном пакете), передавая ему массив, содержащий четыре строки "reboot", "Bob", "Dot" и "Enzo".

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

12.1.1. Загрузка класса Test

Первая попытка выполнить метод main класса Test обнаруживает, что класс Test не загружен — то есть, что в текущей виртуальной машине Java нет двоичного представления этого класса. Затем виртуальная машина Java использует загрузчик классов для попытки найти такое двоичное представление. Если этот процесс завершится неудачей, будет выброшено исключение. Этот процесс загрузки описан далее в §12.2.

12.1.2. Компоновка Test: верификация, подготовка, (необязательно) разрешение

После загрузки Test его необходимо инициализировать, прежде чем можно будет вызвать main. И Test, как и все типы (классы или интерфейсы), должен быть скомпонован до инициализации. Компоновка включает в себя верификацию, подготовку и (необязательно) разрешение. Компоновка описана более подробно в §12.3.

Верификация проверяет, что загруженное представление Test имеет правильную структуру и таблицу символов. Верификация также проверяет, что код, реализующий Test, соответствует семантическим требованиям языка программирования Java и виртуальной машины Java. Если при верификации обнаружена проблема, то генерируется ошибка. Верификация описана более подробно в §12.3.1.

Подготовка включает выделение статического хранилища и любых структур данных, используемых внутренней реализацией виртуальной машины Java, таких как таблицы методов. Подготовка описана более подробно в §12.3.2.

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

Шаг разрешения является необязательным во время начальной компоновки. Реализация может разрешать символические ссылки из класса или интерфейса, который компонуется очень рано, даже до момента разрешения всех символических ссылок на классы и интерфейсы, которые далее ссылаются, рекурсивно. (Это разрешение может привести к ошибкам на последующих шагах загрузки и компоновки.) Этот выбор реализации представляет собой одну крайность и аналогичен типу «статической» компоновки, которая использовалась в течение многих лет в простых реализациях языка C. (В этих реализациях скомпилированная программа обычно представляется как файл "a.out", содержащий полностью скомпонованную версию программы, включая полностью разрешенные ссылки на библиотечные функции, используемые программой. Копии этих библиотечных функций включены в файл "a.out").

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

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

Процесс разрешения описан далее в §12.3.3.

12.1.3. Инициализация Test: выполнение инициализаторов

В нашем примере виртуальная машина Java всё ещё пытается выполнить метод main класса Test. Это допускается только в том случае, если класс был инициализирован (§12.4.1).

Инициализация состоит из выполнения любых инициализаторов переменных класса и статических инициализаторов класса Test в текстовом порядке. Но перед тем, как можно будет инициализировать Test, его непосредственный суперкласс должен быть инициализирован, а также непосредственный суперкласс его непосредственного суперкласса и так далее рекурсивно. В самом простом случае Test имеет Object как свой неявный непосредственный суперкласс; если класс Object ещё не был инициализирован, то он должен быть инициализирован, прежде чем Test будет инициализирован. У класса Object нет суперкласса, поэтому рекурсия здесь завершается.

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

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

Процесс инициализации описан далее в §12.4.

12.1.4. Вызов Test.main

Наконец, после завершения инициализации для класса Test (во время которого могла произойти другая связанная загрузка, компоновка и инициализация), вызывается метод main класса Test.

Метод main должен быть объявлен public, static и void. Он должен указывать формальный параметр (§8.4.1), тип которого — массив String. Поэтому допустимы оба следующих объявления:


public static void main(String[] args)


public static void main(String... args)

12.2. Загрузка классов и интерфейсов

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

Точное значение загрузки приведено в главе 5 спецификации виртуальной машины Java, Java SE 8 Edition. Здесь мы представляем обзор процесса с точки зрения языка программирования Java.

Двоичный формат класса или интерфейса обычно является форматом файла class, описанным в спецификации виртуальной машины Java, Java SE 8 Edition, но возможны и другие форматы, при условии, что они соответствуют требованиям, указанным в §13.1. Метод defineClass класса ClassLoader может быть использован для построения объектов Class из двоичных представлений в формате файла class.

Корректно работающие загрузчики классов сохраняют эти свойства:

  • При одинаковом имени загрузчик должен всегда возвращать один и тот же объект класса.

  • Если загрузчик классов делегирует загрузку класса C другому загрузчику L2, то для любого типа T, являющегося непосредственным суперклассом или непосредственным суперинтерфейсом C, или типом поля в C, или типом формального параметра метода или конструктора в C, или типом возвращаемого значения метода в C, L1 и L2 должны возвращать один и тот же объект Class.

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

Дополнительные сведения об этих вопросах см. в спецификации виртуальной машины Java, Java SE 8 Edition, и статье Динамическая загрузка классов в виртуальной машине Java, авторы Sheng Liang и Gilad Bracha, в Труды OOPSLA '98, опубликованные как ACM SIGPLAN Notices, том 33, номер 10, октябрь 1998 года, страницы 36-44. Основной принцип разработки языка программирования Java заключается в том, что во время выполнения система типов не может быть подорвана кодом, написанным на языке программирования Java, даже реализациями таких чувствительных системных классов, как ClassLoader и SecurityManager.

12.2.1. Процесс загрузки

Процесс загрузки реализуется классом ClassLoader и его подклассами.

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

Если во время загрузки класса возникает ошибка, то экземпляр одного из следующих подклассов класса LinkageError будет брошен в любой точке программы, которая (прямо или косвенно) использует тип:

  • ClassCircularityError: Класс или интерфейс не может быть загружен, потому что он будет своим собственным суперклассом или суперинтерфейсом (§8.1.4, §9.1.3, §13.4.4).

  • ClassFormatError: Двоичные данные, которые, как утверждается, описывают запрашиваемый скомпилированный класс или интерфейс, имеют неправильный формат.

  • NoClassDefFoundError: Определение запрашиваемого класса или интерфейса не было найдено соответствующим загрузчиком классов.

Поскольку загрузка включает выделение новых структур данных, она может завершиться сбоем с OutOfMemoryError.

12.3. Связывание классов и интерфейсов

Связывание — это процесс взятия двоичной формы типа класса или интерфейса и объединения её в состояние выполнения Java Virtual Machine, чтобы её можно было выполнить. Тип класса или интерфейса всегда загружается перед связыванием.

В связывании участвуют три различных действия: проверка, подготовка и разрешение символических ссылок.

Точные семантики связывания приведены в главе 5 спецификации Java Virtual Machine, Java SE 8 Edition. Здесь мы представляем обзор процесса с точки зрения языка программирования Java.

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

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

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

12.3.1. Проверка двоичного представления

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

Если во время проверки произошла ошибка, то в программе в момент, вызвавший проверку класса, будет брошено исключение подкласса класса LinkageError:

  • VerifyError: Двоичное определение класса или интерфейса не прошло набор необходимых проверок, чтобы убедиться, что оно соответствует семантике языка Java Virtual Machine и не может нарушить целостность Java Virtual Machine. (См. §13.4.2, §13.4.4, §13.4.9 и §13.4.17 для некоторых примеров.)

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

Подготовка включает создание полей static (переменных класса и констант) для класса или интерфейса и инициализацию таких полей до значений по умолчанию (§4.12.5). Это не требует выполнения какого-либо исходного кода; явные инициализаторы статических полей выполняются как часть инициализации (§12.4), а не подготовки.

Реализации Java Virtual Machine могут предварительно вычислить дополнительные структуры данных на этапе подготовки, чтобы сделать последующие операции с классом или интерфейсом более эффективными. Одной из особенно полезных структур данных является "таблица методов" или другая структура данных, которая позволяет вызывать любой метод для экземпляров класса без необходимости поиска суперклассов во время вызова.

12.3.3. Разрешение символических ссылок

Двоичное представление класса или интерфейса содержит ссылки на другие классы и интерфейсы, их поля, методы и конструкторы символически, используя двоичные имена (§13.1) других классов и интерфейсов (§13.1). Для полей и методов эти символические ссылки включают имя типа класса или интерфейса, к которому относится поле или метод, а также имя самого поля или метода вместе с соответствующей информацией о типе.

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

Если во время разрешения произошла ошибка, то будет выброшено исключение. Чаще всего это будет исключение одного из подклассов класса IncompatibleClassChangeError, но это также может быть экземпляр какого-либо другого подкласса IncompatibleClassChangeError или даже экземпляр класса IncompatibleClassChangeError. Эта ошибка может быть выброшена в любой точке программы, которая использует символическую ссылку на тип, напрямую или косвенно:

  • IllegalAccessError: Встречена символическая ссылка, которая указывает на использование или присваивание поля, вызов метода или создание экземпляра класса, к которому код, содержащий ссылку, не имеет доступа, потому что поле или метод были объявлены с private, protected или пакетом доступа (не public), или потому что класс не был объявлен public.

    Это может произойти, например, если поле, которое изначально объявлялось public, изменится на private после того, как другой класс, ссылающийся на поле, был скомпилирован (§13.4.7).

  • InstantiationError: Встречена символическая ссылка, используемая в выражении создания экземпляра класса, но экземпляр не может быть создан, потому что ссылка оказалась ссылкой на интерфейс или абстрактный класс.

    Это может произойти, например, если класс, который изначально не abstract, изменится на abstract после того, как другой класс, ссылающийся на данный класс, был скомпилирован (§13.4.1).

  • NoSuchFieldError: Встречена символическая ссылка, которая ссылается на конкретное поле конкретного класса или интерфейса, но класс или интерфейс не содержит поля с таким именем.

    Это может произойти, например, если объявление поля было удалено из класса после того, как другой класс, ссылающийся на поле, был скомпилирован (§13.4.8).

  • NoSuchMethodError: Встречена символическая ссылка, которая ссылается на конкретный метод конкретного класса или интерфейса, но класс или интерфейс не содержит метода с такой сигнатурой.

    Это может произойти, например, если объявление метода было удалено из класса после того, как другой класс, ссылающийся на метод, был скомпилирован (§13.4.12).

Кроме того, UnsatisfiedLinkError, подкласс LinkageError, может быть выброшен, если класс объявляет native метод, для которого нельзя найти реализации. Ошибка произойдёт, если метод будет использован, или раньше, в зависимости от используемой стратегии разрешения, используемой реализацией Java Virtual Machine (§12.3).

12.4. Инициализация классов и интерфейсов

Инициализация класса состоит из выполнения его статических инициализаторов и инициализаторов для static полей (переменных класса), объявленных в классе.

Инициализация интерфейса состоит из выполнения инициализаторов для полей (констант), объявленных в интерфейсе.

12.4.1. Когда происходит инициализация

Тип класса или интерфейса T будет инициализирован непосредственно перед первым случаем одного из следующих:

  • T является классом и создается экземпляр T.

  • Вызывается метод static, объявленный типом T.

  • Переменной static поля, объявленного типом T, присваивается значение.

  • Используется поле static, объявленное типом T, и это поле не является константой (§4.12.4).

  • T является верхнеуровневым классом (§7.6) и выполняется оператор assert (§14.10), лексически вложенный в T (§8.1.3).

При инициализации класса инициализируются его суперклассы (если они еще не были инициализированы), а также все суперинтерфейсы (§8.1.5), которые объявляют любые методы по умолчанию (§9.4.3) (если они еще не были инициализированы). Инициализация интерфейса сама по себе не вызывает инициализацию его суперинтерфейсов.

Ссылка на поле static (§8.3.1.1) вызывает инициализацию только класса или интерфейса, который его фактически объявляет, даже если на него может ссылаться имя подкласса, подинтерфейса или класса, реализующего интерфейс.

Вызов определенных рефлексивных методов в классе Class и в пакете java.lang.reflect также вызывает инициализацию класса или интерфейса.

Класс или интерфейс не будут инициализированы при других обстоятельствах.

Обратите внимание, что компилятор может генерировать синтетические методы по умолчанию в интерфейсе, то есть методы по умолчанию, которые не объявлены явно или неявно (§13.1). Такие методы будут инициировать инициализацию интерфейса, несмотря на то, что исходный код не указывает на необходимость инициализации интерфейса.

Цель состоит в том, чтобы у типа класса или интерфейса был набор инициализаторов, которые приводят его в согласованное состояние, и что это состояние является первым состоянием, которое наблюдается другими классами. Статические инициализаторы и инициализаторы переменных класса выполняются в текстовом порядке и не могут ссылаться на переменные класса, объявленные в классе, чьи объявления появляются в тексте после использования, даже если эти переменные класса находятся в области видимости (§8.3.3). Это ограничение предназначено для обнаружения на этапе компиляции большинства циклических или других неправильных инициализаций.

Тот факт, что код инициализации не ограничен, позволяет создавать примеры, в которых значение переменной класса можно наблюдать, когда оно все еще имеет свое начальное значение по умолчанию, до оценки его инициализирующего выражения, но такие примеры редки на практике. (Такие примеры также можно создать для инициализации переменной экземпляра (§12.5).) В этих инициализаторах доступна вся мощь языка программирования Java; программисты должны проявлять осторожность. Эта мощь возлагает дополнительную нагрузку на генераторы кода, но эта нагрузка возникнет в любом случае, потому что язык программирования Java является параллельным (§12.4.2).

Пример 12.4.1-1. Суперклассы инициализируются перед подклассами

class Super {
    static { System.out.print("Super "); }
}
class One {
    static { System.out.print("One "); }
}
class Two extends Super {
    static { System.out.print("Two "); }
}
class Test {
    public static void main(String[] args) {
        One o = null;
        Two t = new Two();
        System.out.println((Object)o == (Object)t);
    }
}

Эта программа выводит:

Super Two false

Класс One никогда не инициализируется, потому что он не используется активно и поэтому никогда не связывается. Класс Two инициализируется только после того, как его суперкласс Super был инициализирован.


Пример 12.4.1-2. Инициализируется только класс, который объявляет поле static

class Super {
    static int taxi = 1729;
}
class Sub extends Super {
    static { System.out.print("Sub "); }
}
class Test {
    public static void main(String[] args) {
        System.out.println(Sub.taxi);
    }
}

Эта программа выводит только:

1729

потому что класс Sub никогда не инициализируется; ссылка на Sub.taxi — это ссылка на поле, фактически объявленное в классе Super, и не вызывает инициализацию класса Sub.


Пример 12.4.1-3. Инициализация интерфейса не инициализирует суперинтерфейсы

interface I {
    int i = 1, ii = Test.out("ii", 2);
}
interface J extends I {
    int j = Test.out("j", 3), jj = Test.out("jj", 4);
}
interface K extends J {
    int k = Test.out("k", 5);
}
class Test {
    public static void main(String[] args) {
        System.out.println(J.i);
        System.out.println(K.j);
    }
    static int out(String s, int i) {
        System.out.println(s + "=" + i);
        return i;
    }
}

Эта программа выводит:

1
j=3
jj=4
3

Ссылка на J.i относится к константе (§4.12.4); поэтому она не вызывает инициализацию I (§13.4.9).

Ссылка на K.j относится к полю, фактически объявленному в интерфейсе J, которое не является константой; это вызывает инициализацию полей интерфейса J, но не полей его суперинтерфейса I, ни полей интерфейса K.

Несмотря на то, что имя K используется для ссылки на поле j интерфейса J, интерфейс K не инициализируется.


12.4.2. Подробная процедура инициализации

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

Процедура предполагает, что объект Class уже проверен и подготовлен, и что объект Class содержит состояние, указывающее одно из четырёх ситуаций:

  • Данный объект Class проверен и подготовлен, но не инициализирован.

  • Этот объект Class инициируется определённым потоком T.

  • Данный объект Class полностью инициализирован и готов к использованию.

  • Этот объект Class находится в состоянии ошибки, возможно, потому, что попытка инициализации не удалась.

Для каждого класса или интерфейса C существует уникальный замок инициализации LC. Сопоставление от C к LC предоставляется на усмотрение реализации виртуальной машины Java. Процедура инициализации C тогда выглядит следующим образом:

  1. Синхронизироваться на замке инициализации, LC, для C. Это включает ожидание, пока текущий поток сможет получить LC.

  2. Если объект Class для C указывает, что инициализация C происходит другим потоком, то освободить LC и заблокировать текущий поток до получения сообщения о завершении инициализации, после чего повторить этот шаг.

  3. Если объект Class для C указывает, что инициализация C происходит текущим потоком, то это рекурсивный запрос инициализации. Освободить LC и завершить нормально.

  4. Если объект Class для C указывает, что C уже инициализирован, то дальнейших действий не требуется. Освободить LC и завершить нормально.

  5. Если объект Class для C находится в состоянии ошибки, то инициализация невозможна. Освободить LC и бросить исключение NoClassDefFoundError.

  6. В противном случае, отметить, что инициализация объекта Class для C происходит в текущем потоке, и освободить LC.

    Затем инициализируйте поля static C, которые являются константами (переменными) (§4.12.4, §8.3.2, §9.3.1).

  7. Далее, если C — класс, а не интерфейс, и его суперкласс ещё не инициализирован, то пусть SC будет его суперклассом, а SI1, ..., SIn — все суперинтерфейсы C, которые объявляют хотя бы один метод по умолчанию. Порядок суперинтерфейсов определяется рекурсивным перечислением по иерархии суперинтерфейсов каждого интерфейса, непосредственно реализованного C (в лево-право порядке пункта C's implements). Для каждого интерфейса I, непосредственно реализуемого C, перечисление повторяется для суперинтерфейсов I (в лево-право порядке пункта I's extends) перед возвращением I.

    Для каждого S в списке [ SC, SI1, ..., SIn ] рекурсивно выполняется вся эта процедура для S. При необходимости, сначала проверьте и подготовьте S.

    Если инициализация S завершается аварийно из-за брошенного исключения, то получить LC, отметить объект Class для C как ошибочный, уведомить все ожидающие потоки, освободить LC и завершить аварийно, бросив то же исключение, которое возникло при инициализации S.

  8. Далее определите, включены ли утверждения (§14.10) для C путём запроса к определяющему загрузчику класса.

  9. Далее выполните инициализаторы переменных класса и статические инициализаторы класса или инициализаторы полей интерфейса в текстовом порядке, как будто это один блок.

  10. Если выполнение инициализаторов завершится нормально, то получить LC, отметить объект Class для C как полностью инициализированный, уведомить все ожидающие потоки, освободить LC и завершить эту процедуру нормально.

  11. В противном случае инициализаторы должны были завершиться аварийно, сбросив какое-то исключение E. Если класс E не является Error или одним из его подклассов, то создать новый экземпляр класса ExceptionInInitializerError, с E в качестве аргумента, и использовать этот объект вместо E на следующем шаге. Если новый экземпляр ExceptionInInitializerError не может быть создан из-за возникновения OutOfMemoryError, то вместо этого использовать объект OutOfMemoryError вместо E на следующем шаге.

  12. Получить LC, отметить объект Class для C как ошибочный, уведомить все ожидающие потоки, освободить LC и завершить эту процедуру аварийно с причиной E или её заменой, как определено на предыдущем шаге.

Реализация может оптимизировать эту процедуру, исключая получение блокировки на шаге 1 (и освобождение на шаге 4/5), когда она может определить, что инициализация класса уже завершена, при условии, что в терминах модели памяти все упорядочения «происходит до» (happens-before orderings), которые существовали бы, если бы была получена блокировка, по-прежнему существуют, когда выполняется оптимизация.

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

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

12.5. Создание новых экземпляров классов

Новый экземпляр класса явно создается, когда вычисление выражения создания экземпляра класса (§15.9) приводит к созданию экземпляра класса.

Новый экземпляр класса может быть неявно создан в следующих ситуациях:

  • Загрузка класса или интерфейса, содержащего String литерал (§3.10.5), может создать новый String объект для представления этого литерала. (Это может не произойти, если тот же самый String ранее был интернирован (§3.10.5).)

  • Выполнение операции, которая приводит к преобразованию в обертку (§5.1.7). Преобразование в обертку может создать новый объект класса-обертки, связанного с одним из примитивных типов.

  • Выполнение оператора конкатенации строк + (§15.18.1), который не является частью константного выражения (§15.28), всегда создает новый String объект для представления результата. Операторы конкатенации строк также могут создавать временные объекты-обертки для значения примитивного типа.

  • Вычисление выражения ссылки на метод (§15.13.3) или лямбда-выражения (§15.27.4) может потребовать создания нового экземпляра класса, реализующего функциональный интерфейс.

В каждой из этих ситуаций определяется конкретный конструктор (§8.8), который должен быть вызван с указанными аргументами (возможно, ни с какими) в рамках процесса создания экземпляра класса.

При создании нового экземпляра класса для него выделяется память, достаточная для всех переменных экземпляра, объявленных в типе класса, и всех переменных экземпляра, объявленных в каждом суперклассе типа класса, включая все переменные экземпляра, которые могут быть скрыты (§8.3).

Если доступно недостаточно памяти для выделения памяти для объекта, то создание экземпляра класса завершается неожиданно с OutOfMemoryError. В противном случае все переменные экземпляра в новом объекте, включая те, которые объявлены в суперклассах, инициализируются их значениями по умолчанию (§4.12.5).

Непосредственно перед тем, как ссылка на только что созданный объект возвращается в качестве результата, указанный конструктор обрабатывается для инициализации нового объекта с помощью следующей процедуры:

  1. Присвоить аргументы конструктору новым параметрам переменных для этого вызова конструктора.

  2. Если этот конструктор начинается с явного вызова конструктора (§8.8.7.1) другого конструктора в том же классе (используя this), то вычислить аргументы и обработать этот вызов конструктора рекурсивно с использованием тех же пяти шагов. Если этот вызов конструктора завершается неожиданно, то эта процедура завершается по той же причине; в противном случае продолжить с шагом 5.

  3. Этот конструктор не начинается с явного вызова конструктора другого конструктора в том же классе (используя this). Если этот конструктор предназначен для класса, отличного от Object, то этот конструктор будет начинаться с явного или неявного вызова конструктора суперкласса (используя super). Вычислить аргументы и обработать этот вызов конструктора суперкласса рекурсивно с использованием тех же пяти шагов. Если этот вызов конструктора завершается неожиданно, то эта процедура завершается по той же причине. В противном случае продолжить с шагом 4.

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

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

В отличие от C++, язык программирования Java не определяет измененные правила для диспетчеризации методов при создании нового экземпляра класса. Если вызываются методы, переопределенные в подклассах в инициализируемом объекте, то эти переопределенные методы используются даже до полного инициализации нового объекта.

Пример 12.5-1. Вычисление создания экземпляра

class Point {
    int x, y;
    Point() { x = 1; y = 1; }
}
class ColoredPoint extends Point {
    int color = 0xFF00FF;
}
class Test {
    public static void main(String[] args) {
        ColoredPoint cp = new ColoredPoint();
        System.out.println(cp.color);
    }
}

Здесь создается новый экземпляр класса ColoredPoint. Сначала выделяется память для нового ColoredPoint, чтобы содержать поля x, y и color. Все эти поля затем инициализируются их значениями по умолчанию (в этом случае, 0 для каждого поля). Затем вызывается конструктор ColoredPoint без аргументов. Поскольку ColoredPoint не объявляет конструкторов, неявно объявляется конструктор по умолчанию следующего вида:

ColoredPoint() { super(); }

Затем этот конструктор вызывает конструктор Point без аргументов. Конструктор Point не начинается с вызова конструктора, поэтому компилятор Java предоставляет неявный вызов конструктора своего суперкласса без аргументов, как будто он был написан:

Point() { super(); x = 1; y = 1; }

Поэтому вызывается конструктор Object, не принимающий аргументов.

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

Object() { }

Этот конструктор выполняется без эффектов и возвращается.

Далее выполняются все инициализаторы переменных экземпляра класса Point. Как видно, объявления x и y не содержат выражений инициализации, поэтому для этого шага примера не требуется никаких действий. Затем выполняется тело конструктора Point, устанавливающего x в 1 и y в 1.

Далее выполняются инициализаторы переменных экземпляра класса ColoredPoint. На этом шаге значение 0xFF00FF присваивается color. Наконец, выполняется остальная часть тела конструктора ColoredPoint (часть после вызова super); в остальной части тела нет операторов, поэтому дальнейших действий не требуется, и инициализация завершена.


Пример 12.5-2. Динамическая диспетчеризация при создании экземпляра

class Super {
    Super() { printThree(); }
    void printThree() { System.out.println("three"); }
}
class Test extends Super {
    int three = (int)Math.PI;  // That is, 3
    void printThree() { System.out.println(three); }

    public static void main(String[] args) {
        Test t = new Test();
        t.printThree();
    }
}

Эта программа выводит:

0
3

Это показывает, что вызов printThree в конструкторе класса Super не вызывает определение printThree в классе Super, а вызывает переопределенное определение printThree в классе Test. Этот метод, следовательно, выполняется до выполнения инициализаторов поля Test, поэтому первое значение вывода — 0, значение по умолчанию, с которым инициализируется поле three класса Test. Более поздний вызов printThree в методе main вызывает то же определение printThree, но к тому моменту инициализатор для переменной экземпляра three уже выполнился, и поэтому печатается значение 3.


12.6. Инициализация экземпляров классов

Класс Object имеет метод finalize; этот метод может быть переопределён другими классами. Конкретное определение finalize, которое может быть вызвано для объекта, называется финализатором этого объекта. Перед тем, как сборщик мусора освободит память для объекта, виртуальная машина Java вызовет финализатор этого объекта.

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

Язык программирования Java не указывает, как быстро будет вызван финализатор, за исключением того, что это произойдёт перед повторным использованием памяти для объекта.

Язык программирования Java не определяет, какой поток вызовет финализатор для данного объекта.

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

Язык программирования Java не накладывает порядок на вызовы методов finalize. Финализаторы могут вызываться в любом порядке или даже одновременно.

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

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

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

Если во время финализации возникает необработанное исключение, оно игнорируется, и финализация этого объекта завершается.

Завершение конструктора объекта происходит до (§17.4.5) выполнения его метода finalize (в формальном смысле).

Метод finalize, объявленный в классе Object, не выполняет никаких действий. Тот факт, что класс Object объявляет метод finalize, означает, что метод finalize любого класса всегда может вызвать метод finalize своего суперкласса. Это всегда должно делаться, если только программист не намеревается аннулировать действия финализатора в суперклассе. (В отличие от конструкторов, финализаторы не вызывают автоматически финализатор суперкласса; такой вызов должен быть явно запрограммирован).

Для повышения эффективности, реализация может отслеживать классы, которые не переопределяют метод finalize класса Object, или переопределяют его тривиальным образом.

Например:


protected void finalize() throws Throwable {
    super.finalize();
}

Мы рекомендуем реализациям рассматривать такие объекты как имеющие финализатор, который не переопределен, и завершать их более эффективно, как описано в §12.6.1.

Финализатор можно вызвать явно, как и любой другой метод.

Пакет java.lang.ref описывает слабые ссылки, которые взаимодействуют со сборкой мусора и финализацией. Как и с любым API, имеющим особые взаимодействия с языком программирования Java, реализаторы должны учитывать все требования, накладываемые API java.lang.ref. Данное описание не обсуждает слабые ссылки. Читатель может обратиться к документации API для получения подробностей.

12.6.1. Реализация финализации

Каждый объект может быть охарактеризован двумя атрибутами: он может быть доступным, финализируемо-доступным или недоступным, а также он может быть нефинализированным, финализируемым или финализированным.

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

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

Недоступный объект не может быть достигнут ни одним из способов.

Нефинализированный объект никогда не имел автоматического вызова своего финализатора.

Финализированный объект имел автоматический вызов своего финализатора.

Финализируемый объект никогда не имел автоматического вызова своего финализатора, но виртуальная машина Java может в конечном итоге автоматически вызвать его финализатор.

Объект o не становится финализируемым, пока его конструктор не вызовет конструктор Object для o и этот вызов не завершится успешно (то есть без исключений). Каждое запись в поле объекта до финализации должна быть видна финализации этого объекта. Кроме того, ни одно из чтений полей этого объекта до финализации не может видеть записи, которые происходят после начала финализации этого объекта.

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

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

Например, рассмотрим шаблон Наблюдатель финализатора:


class Foo {
    private final Object finalizerGuardian = new Object() {
        protected void finalize() throws Throwable {
            /* finalize outer Foo object */
        }
    }
} 

Наблюдатель финализатора заставляет super.finalize вызываться, если подкласс переопределяет finalize и не вызывает явно super.finalize.

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

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

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

12.6.2. Взаимодействие с моделью памяти

Модель памяти (§17.4) должна иметь возможность определять, когда она может выполнять действия, происходящие в финализаторе. В этом разделе описывается взаимодействие финализации с моделью памяти.

Каждое выполнение имеет ряд точек принятия решения о достижимости, обозначенных di. Каждое действие либо предшествует di, либо следует за di. Помимо явно указанного, порядок «предшествует» в этом разделе не связан ни с каким другим порядком в модели памяти.

Если r — чтение, которое видит запись w, и r предшествует di, то w также должно предшествовать di.

Если x и y — синхронизационные действия над одной переменной или монитором, такие что so(x, y) (§17.4.4) и y предшествует di, то x также должно предшествовать di.

В каждой точке принятия решения о достижимости некоторые объекты помечаются как недостижимые, а некоторые из них — как требующие финализации. Эти точки принятия решения также являются моментами, в которых проверяются, добавляются в очередь и очищаются ссылки в соответствии с правилами, приведенными в документации API для пакета java.lang.ref.

Единственными объектами, которые считаются однозначно достижимыми в момент di, являются те, которые могут быть доказаны достижимыми путем применения этих правил:

  • Объект B является однозначно достижимым в момент di из static полей, если существует запись w1 в поле static v класса C, в которой записанное значение w1 является ссылкой на B, класс C загружен достижимым загрузчиком классов, и не существует записи w2 в v, для которой hb(w2, w1) неверно и обе w1 и w2 предшествуют di.

  • Объект B однозначно достижим из A в момент di, если существует запись w1 в элементе v структуры A, в которой записанное значение w1 является ссылкой на B и не существует записи w2 в v, для которой hb(w2, w1) неверно и обе w1 и w2 предшествуют di.

  • Если объект C однозначно достижим из объекта B, и объект B однозначно достижим из объекта A, то C однозначно достижим из A.

Если объект X помечен как недостижимый в момент di, то:

  • X не должен быть однозначно достижим в момент di из static полей; и

  • Все активные использования X в потоке t, которые следуют за di, должны происходить в вызове финализатора для X или в результате чтения потоком t ссылки на X, которое следует за di; и

  • Все чтения, которые следуют за di и видят ссылку на X, должны видеть записи в элементы объектов, которые были недостижимы в di, или видеть записи, которые произошли после di.

Действие a является активным использованием X тогда и только тогда, когда истинно хотя бы одно из следующих утверждений:

  • a читает или записывает элемент X

  • a блокирует или разблокирует X и существует действие блокировки на X, произошедшее после вызова финализатора для X

  • a записывает ссылку на X

  • a является активным использованием объекта Y, а X однозначно достижим из Y

Если объект X помечен как требующий финализации в di, то:

  • X должен быть помечен как недостижимый в di; и

  • di должно быть единственным местом, где X помечен как требующий финализации; и

  • действия, произошедшие после вызова финализатора, должны следовать за di.

12.7. Выгрузка классов и интерфейсов

Реализация языка программирования Java может выгружать классы.

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

Классы и интерфейсы, загруженные загрузчиком Bootstrap, не могут быть выгружены.

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

Однако, если класс или интерфейс C был выгружен, в то время как его определяющий загрузчик мог быть доступен, то C может быть загружен заново. Никогда нельзя гарантировать, что этого не произойдет. Даже если класс не ссылается ни на какой другой загруженный класс, он может ссылаться на какой-то класс или интерфейс D, который еще не загружен. При загрузке D определяющим загрузчиком C его выполнение может привести к перезагрузке C.

Перезагрузка может быть непрозрачной, если, например, класс содержит static переменные (состояние которых будет потеряно), статические инициализаторы (которые могут иметь побочные эффекты) или native методы (которые могут сохранять статическое состояние). Кроме того, значение хэша объекта Class зависит от его идентичности. Поэтому, как правило, невозможно перезагрузить класс или интерфейс полностью прозрачно.

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

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

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

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

12.8. Выход программы

Программа завершает все свои действия и выходит в одном из двух случаев:

  • Завершаются все потоки, которые не являются демонами.

  • Какой-либо поток вызывает метод exit класса Runtime или класса System, и операция exit не запрещена менеджером безопасности.

© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API