Недавно, как начинающий Java программист, я задался вопросом: как освободить память занимаемую объектом? Мне было уже известно, что непосредственно удалить объект в Java не возможно. В отличие от C++, в Java нет таких функций для освобождения памяти как
free()
или delete()
. Всем занимается сборщик мусора, который в автоматическом режиме освобождает память от тех объектов, которые больше не используются. Но иногда все же может потребоваться вручную освободить память. Что делать в таком случае?Для начала, определим когда может возникнуть такая необходимость. Например, если для работы программы требуется хранить в памяти какой-либо объемный объект и при этом его необходимо периодически обновлять. Тогда, в момент генерации нового экземпляра, в памяти окажется два тяжелых объекта и, если они достаточно велики, может возникнуть
OutOfMemoryError
. Я постарался проиллюстрировать данную проблему в приведенном ниже псевдокоде:class ExampleClass {
Map<Long, String> enormous;
// Code
void refresh() {
enormous = EnormousObjectProvider.getNewInstance();
}
}
class EnormousObjectProvider {
// Code
Map<Long, String> getNewInstance(){
// Some actions (e.g. API or DB requests or special calculations)
// ...
// While we are here, we have the second instance of large object
// it means that if both objects are heavy enough we can be out of memory
return result;
}
}
Почему сборщик мусора не выполнил свою задачу? Для того чтобы объект был утилизирован сборщиком мусора, на него не должно быть действующих ссылок. Видимо, в нашем коде это условие не выполнено.
Чтобы избавиться от ссылок на объект, просто присвоим ему значение
null
. Так, например, работает метод HashMap.clear()
:/**
* Removes all of the mappings from this map.
* The map will be empty after this call returns.
*/
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
Он обходит массив объектов
Node
и присваивает каждому значение null
, но почему такой метод освобождения памяти работает? Если мы присвоили значение null
здесь, гарантирует ли это, что на объект больше нигде нет ссылок и он доступен для утилизации сборщиком мусора? Для этого вспомним как работают ссылочные типы данных в Java.Жизненный цикл объекта начинается с его создания при помощи оператора
new
, например:Person p1 = new Person();
Данная строка означает “выделить пространство в оперативной памяти и присвоить ссылку на эту ячейку памяти переменной
p1
”. Ключевая мысль здесь в том, что p1
это не сам объект, а лишь ссылка на него.Теперь взглянем что происходит когда мы присваиваем значение переменной
p1
новой переменной:Person p2 = p1;
Важно понимать, что мы не создаем две ссылки, которые указывают на один объект, как можно подумать читая многие статьи в интернете. Мы создаем ссылку
p2
на ссылку p1
:В этом очень легко убедиться проведя небольшой эксперимент. Выполним следующий код:
public void demo() {
Person p1;
p1 = new Person("Tom");
Person p2 = p1;
p2 = null;
p1.greeting();
}
class Person {
String name;
Person(String name) {
this.name = name;
}
void greeting() {
System.out.println("Hello, " + name);
}
}
В консоли мы увидим “Hello, Tom”, но стоит нам присвоить
null
переменной p2
, а не p1
и мы получим NullPointerException
. Это означает что ссылки p1
и p2
указывают на разные объекты.Поэтому, если мы присвоим
p1 = null
, то на объект больше не будет указателей, даже если мы передавали его в методы или присваивали значение p1
другим переменным. Это означает, что на объект больше нет ссылок и он доступен для сборщика мусора.В заключение, испытаем изученный подход на практике:
public class Main {
private static final long MEGABYTE = 1024L * 1024L;
public static long bytesToMegabytes(long bytes) {
return bytes / MEGABYTE;
}
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
runtime.gc();
System.out.println("Initial memory: " + usedMemory(runtime));
// Object initialization
SomeClass referenceA = new SomeClass("One");
System.out.println("Memory after object initialization: " + usedMemory(runtime));
// Reference initialization
SomeClass referenceB = referenceA;
System.out.println("Memory after reference initialization: " + usedMemory(runtime));
// Reference to someClassRef
referenceA.exampleMethod();
// Set object to Null
referenceA = null;
System.out.println("Memory after setting object to null: " + usedMemory(runtime));
// Call garbage collector
System.gc();
System.out.println("Memory after calling GC: " + usedMemory(runtime));
}
public static long usedMemory(Runtime runtime) {
return runtime.totalMemory() - runtime.freeMemory();
}
}
class SomeClass {
private String name;
private int field1;
private String filed2;
private HashMap<String, String> bigMap;
public SomeClass(String name){
this.name = name;
this.field1 = Integer.MAX_VALUE;
filed2 = "";
for (int i = 0; i < 100_000; i++) {
filed2 += "A";
}
bigMap = new HashMap<>();
for (int i = 0; i < 100_000; i++) {
bigMap.put(String.valueOf(i), String.valueOf(i));
}
}
public void exampleMethod() {
this.field1 -= 1;
}
}
Ниже приведен результат выполнения программы:
Initial memory: 1276760
Memory after object initialization: 56019128
Memory after reference initialization: 56019128
Memory after setting object to null: 56019128
Memory after calling GC: 16369232
Как мы видим, объем использованной памяти уменьшился, а значит цель достигнута.
ligor
p1 = null;
p2.greeting();
А так продолжает работать. Почему?
Milein
Потому как что p1, что p2 это просто ссылки. То что они в какой-то момент указывали на один и тот же объект, их никак не связывает.
p1 отправили указывать в никуда, а p2 как жила, так и живёт.