Jak utworzyć wyciek pamięci w Javie?


3221

Właśnie miałem wywiad i poproszono mnie o wyciek pamięci w Javie.
Nie muszę dodawać, że czułem się głupio, nie mając pojęcia, jak zacząć je tworzyć.

Jaki byłby przykład?


275
Chciałbym im powiedzieć, że Java używa śmieciarza, i poprosić ich, aby być nieco bardziej szczegółowe na temat ich definicji „wyciek pamięci”, wyjaśniając, że - wyjąwszy błędy JVM - Java nie może przeciekać pamięć w zupełnie taki sam sposób C / C ++ może. Musisz gdzieś mieć odniesienie do obiektu .
Darien,

371
Zabawne jest dla mnie to, że w większości odpowiedzi ludzie szukają takich przypadków i sztuczek i wydają się zupełnie nie rozumieć (IMO). Mogliby po prostu pokazać kod, który przechowuje bezużyteczne odwołania do obiektów, które nigdy więcej nie będą używane, a jednocześnie nigdy nie upuszczają tych odniesień; można powiedzieć, że te przypadki nie są „prawdziwymi” przeciekami pamięci, ponieważ nadal istnieją odniesienia do tych obiektów, ale jeśli program nigdy nie użyje tych odwołań, a także nigdy ich nie usunie, jest to całkowicie równoważne (i tak złe jak) „ prawdziwy wyciek pamięci ”.
ehabkost

62
Szczerze mówiąc, nie mogę uwierzyć, że podobne pytanie, które zadałem na temat „Go” zostało obniżone do -1. Tutaj: stackoverflow.com/questions/4400311/... Zasadniczo wycieki pamięci, o których mówiłem, to te, które uzyskały +200 głosów pozytywnych do OP, a jednak zostałem zaatakowany i obrażony za pytanie, czy „Go” ma ten sam problem. Jakoś nie jestem pewien, czy wszystkie wiki działają tak dobrze.
Składnia T3rr0r

74
@ SyntaxT3rr0r - odpowiedź Dariena nie jest fanboyizmem. wyraźnie przyznał, że niektóre maszyny JVM mogą zawierać błędy, które oznaczają wyciek pamięci. różni się to od samej specyfikacji języka, umożliwiając wycieki pamięci.
Peter Recore

31
@ehabkost: Nie, nie są równoważne. (1) Masz możliwość odzyskania pamięci, podczas gdy w „prawdziwym wycieku” Twój program C / C ++ zapomina przydzielony zakres, nie ma bezpiecznego sposobu na odzyskanie. (2) Możesz bardzo łatwo wykryć problem z profilowaniem, ponieważ możesz zobaczyć, z jakimi obiektami wiąże się „wzdęcie”. (3) „Prawdziwy wyciek” jest jednoznacznym błędem, podczas gdy program, który utrzymuje wiele obiektów do momentu zakończenia, może być celową częścią tego, jak to ma działać.
Darien

Odpowiedzi:


2309

Oto dobry sposób na stworzenie prawdziwego wycieku pamięci (obiekty niedostępne przez uruchomienie kodu, ale nadal przechowywane w pamięci) w czystej Javie:

  1. Aplikacja tworzy długo działający wątek (lub użyj puli wątków, aby przeciekać jeszcze szybciej).
  2. Wątek ładuje klasę za pomocą (opcjonalnie niestandardowego) ClassLoader.
  3. Klasa przydziela dużą część pamięci (np. new byte[1000000]), Przechowuje silne odniesienie do niej w polu statycznym, a następnie przechowuje odniesienie do siebie w a ThreadLocal. Przydzielenie dodatkowej pamięci jest opcjonalne (wystarczy przeciekanie instancji klasy), ale sprawi, że wyciek będzie działał znacznie szybciej.
  4. Aplikacja usuwa wszystkie odwołania do niestandardowej klasy lub z ClassLoaderktórej została załadowana.
  5. Powtarzać.

Ze względu na sposób ThreadLocalimplementacji w JDK Oracle, powoduje to wyciek pamięci:

  • Każda z nich Threadma prywatne pole threadLocals, które faktycznie przechowuje wartości lokalne.
  • Każdy klucz na tej mapie jest słabym odniesieniem do ThreadLocalobiektu, więc po tym, jak ThreadLocalobiekt zostanie wyrzucony do pamięci, jego wpis zostanie usunięty z mapy.
  • Ale każda wartość jest silnym odniesieniem, więc gdy wartość (bezpośrednio lub pośrednio) wskazuje na ThreadLocalobiekt, który jest jej kluczem , obiekt ten nie będzie ani usuwany, ani usuwany z mapy, dopóki żyje wątek.

W tym przykładzie łańcuch silnych odniesień wygląda następująco:

Threadobiekt → threadLocalsmapa → instancja przykładowej klasy → przykładowa klasa → ThreadLocalpole statyczne → ThreadLocalobiekt.

(To ClassLoadertak naprawdę nie odgrywa roli w tworzeniu przecieku, tylko pogarsza wyciek z powodu tego dodatkowego łańcucha referencyjnego: przykładowa klasa → ClassLoader→ wszystkie klasy, które załadował. Było nawet gorzej w wielu implementacjach JVM, szczególnie przed Java 7, ponieważ klasy i ClassLoaders zostały przydzielone bezpośrednio do permgen i nigdy nie były w ogóle śmieciami).

Odmiana tego wzorca powoduje, że pojemniki z aplikacjami (takie jak Tomcat) mogą przeciekać pamięć jak sito, jeśli często ponownie wdrażasz aplikacje, które używają tych, ThreadLocalktóre w jakiś sposób wskazują na siebie. Może się to zdarzyć z wielu subtelnych powodów i często jest trudne do debugowania i / lub naprawy.

Aktualizacja : Ponieważ wiele osób wciąż o to prosi, oto przykładowy kod pokazujący to zachowanie w działaniu .


186
Wycieki +1 ClassLoader to jedne z najbardziej bolesnych wycieków pamięci w świecie JEE, często spowodowane przez biblioteki innych firm, które przekształcają dane (BeanUtils, kodeki XML / JSON). Może się to zdarzyć, gdy lib jest ładowany poza głównym modułem ładującym aplikacji, ale zawiera odniesienia do twoich klas (np. Przez buforowanie). Po cofnięciu / ponownym wdrożeniu aplikacji JVM nie jest w stanie wyczyścić modułu ładującego klasy aplikacji (a zatem wszystkich klas ładowanych przez nią), więc przy ponownym wdrażaniu serwer aplikacji ostatecznie się psuje. Jeśli masz szczęście, możesz dowiedzieć się o ClassCastException. ZxyAbc nie może zostać przesłany do zxyAbc
kamera douszna

7
tomcat wykorzystuje sztuczki i zero WSZYSTKIE zmienne statyczne we WSZYSTKICH załadowanych klasach, tomcat ma jednak wiele baz danych i niewłaściwe kodowanie (trzeba trochę czasu i przesłać poprawki), a także wszystkie zadziwiające ConcurrentLinkedQueue jako pamięć podręczną dla wewnętrznych (małych) obiektów, tak małe, że nawet ConcurrentLinkedQueue.Node zajmuje więcej pamięci.
bestsss 30.06.11

57
+1: wycieki Classloadera to koszmar. Spędziłem tygodnie próbując je rozgryźć. Smutne jest to, że, jak powiedział @earcam, są one głównie spowodowane przez biblioteki innych firm, a także większość profilerów nie może wykryć tych wycieków. Na tym blogu znajduje się dobre i jasne wyjaśnienie dotyczące wycieków Classloader. blogs.oracle.com/fkieviet/entry/…
Adrian M

4
@Nicolas: Jesteś pewien? JRockit domyślnie robi obiekty GC Class, a HotSpot nie, ale AFAIK JRockit nadal nie może GC klasy ani ClassLoadera, do którego odwołuje się ThreadLocal.
Daniel Pryden,

6
Tomcat spróbuje wykryć te wycieki i ostrzec o nich: wiki.apache.org/tomcat/MemoryLeakProtection . Najnowsza wersja czasami nawet naprawi wyciek za Ciebie.
Matthijs Bierman

1212

Odniesienie do obiektu zawierającego pole statyczne [esp pole końcowe]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

Wywołanie String.intern()długiego ciągu

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(Niezamknięte) otwarte strumienie (plik, sieć itp.)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Niezamknięte połączenia

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Obszary nieosiągalne z modułu śmieciowego JVM , takie jak pamięć przydzielana metodami natywnymi

