Java WeakHashMap detaljno (sa analizom izvornog koda)
U Javi uglavnom koristimo Map, na primer konkretnu implementaciju poput HashMap. Na malo naprednijem nivou možemo koristiti i WeakHashMap.
WeakHashMap se u većini ponašanja zapravo poklapa sa HashMap, s tom razlikom što WeakHashMap ne sprečava GC da povrati objekat koji je ključ (ne vrednost). Kako WeakHashMap to postiže — to je glavno pitanje koje proučavamo.
Pre nego što pređemo na WeakHashMap, moramo se upoznati sa slabim referencama.
U Javi postoje četiri tipa referenci:
- Jaka referenca (Strong Reference) — podrazumevani tip reference koji koristimo pri uobičajenom pisanju koda. Jaka referenca je jaka zato što, ako je objekat jakom referencom dostupan od GC Roots, sprečava GC da povrati taj objekat.
- Meka referenca (Soft Reference) ima nešto slabiju moć sprečavanja GC povraćaja; ako je objekat dostupan preko meke reference, zadržaće se u memoriji nešto duže. Sakupljač smeća povraća ovakve objekte dostupne preko meke reference tek kada memorije ponestane.
- Slaba referenca (WeakReference) ne može da spreči GC povraćaj; ako je objekat dostupan preko slabe reference, biće povraćen pri sledećem izvršenju GC-a.
- Fantomska referenca (Phantom Reference) je veoma slaba; njena jedina uloga je da, nakon što objekat na koji ukazuje bude povraćen, sama bude dodata u red referenci, čime se beleži da je objekat na koji je ukazivala uništen.
Uz ovo postoji i koncept reda referenci (Reference Queue):
- U opštem slučaju, kada se objekat označi kao smeće (što ne znači da je i povraćen), dodaje se u red referenci.
- Za fantomsku referencu, objekat na koji ukazuje dodaje se u red referenci tek nakon što bude povraćen, pa se stoga može koristiti za beleženje da li je objekat na koji ukazuje referenca povraćen.
Kako WeakHashMap ne sprečava povraćaj objekata?
private static final class Entry<K, V> extends WeakReference<K> implements
Map.Entry<K, V> {
int hash;
boolean isNull;
V value;
Entry<K, V> next;
interface Type<R, K, V> {
R get(Map.Entry<K, V> entry);
}
Entry(K key, V object, ReferenceQueue<K> queue) {
super(key, queue);
isNull = key == null;
hash = isNull ? 0 : key.hashCode();
value = object;
}Kao što se vidi iz izvornog koda:
- Entry klasa WeakHashMap-a nasleđuje WeakReference.
- Ključ (Key) pritom služi kao objekat na koji ukazuje WeakReference.
- WeakHashMap therefore koristi mehanizam WeakReference-a da ne sprečava GC povraćaj ključa.
Kako se uklanjaju podaci povraćenih ključeva
U Javadoc-u o WeakHashMap stoji opis: kada ključ više nije referenciran, i odgovarajući par ključ/vrednost biće uklonjen.
Kako se to uklanja? Obično imamo dve pretpostavljene strategije:
- Obaveštavanje kada je objekat povraćen.
- WeakHashMap periodično obrađuje nevažeće Entry-e.
WeakHashMap bira pristup periodičnog ispitivanja: u svojim metodima poput put/get/size najpre se poziva metod poll koji proverava i uklanja nevažeće Entry-e.
void poll() {
Entry<K, V> toRemove;
while ((toRemove = (Entry<K, V>) referenceQueue.poll()) != null) {
removeEntry(toRemove);
Log.d(LOGTAG, "removeEntry=" + toRemove.value);
}
}Zašto se nije koristilo prividno bolje obaveštavanje? Mislim da je razlog tome odsustvo pouzdanog callback-a za obaveštavanje u Javi — na primer, često pominjani metod finalize zapravo nije standardizovan; različite JVM implementacije se mogu razlikovati, čak i uopšte ne pozvati taj metod.
Naravno, pored pukog čitanja izvornog koda, razumna provera je važan način da se potvrdi ispravnost analize.
Ovde ćemo prvo definisati klasu MyObject i obraditi metod finalize (na mojoj testnoj mašini se normalno poziva, služi samo kao pomoćno sredstvo provere):
class MyObject(val id: String) : Any() {
protected fun finalize() {
Log.i("MainActivity", "Object($id) finalize method is called")
}
}Zatim sledi kod pozivaoca, koji izgleda ovako:
private val weakHashMap = WeakHashMap<Any, Int>()
var count : Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
dumpWeakInfo()
fab.setOnClickListener { view ->
//System.gc()// ovo retko radi; koristite Android studio force gc
weakHashMap.put(MyObject(count.toString()), count)
count ++
dumpWeakInfo()
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
}
}
fun dumpWeakInfo() {
Log.i("MainActivity", "dumpWeakInfo weakInfo.size=${weakHashMap.size}")
}Postupamo na sledeći način:
- Kliknemo na fab kontrolu; pri svakom kliku dodamo jedan Entry u WeakHashMap objekat i ispišemo size; ponovimo 3 puta.
- Dok ne izazovemo GC, size WeakHashMap objekta stalno raste.
- Ručno izazovemo Force GC; primetićemo da se za MyObject poziva metod finalize.
- Ponovo kliknemo na fab kontrolu; size ispisanog WeakHashMap-a naglo se smanjuje.
- Takođe ćemo videti i log koji se ispisuje pri uklanjanju iz WeakHashMap-a.
I/MainActivity(10202): dumpWeakInfo weakInfo.size=1
I/MainActivity(10202): dumpWeakInfo weakInfo.size=2
I/MainActivity(10202): dumpWeakInfo weakInfo.size=3
I/MainActivity(10202): Object(2) finalize method is called
I/MainActivity(10202): Object(1) finalize method is called
I/MainActivity(10202): Object(0) finalize method is called
I/WeakHashMap(10202): removeEntry=2
I/WeakHashMap(10202): removeEntry=0
I/WeakHashMap(10202): removeEntry=1
I/MainActivity(10202): dumpWeakInfo weakInfo.size=1Napomena: System.gc() ne mora uvek da funkcioniše; preporučuje se korišćenje Force GC iz Android Studija.
Kompletan test kod možete pogledati ovde: https://github.com/androidyue/WeakHashMapSample
