23

23


Конечно, когда объект перемещается из одного места в другое, все ссылки, указывающие на него, должны быть изменены. Ссылки в стеке или в статиче¬ском хранилище переопределяются сразу, но могут быть и другие ссылки на этот объект, которые исправляются позже, во время очередного «прохода». Исправление происходит по мере нахождения ссылок.
Существует два фактора, из-за которых «копирующие сборщики» обладают низкой эффективностью. Во-первых, в системе существует две кучи, и вы «пе¬релопачиваете» память то туда, то сюда между двумя отдельными кучами, при этом половина памяти тратится впустую. Некоторые JVM пытаются решить эту проблему, выделяя память для кучи небольшими порциями по мере необхо¬димости, а затем просто копируя одну порцию в другую.
Второй вопрос — копирование. Как только программа перейдет в фазу ста¬бильной работы, она обычно либо становится «безотходной», либо производит совсем немного «мусора». Несмотря на это, копирующий сборщик все равно не перестанет копировать память из одного места в другое, что расточительно. Не¬которые JVM определяют, что новых «отходов» не появляется, и переключаются на другую схему («адаптивная» часть). Эта схема называется пометить-и-уб¬рать (удалить), и именно на ней работали ранние версии виртуальных машин фирмы Sun. Для повсеместного использования вариант «пометить-и-убрать» чересчур медлителен, но, когда известно, что нового «мусора» мало или вообще нет, он выполняется быстро.
Схема «пометить-и-убрать» использует ту же логику — проверка начинается со стека и статического хранилища, после чего постепенно обнаруживаются все ссылки на «живые» объекты. Однако каждый раз при нахождении объект поме¬чается флагом, но еще продолжает существование. «Уборка» происходит только после завершения процесса проверки и пометки. Все «мертвые» объекты при этом удаляются. Но копирования не происходит, и если сборщик решит «упа¬ковать» фрагментированную кучу, то делается это перемещением объектов внут¬ри нее.
Идея «остановиться-и-копировать» несовместима с фоновым процессом сборки мусора; в начале уборки программа останавливается. В литературе фир¬мы Sun можно найти немало заявлений о том, что сборка мусора является фо¬новым процессом с низким приоритетом, но оказывается, что реализации в та¬ком виде (по крайней мере в первых реализациях виртуальной машины Sun) в действительности не существует. Вместо этого сборщик мусора от Sun начи¬нал выполнение только при нехватке памяти. Схема «пометить-и-убрать» так¬же требует остановки программы.
Как упоминалось ранее, в описываемой здесь виртуальной машине память выделяется большими блоками. При создании большого объекта ему выделяет¬ся собственный блок. Строгая реализация схемы «остановиться-и-копировать» требует, чтобы каждый используемый объект из исходной кучи копировался в новую кучу перед освобождением памяти старой кучи, что сопряжено с боль¬шими перемещениями памяти. При работе с блоками памяти СМ использует незанятые блоки для копирования по мере их накопления. У каждого блока имеется счетчик поколений, следящий за использованием блока. В обычной си¬туации «упаковываются» только те блоки, которые были созданы после послед¬ней сборки мусора; для всех остальных блоков значение счетчика увеличивает¬ся при создании внешних ссылок. Такой подход годится для стандартной ситуации — создания множества временных объектов с коротким сроком жизни. Периодически производится полная очистка — большие блоки не копируются (только наращиваются их счетчики), но блоки с маленькими объектами копи¬руются и «упаковываются». Виртуальная машина постоянно следит за эффек¬тивностью сборки мусора и, если она становится неэффективной, потому что в программе остались только долгоживущие объекты, переключается на схему «пометить-и-убрать». Аналогично JVM следит за успешностью схемы «поме¬тить-и-убрать», и, когда куча становится излишне фрагментированной, СМ пе¬реключается обратно к схеме «остановиться-и-копировать». Это и есть адап¬тивный механизм.
Существуют и другие способы ускорения работы в JVM. Наиболее важ¬ные — это действия загрузчика и то, что называется компиляцией «на лету» (Just-In-Time, JIT). Компилятор JIT частично или полностью конвертирует программу в «родной» машинный код, благодаря чему последний не нуждается в обработке виртуальной машиной и может выполняться гораздо быстрее. При загрузке класса (обычно это происходит при первом создании объекта этого класса) система находит файл .class, и байт-код из этого файла переносится в память. В этот момент можно просто провести компиляцию JIT для кода класса, но такой подход имеет два недостатка: во-первых, это займет чуть больше времени, что вместе с жизненным циклом программы может серьезно отразиться на производительности. Во-вторых, увеличивается размер исполняемого файла (байт-код занимает гораздо меньше места в сравнении с расширенным кодом JIT), что может привести к подкачке памяти, и это тоже замедлит программу. Альтернативная схема отложенного вычисления подразумевает, что код JIT компилируется только тогда, когда это станет необходимо. Иначе говоря, код, который никогда не исполняется, не компилируется JIT. Новая технология Java HotSpot, встроенная в последние версии JDK, делает это похожим образом с применением последовательной оптимизации кода при каждом его выполне¬нии. Таким образом, чем чаще выполняется код, тем быстрее он работает.
Инициализация членов класса
Java иногда нарушает гарантии инициализации переменных перед их использо¬ванием. В случае с переменными, определенными локально, в методе, эта гаран¬тия предоставляется в форме сообщения об ошибке. Скажем, при попытке ис¬пользования фрагмента
void f() { int i;
i++. // Ошибка - переменная i не инициализирована
}
вы получите сообщение об ошибке, указывающее на то, что переменная i не была инициализирована. Конечно, компилятор мог бы присваивать таким переменным значения по умолчанию, но данная ситуация больше похожа на ошибку программиста, и подобный подход лишь скрыл бы ее. Заставить про¬граммиста присвоить переменной значение по умолчанию — значит предотвра¬тить ошибку в программе.
Если примитивный тип является полем класса, то и способ обращения с ним несколько иной. Как было показано в главе 2, каждому примитивному полю класса гарантированно присваивается значение по умолчанию. Следующая про¬грамма подтверждает этот факт и выводит значения:
//. initialization/InitialValues java
// Вывод начальных значений, присваиваемых по умолчанию
import static net mindview util print *;
public class InitialValues { boolean t; char c, byte b; short s: int i;
float f; double d,
Начальное значение"). " + t);
+ с + + b); + s); + i);
+ 1): + f).
+ d).
+ reference).
InitialValues reference; void printlnitialValuesO { printC'Tnn данных print("boolean printC'char print("byte printCshort printC'int print("long print("float print("double print("reference

public static void main(String[] args) {
InitialValues iv = new InitialValuesO.
iv.printlnitialValuesO;
/* Тут возможен следующий вариант-
new InitialValuesO printlnitialValuesO; */
} /* Output- Тип'данных boolean char byte short int long float double reference *///-
Начальное значение
false [ ]
0
0
0
0
0.0
0.0
null


Присмотритесь — даже если значения явно не указываются, они автоматиче¬ски инициализируются. (Символьной переменной char присваивается значение ноль, которое отображается в виде пробела.) По крайней мере, нет опасности случайного использования неинициализированной переменной.
Если ссылка на объект, определямая внутри класса, не связывается с новым объектом, то ей автоматически присваивается специальное значение null (клю¬чевое слово Java).
Явная инициализация
Что делать, если вам понадобится придать переменной начальное значение? Проще всего сделать это прямым присваиванием этой переменной значения в точке ее объявления в классе. (Заметьте, что в С++ такое действие запрещено, хотя его постоянно пытаются выполнить новички.) В следующем примере по¬лям уже знакомого класса InitialValues присвоены начальные значения:
//• initialization/InitialValues2.java
// Явное определение начальных значений переменных
public class Ini ti alValues2 {
boolean bool = true; char ch = 'x'; byte b = 47; short s = Oxff; int i = 999; long Ing = 1, float f = 3.14f; double d = 3.14159; } ///:-
Аналогичным образом можно инициализировать и не-примитивные типы. Если Depth является классом, вы можете добавить переменную и инициализи¬ровать ее следующим образом:
//: initialization/Measurement.java class Depth {}
public class Measurement { Depth d = new DepthO; // ... } ///:-
Если вы попытаетесь использовать ссылку d, которой не задано начальное значение, произойдет ошибка времени исполнения, называемая исключением (исключения подробно описываются в главе 10).
Начальное значение даже может задаваться вызовом метода:
II: initialization/Methodlnit.java public class Methodlnit { int i = f(); int f() { return 11; } } ///:-
Конечно, метод может получать аргументы, но в качестве последних не должны использоваться неинициализированные члены класса. Например, так правильно:
II: initialization/Methodlnit2.java public class MethodInit2 { int i = f(), int j = g(i); int f() { return 11; } int g(int n) { return n * 10; } } ///-
а так нет:
II: initialization/MethodInit3 java public class MethodInit3 { //! int j = g(i); 11 Недопустимая опережающая ссылка int i = f(); int f() { return 11; } int g(int n) { return n * 10, } } ///
Это одно из мест, где компилятор на полном основании выражает недоволь¬ство преждевременной ссылкой, поскольку ошибка связана с порядком инициа¬лизации, а не с компиляцией программы.
Описанный подход инициализации очень прост и прямолинеен. У него есть ограничение — все объекты типа InitialValues получат одни и те же начальные значения. Иногда вам нужно именно это, но в других ситуациях необходима большая гибкость.
Инициализация конструктором
Для проведения инициализации можно использовать конструктор. Это придает большую гибкость процессу программирования, так как появляется возмож¬ность вызова методов и выполнения действия по инициализации прямо во вре¬мя работы программы. Впрочем, при этом необходимо учитывать еще одно об¬стоятельство: оно не исключает автоматической инициализации, происходящей перед выполнением конструктора. Например, в следующем фрагменте
//: initialization/Counter.java public class Counter { int i;
Counter О {i=7, } // .. } ///-
переменной i сначала будет присвоено значение 0, а затем уже 7. Это верно для всех примитивных типов и ссылок на объекты, включая те, которым задаются явные значения в точке определения. По этим причинам компилятор не пыта¬ется заставить вас инициализировать элементы в конструкторе, или в ином определенном месте, или перед их использованием — инициализация и так га¬рантирована.
Порядок инициализации
Внутри класса очередность инициализации определяется порядком следования переменных, объявленных в этом классе. Определения переменных могут быть разбросаны по разным определениям методов, но в любом случае переменные инициализируются перед вызовом любого метода — даже конструктора. На¬пример:

Report Page