W aplikacjach internetowych niektóre obiekty są przechowywane w zakresie aplikacji, dopóki aplikacja nie zostanie wyraźnie zatrzymana lub usunięta.

getServletContext().setAttribute("SOME_MAP", map);

Niepoprawne lub nieodpowiednie opcje JVM , takie jak noclassgcopcja w IBM JDK, która zapobiega nieużywaniu odśmiecania klas

Zobacz ustawienia IBM jdk .


178
Nie zgadzam się, że atrybuty kontekstu i sesji są „przeciekami”. Są tylko zmiennymi długowiecznymi. A statyczne pole końcowe jest mniej więcej stałą. Być może należy unikać dużych stałych, ale nie sądzę, aby nazywać to wyciekami pamięci.
Ian McLaird

80
(Niezamknięte) otwarte strumienie (plik, sieć itp.) , Nie przecieka tak naprawdę, podczas finalizacji (która nastąpi po następnym cyklu GC) planowane close()jest zamknięcie () ( zwykle nie jest wywoływane w finalizatorze wątek, ponieważ może to być operacja blokująca). Nie należy zamykać, ale nie powoduje to wycieku. Niezamknięte połączenie java.sql.Connection jest takie samo.
bestsss

33
W większości rozsądnych maszyn JVM wygląda na to, że klasa String ma jedynie słabe odniesienie do interntreści mieszającej. Jako takie odpowiednio zbierane śmieci, a nie wyciek. (ale IANAJP) mindprod.com/jgloss/interned.html#GC
Matt B.

42
Jak odniesienie obiektu przechowującego pole statyczne [esp pole końcowe] jest przeciekiem pamięci?
Kanagavelu Sugumar,

5
@cHao True. Niebezpieczeństwo, na które natknąłem się, to nie problemy z wyciekiem pamięci ze strumieni. Problem polega na tym, że wyciekają za mało pamięci. Możesz wyciec wiele uchwytów, ale wciąż masz dużo pamięci. Garbage Collector może wtedy nie zadawać sobie trudu, aby wykonać pełny zbiór, ponieważ wciąż ma dużo pamięci. Oznacza to, że finalizator nie jest wywoływany, więc zabrakło uchwytów. Problem polega na tym, że finalizatory będą (zwykle) uruchamiane, zanim zabraknie pamięci z nieszczelnych strumieni, ale może nie zostać wywołane, zanim zabraknie czegoś innego niż pamięć.
Patrick M,

460

Prostą rzeczą jest użycie zestawu HashSet z niepoprawnym (lub nieistniejącym) hashCode()lub equals(), a następnie dodawanie „duplikatów”. Zamiast ignorować duplikaty tak, jak powinno, zestaw będzie się powiększał i nie będzie można ich usunąć.

Jeśli chcesz, aby te złe klucze / elementy wisiały w pobliżu, możesz użyć pola statycznego, takiego jak

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

68
W rzeczywistości możesz usunąć elementy z HashSet, nawet jeśli klasa elementów otrzyma hashCode i będzie równa źle; po prostu pobierz iterator dla zestawu i użyj jego metody remove, ponieważ iterator faktycznie działa na samych wejściowych elementach, a nie na elementach. (Zauważ, że niezaimplementowany hashCode / equals nie wystarczy, aby wywołać wyciek; wartości domyślne implementują prostą tożsamość obiektu, dzięki czemu można uzyskać elementy i usunąć je normalnie.)
Donal Fellows

12
@Donale, jak sądzę, próbuję powiedzieć, czy nie zgadzam się z twoją definicją wycieku pamięci. Rozważałbym (aby kontynuować analogię) twoją technikę usuwania iteratora jako miskę ociekową pod przeciekiem; wyciek nadal istnieje niezależnie od miski ściekowej.
corsiKa

94
Zgadzam się, nie jest to „wyciek” pamięci, ponieważ możesz po prostu usunąć odniesienia do zestawu skrótów i poczekać na uruchomienie GC i presto! pamięć wraca.
user541686,

12
@ SyntaxT3rr0r, zinterpretowałem twoje pytanie jako pytanie, czy w języku jest coś, co naturalnie prowadzi do wycieków pamięci. Odpowiedź brzmi nie. To pytanie dotyczy tego, czy można stworzyć sytuację przypominającą wyciek pamięci. Żaden z tych przykładów nie powoduje wycieku pamięci w sposób, który zrozumiałby to programista C / C ++.
Peter Lawrey

11
@Peter Lawrey: także, co o tym sądzisz: „W języku C nie ma niczego, co naturalnie wycieka do wycieku pamięci, jeśli nie zapomnisz ręcznie zwolnić przydzielonej pamięci” . Jak by to było dla nieuczciwości intelektualnej? W każdym razie jestem zmęczony: możesz mieć ostatnie słowo.
SyntaxT3rr0r

271

Poniżej będzie nieoczywisty przypadek wycieku Java, oprócz standardowego przypadku zapomnianych słuchaczy, statycznych odniesień, fałszywych / modyfikowalnych klawiszy w mapach skrótów lub po prostu wątków utkniętych bez szans na zakończenie ich cyklu życia.

  • File.deleteOnExit() - zawsze przecieka sznur, jeśli ciąg znaków jest podciągiem, wyciek jest jeszcze gorszy (wyciek również jest ukryty char [])- w Java 7 podciąg również kopiuje char[], więc później nie ma zastosowania ; @Daniel, jednak nie ma potrzeby głosowania.

Skoncentruję się na wątkach, aby pokazać niebezpieczeństwo związane z wątkami niezarządzanymi, nie chcę nawet dotykać zamachu.

  • Runtime.addShutdownHooki nie usuwaj ... a nawet nawet z removeShutdownHook z powodu błędu w klasie ThreadGroup dotyczącego nierozpoczętych wątków, które mogą nie zostać zebrane, skutecznie wyciek z ThreadGroup. JGroup ma wyciek w GossipRouter.

  • Tworzenie, ale nie uruchamianie, należy Threaddo tej samej kategorii, co powyżej.

  • Utworzenie wątku dziedziczy ContextClassLoaderi AccessControlContext, oraz, ThreadGroupi wszystkie InheritedThreadLocal, wszystkie te odniesienia są potencjalnymi przeciekami, wraz z całymi klasami ładowanymi przez moduł ładujący klasy i wszystkimi referencjami statycznymi oraz ja-ja. Efekt jest szczególnie widoczny w całym frameworku jucExecutor, który oferuje super prosteThreadFactory interfejsem, jednak większość programistów nie ma pojęcia o czyhającym niebezpieczeństwie. Również wiele bibliotek uruchamia wątki na żądanie (zdecydowanie zbyt wiele popularnych bibliotek branżowych).

  • ThreadLocalskrytki; są złe w wielu przypadkach. Jestem pewien, że wszyscy widzieli dość proste pamięci podręczne oparte na ThreadLocal, cóż, zła wiadomość: jeśli wątek będzie działał dłużej niż oczekiwano, życie w kontekście ClassLoader, jest to całkiem miły mały wyciek. Nie używaj pamięci podręcznych ThreadLocal, chyba że jest to naprawdę potrzebne.

  • Powołanie ThreadGroup.destroy() gdy ThreadGroup nie ma żadnych wątków, ale nadal zachowuje potomne ThreadGroups. Zły wyciek, który uniemożliwi grupie ThreadGroup usunięcie jej elementu nadrzędnego, ale wszystkie dzieci będą niepoliczalne.

  • Użycie WeakHashMap i wartość (in) bezpośrednio odwołuje się do klucza. Trudno go znaleźć bez zrzutu hałdy. Dotyczy to wszystkich rozszerzeń, Weak/SoftReferencektóre mogą utrzymywać twarde odniesienie do strzeżonego obiektu.

  • Korzystanie java.net.URLz protokołu HTTP (S) i ładowanie zasobu z (!). Ten jest wyjątkowy, KeepAliveCachetworzy nowy wątek w systemie ThreadGroup, który przecieka kontekstowy moduł ładujący bieżącego wątku. Wątek jest tworzony na pierwsze żądanie, gdy nie istnieje żaden żywy wątek, więc albo możesz mieć szczęście, albo po prostu wyciek.Wyciek został już naprawiony w Javie 7, a kod tworzący wątek poprawnie usuwa kontekstowy moduł ładujący. Jest jeszcze kilka przypadków (jak ImageFetcher, również naprawione ) tworzenia podobnych wątków.

  • Korzystanie z InflaterInputStreamprzekazywania new java.util.zip.Inflater()konstruktora ( PNGImageDecoderna przykład) i nie wywoływanie end()inflatora. Cóż, jeśli przekażesz konstruktor newbez żadnej szansy ... I tak, wywołanie close()strumienia nie zamyka inflatora, jeśli zostanie ręcznie przekazany jako parametr konstruktora. To nie jest prawdziwy wyciek, ponieważ zostałby wydany przez finalizator ... kiedy uzna to za konieczne. Do tego momentu tak nisko zjada pamięć natywną, że może spowodować, że Linux oom_killer bezkarnie zabije proces. Głównym problemem jest to, że finalizacja w Javie jest bardzo niewiarygodna, a G1 pogorszyło ją do wersji 7.0.2. Morał tej historii: jak najszybciej uwolnij rodzime zasoby; finalizator jest po prostu zbyt słaby.

  • Ta sama sprawa z java.util.zip.Deflater. Ten jest o wiele gorszy, ponieważ Deflater jest bardzo wymagający w Javie, tzn. Zawsze używa 15 bitów (maks.) I 8 poziomów pamięci (9 to maks.) Przydzielając kilkaset KB pamięci rodzimej. Na szczęście Deflaternie jest powszechnie używany i według mojej wiedzy JDK nie zawiera nadużyć. Zawsze dzwoń, end()jeśli ręcznie utworzysz Deflaterlub Inflater. Najlepsza część dwóch ostatnich: nie można ich znaleźć za pomocą zwykłych dostępnych narzędzi do profilowania.

