Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27 Мб
Скачать
☆

Глава 2 • Создание иуничтожение объектов

вследствие «беби-бума», другими словми, родился ли он между 1946 и 1964 годами:

public class Person {

private final Date birthDate;

//Прочие поля, методы и конструкторы опущены

//Никогда так не делайте!

public boolean isBabyBoomer() {

// Ненужное размещение затратных объектов Calendar gmtCal = Calendar.getInstance(TimeZone,getTimeZone(“GMT")); gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0); Date boomStart = gmtCal.getTime(); gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); Date boomEnd = gmtCal.getTime();

return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;

}

}

Метод isBabyBoomer при каждом вызове без всякой надобности создает новые экземпляры Calendar, TimeZone и два экземпляра Date. В следующей версии подобная расточительность пресекается с помо­ щью статического инициализатора:

public class Person {

private final Date birthDate;

// Другие поля, методы и конструкторы опущены

I -к

* Даты начала и конца демографического взрыва

•к/

private static final Date B00M_START; private static final Date B00M_END; static {

Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone(“GMT”)); gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);

30

С татья 5

BOMM_START = gmtCal.getTime();

gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); B0MM_END = gmtCal.getTime();

}

public boolean isBabyBoomer() {

return birthDate.compareTo(B00M_START) >= 0 && birthDate.compareTo(B00M_END) < 0;

}

}

В исправленной версии класса Person экземпляры Calendar, TimeZone и Date создаются только один раз в ходе инициализации вместо того, чтобы создавать их при каждом вызове метода isBabyBoomer. Если данный метод вызывается достаточно часто, это при­ водит к значительному выигрышу в производительности. На моей машине исходная версия программы тратит на 10 миллионов вызовов 32 000 мс, улучшенная - 130 мс, что в 230 раз быстрее. Причем улучшается не только производительность программы, но и нагляд­ ность. Замена локальных переменных boomStart и boomEnd статиче­ скими полями типа final показывает, что эти даты рассматриваются как константы, соответственно, программный код становится более понятным. Для полной ясности заметим, что экономия от подобной оптимизации не всегда будет столь впечатляющей, просто здесь осо­ бенно много ресурсов требует создание экземпляров Calendar.

Если метод isBabyBoomer вызываться не будет, инициализация полей B00M_START и B00M_END в улучшенной версии класса Person ока­ жется напрасной. Ненужных действий можно было бы избежать, использовав для этих полей отложенную инициализацию (lazily initializing) (статья 71), которая бы выполнялась при первом вызове метода isBabyBoomer, однако делать это не рекомендуется. Как ча­ сто бывает в случаях с отложенной инициализацией, это усложнит реализацию и вряд ли приведет к заметному повышению производи­ тельности (статья 33).

Во всех примерах, ранее приведенных в этой статье, было оче­ видно, что рассматриваемые объекты можно использовать повторно,

31

Глава 2 • Создание иуничтожение объектов

поскольку они были неизменяемыми. Есть, однако, другие ситуа­ ции, когда это не столь очевидно. Рассмотрим случай с адаптерами (adapter) [Gamma95, с. 139], которые известны также как представ­ ления (view). Адаптер — это объект, который делегирован ниже­ лежащим объектом, создавая для него альтернативный интерфейс. Поскольку адаптер не имеет иных состояний, помимо состояния нижележащего объекта, то для адаптера, представляющего данный объект, более одного экземпляра создавать не надо.

Например, в интерфейсе Мар метод keySet возвращает для объек­ та Мар представление Set, которое содержит все ключи данной схемы. По незнанию можно подумать, что каждый вызов метода keySet должен создавать новый экземпляр Set. Однако в действительности для некоего объекта Мар любые вызовы keySet могут возвращать один и тот же эк­ земпляр Set. И хотя обычно возвращаемый экземпляр Set является из­ меняемым, все возвращаемые объекты функционально идентичны: ког­ да меняется один из них, то же самое происходит и со всеми остальными экземплярами Set, поскольку за всеми ними стоит один и тот же экзем­ пляр Мар. Нет необходимости создавать несколько экземпляров объекта представления KeySet, хотя их создание и безвредно.

