Одной из наиболее заметных особенностей платформы Java является ее автоматическое управление памятью. Многие люди ошибочно переводят эту функцию в отсутствие утечек памяти в Java . Однако это не так, и у меня сложилось впечатление, что современные платформы Java и платформы на основе Java, особенно платформа Android, все больше противоречат этому ошибочному предположению. Чтобы получить представление о том, как могут происходить утечки памяти на платформе Java, рассмотрим следующую реализацию стека :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
class SimpleStack { private final Object[] objectPool = new Object[ 10 ]; private int pointer = - 1 ; public Object pop() { if (pointer < 0 ) { throw new IllegalStateException( "no elements on stack" ); } return objectPool[pointer--]; } public Object peek() { if (pointer < 0 ) { throw new IllegalStateException( "no elements on stack" ); } return objectPool[pointer]; } public void push(Object object) { if (pointer > 8 ) { throw new IllegalStateException( "stack overflow" ); } objectPool[++pointer] = object; } } |
Эта реализация стека хранит свое содержимое в виде массива и дополнительно управляет целым числом, которое указывает на текущую активную ячейку стека. Эта реализация вводит утечку памяти каждый раз, когда элемент выталкивается с вершины стека. Точнее, стек сохраняет ссылку на верхний элемент в массиве, даже если он больше не будет использоваться. (Если только он снова не будет помещен в стек, это приведет к тому, что ссылка будет переопределена с той же самой ссылкой.) Как следствие, Java не сможет собирать этот объект мусором, даже после того, как все другие ссылки на объект будут освобождены. Поскольку реализация стека не разрешает прямой доступ к базовому пулу объектов, эта недостижимая ссылка предотвратит сборку мусора ссылочного объекта до тех пор, пока новый элемент не будет помещен в тот же индекс стека.
К счастью, эту утечку памяти легко исправить:
01
02
03
04
05
06
07
08
09
10
|
public Object pop() { if (pointer < 1 ) { throw new IllegalStateException( "no elements on stack" ); } try { return objectPool[pointer]; } finally { objectPool[pointer--] = null ; } } |
Конечно, реализация структуры памяти не является обычной задачей в повседневной разработке Java. Поэтому давайте рассмотрим более распространенный пример утечки памяти Java. Такая утечка часто вносится общепринятой схемой наблюдения :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
class Observed { public interface Observer { void update(); } private Collection<Observer> observers = new HashSet<Observer>(); void addListener(Observer observer) { observers.add(observer); } void removeListener(Observer observer) { observers.remove(observer); } } |
На этот раз существует метод, позволяющий напрямую удалить ссылку из базового пула объектов. Пока любой зарегистрированный наблюдатель становится незарегистрированным после его использования извне, в этой реализации нет утечек памяти, которых можно опасаться. Однако представьте себе сценарий, в котором вы или пользователь вашей среды забыли отменить регистрацию после ее использования. Опять же, наблюдатель никогда не будет собирать мусор, потому что наблюдаемое сохраняет ссылку на него. Еще хуже, не имея ссылки на этого ныне бесполезного наблюдателя, невозможно удалить наблюдателя из пула наблюдаемых объектов извне.
Но также эта потенциальная утечка памяти легко устраняется путем использования слабых ссылок , функции платформы Java, о которой лично я хотел бы, чтобы программисты знали больше. Короче говоря, слабые ссылки ведут себя как обычные ссылки, но не мешают сбору мусора. Таким образом, слабая ссылка может быть внезапно найдена равной нулю, если не осталось сильных ссылок, а JVM выполнила сборку мусора. Используя слабые ссылки, мы можем изменить приведенный выше код следующим образом:
1
2
|
private Collection<Observer> observers = Collections.newSetFromMap( new WeakHashMap<Observer, Boolean>()); |
WeakHashMap — это готовая реализация карты, которая оборачивает свои ключи слабыми ссылками. С этим изменением наблюдаемое не помешает уборке мусора его наблюдателями. Тем не менее, вы всегда должны указывать это поведение в ваших документах Java! Это может сбивать с толку, если пользователи вашего кода хотят зарегистрировать постоянного наблюдателя для вашего наблюдателя, например, утилиту регистрации, на которую они не планируют ссылаться. Например, Android OnSharedPreferencesChangeListener использует слабые ссылки на прослушиватели без документирования этой функции. Это может держать вас ночью!
В начале этой записи в блоге я предположил, что многие из сегодняшних платформ требуют тщательного управления памятью со стороны своих пользователей, и я хочу привести как минимум два примера на эту тему, чтобы объяснить эту проблему.
Платформа Android:
Программирование для Android вводит модель программирования жизненного цикла в ваши основные классы приложений. В общем, это означает, что вы не контролируете создание и управление собственными экземплярами объектов этих классов, но вместо этого они создаются ОС Android для вас всякий раз, когда они необходимы. (Например, если ваше приложение должно показывать определенный экран.) Аналогичным образом Android решит, когда ему больше не нужен определенный экземпляр (например, когда экран вашего приложения был закрыт пользователем), и сообщит вам об этом. удаление путем вызова конкретного метода жизненного цикла в экземпляре. Однако если вы позволите ссылке на этот объект ускользнуть в некоторый глобальный контекст, JVM Android не сможет собирать этот экземпляр мусора вопреки его намерениям. Поскольку телефоны Android, как правило, довольно ограничены в памяти, а процедуры создания и уничтожения объектов Android могут вырасти довольно дикими даже для простых приложений, вы должны быть особенно осторожны, чтобы очистить свои ссылки.
К сожалению, ссылка на базовый класс приложения ускользает довольно легко. Можете ли вы найти проскальзывающую ссылку в следующем примере?
01
02
03
04
05
06
07
08
09
10
11
12
|
class ExampleActivity extends Activity { @Override public void onCreate(Bundle bundle) { startService( new Intent( this , ExampleService. class ).putExtra( "mykey" , new Serializable() { public String getInfo() { return "myinfo" ; } })); } } |
Если вы подумали, что это ссылка в конструкторе намерения, вы ошибаетесь. Намерение служит только службой запуска для службы и будет удалено после запуска службы. Вместо этого анонимный внутренний класс будет содержать ссылку на свой включающий класс, который является классом ExampleActivity. Если получающий ExampleService сохраняет ссылку на экземпляр этого анонимного класса, он, как следствие, также сохраняет ссылку на экземпляр ExampleActivity. Из-за этого я могу только предложить разработчикам Android избегать использования анонимных классов.
Фреймворки веб-приложений (в частности, Wicket ):
Платформы веб-приложений обычно хранят полупостоянные пользовательские данные в сеансах. Все, что вы записываете в сеанс, обычно остается в памяти в течение неопределенного периода времени. Если вы засоряете свои сеансы, имея большое количество посетителей, JVM вашего контейнера сервлетов рано или поздно будет упакован. Крайним примером необходимости дополнительной заботы о ваших ссылках является структура Wicket: Wicket сериализует любую страницу, которую посетил пользователь, в версионном состоянии. Упрощенно, это означает, что если один из посетителей вашего веб-сайта щелкнет по вашей странице приветствия десять раз, в стандартной конфигурации Wicket сохранит на вашем жестком диске десять сериализованных объектов. Это требует особой осторожности, потому что любые ссылки, удерживаемые объектом страницы Wicket, приведут к сериализации объектов ссылок вместе со страницей. Посмотрите, например, на этот пример плохой практики Wicket:
1
2
3
4
5
6
7
8
|
class ExampleWelcomePage extends WebPage { private final List<People> peopleList; public ExampleWelcomePage (PageParameters pageParameters) { peopleList = new Service().getWorldPhonebook(); } } |
Нажав на страницу приветствия десять раз, ваш пользователь только что сохранил десять копий телефонной книги в мире на жестком диске вашего сервера. Поэтому всегда используйте LoadableDetachableModel в ваших приложениях Wicket, которые позаботятся об управлении ссылками для вас.
Отслеживание утечек памяти в Java-приложениях может быть утомительным, и поэтому я хочу назвать JProfiler полезным (но, к сожалению, несвободным) инструментом отладки. Он позволяет вам просматривать внутренности вашего работающего Java-приложения в виде, например, дампов кучи. Если утечки памяти являются проблемой для ваших приложений, я рекомендую попробовать JProfiler. Доступна оценочная лицензия.
Для дальнейшего чтения : если вы хотите увидеть еще один интересный случай утечки памяти при настройке загрузчиков классов , обратитесь к блогу Zeroturnaround .