(Mogę dodać więcej marnowania czasu, które napotkałem na żądanie).

Powodzenia i bądź bezpieczny; przecieki są złe!


23
Creating but not starting a Thread...Tak, byłem bardzo ugryziony tym kilka wieków temu! (Java 1.3)
leonbloy

@leonbloy, zanim było jeszcze gorzej, ponieważ wątek został dodany bezpośrednio do grupy wątków, brak rozpoczęcia oznaczał bardzo twardy wyciek. Nie tylko zwiększa to unstartedliczbę, ale także zapobiega niszczeniu grupy nici (mniejsze zło, ale wciąż przeciek)
bestsss

Dziękuję Ci! „Wywoływanie, ThreadGroup.destroy()gdy ThreadGroup nie ma żadnych wątków…” jest niezwykle subtelnym błędem; Ścigam to przez wiele godzin, doprowadziłem na manowce, ponieważ wyliczenie wątku w moim GUI kontrolnym nie pokazało niczego, ale grupa wątków i, prawdopodobnie, przynajmniej jedna grupa potomna, nie zniknie.
Lawrence Dol

1
@bestsss: Jestem ciekawy, dlaczego chcesz usunąć hak zamykania, biorąc pod uwagę, że działa on, no cóż, zamknięcie JVM?
Lawrence Dol

203

Większość przykładów tutaj jest „zbyt skomplikowanych”. Są to przypadki skrajne. W tych przykładach programista popełnił błąd (np. Nie redefiniuj równości / kodu skrótu) lub został ugryziony przez narożny przypadek JVM / JAVA (ładunek klasy ze statycznym ...). Myślę, że to nie jest typ przykładu, którego chce ankieter, ani nawet najczęstszy przypadek.

Ale są naprawdę prostsze przypadki wycieków pamięci. Śmieciarka uwalnia tylko to, o czym już nie ma mowy. Jako programiści Java nie dbamy o pamięć. Przydzielamy go w razie potrzeby i pozwalamy na automatyczne uwolnienie. W porządku.

Ale każda długowieczna aplikacja ma zwykle stan wspólnego. Może to być wszystko, statyka, singletony ... Często nietrywialne aplikacje mają tendencję do tworzenia wykresów złożonych obiektów. Wystarczy zapomnieć o ustawieniu odwołania na wartość null lub częściej zapominając o usunięciu jednego obiektu z kolekcji, aby wyciek pamięci.

Oczywiście wszelkiego rodzaju nasłuchiwania (np. Nasłuchiwania interfejsu użytkownika), pamięci podręczne lub dowolne długotrwałe współdzielone stany mają tendencję do powodowania wycieku pamięci, jeśli nie są odpowiednio obsługiwane. Należy rozumieć, że nie jest to przypadek narożny Java lub problem ze śmieciarzem. Jest to problem projektowy. Projektujemy, że dodajemy detektor do długowiecznego obiektu, ale nie usuwamy detektora, gdy nie jest już potrzebny. Buforujemy obiekty, ale nie mamy strategii ich usuwania z pamięci podręcznej.

Być może mamy złożony wykres, który przechowuje poprzedni stan wymagany przez obliczenia. Ale poprzedni stan sam jest powiązany ze stanem przed i tak dalej.

Podobnie jak musimy zamykać połączenia SQL lub pliki. Musimy ustawić odpowiednie odwołania na wartość null i usunąć elementy z kolekcji. Będziemy mieć odpowiednie strategie buforowania (maksymalny rozmiar pamięci, liczba elementów lub timery). Wszystkie obiekty, które umożliwiają powiadomienie nasłuchiwacza, muszą zawierać zarówno metodę addListener, jak i removeListener. A gdy te powiadomienia nie są już używane, muszą wyczyścić swoją listę słuchaczy.

Wyciek pamięci jest naprawdę możliwy i jest całkowicie przewidywalny. Nie ma potrzeby stosowania specjalnych funkcji językowych ani narożników. Wycieki pamięci są albo wskaźnikiem braku czegoś, albo nawet problemów projektowych.


24
Zabawne jest dla mnie to, że przy innych odpowiedziach ludzie szukają takich skrzynek i sztuczek i wydają się zupełnie nie rozumieć. Mogliby po prostu pokazać kod, który przechowuje bezużyteczne odwołania do obiektów, które nigdy więcej nie będą używane, i nigdy nie usuwają tych odniesień; można powiedzieć, że te przypadki nie są „prawdziwymi” przeciekami pamięci, ponieważ nadal istnieją odniesienia do tych obiektów, ale jeśli program nigdy nie użyje tych odwołań, a także nigdy ich nie usunie, jest to całkowicie równoważne (i tak złe jak) „ prawdziwy wyciek pamięci ”.
ehabkost

