Dlaczego References w Javie to Ważny Temat?
Zarządzanie pamięcią w Javie nie kończy się na Garbage Collectorze. Czasami potrzebujemy więcej kontroli nad tym, kiedy obiekty są usuwane z pamięci. W 2018 roku, gdy aplikacje enterprise stają się coraz większe i bardziej wymagające, znajomość mechanizmów słabych referencji może być różnicą między stabilną aplikacją a tą, która pada pod obciążeniem.
Co się nauczysz:
- Jak działają WeakReference, SoftReference i PhantomReference
- Kiedy używać każdego typu referencji w praktycznych scenariuszach
- Jak implementować cache z SoftReference
- Jak unikać memory leaks przy użyciu słabych referencji
- Jak zaimplementować cleanup mechanizm z PhantomReference
Wymagania wstępne:
- Podstawowa znajomość Java (2-3 lata doświadczenia)
- Zrozumienie działania Garbage Collector
- Znajomość generics i kolekcji
Czym są References w Javie?
W Javie każda zmienna przechowująca obiekt to tak naprawdę strong reference – mocna referencja. Oznacza to, że dopóki istnieje taka referencja, Garbage Collector nie usunie obiektu z pamięci.
// Strong Reference - tradycyjna referencja String strongRef = new String("Hello World"); // Obiekt nie zostanie usunięty dopóki strongRef istnieje // Weak Reference - słaba referencja WeakReferenceweakRef = new WeakReference<>(new String("Hello World")); // Obiekt może zostać usunięty przez GC w każdej chwili
Pakiet java.lang.ref wprowadzony w Java 1.2 oferuje trzy typy słabych referencji, każda z różnym zachowaniem podczas garbage collection.
WeakReference – Najsłabszy Typ Referencji
WeakReference to najsłabszy typ referencji w Javie. Obiekt referencowany przez WeakReference może zostać usunięty przez Garbage Collector w każdej chwili, gdy nie ma żadnych strong references.
### Praktyczny Przykład WeakReference
import java.lang.ref.WeakReference; public class WeakReferenceExample { public static void main(String[] args) { // Tworzymy obiekt i weak reference String strongRef = new String("Important Data"); WeakReferenceweakRef = new WeakReference<>(strongRef); System.out.println("Before GC: " + weakRef.get()); // "Important Data" // Usuwamy strong reference strongRef = null; // Wymuszamy garbage collection System.gc(); // Sprawdzamy czy obiekt nadal istnieje String retrieved = weakRef.get(); if (retrieved == null) { System.out.println("Obiekt został usunięty przez GC"); } else { System.out.println("Obiekt nadal istnieje: " + retrieved); } } }
### Zastosowania WeakReference
**1. Unikanie Cyklicznych Zależności**
public class Parent { private Listchildren = new ArrayList<>(); public void addChild(Child child) { children.add(child); child.setParent(this); } } public class Child { // Używamy WeakReference żeby uniknąć cyklicznej zależności private WeakReference parentRef; public void setParent(Parent parent) { this.parentRef = new WeakReference<>(parent); } public Parent getParent() { return parentRef != null ? parentRef.get() : null; } }
**2. Observer Pattern bez Memory Leaks**
public class EventPublisher { private List> listeners = new ArrayList<>(); public void addListener(EventListener listener) { listeners.add(new WeakReference<>(listener)); } public void fireEvent(String event) { // Usuwamy null references podczas iteracji listeners.removeIf(ref -> ref.get() == null); listeners.forEach(ref -> { EventListener listener = ref.get(); if (listener != null) { listener.onEvent(event); } }); } }
SoftReference – Cache-Friendly Reference
SoftReference to „miękkia” referencja – obiekt zostanie usunięty przez GC dopiero gdy JVM zacznie brakować pamięci. To czyni ją idealną do implementacji cache’ów.
### Implementacja Cache z SoftReference
import java.lang.ref.SoftReference; import java.util.HashMap; import java.util.Map; public class SoftReferenceCache{ private final Map > cache = new HashMap<>(); public void put(K key, V value) { cache.put(key, new SoftReference<>(value)); } public V get(K key) { SoftReference ref = cache.get(key); if (ref != null) { V value = ref.get(); if (value == null) { // Obiekt został usunięty przez GC - czyścimy cache cache.remove(key); } return value; } return null; } public void cleanUp() { // Usuwamy wszystkie nieważne referencje cache.entrySet().removeIf(entry -> entry.getValue().get() == null); } }
### Praktyczny Przykład – Image Cache
public class ImageCache { private final SoftReferenceCachecache = new SoftReferenceCache<>(); public BufferedImage getImage(String path) { BufferedImage image = cache.get(path); if (image == null) { // Ładujemy obraz z dysku tylko gdy nie ma go w cache try { image = ImageIO.read(new File(path)); cache.put(path, image); System.out.println("Załadowano obraz z dysku: " + path); } catch (IOException e) { System.err.println("Błąd ładowania obrazu: " + e.getMessage()); } } else { System.out.println("Pobrano obraz z cache: " + path); } return image; } }
PhantomReference – Cleanup Specialist
PhantomReference to najbardziej specjalistyczny typ referencji. Nie pozwala na dostęp do obiektu (metoda `get()` zawsze zwraca `null`), ale służy do wykrywania kiedy obiekt został usunięty przez GC.
### Implementacja Cleanup Mechanizmu
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; public class ResourceManager { private static final ReferenceQueuereferenceQueue = new ReferenceQueue<>(); private static final Map , String> cleanupTasks = new ConcurrentHashMap<>(); // Thread do przetwarzania cleanup zadań static { Thread cleanupThread = new Thread(() -> { while (true) { try { PhantomReference ref = (PhantomReference ) referenceQueue.remove(); String cleanupTask = cleanupTasks.remove(ref); if (cleanupTask != null) { performCleanup(cleanupTask); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }); cleanupThread.setDaemon(true); cleanupThread.start(); } public static void registerResource(Resource resource, String cleanupInfo) { PhantomReference phantomRef = new PhantomReference<>(resource, referenceQueue); cleanupTasks.put(phantomRef, cleanupInfo); } private static void performCleanup(String cleanupInfo) { System.out.println("Wykonuję cleanup: " + cleanupInfo); // Tutaj kod do zamykania plików, połączeń, etc. } }
### Przykład Użycia z File Resources
public class ManagedFileResource { private final String filePath; private final FileInputStream inputStream; public ManagedFileResource(String filePath) throws IOException { this.filePath = filePath; this.inputStream = new FileInputStream(filePath); // Rejestrujemy resource do automatycznego cleanup ResourceManager.registerResource(this, "Zamykam plik: " + filePath); } public void read() throws IOException { // Kod do czytania z pliku byte[] buffer = new byte[1024]; inputStream.read(buffer); } // Finalize jest deprecated, ale PhantomReference to elegancka alternatywa public void close() throws IOException { inputStream.close(); } }
Porównanie Wszystkich Typów References
Typ Reference | Kiedy GC usuwa obiekt | Główne zastosowanie | Dostęp do obiektu |
---|---|---|---|
Strong Reference | Nigdy (dopóki referencja istnieje) | Normalne przechowywanie obiektów | Zawsze dostępny |
WeakReference | Przy najbliższym GC | Unikanie cyklicznych zależności | Może być null |
SoftReference | Przy braku pamięci | Cache implementacje | Może być null |
PhantomReference | Natychmiast (służy do notyfikacji) | Cleanup mechanizmy | Zawsze null |
Najlepsze Praktyki i Pułapki
### Dobre Praktyki
// ✅ DOBRZE - zawsze sprawdzaj null WeakReferenceweakRef = new WeakReference<>(obj); ExpensiveObject retrieved = weakRef.get(); if (retrieved != null) { retrieved.doSomething(); } else { // Obiekt został usunięty - obsłuż ten przypadek handleMissingObject(); } // ❌ ŹLE - może spowodować NPE WeakReference weakRef = new WeakReference<>(obj); weakRef.get().doSomething(); // NPE risk!
### Monitoring Reference Queue
public class ReferenceMonitor { private final ReferenceQueue
Wydajność i Implikacje
Słabe referencje wprowadzają niewielki overhead w porównaniu do strong references, ale korzyści często przewyższają koszty:
### Kiedy NIE używać słabych referencji
// ❌ ŹLE - ważne dane w weak reference WeakReferencesessionRef = new WeakReference<>(session); // Później w kodzie... UserSession session = sessionRef.get(); if (session == null) { // Ups! Sesja użytkownika zniknęła - duży problem! throw new IllegalStateException("Session expired unexpectedly"); } // ✅ DOBRZE - ważne dane w strong reference, cache w soft reference private UserSession currentSession; // strong reference private SoftReference > cachedData; // cache
Często Zadawane Pytania
Tak! WeakHashMap to gotowa implementacja mapy która używa WeakReference dla kluczy. Automatycznie usuwa wpisy gdy klucze są garbage collected. Idealna do cache’owania gdzie klucz to obiekt którego żywotność kontroluje aplikacja.
Nie! SoftReference to tylko „sugestia” dla GC. Przy bardzo niskiej pamięci JVM może usunąć nawet soft-referenced obiekty. Zawsze sprawdzaj czy get() nie zwraca null.
Zawsze! Metoda finalize() jest deprecated od Java 9 i ma wiele problemów (nieprzewidywalność, performance). PhantomReference z ReferenceQueue daje pełną kontrolę nad cleanup timing.
Tak, ale ostrożnie! Sam get() jest thread-safe, ale musisz zabezpieczyć się przed tym że obiekt zniknie między sprawdzeniem a użyciem. Używaj local variables do przechowania wyniku get().
Użyj JVM flags: -XX:+PrintGCDetails -XX:+PrintGCTimeStamps żeby śledzić GC activity. Można też użyć ReferenceQueue do logowania kiedy referencje stają się nieważne.
W prostych aplikacjach często nie ma potrzeby. Słabe referencje świecą w aplikacjach enterprise z długo żyjącymi objektami, cache’ami i potencjalnymi memory leaks.
Tak! Możesz mieć strong reference do ważnych danych, SoftReference do cache’a tych samych danych, i PhantomReference do cleanup. Każdy typ służy innemu celowi.
Przydatne zasoby:
- Oracle Java Docs – java.lang.ref
- Baeldung – Weak References Guide
- Google Guava – Cache Implementation
- StackOverflow – Reference Types Discussion
🚀 Zadanie dla Ciebie
Zaimplementuj system cache’owania dla aplikacji która ładuje konfiguracje z plików. Użyj SoftReference żeby cache automatycznie się czyścił przy niskiej pamięci, ale dodaj fallback do ponownego ładowania z dysku. Bonus: dodaj monitoring z ReferenceQueue żeby logować kiedy konfiguracje są usuwane z cache’a.