В релизе 1.3 есть и другой способ создания ненужных объектов. Он называется автоупаковка (autoboxing) и позволяет программисту использовать и примитивные им упакованные примитивные типы, вы­ полнять упаковку и распаковку автоматически в случае необходимости. Имеются тонкие семантические различия и не тонкие отличия в произ­ водительности (статья 49). Рассмотрим следующую программу, кото­ рая рассчитывает сумму всех положительных значений in t. Для этого используется арифметическая функция long, так как int недостаточно большая, чтобы хранить сумму всех положительных значений int.

//Ужасно медленная программа! Можете определить, на каком этапе

//создается объект?

public static void main(String[] args) { Long sum = OL;

for (long i = 0; i <= Integer.MAX_VALUE; i++) {

32

С татья 5

sum += 1 ;

}

System.out.printIn(sum);

}

Эта программа дает правильный ответ, но выполняется она на­ много медленнее, чем должна, из-за ошибки только в одном символе. Переменная sum объявлена как Long вместо long, что означает, что программа создает 231 ненужных экземпляров Long (грубо говоря, каждый раз, когда long добавляется к сумме Long). Изменение де­ кларирования sum на long вместо Long уменьшает время выполнения программы на моей машине с 43 до 6,8 секунды. Урок понятен: пред­ почтение надо отдавать примитивным и упакованным примитивным типам и избегать непреднамеренного автоупаковывания.

В этой статье отнюдь не утверждается, что создание объектов требует много ресурсов и его нужно избегать. Наоборот, создание и повторное использование небольших объектов, чьи конструкто­ ры выполняют несложную и понятную работу, необременительно, особенно для современных реализаций JV M . Создание дополни­ тельных объектов ради большей наглядности, упрощения и расши­ рения возможностей программы — это обычно хорошая практика.

И наоборот, отказ от создания объектов и поддержка соб­ ственного пула объектов (object pool) — плохая идея, если только объекты в этом пуле не будут крайне ресурсоемкими. Основной пример объекта, для которого оправданно создание пула, — это может быть соединение с базой данных (database connection). Затраты на установление такого соединения достаточно высоки, и потому многократное использование такого объекта оправдан­ но. Также лицензионная политика вашей БД может накладывать ограничения на фиксированное количество соединений. Одна­ ко в общем случае создание собственного пула объектов загро­ мождает вашу программу, увеличивает расход памяти и снижает производительность программы. Современные реализации JV M имеют хорошо оптимизированные сборщики мусора, которые при

33

Глава 2 • Создание и уничтожение объектов

работе с небольшими объектами с легкостью превосходят подоб­ ные пулы объектов.

В противовес этой статье можно привести статью 39, посвя­ щенную резервному копированию (defensive copying). Если в этой статье говорится: «Не создавайте новый объект, если вы обязаны использовать имеющийся еще раз», то статья 39 гласит: «Не надо использовать имеющийся объект еще раз, если вы обязаны создать новый». Заметим, что ущерб от повторного использования объек­ та, когда требуется резервное копирование, значительно превосходит ущерб от бесполезного создания дублирующего объекта. Отсутствие резервных копий там, где они необходимы, может привести к ковар­ ным ошибкам и дырам в системе безопасности, создание же ненуж­ ных объектов всего лишь влияет на стиль и производительность про­ граммы.

Уничтожайте устаревшие ссылки (на объекты)

Когда вы переходите с языка программирования с ручным управ­ лением памятью, такого как Си или C + + , на язык с автоматической очисткой памяти (garbage-collect — «сборка мусора»), ваша работа как программиста существенно упрощается благодаря тому обстоя­ тельству, что ваши объекты автоматически утилизируются, как толь­ ко вы перестаете с ними работать. Когда вы впервые сталкиваетесь с этим, это производит впечатление почти что волшебства. Легко мо­ жет создаться впечатление, что вам больше не надо думать об управ­ лении памятью, но это не совсем так.

Рассмотрим следующую реализацию простого стека:

// Можете ли вы заметить «утечку памяти»? public class Stack {

private Object[] elements; private int size = 0;

