© 2008 Наталия Македа
Все материалы блога защищены авторским правом. Любая перепечатка или использование материалов этого блога в коммерческих целях возможна лишь с письменного согласия автора. При некоммерческом использовании ссылка на блог обязательна.

воскресенье, 27 апреля 2008 г.

Передача параметров, сборщик мусора (Выпуск 3)

1. Основы языка (окончание)

1.7. Передача параметров

Параметр = Аргумент (argument)

При передаче параметров при вызове функции в Java, на самом деле передаётся копия параметра. Рассмотрим фрагмент кода:

1. double radians = 1.2345;
2. System.out.println("Синус от " + radians + " = " + Math.sin(radians));

Переменная radians состоит из битового шаблона, который представляет собой число 1.2345. Во второй строке копия этого битового шаблона передаётся в аппарат вызывания методов Виртуальной Java Машины (Java Virtual Machine, JVM).

Когда параметр передаётся в метод, изменения значения параметра методом не отражаются на исходном значении параметра вне метода. Для наглядности рассмотрим пример:

1. public void bumper(int bumpMe) {
2. bumpMe += 15;
3. }

Метод bumper вызывается в следующем коде:

1. int xx = 12345;
2. bumper(xx);
3. System.out.println("Сейчас xx равен " + xx);


Во второй строке xx копируется и передаётся в bumper(), который изменяет значение 12345 на 12360 (+15). Но в третьей строке выводится всё равно 12345, потому что вне метода bumper() значение xx не изменяется.

Это также имеет место, когда передаваемый параметр объект, а не переменная примитивного типа. Однако эффект совсем другой. Для понимания этого, рассмотрим концепцию ссылки на объект. Java программы не работают непосредственно с объектами. Когда конструктор создаёт объект, то возвращает некое значение, битовый шаблон, который уникальным образом идентифицирует объект. Это значение называется ссылка на объект. Например, рассмотрим следующий код:

1. Button btn;
2. btn = new Button("Ok");

Во второй строке конструктор Button возвращает ссылку на только что созданнную кнопку посредством переменной btn. Это не сам объект и не его копия, но ссылка на объект. Например, в некоторых имплементациях JVM ссылка - это адрес объекта.

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

Рассмотрим следующий фрагмент кода:

1. Button btn;
2. btn = new Button("Pink");
3. replacer(btn);
4. System.out.println(btn.getLabel());
5.
6. public void replacer(Button replaceMe) {
7. replaceMe = new Button("Blue");
8. }

Во второй строке создаётся кнопка, ссылка на которую сохраняется в переменной btn. В третьей строке копия ссылки на кнопку Pink передаётся в метод replacer(), который (в строке 7) создаёт вторую кнопку Blue, ссылку на которую сохраняет в переданном параметре replaceMe. Но вне метода replacer() ссылка на кнопку Pink остаётся той же самой, поэтому строка 4 напечает Pink.

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

7. replaceMe.setLabel("Blue");

то, хоть переданная ссылка btn и та же самая, что и в предыдущей версии примера, но сам объект изменён. Ссылка на объект не изменяется, но используется для доступа к самому объекту. В результате четвёртая строка выдаст Blue в выходном потоке.

Массивы - это объекты, а значит, программы работают со ссылками на них, а не непосредственно с ними. То есть при передаче ссылки на массив в некоторый метод, возможно изменить содержимое массива, хранимого в вызывающем методе.

Как создать ссылку на переменную примитивного вида

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

1. public class PrimitiveReference {
2. public static void main(String args[]) {
3. int [] myValue = { 1 };
4. modifyIt(myValue);
5. System.out.println("myValue contains " + myValue[0]);
6. }

7. public static void modifyIt(int [] value) {
8. value[0]++;
9. }
10. }

1.8. Сборщик мусора (Garbage Collection)

Большинство современных языков программирования позволяют выделять память во время выполнения программы. В Java это делается явно тогда, когда используется оператор new для создания объекта, и неявно - когда вызывается метод, который имеет локальные переменные или параметры. В случае неявного выделения памяти используется стековое пространство, которое освобождается, как только совершается выход из метода. В случае явного выделения памяти (под объекты) используется пространство кучи (heap). Освобождение этой памяти не так тривиально, но об этом поговорим ниже.

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

public void aMethod() {
MyClass mc = new MyClass();
}

локальная переменная mc - это ссылка, память для которой выделяется в стеке. Но память для объекта типа MyClass, на который ссылается mc, выделяется в куче.

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

В Java вы никогда не освобождаете память явно. Во время выполнения программы выделяемая память и её использование отслеживается. Это выполняется в фоновом режиме потоком (Thread, который называется сборщик мусора), имеющим низкую приоритетность. Как только сборщик мусора находит память, которая не достижима ни из одного из действующих потоков, он освобождает её обратно в кучу для последующего использования.

Сбор мусора может быть осуществлён разными способами, каждый из которых имеет свои недостатки и преимущества в зависимости от типа выполняемой программы. Для систем контроля в реальном времени, например, необходимо обеспечивать беспрепятственную реакцию на прерывания. Сборка муссора для таких приложений должна производиться часто, в коротких временных интервалах и должна быть легко прерываема. Программы, которые интенсивно используют память, работают лучше, если сборка муссора проводится редко и быстро. В настоящее время сборщик мусора "зашит" (hardcoded) в систему выполнения Java программ и болшинство его алгоритмов используют компромис между выгрузкой и реактивностью (способность к реагированию). В будущем, наверное, будет возможно подключать различные сборщики мусора (или JVM) с различными алгоритмами в зависимости от наших нужд. По крайней мере, мы себе этого желаем.

Однако мы до сих пор не ответили явно на вопрос, поставленный вначале. Самый лучший ответ: память не освобождается до тех пор, пока она используется. Даже если объект больше не используется, нельзя уверенно сказать, когда память, выделенная под него, будет освобождена. Это может случиться через 1 милисекунду, или через 100 милисекунд, или не случиться вовсе. Методы System.gc() и Runtime.gc() выглядят, как "запустить сборщик мусора", но и на них нельзя полагаться, поскольку другие более приоритетные потоки могут отложить их выполнение. И на самом деле, документация для методов gc() гласит: "Вызов этих методов подразумевает, что JVM приложит усилия для переработки неиспользуемых объектов".

Сама природа автоматического сбора мусора имеет важное последствие: утечка памяти (ведущая к её нехватке) всё ещё может иметь место. Достижимые ссылки на неиспользуемые объекты позволяют последним занимать память и не быть обработанными сборщиком мусора. Решением этой проблемы может быть явное присвоение null значения ссылке на объект, который вам уже не нужен. Это особенно просто в случае имплементации коллекции. Например, пусть массив storage используется для реализации стека. Типичная имплементация метода pop() такова:

1. public Object pop() {
2. return storage[index--];
3. }

Если метод, вызывающий pop(), не позаботится о вытолкнутом объекте после его использования, память для этого объекта не будет освобождена до тех пор, пока не произойдёт выход из вызывающего метода или пока ссылка на этот объект (находящаяся в массиве storage) не будет перезаписана. Это может занять много времени. Для ускорения процесса, метод pop() может быть модифицирован следующим образом:

1. public Object pop() {
2. Object returnValue = storage[index];
3. storage[index--] = null;
4. return returnValue;
5. }

1 комментарий:

Lord Nighton комментирует...

Неправильное употребление форсирующих методов сборки мусора. Они выглядят следующим образом:
System.gc();

rt=Runtime.getRuntime();
rt.gc();