@Nicolas Bousquet: „Wyciek pamięci jest naprawdę możliwy” Dziękuję bardzo. +15 głosów pozytywnych. Miły. Zostałem tu wykrzyczany za stwierdzenie tego faktu, jako przesłanki pytania o język Go: stackoverflow.com/questions/4400311 To pytanie wciąż ma negatywne opinie :(
SyntaxT3rr0r

GC w Javie i .NET jest w pewnym sensie oparty na grafie założeń obiektów, które zawierają odniesienia do innych obiektów, jest taki sam jak wykres obiektów, które „dbają o” inne obiekty. W rzeczywistości możliwe jest istnienie krawędzi na wykresie odniesienia, które nie reprezentują relacji „opiekuńczych”, i obiekt może martwić się o istnienie innego obiektu, nawet jeśli nie istnieje bezpośrednia lub pośrednia ścieżka odniesienia (nawet przy użyciu WeakReference) od jednego do drugiego. Jeśli odwołanie do obiektu ma zapasowy bit, pomocne może być posiadanie wskaźnika „dba o cel” ...
supercat

... i system powinien powiadamiać (za pomocą środków podobnych do PhantomReference), jeśli stwierdzono, że obiekt nie ma nikogo, kogo to obchodzi. WeakReferencezbliża się nieco, ale musi zostać przekonwertowany na silną referencję, zanim będzie można go użyć; jeśli wystąpi cykl GC, podczas gdy istnieje silne odniesienie, cel zostanie uznany za użyteczny.
supercat

To jest moim zdaniem poprawna odpowiedź. Napisaliśmy symulację lata temu. Jakoś przypadkowo powiązaliśmy poprzedni stan z bieżącym stanem, powodując wyciek pamięci. Z powodu nieprzekraczalnego terminu nigdy nie usunęliśmy wycieku pamięci, ale dokumentowaliśmy go jako „cechę”.
dokładnie

163

Odpowiedź zależy całkowicie od tego, o co pytał ankieter.

Czy w praktyce możliwe jest wyciek Java? Oczywiście, że tak, a w innych odpowiedziach jest mnóstwo przykładów.

Ale może być zadawanych wiele meta-pytań?

  • Czy teoretycznie „idealna” implementacja Java jest podatna na wycieki?
  • Czy kandydat rozumie różnicę między teorią a rzeczywistością?
  • Czy kandydat rozumie, jak działa wyrzucanie elementów bezużytecznych?
  • Lub w jaki sposób zbieranie śmieci powinno działać w idealnym przypadku?
  • Czy wiedzą, że mogą dzwonić w innych językach za pośrednictwem rodzimych interfejsów?
  • Czy wiedzą, że wyciekają pamięć w tych innych językach?
  • Czy kandydat w ogóle wie, czym jest zarządzanie pamięcią i co dzieje się za sceną w Javie?

Czytam twoje meta-pytanie jako „Jakiej odpowiedzi mógłbym użyć w tej sytuacji podczas wywiadu”. Dlatego skupię się na umiejętnościach przeprowadzania wywiadów zamiast na Javie. Uważam, że bardziej prawdopodobne jest, że powtórzysz sytuację, w której nie znasz odpowiedzi na pytanie w wywiadzie, niż będziesz musiał wiedzieć, jak zrobić wyciek z Javy. Mam nadzieję, że to pomoże.

Jedną z najważniejszych umiejętności, które możesz rozwinąć podczas rozmowy kwalifikacyjnej, jest nauczenie się aktywnego słuchania pytań i praca z ankieterami w celu wydobycia ich zamiarów. Pozwala to nie tylko odpowiedzieć na ich pytanie tak, jak chcą, ale także pokazuje, że masz pewne ważne umiejętności komunikacyjne. A jeśli chodzi o wybór między wieloma równie utalentowanymi programistami, zatrudnię tego, który słucha, myśli i rozumie, zanim odpowie za każdym razem.


22
Ilekroć zadałem to pytanie, szukam dość prostej odpowiedzi - ciągle powiększaj kolejkę, nie zamykaj w końcu bazy danych itp., A nie dziwne szczegóły modułu / wątku, oznacza, że ​​rozumieją, co gc może zrobić, a czego nie może zrobić. To zależy od pracy, z którą rozmawiasz.
DaveC

Proszę spojrzeć na moje pytanie, dzięki stackoverflow.com/questions/31108772/...
Daniel Newtown,

130

Poniższy przykład jest dość bezcelowy, jeśli nie rozumiesz JDBC . Lub przynajmniej to, w jaki sposób JDBC oczekuje, że programista się zamknie Connection, Statementi ResultSetinstancje przed ich odrzuceniem lub utratą odniesień do nich, zamiast polegać na implementacji finalize.

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

Problem z powyższym polega na tym, że Connectionobiekt nie jest zamknięty, a zatem fizyczne połączenie pozostanie otwarte, dopóki nie pojawi się śmietnik i nie zauważy, że jest nieosiągalny. GC wywoła tę finalizemetodę, ale istnieją sterowniki JDBC, które nie implementują finalize, przynajmniej nie w ten sam sposób, w jaki Connection.closesą zaimplementowane. Rezultatem jest to, że chociaż pamięć zostanie odzyskana z powodu gromadzenia nieosiągalnych obiektów, zasoby (w tym pamięć) są powiązane zConnection obiektem mogą po prostu nie zostać odzyskane.

W takim razie, gdzie Connection„s finalizemetody nie posprzątać wszystko, można by faktycznie okaże się, że fizyczne połączenie z serwerem bazy danych będzie trwać kilka cykli zbierania śmieci, aż serwer bazy danych w końcu domyśla się, że połączenie nie jest żywy (jeżeli robi) i powinien zostać zamknięty.

Nawet jeśli sterownik JDBC miałby zostać zaimplementowany finalize, możliwe jest zgłaszanie wyjątków podczas finalizacji. Rezultatem tego jest to, że jakakolwiek pamięć związana z „uśpionym” obiektem nie zostanie odzyskana, ponieważ finalizegwarantuje się, że zostanie wywołana tylko raz.

Powyższy scenariusz napotkania wyjątków podczas finalizacji obiektu jest powiązany z innym scenariuszem, który może doprowadzić do wycieku pamięci - wskrzeszenia obiektu. Zmartwychwstanie obiektu często dokonuje się celowo, tworząc silne odniesienie do obiektu przed sfinalizowaniem, z innego obiektu. Niewłaściwe użycie zmartwychwstania obiektu spowoduje wyciek pamięci w połączeniu z innymi źródłami wycieków pamięci.

Istnieje wiele innych przykładów, które możesz wyczarować

  • Zarządzanie Listinstancją, w której dodajesz tylko do listy, a nie usuwasz z niej (chociaż powinieneś pozbywać się elementów, których już nie potrzebujesz), lub
  • Otwieranie Sockets lub Files, ale nie zamykanie ich, gdy nie są już potrzebne (podobnie jak w powyższym przykładzie dotyczącym Connectionklasy).
  • Nie rozładowywanie Singletonów podczas wyłączania aplikacji Java EE. Najwyraźniej Classloader, który załadował klasę singleton, zachowa odniesienie do klasy, a zatem instancja singletonu nigdy nie zostanie zebrana. Po wdrożeniu nowej instancji aplikacji zwykle tworzony jest nowy moduł ładujący klasy, a poprzedni moduł ładujący nadal będzie istniał z powodu singletonu.

98
Osiągniesz maksymalny limit otwartych połączeń, zanim zwykle osiągniesz limit pamięci. Nie pytaj mnie, dlaczego wiem ...
Hardwareguy

Sterownik JDBC Oracle jest znany z tego.
chotchki

@Hardwareguy Uderzyłem w limity połączeń baz danych SQL, dopóki nie wprowadziłem Connection.closeostatecznie bloku wszystkich moich wywołań SQL. Dla dodatkowej zabawy zadzwoniłem do długotrwałych procedur składowanych Oracle, które wymagały blokad po stronie Java, aby zapobiec zbyt dużej liczbie wywołań bazy danych.
Michael Shopsin

@Hardwareguy To ciekawe, ale nie jest tak naprawdę konieczne, aby faktyczne limity połączeń zostały przekroczone dla wszystkich środowisk. Na przykład dla aplikacji wdrożonej na serwerze aplikacji weblogic 11g widziałem wycieki połączeń na dużą skalę. Jednak ze względu na opcję zbierania przecieków połączeń połączenia z bazą danych pozostały dostępne podczas wprowadzania przecieków pamięci. Nie jestem pewien co do wszystkich środowisk.
Aseem Bansal

Z mojego doświadczenia wynika, że ​​wyciek pamięci występuje nawet po zamknięciu połączenia. Najpierw musisz zamknąć ResultSet i PreparedStatement. Miałem serwer, który wielokrotnie się zawieszał po godzinach lub nawet dniach pracy w porządku, z powodu OutOfMemoryErrors, dopóki nie zacząłem tego robić.
Bjørn Stenfeldt

119

Prawdopodobnie jednym z najprostszych przykładów potencjalnego wycieku pamięci i tego, jak tego uniknąć, jest implementacja ArrayList.remove (int):

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

Gdybyś sam to wdrażał, czy pomyślałbyś o wyczyszczeniu elementu tablicy, który nie jest już używany ( elementData[--size] = null)? Ta referencja może utrzymać przy życiu ogromny obiekt ...


5
A gdzie jest wyciek pamięci?
rds

28
@maniek: Nie chciałem sugerować, że ten kod wykazuje wyciek pamięci. Zacytowałem go, aby pokazać, że czasami nieoczywisty kod jest wymagany, aby uniknąć przypadkowego zatrzymania obiektu.
meriton

Co to jest RangeCheck (indeks); ?
Koray Tugay

6
Joshua Bloch podał ten przykład w Effective Java pokazując prostą implementację stosów. Bardzo dobra odpowiedź.
wynajmuje

Ale to nie byłby PRAWDZIWY wyciek pamięci, nawet jeśli zostałby zapomniany. Dostęp do elementu nadal byłby BEZPIECZNY za pomocą Reflection, po prostu nie byłby oczywisty i bezpośrednio dostępny poprzez interfejs List, ale obiekt i referencja nadal tam są, i można bezpiecznie uzyskać do nich dostęp.
DGoiko

68

Za każdym razem, gdy przechowujesz odniesienia do obiektów, których już nie potrzebujesz, masz przeciek pamięci. Zobacz: Obsługa wycieków pamięci w programach Java, aby dowiedzieć się, jak ujawniają się wycieki pamięci w Javie i co możesz z tym zrobić.


14
Nie wierzę, że to „wyciek”. To błąd, który wynika z projektu programu i języka. Wyciek byłby przedmiotem wiszącym wokół, bez żadnych odniesień do niego.
user541686,

29
@ Mehrdad: To tylko jedna wąska definicja, która nie w pełni dotyczy wszystkich języków. Twierdziłbym, że każdy wyciek pamięci jest błędem spowodowanym złym zaprojektowaniem programu.
Bill the Lizard

9
@ Mehrdad: ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. Nie rozumiem, jak wyciągasz taki wniosek. Istnieje mniej sposobów na wyciek pamięci w Javie z dowolnej definicji. To zdecydowanie ważne pytanie.
Bill the Lizard

7
@ 31eee384: Jeśli twój program przechowuje w pamięci obiekty, których nigdy nie będzie mógł użyć, to technicznie jest to wyciek pamięci. Fakt, że masz większe problemy, tak naprawdę tego nie zmienia.
Bill the Lizard

8
@ 31eee384: Jeśli wiesz, że tak nie będzie, to nie może być. Program, jak napisano, nigdy nie uzyska dostępu do danych.
Bill the Lizard

51

Jesteś w stanie spowodować wyciek pamięci dzięki klasie sun.misc.Unsafe . W rzeczywistości ta klasa usług jest używana w różnych klasach standardowych (na przykład w klasach java.nio ). Nie możesz bezpośrednio utworzyć instancji tej klasy , ale możesz użyć do tego refleksji .

Kod nie kompiluje się w Eclipse IDE - skompiluj go za pomocą komendy javac(podczas kompilacji otrzymasz ostrzeżenia)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}

