Spec-Zone.ru › Java Language Specification 7

Глава 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 7 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 и, используя это бинарное представление, создавая объект Class для представления класса или интерфейса.

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

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

Корректно работающие загрузчики классов поддерживают следующие свойства:

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

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

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

Для более подробного обсуждения этих вопросов см. спецификацию Java Virtual Machine, Java SE 7 Edition, и статью «Динамическая загрузка классов в Java Virtual Machine» Шенга Лианга и Гилада Брахи в «Трудах 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 7. Здесь мы представляем обзор процесса с точки зрения языка программирования 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.

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

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

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

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

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

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

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

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

Тот факт, что код инициализации не ограничен, позволяет создавать примеры (§8.3.2.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.

    Затем инициализировать статические переменные и поля класса final и интерфейсов, значения которых являются константами времени компиляции (§8.3.2.1, §9.3.1, §13.4.9, §15.28).

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

  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), когда она может определить, что инициализация класса уже завершилась, при условии, что все отношения happen-before, которые существовали бы, если бы блокировка была получена, по-прежнему существуют, когда выполняется оптимизация.

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

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

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

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

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

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

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

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

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

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

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

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

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

  2. Если этот конструктор начинается с явного вызова другого конструктора в том же классе (используя 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 не объявляет конструкторов, компилятор Java автоматически предоставляет для него конструктор по умолчанию в виде:

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, которое может быть вызвано для объекта, называется финализатором этого объекта. Перед тем, как сборщик мусора reclaims память для объекта, виртуальная машина Java вызовет финализатор этого объекта.

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

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

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

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

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

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

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

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

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

Завершение конструктора объекта происходит до выполнения его метода 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, выполнившего чтение, которое следует за di, ссылки на X; и

  • Все чтения, которые следуют за 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