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?
java
memory
memory-leaks
Mat B.
źródło
źródło
Odpowiedzi:
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:
ClassLoader
.new byte[1000000]
), Przechowuje silne odniesienie do niej w polu statycznym, a następnie przechowuje odniesienie do siebie w aThreadLocal
. Przydzielenie dodatkowej pamięci jest opcjonalne (wystarczy przeciekanie instancji klasy), ale sprawi, że wyciek będzie działał znacznie szybciej.ClassLoader
której została załadowana.Ze względu na sposób
ThreadLocal
implementacji w JDK Oracle, powoduje to wyciek pamięci:Thread
ma prywatne polethreadLocals
, które faktycznie przechowuje wartości lokalne.ThreadLocal
obiektu, więc po tym, jakThreadLocal
obiekt zostanie wyrzucony do pamięci, jego wpis zostanie usunięty z mapy.ThreadLocal
obiekt, 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:
Thread
obiekt →threadLocals
mapa → instancja przykładowej klasy → przykładowa klasa →ThreadLocal
pole statyczne →ThreadLocal
obiekt.(To
ClassLoader
tak 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 iClassLoader
s 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,
ThreadLocal
któ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 .
źródło
Odniesienie do obiektu zawierającego pole statyczne [esp pole końcowe]
Wywołanie
String.intern()
długiego ciągu(Niezamknięte) otwarte strumienie (plik, sieć itp.)
Niezamknięte połączenia
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.
Niepoprawne lub nieodpowiednie opcje JVM , takie jak
noclassgc
opcja w IBM JDK, która zapobiega nieużywaniu odśmiecania klasZobacz ustawienia IBM jdk .
źródło
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.intern
treści mieszającej. Jako takie są odpowiednio zbierane śmieci, a nie wyciek. (ale IANAJP) mindprod.com/jgloss/interned.html#GCProstą rzeczą jest użycie zestawu HashSet z niepoprawnym (lub nieistniejącym)
hashCode()
lubequals()
, 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
źródło
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ż kopiujechar[]
, 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.addShutdownHook
i 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
Thread
do tej samej kategorii, co powyżej.Utworzenie wątku dziedziczy
ContextClassLoader
iAccessControlContext
, oraz,ThreadGroup
i wszystkieInheritedThreadLocal
, 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).ThreadLocal
skrytki; 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/SoftReference
które mogą utrzymywać twarde odniesienie do strzeżonego obiektu.Korzystanie
java.net.URL
z protokołu HTTP (S) i ładowanie zasobu z (!). Ten jest wyjątkowy,KeepAliveCache
tworzy 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
InflaterInputStream
przekazywanianew java.util.zip.Inflater()
konstruktora (PNGImageDecoder
na przykład) i nie wywoływanieend()
inflatora. Cóż, jeśli przekażesz konstruktornew
bez żadnej szansy ... I tak, wywołanieclose()
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ęścieDeflater
nie jest powszechnie używany i według mojej wiedzy JDK nie zawiera nadużyć. Zawsze dzwoń,end()
jeśli ręcznie utworzyszDeflater
lubInflater
. 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!
źródło
Creating but not starting a Thread...
Tak, byłem bardzo ugryziony tym kilka wieków temu! (Java 1.3)unstarted
liczbę, ale także zapobiega niszczeniu grupy nici (mniejsze zło, ale wciąż przeciek)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.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.
źródło
WeakReference
) od jednego do drugiego. Jeśli odwołanie do obiektu ma zapasowy bit, pomocne może być posiadanie wskaźnika „dba o cel” ...PhantomReference
), jeśli stwierdzono, że obiekt nie ma nikogo, kogo to obchodzi.WeakReference
zbliż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.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ń?
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.
źródło
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
,Statement
iResultSet
instancje przed ich odrzuceniem lub utratą odniesień do nich, zamiast polegać na implementacjifinalize
.Problem z powyższym polega na tym, że
Connection
obiekt 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ęfinalize
metodę, ale istnieją sterowniki JDBC, które nie implementująfinalize
, przynajmniej nie w ten sam sposób, w jakiConnection.close
są 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
„sfinalize
metody 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żfinalize
gwarantuje 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ć
List
instancją, w której dodajesz tylko do listy, a nie usuwasz z niej (chociaż powinieneś pozbywać się elementów, których już nie potrzebujesz), lubSocket
s lubFile
s, ale nie zamykanie ich, gdy nie są już potrzebne (podobnie jak w powyższym przykładzie dotyczącymConnection
klasy).źródło
Connection.close
ostatecznie 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.Prawdopodobnie jednym z najprostszych przykładów potencjalnego wycieku pamięci i tego, jak tego uniknąć, jest implementacja ArrayList.remove (int):
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 ...źródło
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ć.
źródło
...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.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)źródło
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.
źródło
Oto prosty / złowrogi za pośrednictwem http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .
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:
Aby uzyskać dodatkową wadę, możesz również
.intern()
podciąg:Spowoduje to zachowanie zarówno oryginalnego długiego łańcucha, jak i pochodnego podciągu w pamięci, nawet po odrzuceniu instancji StringLeaker.
źródło
muchSmallerString
zostanie zwolniony (ponieważStringLeaker
obiekt 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ć.intern
przypadek 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ć.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.
źródło
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.
źródło
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.
źródło
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:
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 ...
źródło
Co to jest wyciek pamięci:
Typowy przykład:
Pamięć podręczna obiektów jest dobrym punktem wyjścia do zepsucia rzeczy.
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ę:
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).
źródło
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.
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.
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.
źródło
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:
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.
źródło
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.
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:
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.
źródło
Wszyscy zawsze zapominają o natywnej trasie kodu. Oto prosty wzór na wyciek:
malloc
. Nie dzwońfree
.Pamiętaj, że przydziały pamięci w natywnym kodzie pochodzą ze sterty JVM.
źródło
Utwórz mapę statyczną i dodawaj do niej twarde odniesienia. Te nigdy nie będą GC'd.
źródło
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:
źródło
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.
źródło
finalize()
nie zostanie wywołany, ale obiekt zostanie zebrany, gdy nie będzie więcej referencji. Śmieciarka nie jest również nazywana.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 wfinalize()
metodzie jest kod zamknięcia zasobu, kod ten nie zostanie ponownie uruchomiony, co może spowodować wyciek pamięci.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:
Po prostu wklej do pliku o nazwie BigJarCreator.java, skompiluj i uruchom go z wiersza poleceń:
Et voilà: w bieżącym katalogu roboczym znajduje się archiwum jar z dwoma plikami.
Stwórzmy drugą klasę:
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):
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:
a Twój proces nie przekroczy 35 MB, niezależnie od liczby iteracji.
Całkiem proste i zaskakujące.
źródło
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 ...
źródło
Innym sposobem tworzenia potencjalnie ogromne wycieki pamięci jest utrzymanie odniesienia do
Map.Entry<K,V>
tematyceTreeMap
.Trudno jest ocenić, dlaczego dotyczy to tylko
TreeMap
s, ale patrząc na implementację, przyczyną może być to, że:TreeMap.Entry
przechowuje odniesienia do swojego rodzeństwa, dlatego jeśliTreeMap
jest gotowe do zebrania, ale niektóre inne klasy zawierają odniesienie do dowolnego z jegoMap.Entry
, wtedy cała mapa zostanie zachowana w pamięci.Scenariusz z życia:
Wyobraź sobie zapytanie db, które zwraca
TreeMap
strukturę dużych danych. Ludzie zwykle używająTreeMap
s, ponieważ zachowywana jest kolejność wstawiania elementów.Jeśli zapytanie było wywoływane wiele razy, a dla każdego zapytania (a więc dla każdego
Map
zwracanego)Entry
gdzieś zapisujesz , pamięć stale się powiększa.Rozważ następującą klasę opakowania:
Podanie:
Po każdym
pseudoQueryDatabase()
wywołaniumap
instancje powinny być gotowe do zebrania, ale tak się nie stanie, ponieważ co najmniej jedenEntry
jest przechowywany gdzie indziej.W zależności od
jvm
ustawień aplikacja może ulec awarii na wczesnym etapie z powoduOutOfMemoryError
.Na tym
visualvm
wykresie widać, jak pamięć się powiększa.To samo nie dotyczy haszowanej struktury danych (
HashMap
).To jest wykres przy użyciu
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 .
źródło
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:
Wzywaj
System.gc()
, jak chcesz, ale przekazany obiektleakMe
nigdy nie umrze.(* edytowany *)
źródło
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ć.
źródło
Ankieter mógł szukać okrągłego rozwiązania referencyjnego:
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
źródło
first
nie 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.