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?
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?
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 a ThreadLocal
. 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 pole threadLocals
, które faktycznie przechowuje wartości lokalne.ThreadLocal
obiektu, więc po tym, jak ThreadLocal
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 i ClassLoader
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 .
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 noclassgc
opcja w IBM JDK, która zapobiega nieużywaniu odśmiecania klas
Zobacz ustawienia IBM jdk .
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#GC
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.
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, 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.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
i AccessControlContext
, oraz, ThreadGroup
i 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).
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
przekazywania new java.util.zip.Inflater()
konstruktora ( PNGImageDecoder
na przykład) i nie wywoływanie end()
inflatora. Cóż, jeśli przekażesz konstruktor new
bez ż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 Deflater
nie jest powszechnie używany i według mojej wiedzy JDK nie zawiera nadużyć. Zawsze dzwoń, end()
jeśli ręcznie utworzysz Deflater
lub 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!
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.
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.
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
i ResultSet
instancje 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 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 jaki Connection.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
„s finalize
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 lub File
s, ale nie zamykanie ich, gdy nie są już potrzebne (podobnie jak w powyższym przykładzie dotyczącym Connection
klasy).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):
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 ...
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ć.
...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)
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();
}
}
}
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.
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.
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.
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.
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.
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 ...
Co to jest wyciek pamię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ę:
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).
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.
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.
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.
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.
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); }
}
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());
}
}
}
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.
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 w finalize()
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:
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.
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 ...
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 TreeMap
s, ale patrząc na implementację, przyczyną może być to, że: TreeMap.Entry
przechowuje odniesienia do swojego rodzeństwa, dlatego jeśli TreeMap
jest 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 TreeMap
strukturę dużych danych. Ludzie zwykle używają TreeMap
s, 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 Map
zwracanego) Entry
gdzieś 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 map
instancje powinny być gotowe do zebrania, ale tak się nie stanie, ponieważ co najmniej jeden Entry
jest przechowywany gdzie indziej.
W zależności od jvm
ustawień aplikacja może ulec awarii na wczesnym etapie z powodu OutOfMemoryError
.
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 .
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 leakMe
nigdy nie umrze.
(* edytowany *)
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ć.
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
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.