Jeśli nie zadzwonisz System.gc()
, system zgłosi wyjątek OutOfMemoryException. Nie wiem, dlaczego muszę dzwonić System.gc()
wprost; JVM powinien się nazywać gc()
, prawda? Proszę doradź.
Oto mój kod testowy:
public static void main(String[] args) throws InterruptedException {
WeakHashMap<String, int[]> hm = new WeakHashMap<>();
int i = 0;
while(true) {
Thread.sleep(1000);
i++;
String key = new String(new Integer(i).toString());
System.out.println(String.format("add new element %d", i));
hm.put(key, new int[1024 * 10000]);
key = null;
//System.gc();
}
}
W następujący sposób dodaj, -XX:+PrintGCDetails
aby wydrukować informacje GC; jak widać, JVM próbuje wykonać pełne uruchomienie GC, ale kończy się niepowodzeniem; I nadal nie wiem dlaczego. To bardzo dziwne, że jeśli odkomentuję System.gc();
linię, wynik będzie pozytywny:
add new element 1
add new element 2
add new element 3
add new element 4
add new element 5
[GC (Allocation Failure) --[PSYoungGen: 48344K->48344K(59904K)] 168344K->168352K(196608K), 0.0090913 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 48344K->41377K(59904K)] [ParOldGen: 120008K->120002K(136704K)] 168352K->161380K(196608K), [Metaspace: 5382K->5382K(1056768K)], 0.0380767 secs] [Times: user=0.09 sys=0.03, real=0.04 secs]
[GC (Allocation Failure) --[PSYoungGen: 41377K->41377K(59904K)] 161380K->161380K(196608K), 0.0040596 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 41377K->41314K(59904K)] [ParOldGen: 120002K->120002K(136704K)] 161380K->161317K(196608K), [Metaspace: 5382K->5378K(1056768K)], 0.0118884 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at test.DeadLock.main(DeadLock.java:23)
Heap
PSYoungGen total 59904K, used 42866K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
eden space 51712K, 82% used [0x00000000fbd80000,0x00000000fe75c870,0x00000000ff000000)
from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
to space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
ParOldGen total 136704K, used 120002K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
object space 136704K, 87% used [0x00000000f3800000,0x00000000fad30b90,0x00000000fbd80000)
Metaspace used 5409K, capacity 5590K, committed 5760K, reserved 1056768K
class space used 576K, capacity 626K, committed 640K, reserved 1048576K
java
java-8
garbage-collection
out-of-memory
weak-references
Dominic Peng
źródło
źródło
Odpowiedzi:
JVM zadzwoni do GC samodzielnie, ale w tym przypadku będzie za mało za późno. W tym przypadku nie tylko GC jest odpowiedzialna za czyszczenie pamięci. Wartości map są łatwo osiągalne i są usuwane przez samą mapę, gdy wywoływane są na niej pewne operacje.
Oto wynik, jeśli włączysz zdarzenia GC (XX: + PrintGC):
GC nie jest uruchamiane do ostatniej próby wprowadzenia wartości do mapy.
WeakHashMap nie może usunąć starych wpisów, dopóki klucze map nie pojawią się w kolejce referencyjnej. A klucze mapy nie pojawiają się w kolejce referencyjnej, dopóki nie zostaną wyrzucone. Przydział pamięci dla nowej wartości mapy jest uruchamiany, zanim mapa ma szansę się wyczyścić. Gdy alokacja pamięci kończy się niepowodzeniem i wyzwala GC, klucze mapy są gromadzone. Ale jest już za późno - nie zwolniono wystarczającej ilości pamięci, aby przydzielić nową wartość mapy. Jeśli zmniejszysz ładunek, prawdopodobnie skończy się wystarczająca ilość pamięci, aby przydzielić nową wartość mapy, a stare wpisy zostaną usunięte.
Innym rozwiązaniem może być zawijanie samych wartości do WeakReference. Pozwoli to GC wyczyścić zasoby bez czekania, aż mapa zrobi to sama. Oto wynik:
Dużo lepiej.
źródło
java.util.WeakHashMap.expungeStaleEntries
która odczytuje kolejkę referencyjną i usuwa wpisy z mapy, czyniąc wartości niedostępnymi i podlegającymi gromadzeniu. Dopiero po tym drugie przejście GC zwolni trochę pamięci.expungeStaleEntries
jest wywoływany w wielu przypadkach, takich jak get / put / size lub prawie wszystko, co zwykle robisz z mapą. To jest haczyk.Druga odpowiedź jest rzeczywiście poprawna, zredagowałem moją. Jako mały dodatek,
G1GC
nie wykaże tego zachowania, w przeciwieństwie doParallelGC
; która jest domyślna podjava-8
.Jak myślisz, co się stanie, jeśli nieznacznie zmienię twój program na (działam
jdk-8
z-Xmx20m
)Będzie dobrze działać. Dlaczego? Ponieważ daje Twojemu programowi wystarczającą swobodę, aby mogły zostać wprowadzone nowe przydziały, zanim
WeakHashMap
wyczyści wpisy. A druga odpowiedź już wyjaśnia, jak to się dzieje.Teraz
G1GC
sprawy potoczyłyby się trochę inaczej. Gdy przydzielany jest tak duży obiekt ( zwykle ponad 1/2 MB ), nazywa się to ahumongous allocation
. Kiedy tak się stanie, uruchomione zostanie współbieżne GC. W ramach tego cyklu: uruchomiona zostanie młoda kolekcja iCleanup phase
zostanie zainicjowana taka, która zajmie się wysłaniem wydarzenia doReferenceQueue
, abyWeakHashMap
wyczyścić wpisy.Więc dla tego kodu:
że uruchamiam z jdk-13 (gdzie
G1GC
jest domyślna)Oto część dzienników:
To już robi coś innego. Zaczyna się
concurrent cycle
(zrobione podczas działania aplikacji), ponieważ byłoG1 Humongous Allocation
. W ramach tego współbieżnego cyklu wykonuje młody cykl GC (który zatrzymuje aplikację podczas działania)W ramach tego młodego GC usuwa także ogromne obszary , oto wada .
Teraz możesz to zobaczyć
jdk-13
nie czeka na gromadzenie się śmieci w starym regionie, gdy przydzielane są naprawdę duże obiekty, ale uruchamia współbieżny cykl GC, który uratował dzień; w przeciwieństwie do jdk-8.Możesz przeczytać, co
DisableExplicitGC
i / lubExplicitGCInvokesConcurrent
znaczy, w połączeniu zSystem.gc
i zrozumieć, dlaczego dzwonienieSystem.gc
naprawdę tutaj pomaga.źródło
ParalleGC
, edytowałem ją i przepraszam (i dziękuję) za udowodnienie, że się mylę.-XX:+UseG1GC
mu działać w Javie 8, tak jak-XX:+UseParallelOldGC
w przypadku nowych JVM.