34

С татья 6

private static final int DEFAULT_INITIAL_CAPACITY = 16;

public Stack() {

elements = new Object[DEFAULT_INITIAL_CAPACITY];

}

public void push(Object e) { ensureCapacity(); elements[size++] = e;

}

public Object pop() { if (size == 0)

throw new EmptyStackException(); return elements[- size];

}

f * *

* убедиться в том,

что

в стеке есть место хотя бы еще для

* одного

элемента,

каждый раз, когда нужно увеличить массив,

* просто

удваивать

его

емкость

*/

private void ensureCapacity() {

if (elements.length == size)

elements = Arrays.copyOf(elements, 2 * size +1);

}

}

В этой программе нет погрешностей, которые бросались бы в глаза (но посмотрите в статье 26 оригинальную версию). Вы можете тщательно ее тестировать, любое испытание она пройдет с успехом, но в ней все же скрыта одна проблема. Грубо говоря, в этой програм­ ме имеется «утечка памяти», которая может тихо проявляться в виде снижения производительности, в связи с усиленной работой сборщи­ ка мусора либо увеличения размера используемой памяти. В крайнем случае подобная утечка памяти может привести к началу подкачки страниц с диска и даже аварийному завершению программы с диа­ гностикой OutOfMemoryError, хотя подобные отказы встречаются от­ носительно редко.

35

Глава 2 • Создание иуничтожение объектов

Так где же происходит утечка? Если стек растет, а затем умень­ шается, то объекты, которые были вытолкнуты из стека, не могут быть удалены, даже если программа, пользующаяся этим стеком, уже не имеет ссылок на них. Все дело в том, что этот стек сохраняет устаревшие ссылки (obsolete reference) на эти объекты. Устаревшая ссылка — это такая ссылка, которая уже никогда не будет разымено­ вана. В данном случае устаревшими являются любые ссылки, оказав­ шиеся за пределами активной части этого массива элементов. Актив­ ная же часть стека включает элементы, чей индекс меньше значения переменной size.

Утечка памяти в язьжах с автоматической сборкой мусора (или, точнее, непреднамеренное сохранение объектов — unintentional object retention) очень коварна. Если ссылка на объект была непреднамеренно сохранена, сборщик мусора не сможет удалить не только этот объект, но и все объекты, на которые он ссылается, и т.д. Если даже непред­ намеренно было сохранено всего несколько объектов, многие и многие объекты могут стать недоступны сборщику мусора, а это может ока­ зать большое влияние на производительность программы.

Решаются проблемы такого типа очень просто: как только ссыл­ ки устаревают, их нужно обнулять. В случае с нашим классом Stack ссылка становится устаревшей, как только ее объект был вытолкнут из стека. Исправленный вариант метода pop выглядит следующим образом:

public Object рор() { if (size == 0)

throw new EinptyStackException(); Object result = elements[- size];

elements[size] = null; // Убираем устаревшую ссылку return result;

}

Обнуление устаревших ссылок дает и другое преимущество: если впоследствии кто-то по ошибке попытается разыменовать ка­ кую-либо из этих ссылок, программа незамедлительно завершится

36

С татья 6

с диагностикой Null Point erExcept ion вместо того, чтобы спокойно выполнять неправильную работу. Всегда выгодно обнаруживать ошибки программирования настолько быстро, насколько это воз­ можно.

Когда программисты впервые сталкиваются с подобной пробле­ мой, он начинают перестраховываться, обнуляя все ссылки на объ­ екты, лишь только программа заканчивает работу с ними. Это не­ желательно и не необходимо, поскольку это без необходимости загромождает программу и, по-видимому, снизит ее производитель­ ность. Обнуление ссылок на объект должно быть не нормой,

аисключением. Лучший способ избавиться от устаревшей ссыл­ ки — вновь использовать переменную, в которой она находилась, либо выйти из области видимости этой переменной. Это происходит естественным образом, если для каждой переменной вы задаете са­ мую ограниченную область видимости (статья 45).

