Глава 12. Выполнение
Содержание
- 12.1. Запуск виртуальной машины Java
- 12.2. Загрузка классов и интерфейсов
- 12.3. Связывание классов и интерфейсов
- 12.4. Инициализация классов и интерфейсов
- 12.5. Создание новых экземпляров классов
- 12.6. Заключительная обработка экземпляров классов
- 12.7. Выгрузка классов и интерфейсов
- 12.8. Выход из программы
В этой главе описываются действия, выполняемые во время выполнения программы. Она организована вокруг жизненного цикла виртуальной машины Java и классов, интерфейсов и объектов, которые формируют программу.
Виртуальная машина Java начинает свою работу с загрузки указанного класса или интерфейса, а затем вызывает метод main в этом указанном классе или интерфейсе. Раздел §12.1 описывает этапы загрузки, связывания и инициализации, необходимые для выполнения main, как введение в концепции этой главы. Дополнительные разделы описывают подробности загрузки (§12.2), связывания (§12.3) и инициализации (§12.4).
Глава продолжается описанием процедур создания новых экземпляров классов (§12.5); и заключительной обработки экземпляров классов (§12.6). Она завершается описанием выгрузки классов (§12.7) и процедуры, выполняемой при выходе из программы (§12.8).
END_OF_DOCUMENT_MARKER Виртуальная машина Java начинает выполнение, вызывая метод main некоторого указанного класса или интерфейса, передавая ему один аргумент — массив строк. В примерах в этом спецификации этот первый класс обычно называется Test.
Точные семантики запуска виртуальной машины Java приведены в главе 5 спецификации Спецификация виртуальной машины Java, издание Java SE 11. Здесь мы представляем обзор процесса с точки зрения языка программирования Java.
Способ, которым начальный класс или интерфейс указывается виртуальной машине Java, выходит за рамки этой спецификации, но типично, в средах, использующих командную строку, полное имя класса или интерфейса указывается в качестве аргумента командной строки, а последующие аргументы командной строки используются в качестве строк, передаваемых в качестве аргумента методу main.
Например, в реализации UNIX командная строка:
java Test reboot Bob Dot Enzo
обычно запускает виртуальную машину Java, вызывая метод main класса Test (класс в безымянном пакете), передавая ему массив, содержащий четыре строки "reboot", "Bob", "Dot" и "Enzo".
Теперь мы наметим шаги, которые виртуальная машина Java может предпринять для выполнения Test, как пример процесса загрузки, линковки и инициализации, которые описываются более подробно в последующих разделах.
Первая попытка выполнить метод main класса Test обнаруживает, что класс Test не загружен — то есть, что виртуальная машина Java в настоящее время не содержит двоичного представления для этого класса. Затем виртуальная машина Java использует загрузчик классов для попытки найти такое двоичное представление. Если этот процесс завершается неудачно, генерируется ошибка. Этот процесс загрузки описывается подробнее в §12.2.
После загрузки 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.
В нашем примере виртуальная машина Java по-прежнему пытается выполнить метод main класса Test. Это разрешено только в том случае, если класс был инициализирован (§12.4.1).
Инициализация состоит из выполнения всех инициализаторов переменных класса и статических инициализаторов класса Test в текстовом порядке. Но перед тем, как Test можно инициализировать, его непосредственный суперкласс должен быть инициализирован, а также непосредственный суперкласс его непосредственного суперкласса и так далее рекурсивно. В простом случае Test имеет Object как свой неявный непосредственный суперкласс; если класс Object еще не был инициализирован, то его необходимо инициализировать перед инициализацией Test. Класс Object не имеет суперклассов, поэтому рекурсия здесь завершается.
Если класс Test имеет другой класс Super в качестве своего суперкласса, то Super должен быть инициализирован перед Test. Это требует загрузки, проверки и подготовки Super, если это еще не сделано, и, в зависимости от реализации, может также включать разрешение символических ссылок из Super и так далее рекурсивно.
Таким образом, инициализация может привести к ошибкам загрузки, линковки и инициализации, включая такие ошибки, связанные с другими типами.
Процесс инициализации описывается подробнее в §12.4.
Наконец, после завершения инициализации для класса Test (в ходе которой могут произойти и другие следственные загрузки, линковки и инициализации), вызывается метод main класса Test.
Метод main должен быть объявлен public, static и void. Он должен определять формальный параметр (§8.4.1), тип которого — массив String. Поэтому допустимы следующие объявления:
public static void main(String[] args)
public static void main(String... args)
Загрузка относится к процессу поиска бинарной формы класса или интерфейса с определенным именем, возможно, вычисляя его на лету, но чаще всего извлекая ранее вычисленное бинарное представление из исходного кода с помощью компилятора Java и создавая из этого бинарного представления объект Class для представления класса или интерфейса.
Точные семантики загрузки приведены в главе 5 спецификации виртуальной машины Java, Java SE 11 Edition. Здесь мы представляем общий обзор процесса с точки зрения языка программирования Java.
Бинарный формат класса или интерфейса обычно является форматом файла class, описанным в спецификации виртуальной машины Java, Java SE 11 Edition, указанной выше, но возможны и другие форматы, при условии, что они соответствуют требованиям, указанным в §13.1. Метод defineClass класса ClassLoader может использоваться для создания объектов Class из бинарных представлений в формате файла class.
Корректно работающие загрузчики классов сохраняют эти свойства:
-
При одинаковом имени загрузчик класса должен всегда возвращать один и тот же объект класса.
-
Если загрузчик класса
L1делегирует загрузку класса C другому загрузчикуL2, то для любого типа T, который является непосредственным суперклассом или непосредственным суперинтерфейсом C, или типом поля в C, или типом формального параметра метода или конструктора в C, или типом возвращаемого значения метода в C,L1иL2должны возвращать один и тот же объектClass.
Злонамеренный загрузчик классов может нарушить эти свойства. Однако он не может подорвать безопасность системы типов, поскольку виртуальная машина Java защищает от этого.
Для более подробного обсуждения этих вопросов см. спецификацию виртуальной машины Java, Java SE 11 Edition и статью «Динамическая загрузка классов в виртуальной машине Java» авторов Шенга Лианга и Гилада Брахи, опубликованную в Трудах OOPSLA '98, как «ACM SIGPLAN Notices», том 33, номер 10, октябрь 1998 года, страницы 36-44. Основным принципом проектирования языка программирования Java является то, что во время выполнения система типов не может быть подорвана кодом, написанным на языке программирования Java, даже реализациями таких чувствительных системных классов, как ClassLoader и SecurityManager.
Процесс загрузки реализуется классом ClassLoader и его подклассами.
Разные подклассы ClassLoader могут реализовывать разные политики загрузки. В частности, загрузчик класса может кэшировать бинарные представления классов и интерфейсов, предзагружать их на основе ожидаемого использования или загружать группу связанных классов вместе. Эти действия могут быть не полностью прозрачны для работающего приложения, если, например, новая скомпилированная версия класса не найдена, так как кэшируется более старая версия загрузчиком класса. Однако загрузчик класса должен отражать ошибки загрузки только в тех местах программы, где они могли возникнуть без предварительной загрузки или групповой загрузки.
Если при загрузке класса произойдет ошибка, то экземпляр одного из следующих подклассов класса LinkageError будет выброшен в любой точке программы, которая (прямо или косвенно) использует тип:
-
ClassCircularityError: Класс или интерфейс не может быть загружен, поскольку он является собственным суперклассом или суперинтерфейсом (§8.1.4, §9.1.3, §13.4.4). -
ClassFormatError: Бинарные данные, которые претендуют на описание запрошенного скомпилированного класса или интерфейса, имеют неправильный формат. -
NoClassDefFoundError: Определение запрошенного класса или интерфейса не найдено соответствующим загрузчиком класса.
Поскольку загрузка связана с выделением новых структур данных, она может завершиться сбоем с ошибкой OutOfMemoryError.
Связывание — это процесс взятия бинарной формы типа класса или интерфейса и объединения её в состояние выполнения Java-виртуальной машины, чтобы её можно было выполнить. Тип класса или интерфейса всегда загружается до его связывания.
В связывании участвуют три различных действия: проверка, подготовка и разрешение символических ссылок.
Точные семантики связывания приведены в главе 5 спецификации Java-виртуальной машины, Java SE 11 Edition. Здесь мы представляем общий обзор процесса с точки зрения языка программирования Java.
Эта спецификация предоставляет реализации гибкость в отношении того, когда происходят действия связывания (и, из-за рекурсии, загрузки), при условии соблюдения семантики языка программирования Java, что класс или интерфейс полностью проверены и подготовлены перед его инициализацией, и что ошибки, обнаруженные во время связывания, выбрасываются в точке программы, где выполняется какое-либо действие программы, которое может потребовать связывания с классом или интерфейсом, участвующим в ошибке.
Например, реализация может выбрать разрешение каждой символической ссылки в классе или интерфейсе индивидуально только при её использовании (ленивое или отложенное разрешение), или разрешить их все сразу, пока класс проверяется (статическое разрешение). Это означает, что процесс разрешения может продолжаться в некоторых реализациях после инициализации класса или интерфейса.
Поскольку связывание включает выделение новых структур данных, оно может завершиться ошибкой OutOfMemoryError.
Проверка гарантирует, что бинарное представление класса или интерфейса структурно правильно. Например, она проверяет, что каждая инструкция имеет действительный код операции; что каждая инструкция ветвления переходит к началу другой инструкции, а не в середину инструкции; что каждый метод имеет структурно правильную сигнатуру; и что каждая инструкция соблюдает дисциплину типов языка Java-виртуальной машины.
Если во время проверки произойдёт ошибка, в программе в точке, которая вызвала проверку класса, будет брошено исключение подкласса класса LinkageError:
-
VerifyError: Бинарное определение класса или интерфейса не прошло набор необходимых проверок для подтверждения того, что оно соответствует семантике языка Java-виртуальной машины и что оно не может нарушить целостность Java-виртуальной машины. (См. §13.4.2, §13.4.4, §13.4.9 и §13.4.17 для некоторых примеров.)
Подготовка подразумевает создание полей static (переменных и констант класса) для класса или интерфейса и инициализацию этих полей значениями по умолчанию (§4.12.5). Это не требует выполнения кода; явные инициализаторы статических полей выполняются в рамках инициализации (§12.4), а не подготовки.
Реализации Java-виртуальной машины могут предварительно вычислить дополнительные структуры данных во время подготовки, чтобы сделать последующие операции с классом или интерфейсом более эффективными. Одна особенно полезная структура данных — это "таблица методов" или другая структура данных, которая позволяет вызывать любой метод для экземпляров класса без необходимости поиска суперклассов во время вызова.
Бинарное представление класса или интерфейса ссылается на другие классы и интерфейсы, а также их поля, методы и конструкторы символически, используя бинарные имена (§13.1) других классов и интерфейсов (§13.1). Для полей и методов эти символические ссылки включают имя типа класса или интерфейса, которому принадлежит поле или метод, а также имя самого поля или метода, вместе с соответствующей информацией о типе.
Перед использованием символической ссылки она должна быть разрешена, при этом символическая ссылка проверяется на корректность и, как правило, заменяется прямой ссылкой, которую можно обрабатывать более эффективно при многократном использовании ссылки.
Если во время разрешения произойдёт ошибка, будет выброшено исключение. Чаще всего это будет экземпляр одного из следующих подклассов класса IncompatibleClassChangeError, но это также может быть экземпляр какого-либо другого подкласса IncompatibleClassChangeError или даже экземпляр класса IncompatibleClassChangeError. Эта ошибка может быть выброшена в любой точке программы, использующей символическую ссылку на тип, напрямую или косвенно:
-
IllegalAccessError: Встречена символическая ссылка, которая указывает на использование или присваивание поля, или вызов метода, или создание экземпляра класса, к которому у кода, содержащего ссылку, нет доступа, потому что поле или метод были объявлены сprivate,protectedили пакетом доступа (неpublic), или потому что класс не был объявленpublicв пакете, который экспортирован или открыт для кода, содержащего ссылку.Это может произойти, например, если поле, изначально объявленное
public, изменяется наprivateпосле компиляции другого класса, который ссылается на поле (§13.4.7); или если пакет, в котором объявлен классpublic, перестаёт экспортироваться его модулем после компиляции другого модуля, который ссылается на класс (§13.3). -
InstantiationError: Встречена символическая ссылка, которая используется в выражении создания экземпляра класса, но экземпляр нельзя создать, потому что ссылка, как оказалось, ссылается на интерфейс или абстрактный класс.Это может произойти, например, если класс, изначально не
abstract, изменяется наabstractпосле компиляции другого класса, который ссылается на этот класс (§13.4.1). -
NoSuchFieldError: Встречена символическая ссылка, которая ссылается на конкретное поле конкретного класса или интерфейса, но класс или интерфейс не содержит поля с таким именем.Это может произойти, например, если объявление поля было удалено из класса после компиляции другого класса, который ссылается на поле (§13.4.8).
-
NoSuchMethodError: Встречена символическая ссылка, которая ссылается на конкретный метод конкретного класса или интерфейса, но класс или интерфейс не содержит метода с такой сигнатурой.Это может произойти, например, если объявление метода было удалено из класса после компиляции другого класса, который ссылается на метод (§13.4.12).
Кроме того, UnsatisfiedLinkError, подкласс LinkageError, может быть выброшен, если класс объявляет метод native, для которого не найдено реализации. Ошибка возникнет при использовании метода или раньше, в зависимости от того, какая стратегия разрешения используется реализацией Java-виртуальной машины (§12.3).
Инициализация класса состоит из выполнения его статических инициализаторов и инициализаторов для static полей (переменных класса), объявленных в классе.
Инициализация интерфейса состоит из выполнения инициализаторов для полей (констант), объявленных в интерфейсе.
Тип класса или интерфейса T будет инициализирован непосредственно перед первым появлением одного из следующего:
-
T — это класс, и создаётся экземпляр T.
-
Вызывается метод, объявленный типом T.
-
Переменной
staticполя, объявленного типом T, присваивается значение. -
Используется
staticполе, объявленное типом T, и это поле не является константой (§4.12.4).
При инициализации класса его суперклассы инициализируются (если они ещё не были инициализированы), а также любые суперинтерфейсы (§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 не инициализируется.
Поскольку язык программирования Java многопоточный, инициализация класса или интерфейса требует тщательной синхронизации, так как другой поток может пытаться инициализировать тот же класс или интерфейс одновременно. Также существует возможность, что инициализация класса или интерфейса может быть запрошена рекурсивно как часть инициализации этого класса или интерфейса; например, инициализатор переменной в классе A может вызвать метод несвязанного класса B, который, в свою очередь, может вызвать метод класса A. Реализация виртуальной машины Java отвечает за обработку синхронизации и рекурсивной инициализации, используя следующую процедуру.
Процедура предполагает, что объект Class уже был проверен и подготовлен, а объект Class содержит состояние, указывающее одно из четырёх ситуаций:
-
Этот объект
Classпроверен и подготовлен, но не инициализирован. -
Этот объект
Classинициализируется конкретным потокомT. -
Этот объект
Classполностью инициализирован и готов к использованию. -
Этот объект
Classнаходится в ошибочном состоянии, возможно, потому, что попытка инициализации завершилась неудачей.
Для каждого класса или интерфейса C существует уникальный блокировка инициализации LC. Сопоставление из C в LC предоставляется на усмотрение реализации виртуальной машины Java. Процедура инициализации C выглядит следующим образом:
-
Синхронизироваться на блокировке инициализации,
LC, для C. Это включает ожидание, пока текущий поток сможет получитьLC. -
Если объект
Classдля C указывает, что инициализация C происходит в другом потоке, то освободитьLCи заблокировать текущий поток до получения информации о завершении инициализации, после чего повторить этот шаг. -
Если объект
Classдля C указывает, что инициализация C происходит в текущем потоке, то это рекурсивный запрос на инициализацию. ОсвободитьLCи завершить обычно. -
Если объект
Classдля C указывает, что C уже был инициализирован, то дальнейших действий не требуется. ОсвободитьLCи завершить обычно. -
Если объект
Classдля C находится в ошибочном состоянии, то инициализация невозможна. ОсвободитьLCи бросить исключениеNoClassDefFoundError. -
В противном случае, зафиксировать тот факт, что инициализация объекта
Classдля C происходит в текущем потоке, и освободитьLC.Затем инициализировать поля
staticкласса C, которые являются константными переменными (§4.12.4, §8.3.2, §9.3.1). -
Далее, если C является классом, а не интерфейсом, пусть SC будет его суперклассом, а SI1, ..., SIn - все суперинтерфейсы C, которые объявляют хотя бы один метод по умолчанию. Порядок суперинтерфейсов задаётся рекурсивным перечислением по иерархии суперинтерфейсов каждого интерфейса, реализованного напрямую C (в лево-право порядке предложения C's
implements). Для каждого интерфейса I, реализованного напрямую C, перечисление рекурсивно повторяется для суперинтерфейсов I (в лево-право порядке предложения I'sextends) перед возвращением I.Для каждого S в списке [ SC, SI1, ..., SIn ], если S ещё не был инициализирован, то рекурсивно выполните всю эту процедуру для S. При необходимости сначала проверьте и подготомьте S.
Если инициализация S завершается аварийно из-за брошенного исключения, то получить
LC, отметить объектClassдля C как ошибочный, уведомить все ожидающие потоки, освободитьLCи завершить аварийно, бросив то же исключение, что и при инициализации S. -
Далее, определить, включены ли проверки утверждений (§14.10) для C, запросив у его определяющего загрузчика класса.
-
Далее, выполните либо инициализаторы переменных класса и статические инициализаторы класса, либо инициализаторы полей интерфейса в текстовом порядке, как будто они образуют единый блок.
-
Если выполнение инициализаторов завершается нормально, то получить
LC, пометить объектClassдля C как полностью инициализированный, уведомить все ожидающие потоки, освободитьLCи завершить эту процедуру нормально. -
В противном случае, инициализаторы должны были завершиться аварийно, бросив какое-то исключение E. Если класс E не является
Errorили одним из его подклассов, то создайте новый экземпляр классаExceptionInInitializerError, с E в качестве аргумента, и используйте этот объект вместо E на следующем шаге. Если новый экземплярExceptionInInitializerErrorне может быть создан из-за возникновенияOutOfMemoryError, то вместо этого используйте объектOutOfMemoryErrorвместо E на следующем шаге. -
Получить
LC, пометить объектClassдля C как ошибочный, уведомить все ожидающие потоки, освободитьLCи завершить эту процедуру аварийно с причиной E или ее заменой, как определено на предыдущем шаге.
Реализация может оптимизировать эту процедуру, исключив получение блокировки на шаге 1 (и освобождение на шаге 4/5), когда можно определить, что инициализация класса уже завершена, при условии, что во всех случаях, если бы блокировка была получена, в модели памяти по-прежнему существуют все упорядочения «происходит до».
Генераторы кода должны сохранять точки возможной инициализации класса или интерфейса, вставляя вызов описанной выше процедуры инициализации. Если эта процедура инициализации завершается нормально и объект Class полностью инициализирован и готов к использованию, то вызов процедуры инициализации больше не нужен и может быть исключён из кода – например, путем исправления или перегенерации кода.
Анализ во время компиляции в некоторых случаях может исключить многие проверки того, что тип был инициализирован из сгенерированного кода, если порядок инициализации группы связанных типов может быть определён. Однако такой анализ должен полностью учитывать одновременность и тот факт, что код инициализации не ограничен.
Новый экземпляр класса явно создается, когда вычисление выражения создания экземпляра класса (§15.9) приводит к созданию экземпляра класса.
Новый экземпляр класса может быть неявно создан в следующих ситуациях:
-
Загрузка класса или интерфейса, содержащего строковую литерал (§3.10.5), может создать новый
Stringобъект для представления этой литералы. (Это не произойдёт, если строка, обозначающая ту же последовательность кодовых точек Unicode, была ранее интернирована.) -
Выполнение операции, которая приводит к преобразованию к упакованному типу (§5.1.7). Преобразование к упакованному типу может создать новый объект класса-обертки (
Boolean,Byte,Short,Character,Integer,Long,Float,Double) связанного с одним из примитивных типов. -
Выполнение оператора конкатенации строк
+(§15.18.1), который не является частью константного выражения (§15.28), всегда создает новыйStringобъект для представления результата. Операторы конкатенации строк также могут создавать временные объекты-обертки для значений примитивных типов. -
Вычисление выражения ссылки на метод (§15.13.3) или лямбда-выражения (§15.27.4) может потребовать создания нового экземпляра класса, реализующего функциональный интерфейс.
В каждой из этих ситуаций идентифицируется конкретный конструктор (§8.8), который должен быть вызван со специфицированными аргументами (возможно, ни с одним) в рамках процесса создания экземпляра класса.
Всякий раз, когда создается новый экземпляр класса, выделяется память для него с местом для всех переменных экземпляра, объявленных в типе класса, и всех переменных экземпляра, объявленных в каждом суперклассе типа класса, включая все переменные экземпляра, которые могут быть скрыты (§8.3).
Если доступно недостаточно памяти для выделения памяти для объекта, то создание экземпляра класса завершается неожиданно с OutOfMemoryError. В противном случае все переменные экземпляра в новом объекте, включая те, которые объявлены в суперклассах, инициализируются их значениями по умолчанию (§4.12.5).
Непосредственно перед тем, как ссылка на только что созданный объект возвращается в качестве результата, указанный конструктор обрабатывается для инициализации нового объекта с использованием следующей процедуры:
-
Присвоить аргументы конструктора новым переменным параметров для этого вызова конструктора.
-
Если этот конструктор начинается с явного вызова конструктора (§8.8.7.1) другого конструктора в том же классе (используя
this), то вычислить аргументы и обработать вызов этого конструктора рекурсивно, используя эти же пять шагов. Если этот вызов конструктора завершается неожиданно, то и эта процедура завершается по той же причине; в противном случае продолжить с шагом 5. -
Этот конструктор не начинается с явного вызова конструктора другого конструктора в том же классе (используя
this). Если этот конструктор предназначен для класса, отличного отObject, то этот конструктор будет начинаться с явного или неявного вызова конструктора суперкласса (используяsuper). Вычислить аргументы и обработать вызов конструктора суперкласса рекурсивно, используя эти же пять шагов. Если этот вызов конструктора завершается неожиданно, то и эта процедура завершается по той же причине. В противном случае перейти к шагу 4. -
Выполнить инициализаторы экземпляра и инициализаторы переменных экземпляра для этого класса, присваивая значения инициализаторов переменных экземпляра соответствующим переменным экземпляра в порядке слева направо, в котором они появляются в тексте исходного кода класса. Если выполнение любого из этих инициализаторов приводит к исключению, то дальнейшие инициализаторы не обрабатываются, и эта процедура завершается неожиданно с тем же исключением. В противном случае перейти к шагу 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.
Класс Object имеет метод finalize; этот метод может быть переопределен другими классами. Конкретное определение finalize, которое может быть вызвано для объекта, называется финализатором этого объекта. Перед тем, как сборщик мусора заберет память, используемую объектом, виртуальная машина 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.
Каждый объект можно охарактеризовать двумя атрибутами: он может быть доступным, доступным для финализатора или недоступным, и он также может быть неинициализированным, подлежащим финализации или инициализированным.
Доступный объект — это любой объект, к которому можно получить доступ в любом потенциально продолжающемся вычислении из любого активного потока.
Объект, доступный для финализации, может быть достигнут от некоторого объекта, подлежащего финализации, через некоторую цепочку ссылок, но не из любого активного потока.
Недоступный объект нельзя достичь ни каким из этих способов.
Неинициализированный объект никогда не имел вызываемый автоматически финализатор.
Инициализированный объект имел вызываемый автоматически финализатор.
Объект, подлежащий финализации, никогда не имел вызываемый автоматически финализатор, но виртуальная машина Java может в конечном итоге автоматически вызвать его финализатор.
Объект o не подлежит финализации, пока его конструктор не вызовет конструктор Object для o, и этот вызов не будет выполнен успешно (то есть без выброса исключения). Каждое запись до финализации в поле объекта должна быть видна финализации этого объекта. Кроме того, ни одно из чтений полей этого объекта до финализации не может увидеть записи, происходящие после начала финализации этого объекта.
Можно разработать оптимизирующие преобразования программы, которые уменьшают количество доступных объектов до тех, которые бы наивно считались доступными. Например, компилятор или генератор кода Java может выбрать установку переменной или параметра, которые больше не будут использоваться, на null, чтобы память такого объекта могла потенциально освободиться раньше.
Другим примером этого является случай, когда значения в полях объекта хранятся в регистрах. Тогда программа может обратиться к регистрам, а не к объекту, и больше никогда не обращаться к объекту. Это означало бы, что объект является мусором. Обратите внимание, что такая оптимизация разрешена только в том случае, если ссылки находятся в стеке, а не хранятся в куче.
Например, рассмотрим шаблон Guardian финализатора:
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, происходящим раньше, чем можно было бы ожидать. Для того, чтобы позволить пользователю предотвратить это, мы вводим понятие, что синхронизация может поддерживать объект живым. Если финализатор объекта может привести к синхронизации на этом объекте, то этот объект должен быть активным и считаться доступным, когда на нём удерживается блокировка.
Обратите внимание, что это не препятствует устранению синхронизации: синхронизация поддерживает объект живым только в том случае, если финализатор может синхронизироваться на нём. Поскольку финализатор выполняется в другом потоке, во многих случаях синхронизация всё равно не могла бы быть удалена.
Модель памяти (§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 в полеstaticvкласса C, значение, записанное w1 — ссылка наB, класс C загружен достижимым загрузчиком классов, и не существует записи w2 вv, такой, что hb(w2, w1) не верно и обе w1 и w2 предшествуют di. -
Объект
Bопределённо достижим изAв точке di, если существует запись w1 в элементеvA, значение, записанное 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.
Реализация языка программирования Java может выгружать классы.
Класс или интерфейс может быть выгружен только в том случае, если его определяющий загрузчик классов может быть удалён сборщиком мусора, как обсуждается в §12.6.
Классы и интерфейсы, загруженные загрузчиком bootstrap, не могут быть выгружены.
Выгрузка классов — это оптимизация, которая помогает сократить использование памяти. Очевидно, семантика программы не должна зависеть от того, выбирает ли система реализовать оптимизацию, такую как выгрузка классов, и как она это делает. Иначе это подорвёт переносимость программ. Следовательно, то, был ли класс или интерфейс выгружен или нет, должно быть прозрачно для программы.
Однако, если класс или интерфейс C был выгружен, в то время как его определяющий загрузчик потенциально достижим, то C может быть перезагружен. Никогда нельзя гарантировать, что этого не произойдёт. Даже если к классу не ссылается ни один другой загруженный класс, на него может ссылаться какой-либо класс или интерфейс D, который ещё не был загружен. Когда D загружается определяющим загрузчиком C, его выполнение может привести к перезагрузке C.
Перезагрузка может быть непрозрачной, если, например, класс имеет static переменные (состояние которых будет потеряно), статические инициализаторы (которые могут иметь побочные эффекты) или native методы (которые могут сохранять статическое состояние). Кроме того, значение хеша объекта Class зависит от его идентичности. Поэтому, как правило, невозможно перезагрузить класс или интерфейс полностью прозрачным способом.
Поскольку мы никогда не можем гарантировать, что выгрузка класса или интерфейса, определяющего загрузчик которого потенциально достижим, не приведёт к перезагрузке, а перезагрузка никогда не является прозрачной, но выгрузка должна быть прозрачной, следует, что класс или интерфейс не следует выгружать, пока его загрузчик потенциально достижим. Аналогичный ход рассуждений можно использовать для вывода, что классы и интерфейсы, загруженные загрузчиком bootstrap, никогда не могут быть выгружены.
Также необходимо аргументировать, почему безопасно выгрузить класс C, если его определяющий загрузчик может быть удалён. Если определяющий загрузчик может быть удалён, значит, на него не может быть никаких активных ссылок (включая ссылки, которые не активны, но могут быть восстановлены финализаторами). Это, в свою очередь, может быть истинным только в том случае, если никогда не может быть активных ссылок на какие-либо классы, определённые этим загрузчиком, включая C, ни из их экземпляров, ни из кода.
Выгрузка классов — это оптимизация, которая значима только для приложений, которые загружают большое количество классов и которые перестают использовать большинство этих классов после некоторого времени. Ярким примером такого приложения является веб-браузер, но есть и другие. Характерной чертой таких приложений является то, что они управляют классами с помощью явного использования загрузчиков классов. В результате описанная выше политика хорошо работает для них.
Строго говоря, обсуждение вопроса выгрузки классов не является обязательным для этого спецификации, поскольку выгрузка классов — это всего лишь оптимизация. Однако вопрос очень тонкий, и поэтому он упоминается здесь для уточнения.
Программа завершает все свои действия и выходит, когда происходит одно из двух:
-
Все потоки, которые не являются демонами, завершаются.
-
Какой-то поток вызывает метод
exitклассаRuntimeили классаSystem, и операцияexitне запрещена менеджером безопасности.
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.