1
Przydzielona pamięć jest niewidoczna dla śmieciarza
stemm

4
Przydzielona pamięć również nie należy do Javy.
bestsss

Czy to JVM dla Sun / Oracle? Np. Czy to zadziała na IBM?
Berlin Brown

2
Pamięć z pewnością „należy do Javy”, przynajmniej w tym sensie, że i) nie jest dostępna dla nikogo innego, ii) po wyjściu z aplikacji Java zostanie zwrócona do systemu. To jest tuż poza JVM.
Michael Anderson

3
Spowoduje to kompilację w środowisku Eclipse (przynajmniej w najnowszych wersjach), ale musisz zmienić ustawienia kompilatora: w oknie> Preferencje> Java> Kompilator> Błędy / Ostrzeżenie> Przestarzałe i ograniczone zestaw API Zakazane odniesienia (reguły dostępu) na „Ostrzeżenie „.
Michael Anderson

43

Mogę skopiować stąd odpowiedź: Najłatwiejszy sposób na spowodowanie wycieku pamięci w Javie?

„Wyciek pamięci w informatyce (lub wyciek w tym kontekście) występuje, gdy program komputerowy zużywa pamięć, ale nie jest w stanie zwolnić jej z powrotem do systemu operacyjnego.” (Wikipedia)

Prosta odpowiedź brzmi: nie możesz. Java wykonuje automatyczne zarządzanie pamięcią i zwalnia zasoby, które nie są potrzebne. Nie możesz tego powstrzymać. ZAWSZE będzie w stanie uwolnić zasoby. W programach z ręcznym zarządzaniem pamięcią jest inaczej. Nie można uzyskać trochę pamięci w C za pomocą malloc (). Aby zwolnić pamięć, potrzebujesz wskaźnika, który zwrócił malloc i wywołaj na niej free (). Ale jeśli nie masz już wskaźnika (nadpisanego lub przekroczono czas życia), niestety nie jesteś w stanie zwolnić tej pamięci, a zatem masz wyciek pamięci.

Wszystkie pozostałe odpowiedzi jak dotąd nie są tak naprawdę przeciekami pamięci. Wszystkie mają na celu szybkie wypełnienie pamięci bezcelowymi rzeczami. Ale w każdej chwili możesz nadal odrywać się od obiektów, które stworzyłeś, a tym samym zwalniając pamięć -> NO LEAK. Odpowiedź Acconradu jest jednak dość bliska, jak muszę przyznać, ponieważ jego rozwiązaniem jest po prostu „rozbicie” modułu wyrzucającego śmieci przez wymuszenie go w nieskończonej pętli).

Długa odpowiedź brzmi: możesz dostać wyciek pamięci, pisząc bibliotekę dla Java za pomocą JNI, która może mieć ręczne zarządzanie pamięcią, a tym samym mieć przecieki pamięci. Jeśli wywołasz tę bibliotekę, proces Java spowoduje wyciek pamięci. Lub możesz mieć błędy w JVM, tak że JVM traci pamięć. Prawdopodobnie w JVM występują błędy, mogą nawet istnieć pewne znane, ponieważ odśmiecanie nie jest aż tak trywialne, ale nadal jest błędem. Z założenia nie jest to możliwe. Możesz poprosić o kod Java, który jest spowodowany przez taki błąd. Przepraszam, nie znam żadnego z nich, ale i tak może nie być już błędem w następnej wersji Java.


12
To niezwykle ograniczona (i niezbyt przydatna) definicja wycieków pamięci. Jedyną definicją, która ma sens ze względów praktycznych, jest „wyciek pamięci to każdy warunek, w którym program nadal przetrzymuje przydzieloną pamięć po tym, jak przechowywane przez nią dane nie są już potrzebne”.
Mason Wheeler

1
Wspomniana odpowiedź acconrad została usunięta?
Tomáš Zato - Przywróć Monikę

1
@ TomášZato: Nie, nie jest. Odwróciłem powyższe odniesienie, aby połączyć teraz, abyś mógł je łatwo znaleźć.
yankee

Co to jest zmartwychwstanie obiektu? Ile razy wywoływany jest destruktor? Jak te pytania obalają tę odpowiedź?
autystyczny

1
Cóż, na pewno możesz stworzyć przeciek pamięci w Javie, pomimo GC, i nadal spełniać powyższą definicję. Wystarczy mieć strukturę danych tylko do dołączania, chronioną przed dostępem zewnętrznym, aby żaden inny kod nie usunął go - program nie może zwolnić pamięci, ponieważ nie ma kodu.
toolforger

39

Oto prosty / złowrogi za pośrednictwem http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

Ponieważ podciąg odnosi się do wewnętrznej reprezentacji oryginalnego, znacznie dłuższego łańcucha, oryginał pozostaje w pamięci. Tak więc, dopóki masz StringLeaker w grze, masz również cały oryginalny ciąg w pamięci, nawet jeśli możesz pomyśleć, że trzymasz się łańcucha jednoznakowego.

Aby uniknąć przechowywania niechcianych odniesień do oryginalnego ciągu znaków, wykonaj następujące czynności:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

Aby uzyskać dodatkową wadę, możesz również .intern()podciąg:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

Spowoduje to zachowanie zarówno oryginalnego długiego łańcucha, jak i pochodnego podciągu w pamięci, nawet po odrzuceniu instancji StringLeaker.


4
Nie nazwałbym że przeciek pamięci, per se . Kiedy muchSmallerStringzostanie zwolniony (ponieważ StringLeakerobiekt zostanie zniszczony), długi łańcuch również zostanie zwolniony. To, co nazywam wyciekiem pamięci, to pamięć, której nigdy nie można zwolnić w tym przypadku JVM. Jednak wykazały się jak uwolnić pamięć: this.muchSmallerString=new String(this.muchSmallerString). Przy prawdziwym wycieku pamięci nic nie możesz zrobić.
rds

2
@rds, to słuszna kwestia. Nie- internprzypadek może być bardziej „niespodzianką pamięci” niż „wyciekiem pamięci”. .intern()Jednak wprowadzenie podciągu z pewnością stwarza sytuację, w której odniesienie do dłuższego łańcucha jest zachowane i nie można go zwolnić.
Jon Chambers

15
Metoda substring () tworzy nowy ciąg w java7 (jest to nowe zachowanie)
anstarovoyt

Nie musisz nawet samodzielnie wykonywać substring (): użyj dopasowującego wyrażenia regularnego, aby dopasować niewielką część ogromnego wejścia i noś „wyodrębniony” ciąg znaków przez długi czas. Ogromny wkład utrzymuje się aż do Java 6.
Bananeweizen

37

