Неопределенные ссылки
Помимо обычных (т. н. сильных, strong) ссылок, которые создаются операторомnew, можно создавать мягкие, слабые и фантомные ссылки (общее название неопределенные ссылки) с помощью соответствующих классов-наследников абстрактного класса Reference:
HashMap<String, String> map = new HashMap<>();
SoftReference<HashMap<String, String>> mapSoftRef = new SoftReference<>(map);
WeakReference<HashMap<String, String>> mapWeakRef = new WeakReference<>(map);
PhantomReference<HashMap<String, String>> mapPhanRef = new PhantomReference<>(map, null);Объекты этих классов при своём инстанцировании принимают аргументом сильную ссылку - референт (referent) и помещаются в специальные внутренние очереди JVM (для каждого Reference-типа своя очередь). Сборщик мусора обрабатывает неопределенные ссылки по правилам, соответствующим её типу. Флаг -XX:+PrintReferenceGC позволяет узнать время, затраченное сборщиком мусора на обработку неопределенных ссылок.
SoftReference
|
Референт является кандидатом на удаление за давностью обращения, если
System.currentTimeMillis() — время_последнего_обращения > значение_флага_SoftRefLRUPolicyMSPerMB * объем_свободной_памяти_heap_Мб
Мягкие ссылки на объект применяются при большой вероятности повторного использования объекта и, если этого не произошло, допустимости его уничтожения, а так же если JVM угрожает критическая нехватка памяти. Таким образом, референт мягкой ссылки может существовать продолжительное время даже в отсутствие сильной ссылки на него из приложения.
После удаления референта, на reference продолжают указывать две ссылки - из приложения и из очереди soft-ссылок. Потребуется несколько (в лучшем случае два) циклов сборки мусора для полного удаления soft-ссылок.
Применение soft-ссылки схематически демонстрирует следующий пример. Во внешнем ресурсе хранится большой объём данных, с которым должны быть объединены данные пользователя. Загрузка данных большого объёма затратна, поэтому ссылка на них помещяется в поле типа SoftReference класса при запуске приложения. Метод concatWithBigData сначала объявляет локальную сильную ссылку, указывающую на большией данные в soft-ссылке, чтобы они не были удалены сборщиком мусора во время работы метода. Если уже удалены - загружаются вновь, создаётся новая soft-ссылка и присваивается полю класса. После конкатенации локальная сильная ссылка обнуляется и на большие данные остаётся только указатель из soft-ссылки.
public class BigDataService {
private SoftReference<InputStream> bigDataRef = new SoftReference<>(loadBigData());
public InputStream concatWithBigData(InputStream data) {
InputStream bigData = bigDataRef.get();
if (bigData == null) {
bigData = loadBigData();
bigDataRef = new SoftReference<>(bigData);
}
InputStream result = concat(bigData, data);
bigData = null;
...
return result;
}
}Следующая проверка того, что референт не удален сборщиком мусора не безопасна, так как последующия работа с референтом может привести к попытке работы с уже удаленным к тому времени объектом:
if(mapSoftRef.get() != null) {
map = mapSoftRef.get(); // может быть null
...
}Для достоверности проверки следует создавать неопределенные ссылки с передачей в конструктор ссылки на очередьReferenceQueue. В этом случае сборщик мусора выполнит атомарную операцию удаления референта и помещения reference в ReferenceQueue.
Map<String, String> map = new HashMap<>();
ReferenceQueue<Map<String, String>> queue = new ReferenceQueue<>();
SoftReference<Map<String, String>> mapSoftRef = new SoftReference<>(map, queue);
...
SoftReference<Map<String,String>> ref = (SoftReference<Map<String,String>>) queue.poll();
if (ref != null) {
// referent удален сборщиком мусора
...
}Такой подход также позволяет выполнить проверку максимально эффективно (производи-тельность работы с очередью оценивается как O(1)).
WeakReference
|
Этот тип ссылок следует использовать, если референт будет использоваться несколькими потоками, т. е. конкурентно. Например, пользователь запускает вычислительный поток, который создаёт референта. Этот референт с большой вероятностью потребуется другому пользователю, который однажды запустит свой собственный поток. Если первый пользователь при первичном создании референта помещает его в виде WeakReference в структуру данных, доступную другому пользователю и сохраняет на референта сильную ссылку в своем потоке, второму пользователю не потребуется конструировать свою копию референта. Если первый пользователь завершил свой поток (утратил ссылку на референта), а второй к этому времени не создал своей сильной ссылки на референта, ему придётся создать референта заново. Всё выгладит так, словно мы говорим JVM: «Пока кого-то еще интересует этот объект (т. е. кто-то удерживает на него сильную ссылку) - скажи, где он находится, а когда он станет никому не нужен - уничтожь его».
Классы коллекций обычно являются источником утечек памяти: приложение помещает объекты, например, в HashMap и никогда не удаляет их. Для исключения подобных ситуаций можно использовать предоставляемые JDKWeakHashMap иWeakIdentityMap. WeakHashMap использует слабые ссылки для своих ключей. Когда референт (т. е. ключ) становится недостижимым, WeakHashMap удаляет значение ключа. Эта операция выполняется при каждом обращении к WeakHashMap: ReferenceQueue c объектами WeakReference обрабатывается и значение, ассоциированное с WeakReference удаляется. Для указанных структур данных сама JVM отвечает за освобождение памяти, занятой вхождениями в Map.
FinalReference
Единственной реализацией приватного абстрактного классаFinalReference является классjava.lang.ref.Finalizer. JVM использует его для отслеживания объектов, определивших методfinalize(). При создании объекта, располагающего непустым методомfinalize(), JVM создаёт объект Finalizer с текущим пользовательским объектом в качестве референта. Когда референт становится недостижим из приложения, объект Finalizer помещается в глобальную очередь из объектов Finalizer. Отдельный поток JVM обрабатывает эту очередь, выполняя методыfinalize() референтов. После этого ссылка на референта обнуляется, референт, а вслед за ним и Finalizer могут быть удалены.
Описанный маханизм не позволяет безопасно реализовать свою цель - автоматическое управление ресурсами, поскольку сборщик мусора не имеет никаких гарантий времени выполнения. Это означает, что в данном механизме нет ничего, что связывает освобождение ресурса со временем жизни объекта, так что всегда существует вероятность исчерпания ресурса.
Говоря о методе finalize(), можно указать несколько его недостатков:
момент вызова
finalize()не определен;finalize()может не быть вызван, если JVM завершилась;finalize()может непреднамеренно создать новую ссылку на referent-а ("воскресить") и когда referent снова станет пригодным для уборки, повторного вызоваfinalize()не произойдёт.
По указанным причинамfinalize()следует использовать как можно реже.
Кастомная реализация освобождения русурсов (для ранних версий Java)
В Java 8 и более ранних версиях, не имеющих продвинутых средств освобождения ресурсов, таких как очистителиjava.lang.ref.Cleaner, заменой финализаторам может служить кастомная реализация финализации, основанная на Weak- или SoftReference.
Чтобы создать замену финализатору, необходимо создать подскласс класса неопределенной (Weak- или Soft-) ссылки для хранения любой информации подлежащей освобождению после того как референт стал пригодным для удаления. Освобождение ресу��са выполняется в методе объекта неопределенной ссылки, а не в методеfinalize() референта.
В следующем примере в классе MemAllocatorв (14) выделяется внешняя память, указатель на которую помещается в объект класса-контейнера NativeMemoryHandle. Этот объект является референтом для объекта слабой ссылки типа NativeMemoryManager. MemAllocatorреализует интерфейсAutoCloseable, метод close() которого позволяет обнулить ссылку на референта и освободить внешнюю память в (26, 27). Класс слабой ссылки NativeMemoryManager обладает статической ссылкой (35) на глобальную очередь ReferenceQueueиз объектов собственного типа - при обнулении указателя на внешнюю память, соответствующая слабая ссылка NativeMemoryManager автоматически помещаются в эту очередь. При первом инстанцировании объекта NativeMemoryManager в (59, 60) запускается глобальный поток-демон для чтения с удалением объектаNativeMemoryManager из очереди. После этого происходит освобождение внешней памяти в (48). Таким образом, в случае непреднамеренного опускания вызова ручной очистки в (27), это всё равно произойдет (если приложение на завершится раньше) в потоке очистки. Необходимо гарантировать, что в промежуток времени между обнулением ссылки на референта в (26) и вызовом (27) сборщик мусора не уничтожит слабую ссылкуNativeMemoryManager оставив память неосвобожденной. Для этого классNativeMemoryManager обладает глобальным множеством сильных ссылок на объекты собственного класса. Ссылка из этого множества удаляется после освобождения памяти в (73) и (82).
1 @Getter
2 @RequiredArgsConstructor
3 public class NativeMemoryHandle {
4 private final long handle;
5 }
6
7 @Slf4j
8 public class MemAllocator implements AutoCloseable {
9
10 private NativeMemoryHandle nativeHandle;
11 private final NativeMemoryManager memoryManager;
12
13 public MemAllocator() {
14 long handle = allocateNativeMemory();
15 nativeHandle = new NativeMemoryHandle(handle);
16 memoryManager = new NativeMemoryManager(nativeHandle);
17 }
18
19 @Override
20 public void close() {
21 clean();
22 }
23
24 private void clean() {
25 if(nativeHandle != null) {
26 nativeHandle = null;
27 memoryManager.cleanupNow();
28 }
29 }
30
31 private static class NativeMemoryManager extends WeakReference<NativeMemoryHandle>{
32
33 private final long handle;
34
35 private static ReferenceQueue<NativeMemoryHandle> NMH_QUEUE =
36 new ReferenceQueue<>();
37 private static HashSet<NativeMemoryManager> ACTIVE_MANAGERS = new HashSet<>();
38
39 private boolean cleaned = false;
40
41 static {
42 Runnable runnable = new Runnable() {
43 public void run() {
44 while (!Thread.currentThread().isInterrupted()) {
45 try {
46 NativeMemoryManager manager =
47 (NativeMemoryManager) NMH_QUEUE.remove();
48 manager.cleanUp();
49 } catch (InterruptedException e) {
50 Thread.currentThread().interrupt();
51 break;
52 } catch (Exception e) {
53 log.error(e);
54 }
55 }
56 }
57 };
58 Thread t = new Thread(runnable);
59 t.setDaemon(true);
60 t.start();
61 }
62
63 public NativeMemoryManager(NativeMemoryHandle nativeMemoryHandle) {
64 super(nativeMemoryHandle, NMH_QUEUE);
65 this.handle = nativeMemoryHandle.getHandle();
66 ACTIVE_MANAGERS.add(this);
67 }
68
69 // Явное освобождение
70 public synchronized void cleanupNow() {
71 if (!cleaned) {
72 deallocateNativeMemory(this.handle);
73 ACTIVE_MANAGERS.remove(this);
74 cleaned = true;
75 }
76 }
77
78 // Вызывается потоком-очистителем, когда этот WeakReference попадает в очередь.
79 private synchronized void cleanUp() {
80 if (!cleaned) {
81 deallocateNativeMemory(this.handle);
82 ACTIVE_MANAGERS.remove(this);
83 cleaned = true;
84 // Вызываем clear() родительского WeakReference
85 this.clear();
86 }
87 }
88
89 private native void deallocateNativeMemory(long handle);
90 }
91
92 private native long allocateNativeMemory();
93 }PhantomReference
СсылкиPhantomReference предназначены для использования в контексте Cleaner API (начиная с Java 9) для замены финализаторов Finalizer(и методовfinalize()). Без применения Cleaner, при правильном использованииWeakReference, Phantom-ссылки малоотличимы от WeakReference. Под правильным использованием понимается отсутствие у референта методаfinalize() и контролирование пригодности референта к уничтожению просмотромReferenceQueue.
Методget() на Phantom-ссылке всегда возвращаетnull, java.lang.ref.Cleaner неявно используетPhantomReference, а Cleaner API вцелом предлагает удобный способ подстраховки в случае непреднамеренного оставления системы без освобождения критических ресурсов перед удалением референта сборщиком мусора. Таким образом, применениеPhantomReference и Cleaner API относится больше к защите от неправильного использования неопределенных ссылок.
Далее приводится пример использования очистителя. Класс NativeMemoryManager получает указатель на внешнюю память в своём конструкторе и в нем же создаётся объект DeallocTask, задающий действие для её освобождения. Далее NativeMemoryManager и DeallocTask регистрируются в очистителе. МетодDeallocTask.run() может быть вызван одним из способов - непосредственным вызовом методаclose() из кода, клиентского по отношению к NativeMemoryManager или очистителем после того, как NativeMemoryManager станет пригодным для удаления сборщиком мусора.
public class NativeMemoryManager implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
private final DeallocTask deallocTask;
private final Cleaner.Cleanable cleanable;
public NativeMemoryManager(long handle) {
deallocTask = new DeallocTask(handle);
cleanable = CLEANER.register(this, deallocTask);
}
@Override
public void close() {
cleanable.clean();
}
@RequiedArgsConstructor
private static class DeallocTask implements Runnable {
private final long handle;
@Override
public void run() {
deallocateNativeMemory(handle);
}
}
}ЭкземплярDeallocTask не должен ссылаться его экземпляр NativeMemoryManager во избежание циклической ссылки, которая не позволит сборщику мусора удалить экземпляр NativeMemoryManager и не произойдет автоматическая очистка. DeallocTaskдолжен быть статическим вложенным классом, потому что нестатические вложенные классы содержат ссылки на охватывающие их экземпляры.
РеализацияAutoCloseable предполагает использование NativeMemoryManager-а в блоке try-с-ресурсами:
try (NativeMemoryManager m = new NativeMemoryManager(123)) {
...
}По выходу из блока внешняя память будет освобождена вызовом метода close(). Если же такое использование не будет исполнено, а программа завершится до запуска очистителя, то ресурс памяти может оказаться не освобожденным. Спецификация Cleaner отмечает: “Поведение очистителей во времяSystem.exit зависит от конкретной реализации. Нет никакой гарантии относительно того, будут ли выполнены действия очистителя”.