Так когда же вы должны обнулять ссылку? Какая особенность класса Stack сделала его восприимчивым к утечке памяти? Просто класс Stack управляет своей памятью. Пул хранения состоит из мас­ сива элементов (причем его ячейками являются ссылки на объекты,

ане сами объекты). Как было указано выше, элементы из актив­ ной части массива считаются занятыми, в остальной — свободными. Сборщик мусора этого знать никак не может, и для него все ссылки на объекты, хранящиеся в массиве, в равной степени действительны. Только программист знает, что неактивная часть массива не нужна. Реально сообщить этот факт сборщику мусора программист может, лишь вручную обнуляя элементы массива по мере того, как они пере­ ходят в неактивную часть массива.

Вообще говоря, как только какой-либо класс начинает управлять своей памятью, программист должен озаботиться утечкой памяти. Как только элемент массива освобождается, лю­ бые ссылки на объекты, имевшиеся в этом элементе, необходимо обнулять.

37

java. util. Linked-

Глава 2 • Создание и уничтожение объектов

Другим распространенным источником утечек памяти яв­ ляются кэши. Поместив однажды в кэш ссылку на некий объект, легко можно забыть о том, что она там есть, и держать ссылку в кэше еще долгое время после того, как она стала недействитель­ ной. У этой проблемы возможны несколько решений. Если вам посчастливилось создавать кэш, в котором запись остается зна­ чимой ровно до тех пор, пока за пределами кэша остаются ссылки на ее ключ, представьте этот кэш как WeakHashMap: когда запи­ си устареют, они будут удалены автоматически. Не забывайте, что использование WeakHashMap имеет смысл, только если жела­ емое время жизни записей кэша определено внешними ссылками на ключ, а не на значение.

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

(возможно, Timer или ScheduledThreadPoolExecutor) либо побочный

эффект от добавления в кэш новых записей. Реализации второго под­ хода помогает метод removeEldestEntгу из класса

HashMap.

Третий распространенный путь утечки памяти —это при­ ложения в режиме ожидания и прочие обратные вызовы. Если вы реализуете A P I, в котором клиенты регистрируют обратные вызовы, но не отменяют свою регистрацию, то они накапливают­ ся, если вы ничего не предпримете. Лучший способ убедиться, что обратные вызовы не обошли вниманием программы «сборщики мусора», — это хранить только слабые ссылки на них, например сохраняя их лишь в качестве ключей в WeakHashMap.

Поскольку утечка памяти обычно не обнаруживает себя в виде очевидного сбоя, она может оставаться в системе годами. Как пра­ вило, обнаруживают ее лишь в результате тщательной инспекции программного кода или с помощью инструмента отладки, известного

38

С татья 7

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

Г Остерегайтесь методов finalize

Методы finalize непредсказуемы, часто опасны и, как пра­ вило, не нужны. Их использование может привести к странному поведению программы, низкой производительности и проблемам с переносимостью. У методов finalize есть лишь несколько областей применения, о которых мы поговорим в этой статье позднее, а глав­ ное правило таково: следует избегать методов finalize.

Программистов, пишущих на C + + , следует предостеречь о том, что думать о методах finalize как об аналоге деструкторов в C + + , нельзя. В C + + деструктор — это нормальный способ ути­ лизации ресурсов, связанных с объектом, обязательное дополнение к конструктору. В языке программирования Java, когда объект ста­ новится недоступен, очистку связанной с ним памяти осуществля­ ет сборщик мусора. Со стороны же программиста никаких специ­ альных действий не требуется. В C + + деструкторы используются для освобождения не только памяти, но и других ресурсов системы. В языке программирования Java для этого обычно используется

блок t гу—finally.

Нет гарантии, что метод finalize будут вызван немедленно [JUS,12.6]. С момента, когда объект становится недоступен, и до момента выполнения метода finalize может пройти сколь угодно длительное время. Это означает, что с помощью метода finalize

нельзя выполнять никаких операций, критичных по времени.

Например, будет серьезной ошибкой ставить процедуру закрытия открытых файлов в зависимость от метода finalize, поскольку де­ скрипторы открытых файлов — ресурс ограниченный. Если из-за того, что JV M медлит с запуском методов finalize, открытыми бу-

39