Typowym przykładem tego w kodzie GUI jest tworzenie widgetu / komponentu i dodawanie detektora do jakiegoś obiektu statycznego / o zasięgu aplikacji, a następnie nie usuwanie detektora, gdy widget jest zniszczony. Dostajesz nie tylko wycieku pamięci, ale także wydajności, ponieważ gdy cokolwiek słuchasz zdarzeń pożaru, wszyscy twoi starzy słuchacze są również nazywani.


1
Platforma Android podaje przykład wycieku pamięci utworzonego przez buforowanie mapy bitowej w polu statycznym Widoku .
rds

36

Weź dowolną aplikację internetową działającą w dowolnym pojemniku serwletu (Tomcat, Jetty, Glassfish, cokolwiek ...). Wdróż aplikację ponownie 10 lub 20 razy z rzędu (wystarczy dotknąć WAR w katalogu automatycznego wdrażania serwera.

O ile nikt tego nie przetestował, istnieje duże prawdopodobieństwo, że po kilku wdrożeniach dostaniesz błąd OutOfMemoryError, ponieważ aplikacja nie zajęła się czyszczeniem po sobie. Możesz nawet znaleźć błąd na swoim serwerze dzięki temu testowi.

Problem polega na tym, że żywotność kontenera jest dłuższa niż żywotność aplikacji. Musisz upewnić się, że wszystkie odwołania kontenera do obiektów lub klas aplikacji mogą być usuwane.

Jeśli istnieje tylko jedno odniesienie, które przetrwało niewdrożenie aplikacji internetowej, odpowiedni moduł ładujący klasy, a w konsekwencji wszystkie klasy aplikacji internetowej nie mogą zostać wyrzucone.

Wątki rozpoczęte przez twoją aplikację, zmienne ThreadLocal, rejestrujące moduły dołączające są jednymi z typowych podejrzanych, które powodują wycieki klasyloadera.


1
Nie dzieje się tak z powodu wycieku pamięci, ale dlatego, że moduł ładujący klasy nie zwalnia poprzedniego zestawu klas. Dlatego nie jest zalecane ponowne wdrażanie serwera aplikacji bez ponownego uruchamiania serwera (nie fizycznego komputera, ale serwera aplikacji). Widziałem ten sam problem z WebSphere.
Sven

35

Może używając zewnętrznego kodu natywnego przez JNI?

W czystej Javie jest to prawie niemożliwe.

Ale chodzi o „standardowy” wyciek pamięci, kiedy nie można już uzyskać dostępu do pamięci, ale nadal jest ona własnością aplikacji. Zamiast tego możesz przechowywać odniesienia do nieużywanych obiektów lub otwierać strumienie bez zamykania ich później.


22
To zależy od definicji „wycieku pamięci”. Jeśli „pamięć jest utrzymywana, ale nie jest już potrzebna”, jest to łatwe w Javie. Jeśli jest to „pamięć przydzielona, ​​ale w ogóle niedostępna dla kodu”, staje się nieco trudniejsza.
Joachim Sauer

@Jachachim Sauer - miałem na myśli drugi typ. Pierwszy jest dość łatwy do wykonania :)
Rogach

6
„Z czystą Javą jest to prawie niemożliwe”. Cóż, moje doświadczenie jest inne, szczególnie jeśli chodzi o wdrażanie pamięci podręcznych przez osoby, które nie są świadome pułapek.
Fabian Barney

4
@Rogach: istnieje w zasadzie +400 głosów na różne odpowiedzi od osób z +10 000 powtórzeń, co pokazuje, że w obu przypadkach Joachim Sauer stwierdził, że jest to bardzo możliwe. Więc twoje „prawie niemożliwe” nie ma sensu.
SyntaxT3rr0r

32

Miałem ładny „wyciek pamięci” w związku z parsowaniem PermGen i XML. Parser XML, którego użyliśmy (nie pamiętam, który to był) wykonał String.intern () na nazwach znaczników, aby przyspieszyć porównywanie. Jeden z naszych klientów wpadł na świetny pomysł, aby przechowywać wartości danych nie w atrybutach XML lub tekście, ale jako zmienne, dlatego mieliśmy taki dokument:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

W rzeczywistości nie używali liczb, ale dłuższe identyfikatory tekstowe (około 20 znaków), które były unikalne i przychodziły w tempie 10-15 milionów dziennie. To sprawia, że ​​200 MB śmieci dziennie, co nigdy więcej nie jest potrzebne, i nigdy GCed (ponieważ jest w PermGen). Ustawiliśmy permgen na 512 MB, więc pojawienie się wyjątku braku pamięci (OOME) zajęło około dwóch dni ...


4
Wystarczy wyodrębnić przykładowy kod: Myślę, że liczby (lub ciągi zaczynające się od liczb) nie są dozwolone jako nazwy elementów w XML.
Paŭlo Ebermann

Zauważ, że nie jest to już prawdą w przypadku JDK 7+, gdzie internowanie ciągów odbywa się na stercie. Szczegółowy opis znajduje się w tym artykule: java-performance.info/string-intern-in-java-6-7-8
jmiserez

Więc myślę, że użycie StringBuffer zamiast String rozwiązałoby ten problem? prawda?
anubhs

24

Co to jest wyciek pamięci:

  • Jest to spowodowane błędem lub złym projektem.
  • To strata pamięci.
  • Z czasem staje się gorzej.
  • Śmieciarka nie może go wyczyścić.

Typowy przykład:

Pamięć podręczna obiektów jest dobrym punktem wyjścia do zepsucia rzeczy.

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

Twoja pamięć podręczna rośnie i rośnie. Wkrótce cała baza danych zostaje wciągnięta do pamięci. Lepszy projekt wykorzystuje LRUMap (Przechowuje tylko ostatnio używane obiekty w pamięci podręcznej).

Jasne, możesz skomplikować sprawę:

  • za pomocą konstrukcji ThreadLocal .
  • dodawanie bardziej złożonych drzew referencyjnych .
  • lub wycieki spowodowane przez biblioteki stron trzecich .

Co często się zdarza:

Jeśli ten obiekt Info ma odniesienia do innych obiektów, które ponownie mają odniesienia do innych obiektów. W pewien sposób można również uznać to za wyciek pamięci (spowodowany złym projektem).


22

Pomyślałem, że to interesujące, że nikt nie używał wewnętrznych przykładów klas. Jeśli masz klasę wewnętrzną; z natury utrzymuje odniesienie do zawierającej klasy. Oczywiście nie jest to wyciek pamięci, ponieważ Java W końcu to wyczyści; ale może to spowodować, że klasy będą się trzymały dłużej niż oczekiwano.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

Teraz, jeśli wywołasz Przykład1 i otrzymujesz Przykład2 odrzucający Przykład1, z natury nadal będziesz miał link do obiektu Przykład1.

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

Słyszałem również pogłoskę, że jeśli masz zmienną, która istnieje dłużej niż przez określony czas; Java zakłada, że ​​zawsze będzie istniał i nigdy nie będzie próbował go wyczyścić, jeśli nie będzie już dostępny w kodzie. Ale to jest całkowicie niezweryfikowane.


2
klasy wewnętrzne rzadko stanowią problem. Są to proste przypadki i bardzo łatwe do wykrycia. Plotka jest tylko plotką.
bestsss,

2
„Plotka” brzmi tak, jakby ktoś w połowie przeczytał o tym, jak działa pokoleniowy GC. Przedmioty, które już dawno przeżyły, ale teraz są nieosiągalne, mogą rzeczywiście trzymać się i zajmować przez chwilę przestrzeń, ponieważ JVM wypromowało je wśród młodszych pokoleń, aby przestało je sprawdzać przy każdym przejściu. Unikną żmudnych przejść „wyczyść moje 5000 ciągów temp” z założenia. Ale nie są nieśmiertelni. Nadal kwalifikują się do gromadzenia, a jeśli maszyna wirtualna jest powiązana z pamięcią RAM, w końcu uruchomi pełny przegląd GC i przejmie tę pamięć.
cHao

22

Niedawno napotkałem sytuację wycieku pamięci spowodowaną w pewien sposób przez log4j.

Log4j ma ten mechanizm o nazwie Nested Diagnostic Context (NDC), który jest instrumentem służącym do odróżniania przeplatanych danych wyjściowych dziennika z różnych źródeł. Granulacja, w której działa NDC, to wątki, dlatego rozróżnia oddzielnie wyniki dziennika z różnych wątków.

