Глава 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).
Виртуальная машина Java начинает выполнение, вызывая метод main некоторого указанного класса или интерфейса, передавая ему единственный аргумент — массив строк. В примерах в этом документе этот первый класс обычно называется Test.
Точные семантические правила запуска виртуальной машины Java приведены в главе 5 спецификации Спецификация виртуальной машины Java, Java SE 17 издание. Здесь мы представим обзор процесса с точки зрения языка программирования 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 для представления класса или интерфейса (§1.4).
Точные семантики загрузки приводятся в главе 5 спецификации Java Virtual Machine, Java SE 17 Edition. Здесь мы представляем обзор процесса с точки зрения языка программирования Java.
Двоичное представление класса или интерфейса обычно имеет формат файла class, описанный в главе 4 спецификации Java Virtual Machine, Java SE 17 Edition, но возможны и другие представления, при условии соблюдения требований, указанных в §13.1.
Процесс загрузки реализуется классом ClassLoader и его подклассами. Метод defineClass класса ClassLoader может использоваться для построения объектов Class из двоичных представлений в формате файла class (§1.4).
Разные подклассы класса ClassLoader могут реализовывать разные политики загрузки. В частности, загрузчик классов может кешировать двоичные представления классов и интерфейсов, предзагружать их на основе ожидаемого использования или загружать группу связанных классов вместе. Эти действия могут не быть полностью прозрачными для работающего приложения, если, например, новая скомпилированная версия класса не найдена из-за кеширования более старой версии загрузчиком классов. Однако загрузчик классов отвечает за отражение ошибок загрузки только в тех точках программы, где они могли возникнуть без предзагрузки или групповой загрузки.
Если во время загрузки класса произошла ошибка, то экземпляр одного из следующих подклассов класса LinkageError будет сгенерирован в любой точке программы, которая (прямо или косвенно) использует запрашиваемый класс или интерфейс:
-
ClassCircularityError: Запрашиваемый класс или интерфейс не мог быть загружен, потому что он является собственным суперклассом или суперинтерфейсом (§8.1.4, §9.1.3, §13.4.4). -
ClassFormatError: Двоичные данные, которые претендуют на указание скомпилированного класса или интерфейса, имеют неправильный формат. -
NoClassDefFoundError: Определение запрошенного класса или интерфейса не было найдено соответствующим загрузчиком классов.
Поскольку загрузка подразумевает выделение новых структур данных, она может завершиться ошибкой с OutOfMemoryError.
Корректно работающие загрузчики классов сохраняют эти свойства:
-
При одинаковом имени загрузчик классов должен всегда возвращать один и тот же объект
Class. -
Если загрузчик классов
L1делегирует загрузку класса или интерфейса C другому загрузчикуL2, то для любого класса или интерфейса D, который назван прямым суперклассом типа C или прямым суперинтерфейсом типа C, или типом поля в C, или типом формального параметра метода или конструктора в C, или типом возвращаемого значения метода в C,L1иL2должны возвращать один и тот же объектClassдля D.
Злоумышленный загрузчик классов может нарушить эти свойства. Однако он не сможет подорвать безопасность системы типов, потому что Java Virtual Machine защищает от этого.
Для дальнейшего обсуждения этих вопросов см. спецификацию Java Virtual Machine, Java SE 17 Edition, и статью Dynamic Class Loading in the Java Virtual Machine, авторы Sheng Liang и Gilad Bracha, опубликованную в Proceedings of OOPSLA '98, как ACM SIGPLAN Notices, том 33, номер 10, октябрь 1998, страницы 36-44. Основным принципом разработки языка программирования Java является то, что система типов во время выполнения не может быть подорвана кодом, написанным на языке программирования Java, даже реализациями таких чувствительных системных классов, как ClassLoader и SecurityManager.
Связывание — это процесс взятия двоичной формы класса или интерфейса и объединения её в состояние выполнения Java Virtual Machine, чтобы её можно было выполнить. Класс или интерфейс всегда загружается перед связыванием.
Точные семантики связывания приведены в главе 5 спецификации Java Virtual Machine, Java SE 17 Edition. Здесь мы представляем общий обзор процесса с точки зрения языка программирования Java.
В связывании участвуют три разных действия: проверка, подготовка и разрешение символических ссылок.
Данная спецификация предоставляет реализации гибкость относительно того, когда происходят действия по связыванию (и, из-за рекурсии, загрузки), при условии соблюдения семантики языка программирования Java, что класс или интерфейс полностью проверен и подготовлен перед инициализацией, и что ошибки, обнаруженные во время связывания, выбрасываются в точке в программе, где выполняется какое-либо действие, которое может потребовать связывания с классом или интерфейсом, связанным с ошибкой.
Например, реализация может выбрать разрешение каждой символической ссылки в классе или интерфейсе индивидуально только при её использовании (ленивое или позднее разрешение) или разрешение их всех сразу во время проверки класса (статическое разрешение). Это означает, что процесс разрешения может продолжаться в некоторых реализациях после инициализации класса или интерфейса.
Поскольку связывание включает выделение новых структур данных, оно может завершиться с ошибкой OutOfMemoryError.
Проверка гарантирует, что двоичное представление класса или интерфейса структурно правильно. Например, она проверяет, что каждая инструкция имеет допустимый код операции; что каждая инструкция ветвления переходит к началу другой инструкции, а не в середину инструкции; что у каждого метода есть структурно правильная сигнатура; и что каждая инструкция подчиняется дисциплине типов Java Virtual Machine.
Если во время проверки произошла ошибка, в точке программы, которая вызвала проверку класса, будет выброшено исключение следующего подкласса класса LinkageError:
Подготовка подразумевает создание полей static (переменные и константы класса) для класса или интерфейса и инициализацию этих полей значениями по умолчанию (§4.12.5). Это не требует выполнения какого-либо исходного кода; явные инициализаторы статических полей выполняются как часть инициализации (§12.4), а не подготовки.
Реализации Java Virtual Machine могут предварительно вычислить дополнительные структуры данных во время подготовки, чтобы сделать последующие операции с классом или интерфейсом более эффективными. Одной из особенно полезных структур данных является «таблица методов» или другая структура данных, которая позволяет вызывать любой метод на экземплярах класса без необходимости поиска предков во время вызова.
Двоичное представление класса или интерфейса ссылается на другие классы и интерфейсы, а также на их поля, методы и конструкторы символически, используя двоичные имена (§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 Virtual Machine (§12.3).
Инициализация класса состоит из выполнения его статических инициализаторов и инициализаторов полей (переменных класса) объявленных в классе.
Инициализация интерфейса состоит из выполнения инициализаторов полей (констант) объявленных в интерфейсе.
Класс или интерфейс T будет инициализирован непосредственно перед первым возникновением любого из следующего:
-
T - это класс, и создается экземпляр T.
-
Вызывается метод, объявленный классом T.
-
Переменной поля, объявленной классом T, присваивается значение.
-
Переменной поля, объявленной классом 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 Virtual Machine отвечает за обработку синхронизации и рекурсивной инициализации, используя следующую процедуру.
Процедура предполагает, что объект Class уже был проверен и подготовлен, и что объект Class содержит состояние, указывающее одну из четырёх ситуаций:
-
Этот объект
Classпроверен и подготовлен, но не инициализирован. -
Этот объект
Classинициализируется определенной нитьюT. -
Этот объект
Classполностью инициализирован и готов к использованию. -
Этот объект
Classнаходится в состоянии ошибки, возможно, потому что попытка инициализации не удалась.
Для каждого класса или интерфейса C существует уникальная блокировка инициализации LC. Сопоставление C с LC остается на усмотрение реализации Java Virtual Machine. Процедура инициализации 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) или текстовый блок (§3.10.6), может создать новый
Stringобъект для обозначения строки, представленной строковой литеральной или текстовым блоком. (Это создание объекта не произойдёт, если экземплярString, обозначающий ту же последовательность кодовых точек Юникода, что и строка, представленная строковой литеральной или текстовым блоком, ранее был интернирован.) -
Выполнение операции, которая приводит к преобразованию к объектной форме (§5.1.7). Преобразование к объектной форме может создать новый объект класса-обертки (
Boolean,Byte,Short,Character,Integer,Long,Float,Double), связанного с одним из примитивных типов. -
Выполнение оператора конкатенации строк
+(§15.18.1), который не является частью выражения константы (§15.29), всегда создаёт новыйStringобъект для представления результата. Операторы конкатенации строк также могут создавать временные объекты-обёртки для значения примитивного типа. -
Вычисление выражения ссылки на метод (§15.13.3) или лямбда-выражения (§15.27.4) может потребовать создания нового экземпляра класса, реализующего функциональный интерфейс (§9.8).
В каждой из этих ситуаций идентифицируется конкретный конструктор (§8.8), который вызывается с указанными аргументами (возможно, без них) как часть процесса создания экземпляра класса.
При создании нового экземпляра класса для него выделяется память, достаточная для всех переменных экземпляра, объявленных в классе и во всех его суперклассах, включая все переменные экземпляра, которые могут быть скрыты (§8.3).
Если доступной памяти недостаточно для выделения памяти для объекта, то создание экземпляра класса завершается внезапно с OutOfMemoryError. В противном случае все переменные экземпляра в новом объекте, включая те, что объявлены в суперклассах, инициализируются их значениями по умолчанию (§4.12.5).
Непосредственно перед возвращением ссылки на вновь созданный объект в качестве результата, указанный конструктор обрабатывается для инициализации нового объекта с помощью следующей процедуры:
-
Присвойте аргументы конструктора вновь созданным параметрам переменных для этого вызова конструктора.
-
Если этот конструктор начинается с явного вызова другого конструктора в том же классе (используя
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 имеет метод protected, называемый finalize; этот метод может быть переопределён другими классами. Конкретное определение finalize, которое может быть вызвано для объекта, называется финализатором этого объекта. Перед тем, как сборщик мусора получит доступ к памяти объекта, виртуальная машина Java вызовет финализатор этого объекта.
Финализаторы предоставляют возможность освободить ресурсы, которые не могут быть освобождены автоматически управляющей системой памяти. В таких ситуациях простое освобождение памяти, занимаемой объектом, не гарантирует освобождение ресурсов, которые он удерживал.
Язык программирования Java не определяет, когда будет вызван финализатор, за исключением того, что это произойдёт до повторного использования памяти объекта.
Язык программирования Java не определяет, какой поток вызовет финализатор для данного объекта.
Важно отметить, что активными могут быть несколько потоков финализаторов (это иногда необходимо на многопроцессорных системах с общей памятью), и если большая связанная структура данных становится мусором, все методы finalize для каждого объекта в этой структуре данных могут быть вызваны одновременно, каждый вызов финализатора выполняется в другом потоке.
Язык программирования Java не накладывает никакого порядка на вызовы метода finalize. Финализаторы могут вызываться в любом порядке или даже одновременно.
Например, если циклически связанная группа неинициализированных объектов становится недоступной (или финализируемой), то все объекты могут быть готовы к финализации вместе. В конечном итоге финализаторы этих объектов могут быть вызваны в любом порядке или даже одновременно, используя несколько потоков. Если управляющая система памяти позже обнаружит, что объекты недоступны, то их память может быть освобождена.
Несложно реализовать класс, который вызовет набор методов, подобных финализаторам, в заданном порядке для набора объектов, когда все объекты станут недоступными. Определение такого класса оставлено читателю в качестве упражнения.
Гарантируется, что поток, вызывающий финализатор, не будет удерживать какие-либо видимые пользователю блокировки синхронизации при вызове финализатора.
Если во время финализации возникает необработанное исключение, то исключение игнорируется, и финализация этого объекта завершается.
Завершение конструктора объекта происходит до (§17.4.5) выполнения его метода finalize (в формальном смысле предшествования).
Метод finalize, объявленный в классе Object, не выполняет никаких действий. Тот факт, что класс Object объявляет метод finalize, означает, что метод finalize для любого класса всегда может вызвать метод finalize для своего предка. Это всегда должно выполняться, если только программист не намерен аннулировать действия финализатора в базовом классе. (В отличие от конструкторов, финализаторы не вызывают автоматически финализатор базового класса; такой вызов необходимо программировать явно).
Для повышения эффективности реализация может отслеживать классы, которые не переопределяют метод finalize класса Object, или переопределяют его тривиальным способом.
Например:
protected void finalize() throws Throwable {
super.finalize();
}
Мы рекомендуем реализациям рассматривать такие объекты как имеющие финализатор, который не переопределён, и выполнять финализацию более эффективно, как описано в §12.6.1.
Финализатор может быть вызван явно, как и любой другой метод.
Пакет java.lang.ref описывает слабые ссылки, которые взаимодействуют со сборкой мусора и финализацией. Как и с любым API, имеющим особые взаимодействия с языком программирования Java, реализаторы должны учитывать любые требования, накладываемые API java.lang.ref. В этом спецификации слабые ссылки не рассматриваются. Читатели могут обратиться к документации API для получения подробностей.
Каждый объект можно охарактеризовать двумя атрибутами: он может быть доступным, доступным для финализации или недоступным, и он также может быть неинициализированным, готовым к финализации или инициализированным.
Доступный объект — это любой объект, к которому можно получить доступ в любом потенциально продолжающемся вычислении из любого активного потока.
Объект, доступный для финализации, может быть достигнут от какого-либо объекта, готового к финализации, через цепочку ссылок, но не из любого активного потока.
Недоступный объект не может быть достигнут ни одним из этих способов.
Неинициализированный объект никогда не имел автоматически вызванного финализатора.
Инициализированный объект имел автоматически вызванный финализатор.
Готовый к финализации объект никогда не имел автоматически вызванного финализатора, но виртуальная машина Java может в конечном итоге автоматически вызвать его финализатор.
Объект o не готов к финализации, пока его конструктор не вызовет конструктор Object для o и этот вызов не завершится успешно (то есть без генерации исключения). Каждая запись, выполняемая перед финализацией, в поле объекта, должна быть видна при финализации этого объекта. Кроме того, ни одно из чтений полей этого объекта, выполненных до финализации, не должно видеть записи, выполненные после того, как начата финализация этого объекта.
Можно спроектировать оптимизирующие преобразования программы, которые уменьшат количество доступных объектов меньше, чем те, которые были бы наивно считаны доступными. Например, компилятор или генератор кода Java может выбрать установку переменной или параметра, которые больше не будут использоваться, в null, чтобы вызвать более раннее потенциальное освобождение памяти для такого объекта.
Другой пример — если значения полей объекта хранятся в регистрах. Затем программа может получить доступ к регистрам вместо объекта и никогда больше не обращаться к объекту. Это означает, что объект является мусором. Обратите внимание, что такой вид оптимизации разрешён только для ссылок, находящихся в стеке, а не в куче.
Например, рассмотрим шаблон Finalizer 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 в полеstaticкласса C, значение которой является ссылкой наB, класс C загружен достижимым загрузчиком классов, и не существует записи w2 вv, такая что hb(w2, w1) ложно, и обе w1 и w2 предшествуют di. -
Объект
Bбезусловно достижим изAв точке di, если существует запись w1 в элементеvструктурыA, значение которой является ссылкой наB, и не существует записи w2 вv, такая что hb(w2, w1) ложно, и обе w1 и w2 предшествуют di. -
Если объект
Cбезусловно достижим из объектаB, и объектBбезусловно достижим из объектаA, тоCбезусловно достижим изA.
Если объект X помечен как недостижимый в di, то:
-
Xне должен быть безусловно достижим в di изstaticполей; и -
Все активные использования
Xв потокеt, которые следуют за di, должны происходить в вызове финализатора дляXили в результате выполнения потокомtчтения ссылки наX, которое следует за di; и -
Все чтения, которые следуют за di и видят ссылку на
X, должны видеть записи в элементы объектов, которые были недостижимыми в di, или видеть записи, которые произошли после di.
Действие a является активным использованием X, если и только если выполняется хотя бы одно из следующих условий:
-
a считывает или записывает элемент
X -
a блокирует или разблокирует
X, и существует действие блокировки наX, происходящее после вызова финализатора дляX -
a записывает ссылку на
X -
a является активным использованием объекта
Y, иXбезусловно достижим изY
Если объект X помечен как подлежащий финализации в di, то:
-
Xдолжен быть помечен как недостижимый в di; и -
di должно быть единственной точкой, где
Xпомечен как подлежащий финализации; и -
действия, происходящие после вызова финализатора, должны следовать за di.
Реализация языка программирования 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.