Java Language
Управление памятью Java
Поиск…
замечания
В Java объекты выделяются в куче, а память кучи утилизируется с помощью автоматической сборки мусора. Приложение не может явно удалить объект Java.
Основные принципы сборки мусора Java описаны в примере коллекции Мусора . В других примерах описывается окончательная доработка, как вручную запускать сборщик мусора и проблема утечек хранилища.
завершение
Объект Java может объявить метод finalize
. Этот метод вызывается непосредственно перед тем, как Java освобождает память для объекта. Он будет выглядеть следующим образом:
public class MyClass {
//Methods for the class
@Override
protected void finalize() throws Throwable {
// Cleanup code
}
}
Однако есть некоторые важные предостережения о поведении завершения Java.
- Java не дает никаких гарантий относительно того, когда вызывается метод
finalize()
. - Java даже не гарантирует, что метод
finalize()
будет вызываться некоторое время в течение всего срока действия исполняемого приложения. - Единственное, что гарантировано, это то, что метод будет вызываться до того, как объект будет удален ... если объект будет удален.
Предостережения выше означают, что плохой идеей полагаться на метод finalize
для выполнения действий по очистке (или других), которые должны выполняться своевременно. Опора на завершение может привести к утечкам памяти, утечкам памяти и другим проблемам.
Короче говоря, очень мало ситуаций, когда финализация на самом деле является хорошим решением.
Финализаторы запускаются только один раз
Обычно объект удаляется после его завершения. Однако этого не происходит постоянно. Рассмотрим следующий пример 1 :
public class CaptainJack {
public static CaptainJack notDeadYet = null;
protected void finalize() {
// Resurrection!
notDeadYet = this;
}
}
Когда экземпляр CaptainJack
становится недоступным и сборщик мусора пытается его восстановить, метод finalize()
назначит ссылку на экземпляр переменной notDeadYet
. Это сделает экземпляр доступным еще раз, и сборщик мусора не удалит его.
Вопрос: Капитан Джек бессмертен?
Ответ: Нет.
Ловушка JVM будет запускать только финализатор на объект один раз в жизни. Если вы назначаете null
для notDeadYet
чтобы notDeadYet
недопустимый экземпляр, сборщик мусора не будет вызывать finalize()
для объекта.
1 - См. Https://en.wikipedia.org/wiki/Jack_Harkness .
Ручное включение GC
Вы можете вручную запустить сборщик мусора, позвонив
System.gc();
Однако Java не гарантирует, что сборщик мусора запускается при возврате вызова. Этот метод просто «предлагает» JVM (виртуальная машина Java), что вы хотите, чтобы он запускал сборщик мусора, но не заставлял его это делать.
Обычно считается неправильной практикой попытки вручную запускать сборку мусора. JVM можно запустить с -XX:+DisableExplicitGC
чтобы отключить вызовы System.gc()
. Запуск сбора мусора, вызвав System.gc()
может привести к нарушению нормальной работы по управлению мусором / объектам для конкретной реализации сборщика мусора, используемого JVM.
Вывоз мусора
Подход C ++ - новый и удаленный
На языке, подобном C ++, прикладная программа отвечает за управление памятью, используемой динамически распределенной памятью. Когда объект создается в куче C ++ с использованием new
оператора, должно быть соответствующее использование оператора delete
для утилизации объекта:
Если программа забывает
delete
объект и просто «забывает» об этом, связанная с ним память будет потеряна для приложения. Термин для этой ситуации - утечка памяти , и слишком много утечек памяти приложение может использовать все больше и больше памяти и в конечном итоге сбой.С другой стороны, если приложение пытается дважды
delete
тот же объект или использовать объект после его удаления, приложение может быть повреждено из-за проблем с повреждением памяти
В сложной программе на C ++ реализация управления памятью с использованием new
и delete
может занять много времени. Действительно, управление памятью является общим источником ошибок.
Подход Java - сбор мусора
Java использует другой подход. Вместо явного оператора delete
Java предоставляет автоматический механизм, известный как сбор мусора, для восстановления памяти, используемой объектами, которые больше не нужны. Система времени выполнения Java берет на себя ответственность за поиск объектов, которые должны быть удалены. Эта задача выполняется компонентом, называемым сборщиком мусора , или GC для краткости.
В любое время во время выполнения Java-программы мы можем разделить набор всех существующих объектов на два разных подмножества 1 :
Достижимые объекты определяются JLS следующим образом:
Досягаемым объектом является любой объект, к которому можно получить доступ в любом потенциальном продолжающемся вычислении из любой живой нити.
На практике это означает, что существует цепочка ссылок, начиная с локальной переменной области или
static
переменной, с помощью которой какой-то код может быть доступен для объекта.Недостижимыми объектами являются объекты, которые не могут быть достигнуты, как указано выше.
Любые объекты, недостижимые, имеют право на сбор мусора. Это не означает, что они будут собирать мусор. По факту:
- Недоступный объект не получает сразу, когда становится недоступным. 1 .
- Недостижимый объект может не быть собранным мусором.
Спецификация языка Java дает большую широту реализации JVM, чтобы решить, когда собирать недостижимые объекты. Он также (на практике) дает разрешение для реализации JVM быть консервативным в том, как он обнаруживает недоступные объекты.
Единственное , что гарантирует JLS , что никакие достижимые объекты никогда не будет мусора.
Что происходит, когда объект становится недоступным
Прежде всего, ничего не происходит, когда объект становится недоступным. Все происходит только тогда, когда работает сборщик мусора, и он обнаруживает, что объект недоступен. Кроме того, для GC-GC обычно не обнаруживать все недостижимые объекты.
Когда GC обнаруживает недостижимый объект, могут произойти следующие события.
Если есть какие-либо объекты
Reference
, относящиеся к объекту, эти ссылки будут удалены до того, как объект будет удален.Если объект финализируемый, то он будет завершен. Это происходит до удаления объекта.
Объект можно удалить, и память, которую он занимает, может быть восстановлена.
Обратите внимание, что существует четкая последовательность, в которой могут произойти указанные события, но ничто не требует, чтобы сборщик мусора выполнял окончательное удаление какого-либо конкретного объекта в любом конкретном временном кадре.
Примеры достижимых и недоступных объектов
Рассмотрим следующие примеры классов:
// A node in simple "open" linked-list.
public class Node {
private static int counter = 0;
public int nodeNumber = ++counter;
public Node next;
}
public class ListTest {
public static void main(String[] args) {
test(); // M1
System.out.prinln("Done"); // M2
}
private static void test() {
Node n1 = new Node(); // T1
Node n2 = new Node(); // T2
Node n3 = new Node(); // T3
n1.next = n2; // T4
n2 = null; // T5
n3 = null; // T6
}
}
Давайте посмотрим, что происходит, когда вызывается test()
. Заявления T1, T2 и T3 создают объекты Node
, и все объекты достижимы через переменные n1
, n2
и n3
соответственно. Заявление T4 назначает ссылку на объект второго Node
на next
поле первого. Когда это будет сделано, 2-й Node
доступен по двум путям:
n2 -> Node2
n1 -> Node1, Node1.next -> Node2
В заявлении T5 мы присваиваем значение null
для n2
. Это разрушает первую из цепей достижимости для Node2
, но вторая остается неизменной, поэтому Node2
все еще доступен.
В заявлении T6 мы присваиваем значение null
для n3
. Это нарушает единственную цепочку достижимости для Node3
, что делает Node3
недоступным. Однако Node1
и Node2
все еще доступны через переменную n1
.
Наконец, когда метод test()
возвращается, его локальные переменные n1
, n2
и n3
выходят за пределы области видимости и поэтому не могут быть доступны никем. Это разрушает оставшиеся цепи достижимости для Node1
и Node2
, а все объекты Node
также недоступны и не могут быть использованы для сбора мусора.
1 - Это упрощение, которое игнорирует завершение и Reference
классы. 2 - Гипотетически, реализация Java может сделать это, но стоимость выполнения этого делает ее нецелесообразной.
Установка размеров кучи, пермгана и стека
Когда запускается виртуальная машина Java, она должна знать, как увеличить размер кучи и размер по умолчанию для стеков потоков. Они могут быть указаны с помощью параметров командной строки в команде java
. Для версий Java до Java 8 вы также можете указать размер области PermGen кучи.
Обратите внимание, что PermGen был удален в Java 8, и если вы попытаетесь установить размер PermGen, этот параметр будет проигнорирован (с предупреждающим сообщением).
Если вы явно не укажете размеры кучи и стека, JVM будет использовать значения по умолчанию, которые рассчитываются в версии и на платформе. Это может привести к тому, что ваше приложение будет использовать слишком мало или слишком много памяти. Обычно это нормально для стеков потоков, но это может быть проблематично для программы, которая использует много памяти.
Настройка размеров кучи, PermGen и стандартных стеков:
Следующие параметры JVM задают размер кучи:
-
-Xms<size>
- устанавливает начальный размер кучи -
-Xmx<size>
- устанавливает максимальный размер кучи -
-XX:PermSize<size>
- устанавливает начальный размер PermGen -
-XX:MaxPermSize<size>
- устанавливает максимальный размер PermGen -
-Xss<size>
- устанавливает размер стека по умолчанию
Параметр <size>
может быть числом байтов или может иметь суффикс k
, m
или g
. Последние определяют размер в килобайтах, мегабайтах и гигабайтах соответственно.
Примеры:
$ java -Xms512m -Xmx1024m JavaApp
$ java -XX:PermSize=64m -XX:MaxPermSize=128m JavaApp
$ java -Xss512k JavaApp
Поиск размеров по умолчанию:
Опция -XX:+printFlagsFinal
может использоваться для печати значений всех флагов перед запуском JVM. Это можно использовать для печати значений по умолчанию для настроек размера кучи и размера стека следующим образом:
Для Linux, Unix, Solaris и Mac OSX
$ java -XX: + PrintFlagsFinal -version | grep -iE 'HeapSize | PermSize | ThreadStackSize'
Для Windows:
java -XX: + PrintFlagsFinal -version | findstr / i "HeapSize PermSize ThreadStackSize"
Вывод приведенных выше команд будет выглядеть следующим образом:
uintx InitialHeapSize := 20655360 {product}
uintx MaxHeapSize := 331350016 {product}
uintx PermSize = 21757952 {pd product}
uintx MaxPermSize = 85983232 {pd product}
intx ThreadStackSize = 1024 {pd product}
Размеры указаны в байтах.
Утечка памяти в Java
В примере коллекции Garbage мы подразумеваем, что Java решает проблему утечки памяти. На самом деле это не так. Программа Java может просачивать память, хотя причины утечек довольно разные.
Доступные объекты могут протекать
Рассмотрим следующую реализацию наивного стека.
public class NaiveStack {
private Object[] stack = new Object[100];
private int top = 0;
public void push(Object obj) {
if (top >= stack.length) {
throw new StackException("stack overflow");
}
stack[top++] = obj;
}
public Object pop() {
if (top <= 0) {
throw new StackException("stack underflow");
}
return stack[--top];
}
public boolean isEmpty() {
return top == 0;
}
}
Когда вы push
объект, а затем сразу же pop
, все равно будет ссылка на объект в массиве stack
.
Логика реализации стека означает, что эта ссылка не может быть возвращена клиенту API. Если объект был вытолкнут, мы можем доказать, что его нельзя «получить доступ к любому потенциальному продолжающемуся вычислению из любой живой нити» . Проблема в том, что JVM текущего поколения не может этого доказать. Существующие JVM поколения не рассматривают логику программы при определении доступности ссылок. (Для начала это не практично.)
Но, отвлекаясь от вопроса о том, что означает действительно достижимость , у нас явно есть ситуация, когда реализация NaiveStack
«висит на» объектах, которые должны быть исправлены. Это утечка памяти.
В этом случае решение прост:
public Object pop() {
if (top <= 0) {
throw new StackException("stack underflow");
}
Object popped = stack[--top];
stack[top] = null; // Overwrite popped reference with null.
return popped;
}
Кэши могут быть утечками памяти
Общей стратегией повышения эффективности обслуживания является кэширование результатов. Идея состоит в том, что вы сохраняете запись общих запросов и их результатов в структуре данных в памяти, известной как кеш. Затем каждый раз, когда запрос выполняется, вы просматриваете запрос в кеше. Если поиск выполняется успешно, вы возвращаете соответствующие сохраненные результаты.
Эта стратегия может быть очень эффективной, если ее выполнить правильно. Однако, если они реализованы неправильно, кеш может быть утечкой памяти. Рассмотрим следующий пример:
public class RequestHandler {
private Map<Task, Result> cache = new HashMap<>();
public Result doRequest(Task task) {
Result result = cache.get(task);
if (result == null) {
result == doRequestProcessing(task);
cache.put(task, result);
}
return result;
}
}
Проблема с этим кодом заключается в том, что, хотя любой вызов doRequest
может добавить новую запись в кеш, их не удалять. Если служба постоянно выполняет разные задачи, то кэш в конечном итоге будет потреблять всю доступную память. Это форма утечки памяти.
Один из подходов к решению этого - использовать кеш с максимальным размером и выкидывать старые записи, когда кеш превышает максимальный. (Выдача наименее недавно использованной записи - хорошая стратегия.) Другой подход заключается в создании кеша с использованием WeakHashMap
чтобы JVM могла выселить записи кэша, если куча начинает слишком WeakHashMap
.