Aby przechowywać tagi specyficzne dla wątku, klasa NDC log4j używa tablicy mieszającej, która jest kluczowana przez sam obiekt Thread (w przeciwieństwie do powiedzenia id wątku), a zatem dopóki tag NDC nie zostanie w pamięci, wszystkie obiekty, które zwisają z wątku obiekt również pozostaje w pamięci. W naszej aplikacji internetowej używamy NDC do oznaczania logoutputs identyfikatorem żądania w celu oddzielnego oddzielenia logów od pojedynczego żądania. Kontener, który wiąże tag NDC z wątkiem, usuwa go również podczas zwracania odpowiedzi z żądania. Problem wystąpił, gdy w trakcie przetwarzania żądania pojawił się wątek potomny, podobny do następującego kodu:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

Tak więc kontekst NDC był powiązany z wbudowanym wątkiem, który został spawnowany. Obiekt wątku, który był kluczem do tego kontekstu NDC, jest wątkiem wbudowanym, który ma zwisający z niego obiekt hugeList. Dlatego nawet po tym, jak wątek skończył robić to, co robił, odwołanie do ogromnej listy było utrzymywane przy życiu przez kontekst NDC Hastable, powodując w ten sposób wyciek pamięci.


To jest do bani. Należy sprawdzić tę bibliotekę rejestrowania, która przydziela pamięć ZERO podczas logowania do pliku: mentalog.soliveirajr.com
TraderJoeChicago,

+1 Czy wiesz od razu, czy istnieje podobny problem z MDC w slf4j / logback (następcy tego samego autora)? Mam zamiar zrobić głębokie nurkowanie na źródle, ale najpierw chciałem to sprawdzić. Tak czy inaczej, dziękuję za opublikowanie tego.
sparc_spread

20

Ankieter prawdopodobnie szukał okrągłego odwołania, takiego jak poniższy kod (który nawiasem mówiąc, przecieka pamięć tylko w bardzo starych JVM, które korzystały z liczenia odniesień, co już nie jest prawdą). Ale to dość niejasne pytanie, więc jest to doskonała okazja, aby pochwalić się zrozumieniem zarządzania pamięcią JVM.

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

Następnie możesz wyjaśnić, że przy liczeniu referencji powyższy kod przeciekałby pamięć. Ale większość współczesnych maszyn JVM nie korzysta już z liczenia referencji, większość używa wymiatacza śmieci, który faktycznie zbiera tę pamięć.

Następnie możesz wyjaśnić tworzenie obiektu, który ma podstawowy zasób natywny, na przykład:

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

Następnie możesz wyjaśnić, że technicznie jest to wyciek pamięci, ale tak naprawdę wyciek jest spowodowany przez natywny kod w JVM alokujący podstawowe zasoby rodzime, które nie zostały uwolnione przez twój kod Java.

Pod koniec dnia za pomocą nowoczesnej maszyny JVM musisz napisać kod Java, który przydziela natywny zasób poza normalnym zakresem świadomości JVM.


19

Wszyscy zawsze zapominają o natywnej trasie kodu. Oto prosty wzór na wyciek:

  1. Zadeklaruj metodę natywną.
  2. W metodzie natywnej wywołaj malloc. Nie dzwoń free.
  3. Wywołaj metodę natywną.

Pamiętaj, że przydziały pamięci w natywnym kodzie pochodzą ze sterty JVM.


1
Oparte na prawdziwej historii.
Reg

18

Utwórz mapę statyczną i dodawaj do niej twarde odniesienia. Te nigdy nie będą GC'd.

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

87
Jak to jest wyciek? Robi dokładnie to, o co go prosisz. W przypadku wycieku tworzenie i przechowywanie obiektów w dowolnym miejscu jest wyciekiem.
Falmarri

3
Zgadzam się z @Falmarri. Nie widzę tam przecieku, tylko tworzysz obiekty. Z pewnością możesz „odzyskać” pamięć, którą właśnie przydzieliłeś inną metodą o nazwie „removeFromCache”. Wyciek ma miejsce, gdy nie można odzyskać pamięci.
Kyle,

3
Chodzi mi o to, że ktoś, kto ciągle tworzy obiekty, być może umieszczając je w pamięci podręcznej, może skończyć się błędem OOM, jeśli nie będzie ostrożny.
duffymo

8
@duffymo: Ale to nie tak naprawdę pytanie. Nie ma to nic wspólnego z wykorzystaniem całej pamięci.
Falmarri

3
Absolutnie nieważne. Po prostu zbierasz kilka obiektów w kolekcji Map. Ich odniesienia zostaną zachowane, ponieważ mapa je zawiera.
gyorgyabraham

16

Można utworzyć przeciek pamięci ruchomej, tworząc nową instancję klasy w metodzie finalizacji tej klasy. Punkty bonusowe, jeśli finalizator tworzy wiele wystąpień. Oto prosty program, który wycieka całą stertę w czasie od kilku sekund do kilku minut, w zależności od wielkości sterty:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}

15

Nie sądzę, żeby ktokolwiek to powiedział: możesz wskrzesić obiekt, nadpisując metodę finalize (), tak aby finalize () przechowywał gdzieś referencję. Śmieciarka będzie wywoływana tylko raz na obiekcie, po czym obiekt nigdy nie zostanie zniszczony.


10
To nieprawda. finalize()nie zostanie wywołany, ale obiekt zostanie zebrany, gdy nie będzie więcej referencji. Śmieciarka nie jest również nazywana.
bestsss,

1
Ta odpowiedź jest myląca, finalize()JVM może wywołać tę metodę tylko raz, ale nie oznacza to, że nie można jej ponownie zebrać, jeśli obiekt zostanie wskrzeszony, a następnie ponownie wyzerowany. Jeśli w finalize()metodzie jest kod zamknięcia zasobu, kod ten nie zostanie ponownie uruchomiony, co może spowodować wyciek pamięci.
Tom Cammann,

15

Ostatnio natrafiłem na bardziej subtelny rodzaj wycieku zasobów. Otwieramy zasoby za pomocą getResourceAsStream modułu ładującego klasy i zdarzyło się, że uchwyty strumienia wejściowego nie zostały zamknięte.

Uhm, możesz powiedzieć, co za idiota.

Cóż, to sprawia, że ​​jest to interesujące: w ten sposób możesz wyciec pamięć sterty bazowego procesu, a nie stertę JVM.

Wszystko czego potrzebujesz to plik jar z plikiem wewnątrz, do którego będzie odwoływał się kod Java. Im większy plik jar, tym szybciej przydzielana jest pamięć.

Możesz łatwo utworzyć taki słoik za pomocą następującej klasy:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

Po prostu wklej do pliku o nazwie BigJarCreator.java, skompiluj i uruchom go z wiersza poleceń:

javac BigJarCreator.java
java -cp . BigJarCreator

Et voilà: w bieżącym katalogu roboczym znajduje się archiwum jar z dwoma plikami.

Stwórzmy drugą klasę:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

Ta klasa zasadniczo nic nie robi, ale tworzy niepowiązane obiekty InputStream. Obiekty te zostaną natychmiast wyrzucone śmieci, a zatem nie wpływają na wielkość sterty. W naszym przykładzie ważne jest, aby załadować istniejący zasób z pliku jar, a rozmiar ma tutaj znaczenie!

Jeśli masz wątpliwości, spróbuj skompilować i uruchomić klasę powyżej, ale upewnij się, że wybrałeś przyzwoity rozmiar sterty (2 MB):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

Nie napotkasz tutaj błędu OOM, ponieważ nie zachowano żadnych odniesień, aplikacja będzie działać bez względu na to, jak duży wybierzesz ITERACJE w powyższym przykładzie. Zużycie pamięci przez proces (widoczne w górnej części (RES / RSS) lub eksploratorze procesów) rośnie, chyba że aplikacja dostanie polecenie czekania. W powyższej konfiguracji przydzieli około 150 MB pamięci.

Jeśli chcesz, aby aplikacja działała bezpiecznie, zamknij strumień wejściowy dokładnie tam, gdzie został utworzony:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

a Twój proces nie przekroczy 35 MB, niezależnie od liczby iteracji.

Całkiem proste i zaskakujące.


14

Jak sugeruje wiele osób, wycieki zasobów są dość łatwe do spowodowania - podobnie jak w przykładach JDBC. Rzeczywiste wycieki pamięci są nieco trudniejsze - szczególnie jeśli nie polegasz na złamanych bitach JVM, aby zrobić to za Ciebie ...

Pomysły tworzenia obiektów o bardzo dużej powierzchni, a następnie niemożności dostępu do nich, nie są również prawdziwymi przeciekami pamięci. Jeśli nic nie będzie miało do niego dostępu, to zostanie ono wyrzucone, a jeśli coś będzie miało do niego dostęp, to nie będzie to przeciek ...

Jednym ze sposobów, który kiedyś działał - i nie wiem, czy nadal działa - jest posiadanie trzech głębokich okrągłych łańcuchów. Ponieważ w obiekcie A znajduje się odniesienie do obiektu B, obiekt B ma odniesienie do obiektu C, a obiekt C ma odniesienie do obiektu A. GC był wystarczająco sprytny, aby wiedzieć, że dwa głębokie łańcuchy - jak w przypadku A <--> B - może być bezpiecznie zebrany, jeśli A i B nie są dostępne dla niczego innego, ale nie mogłyby poradzić sobie z łańcuchem trójstronnym ...


7
Od pewnego czasu tak nie było. Współczesne GC wiedzą, jak obsługiwać odniesienia cykliczne.
assylias

13

Innym sposobem tworzenia potencjalnie ogromne wycieki pamięci jest utrzymanie odniesienia do Map.Entry<K,V>tematyce TreeMap.

Trudno jest ocenić, dlaczego dotyczy to tylko TreeMaps, ale patrząc na implementację, przyczyną może być to, że: TreeMap.Entryprzechowuje odniesienia do swojego rodzeństwa, dlatego jeśli TreeMapjest gotowe do zebrania, ale niektóre inne klasy zawierają odniesienie do dowolnego z jego Map.Entry, wtedy cała mapa zostanie zachowana w pamięci.


Scenariusz z życia:

Wyobraź sobie zapytanie db, które zwraca TreeMapstrukturę dużych danych. Ludzie zwykle używają TreeMaps, ponieważ zachowywana jest kolejność wstawiania elementów.

public static Map<String, Integer> pseudoQueryDatabase();

Jeśli zapytanie było wywoływane wiele razy, a dla każdego zapytania (a więc dla każdego Mapzwracanego) Entrygdzieś zapisujesz , pamięć stale się powiększa.

Rozważ następującą klasę opakowania:

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

Podanie:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

Po każdym pseudoQueryDatabase()wywołaniu mapinstancje powinny być gotowe do zebrania, ale tak się nie stanie, ponieważ co najmniej jeden Entryjest przechowywany gdzie indziej.

W zależności od jvmustawień aplikacja może ulec awarii na wczesnym etapie z powodu OutOfMemoryError.

Na tym visualvmwykresie widać, jak pamięć się powiększa.

Zrzut pamięci - TreeMap

To samo nie dotyczy haszowanej struktury danych ( HashMap).

To jest wykres przy użyciu HashMap.

Zrzut pamięci - HashMap

Rozwiązanie? Po prostu bezpośrednio zapisz klucz / wartość (jak zapewne już to robisz) zamiast zapisywać Map.Entry.


Tutaj napisałem obszerniejszy test porównawczy .


11

Wątki nie są gromadzone, dopóki się nie zakończą. Służą jako korzenie śmieci. Są jednym z niewielu obiektów, których nie można odzyskać, po prostu zapominając o nich lub usuwając odniesienia do nich.

Zastanów się: podstawowym wzorem do zakończenia wątku roboczego jest ustawienie zmiennej zmiennej widzianej przez wątek. Wątek może okresowo sprawdzać zmienną i używać tego jako sygnału do zakończenia. Jeśli zmienna nie zostanie zadeklarowana volatile, zmiana może nie być widoczna dla wątku, więc nie będzie wiedział, jak ją zakończyć. Albo wyobraź sobie, że niektóre wątki chcą zaktualizować współużytkowany obiekt, ale zakleszczenie przy próbie zablokowania go.

Jeśli masz tylko kilka wątków, błędy te będą prawdopodobnie oczywiste, ponieważ Twój program przestanie działać poprawnie. Jeśli masz pulę wątków, która tworzy więcej wątków w razie potrzeby, przestarzałe / zablokowane wątki mogą nie zostać zauważone i będą się gromadzić w nieskończoność, powodując wyciek pamięci. Wątki prawdopodobnie wykorzystają inne dane w Twojej aplikacji, więc również zapobiegną gromadzeniu wszystkiego, do czego bezpośrednio się odnoszą.

Jako przykład zabawki:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

Wzywaj System.gc(), jak chcesz, ale przekazany obiekt leakMenigdy nie umrze.

(* edytowany *)


1
@Spidey Nic się nie utknęło. Metoda wywołująca natychmiast wraca, a przekazany obiekt nigdy nie zostanie odzyskany. To właśnie wyciek.
Boann

1
Będziesz miał wątek „działający” (lub śpiąc, cokolwiek) przez cały czas trwania programu. Dla mnie to się nie liczy. Pula nie liczy się jako wyciek, nawet jeśli nie wykorzystasz jej całkowicie.
Spidey

1
@Spidey „Będziesz miał [rzecz] przez cały okres swojego programu. Nie liczy się to dla mnie jako wyciek”. Słyszysz siebie
Boann

3
@Spidey Jeśli policzysz pamięć, o której proces wie, że nie wyciekła, wszystkie odpowiedzi tutaj są niepoprawne, ponieważ proces zawsze śledzi, które strony w wirtualnej przestrzeni adresowej są mapowane. Po zakończeniu procesu system operacyjny usuwa wszystkie przecieki, umieszczając strony z powrotem na wolnym stosie stron. Aby przejść do następnej skrajności, można pobić każdy argumentowany wyciek, wskazując, że żaden z fizycznych bitów w układach pamięci RAM lub w przestrzeni wymiany na dysku nie został fizycznie umieszczony lub zniszczony, dzięki czemu można wyłączyć komputer i ponownie, aby usunąć wszelkie wycieki.
Boann

1
Praktyczna definicja wycieku polega na tym, że utraciła pamięć, o której nie wiemy, a zatem nie może wykonać procedury niezbędnej do odzyskania tylko tego; musielibyśmy zburzyć i odbudować całą przestrzeń pamięci. Taki nieuczciwy wątek może powstać naturalnie w wyniku impasu lub podejrzanej implementacji puli wątków. Obiekty, do których odwołują się takie wątki, nawet pośrednio, nie są już nigdy gromadzone, więc mamy pamięć, która nie będzie w naturalny sposób odzyskiwana ani ponownie wykorzystywana w trakcie trwania programu. Nazwałbym to problemem; w szczególności jest to wyciek pamięci.
Boann

10

Wydaje mi się, że poprawnym przykładem może być użycie zmiennych ThreadLocal w środowisku, w którym pule są łączone.

Na przykład używanie zmiennych ThreadLocal w serwletach do komunikowania się z innymi komponentami WWW, tworzenie wątków przez kontener i utrzymywanie bezczynności w puli. Zmienne ThreadLocal, jeśli nie zostaną poprawnie wyczyszczone, będą tam istnieć, dopóki ten sam komponent sieciowy nie nadpisze ich wartości.

Oczywiście po zidentyfikowaniu problem można łatwo rozwiązać.


10

Ankieter mógł szukać okrągłego rozwiązania referencyjnego:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

Jest to klasyczny problem z liczeniem odwołań do śmieciarek. Następnie uprzejmie wytłumaczysz, że JVM używają znacznie bardziej zaawansowanego algorytmu, który nie ma tego ograniczenia.

-Tes Tarle


12
Jest to klasyczny problem z liczeniem odwołań do śmieciarek. Nawet 15 lat temu Java nie korzystała z liczenia referencji. Nr ref. liczenie jest również wolniejsze niż GC.
bestsss,

4
Nie wyciek pamięci. Po prostu nieskończona pętla.
Esben Skov Pedersen

2
@Esben Przy każdej iteracji poprzednia firstnie jest przydatna i należy ją wyrzucać . W liczeniu odwołań śmieciarek obiekt nie zostałby zwolniony, ponieważ istnieje na nim aktywne odwołanie (samo w sobie). Nieskończona pętla służy tutaj do zlikwidowania wycieku: po uruchomieniu programu pamięć wzrośnie na czas nieokreślony.
rds

@rds @ Wesley Tarle przypuśćmy, że pętla nie była nieskończona. Czy nadal będzie wyciek pamięci?
nz_21
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.