Anatomia „wycieku pamięci”

172

W perspektywie .NET:

  • Co to jest wyciek pamięci ?
  • Jak możesz ustalić, czy Twoja aplikacja przecieka? Jakie są efekty?
  • Jak możesz zapobiec wyciekowi pamięci?
  • Jeśli w Twojej aplikacji występuje wyciek pamięci, czy znika on po zakończeniu procesu lub jego zabiciu? A może wycieki pamięci w Twojej aplikacji wpływają na inne procesy w systemie nawet po zakończeniu procesu?
  • A co z niezarządzanym kodem, do którego można uzyskać dostęp przez COM Interop i / lub P / Invoke?
huseyint
źródło

Odpowiedzi:

110

Najlepsze wyjaśnienie, jakie widziałem, znajduje się w rozdziale 7 darmowego e-booka Podstawy programowania .

Zasadniczo w .NET występuje przeciek pamięci, gdy obiekty, do których istnieją odwołania, są zrootowane i nie można ich usunąć. Dzieje się tak przypadkowo, gdy trzymasz odniesienia poza zamierzonym zakresem.

Dowiesz się, że masz wycieki, gdy zaczniesz otrzymywać OutOfMemoryExceptions lub zużycie pamięci przekroczy to, czego się spodziewałeś ( PerfMon ma ładne liczniki pamięci).

Zrozumienie modelu pamięci .NET to najlepszy sposób na uniknięcie go. W szczególności, aby zrozumieć, jak działa garbage collector i jak działają referencje - ponownie odsyłam do rozdziału 7 e-booka. Pamiętaj też o typowych pułapkach, prawdopodobnie najczęstszych zdarzeniach. Jeśli obiekt A jest zarejestrowany w zdarzeniu na obiekcie B , to obiekt A będzie się trzymał, dopóki obiekt B nie zniknie, ponieważ B posiada odniesienie do A . Rozwiązaniem jest wyrejestrowanie wydarzeń po zakończeniu.

Oczywiście dobry profil pamięci pozwoli Ci zobaczyć wykresy obiektów i zbadać zagnieżdżanie / odwoływanie się do twoich obiektów, aby zobaczyć, skąd pochodzą odwołania i jaki obiekt główny jest odpowiedzialny ( profil mrówek red-gate , JetBrains dotMemory, memprofiler są naprawdę dobre wybory lub możesz użyć tekstowego WinDbg i SOS , ale zdecydowanie polecam produkt komercyjny / wizualny, chyba że jesteś prawdziwym guru).

Uważam, że niezarządzany kod podlega typowym wyciekom pamięci, z wyjątkiem tego, że współdzielone odwołania są zarządzane przez moduł odśmiecania pamięci. Mogę się mylić co do ostatniego punktu.

Karl Seguin
źródło
11
Och, lubisz książkę, prawda? Od czasu do czasu widziałem, jak autor pojawia się na stackoverflow.
Johnno Nolan
Niektóre obiekty .NET mogą również zrootować się i stać się niemożliwe do pobrania. Wszystko, co jest IDisposable, powinno zostać z tego powodu usunięte.
kyoryu
1
@kyoryu: W jaki sposób obiekt sam się rootuje?
Andrei Rînea
2
@Andrei: Myślę, że działający wątek jest prawdopodobnie najlepszym przykładem zakorzenienia się obiektu. Obiekt, który umieszcza odwołanie do siebie w statycznej, niepublicznej lokalizacji (na przykład subskrybowanie zdarzenia statycznego lub implementacja singletona przez inicjalizację pola statycznego) może równie dobrze zakorzenić się, ponieważ nie ma oczywistego sposobu na ... um ... „wyrwać” go z cumowania.
Jeffrey Hantin,
@Jeffry to niekonwencjonalny sposób opisania tego, co się dzieje i podoba mi się to!
Exitos
35

Ściśle mówiąc, wyciek pamięci zużywa pamięć, która nie jest już używana przez program.

„Nie używany” ma więcej niż jedno znaczenie, może oznaczać „koniec z odniesieniem do niego”, to znaczy całkowicie nieodwracalny lub może oznaczać odwołanie, odzyskanie, nieużywane, ale program i tak zachowuje odniesienia. Dopiero później dotyczy .Net dla obiektów doskonale zarządzanych . Jednak nie wszystkie klasy są doskonałe iw pewnym momencie podstawowa niezarządzana implementacja może trwale wyciekać zasoby dla tego procesu.

We wszystkich przypadkach aplikacja zużywa więcej pamięci niż jest to bezwzględnie konieczne. Efekty uboczne, w zależności od wyciekającej ilości, mogą od zera do spowolnienia spowodowanego nadmiernym gromadzeniem, do serii wyjątków pamięci, a na końcu do błędu krytycznego, po którym następuje wymuszone zakończenie procesu.

Wiesz, że aplikacja ma problem z pamięcią, gdy monitorowanie pokazuje, że procesowi przydzielana jest coraz większa ilość pamięci po każdym cyklu czyszczenia pamięci . W takim przypadku albo przechowujesz zbyt dużo pamięci, albo przecieka jakaś podstawowa niezarządzana implementacja.

W przypadku większości wycieków zasoby są odzyskiwane po zakończeniu procesu, jednak niektóre zasoby nie zawsze są odzyskiwane w niektórych konkretnych przypadkach, znane są z tego uchwyty kursora GDI. Oczywiście, jeśli masz mechanizm komunikacji międzyprocesowej, pamięć przydzielona w innym procesie nie zostanie zwolniona, dopóki ten proces nie zwolni go lub nie zakończy.

Coincoin
źródło
32

Myślę, że na pytania „co to jest wyciek pamięci” i „jakie są skutki” zostały już dobrze udzielone odpowiedzi, ale chciałem dodać kilka rzeczy do innych pytań ...

Jak sprawdzić, czy Twoja aplikacja przecieka

Ciekawym sposobem jest otwarcie perfmon i dodanie śladów dla # bajtów we wszystkich stosach i kolekcjach # Gen 2 , w każdym przypadku patrząc tylko na Twój proces. Jeśli ćwiczenie określonej funkcji powoduje zwiększenie łącznej liczby bajtów, a ta pamięć pozostaje przydzielona po następnej kolekcji Gen 2, można powiedzieć, że funkcja przecieka pamięć.

Jak zapobiegać

Wydano inne dobre opinie. Chciałbym tylko dodać, że być może najczęściej pomijaną przyczyną wycieków pamięci .NET jest dodawanie programów obsługi zdarzeń do obiektów bez ich usuwania. Procedura obsługi zdarzeń dołączona do obiektu jest formą odniesienia do tego obiektu, więc uniemożliwi zbieranie danych nawet po usunięciu wszystkich innych odwołań. Zawsze pamiętaj, aby odłączyć programy obsługi zdarzeń (używając -=składni w C #).

Czy wyciek znika po zakończeniu procesu, a co z interopcją COM?

Po zakończeniu procesu cała pamięć zamapowana na jego przestrzeń adresową jest odzyskiwana przez system operacyjny, w tym wszystkie obiekty COM obsługiwane z bibliotek DLL. Stosunkowo rzadko obiekty COM mogą być obsługiwane z oddzielnych procesów. W takim przypadku po zakończeniu procesu nadal możesz być odpowiedzialny za przydzieloną pamięć w dowolnym używanym procesie serwera COM.

Jaskółka oknówka
źródło
19

Zdefiniowałbym wycieki pamięci jako obiekt, który nie zwalnia całej przydzielonej pamięci po jego zakończeniu. Zauważyłem, że może się to zdarzyć w twojej aplikacji, jeśli używasz Windows API i COM (tj. Niezarządzanego kodu, który ma błąd lub nie jest prawidłowo zarządzany), we frameworku i komponentach innych firm. Zauważyłem również, że problem może powodować brak porządkowania po użyciu niektórych przedmiotów, takich jak długopisy.

Osobiście doświadczyłem wyjątków braku pamięci, które mogą być spowodowane, ale nie są wyłączne dla wycieków pamięci w aplikacjach dot net. (OOM może również pochodzić z przypinania, patrz Przypinanie elementów ). Jeśli nie otrzymujesz błędów OOM lub musisz potwierdzić, czy przyczyną jest wyciek pamięci, jedynym sposobem jest profilowanie aplikacji.

Postaram się również zapewnić, co następuje:

a) Wszystko, co implementuje Idisposable, jest usuwane za pomocą bloku final lub instrukcji using, w tym pędzli, długopisów itp. (niektórzy twierdzą, że dodatkowo ustawiają wszystko na nic)

b) Wszystko, co ma metodę close, jest ponownie zamykane za pomocą instrukcji final lub using (chociaż odkryłem, że używanie nie zawsze zamyka się w zależności od tego, czy zadeklarowałeś obiekt poza instrukcją using)

c) Jeśli używasz niezarządzanego kodu / Windows API, to są one obsługiwane poprawnie po. (niektórzy mają metody czyszczenia, aby zwolnić zasoby)

Mam nadzieję że to pomoże.

Jan
źródło
19

Jeśli chcesz zdiagnozować wyciek pamięci w .NET, sprawdź te linki:

http://msdn.microsoft.com/en-us/magazine/cc163833.aspx

http://msdn.microsoft.com/en-us/magazine/cc164138.aspx

W tych artykułach opisano, jak utworzyć zrzut pamięci procesu i jak go przeanalizować, aby najpierw określić, czy wyciek nie jest zarządzany, czy zarządzany, a jeśli jest zarządzany, jak ustalić, skąd pochodzi.

Microsoft ma również nowsze narzędzie do pomocy w generowaniu zrzutów awaryjnych, które zastępuje ADPlus, o nazwie DebugDiag.

http://www.microsoft.com/downloads/details.aspx?FamilyID=28bd5941-c458-46f1-b24d-f60151d875a3&displaylang=en

Eric Z Beard
źródło
15

Najlepszym wyjaśnieniem działania garbage collectora jest Jeff Richters CLR w książce C # (rozdz. 20). Czytanie tego daje świetne podstawy do zrozumienia, jak przedmioty przetrwały.

Jedną z najczęstszych przyczyn przypadkowego zakorzenienia obiektów jest podłączenie zdarzeń poza klasą. Jeśli podłączysz zdarzenie zewnętrzne

na przykład

SomeExternalClass.Changed += new EventHandler(HandleIt);

i zapomnij o odczepieniu się od niego, gdy usuwasz, wtedy SomeExternalClass ma odniesienie do twojej klasy.

Jak wspomniano powyżej, profiler pamięci SciTech doskonale pokazuje korzenie obiektów, które podejrzewasz, że przeciekają.

Ale istnieje również bardzo szybki sposób sprawdzenia konkretnego typu, po prostu użyj WnDBG (możesz nawet użyć tego w bezpośrednim oknie VS.NET po podłączeniu):

.loadby sos mscorwks
!dumpheap -stat -type <TypeName>

Teraz zrób coś, co Twoim zdaniem usunie obiekty tego typu (np. Zamknij okno). Przydatne jest tutaj mieć przycisk debugowania, który będzie działałSystem.GC.Collect() kilka razy.

Następnie biegnij !dumpheap -stat -type <TypeName>ponownie. Jeśli liczba nie spadła lub nie spadła tak bardzo, jak się spodziewasz, masz podstawę do dalszych badań. (Mam tę wskazówkę z seminarium prowadzonego przez Ingo Rammera ).

Gus Paul
źródło
14

Wydaje mi się, że w zarządzanym środowisku wyciekiem byłoby zachowanie niepotrzebnego odniesienia do dużej ilości pamięci.

Bernarda
źródło
11

Dlaczego ludzie myślą, że wyciek pamięci w .NET to nie to samo, co każdy inny wyciek?

Wyciek pamięci występuje, gdy przyłączasz się do zasobu i nie puszczasz go. Możesz to zrobić zarówno w kodowaniu zarządzanym, jak i niezarządzanym.

Jeśli chodzi o .NET i inne narzędzia programistyczne, pojawiły się pomysły dotyczące zbierania elementów bezużytecznych i innych sposobów minimalizowania sytuacji, które spowodują wyciek z aplikacji. Ale najlepszą metodą zapobiegania wyciekom pamięci jest zrozumienie swojego podstawowego modelu pamięci i tego, jak to działa na używanej platformie.

Wiara w to, że GC i inna magia oczyści twój bałagan, jest krótką drogą do wycieków pamięci i będzie trudna do znalezienia później.

Podczas kodowania niezarządzanego zwykle upewniasz się, że posprzątałeś, wiesz, że za posprzątanie zasobów, które posiadasz, odpowiadasz ty, a nie woźny.

Z drugiej strony w .NET wiele osób uważa, że ​​GC wszystko wyczyści. Cóż, robi to dla ciebie, ale musisz się upewnić, że tak jest. NET zawiera wiele rzeczy, więc nie zawsze wiesz, czy masz do czynienia z zarządzanym, czy niezarządzanym zasobem, i musisz się upewnić, z czym masz do czynienia. Obsługa czcionek, zasobów GDI, Active Directory, baz danych itp. To zazwyczaj rzeczy, na które należy zwrócić uwagę.

Jeśli chodzi o zarządzanie, postawię szyję na krawędzi, aby powiedzieć, że zniknie, gdy proces zostanie zabity / usunięty.

Widzę jednak, że wiele osób to ma i naprawdę mam nadzieję, że to się skończy. Nie możesz poprosić użytkownika o zamknięcie aplikacji, aby posprzątać bałagan! Spójrz na przeglądarkę, która może być IE, FF itp., A następnie otwórz, powiedzmy, Czytnik Google, pozwól mu pozostać na kilka dni i zobacz, co się stanie.

Jeśli następnie otworzysz kolejną kartę w przeglądarce, przejdziesz do jakiejś witryny, a następnie zamkniesz kartę, na której była hostowana druga strona, która spowodowała wyciek przeglądarki, czy myślisz, że przeglądarka zwolni pamięć? Nie tak z IE. Na moim komputerze IE z łatwością zjada 1 GiB pamięci w krótkim czasie (około 3-4 dni), jeśli używam Czytnika Google. Niektóre gazety są jeszcze gorsze.

neslekkiM
źródło
10

Wydaje mi się, że w zarządzanym środowisku wyciekiem byłoby zachowanie niepotrzebnego odniesienia do dużej ilości pamięci.

Absolutnie. Ponadto niestosowanie metody .Dispose () na obiektach jednorazowego użytku, gdy jest to stosowne, może spowodować wycieki pamięci. Najłatwiej to zrobić za pomocą bloku using, ponieważ na końcu automatycznie wykonuje .Dispose ():

StreamReader sr;
using(sr = new StreamReader("somefile.txt"))
{
    //do some stuff
}

A jeśli utworzysz klasę, która używa niezarządzanych obiektów, jeśli nie implementujesz poprawnie IDisposable, możesz powodować wycieki pamięci dla użytkowników Twojej klasy.

Seibar
źródło
9

Wszystkie wycieki pamięci są usuwane przez zakończenie programu.

Wyciek wystarczającej ilości pamięci, a system operacyjny może zdecydować o rozwiązaniu problemu w Twoim imieniu.

Josh
źródło
8

Zgadzam się z Bernardem co do tego, czym w .net byłby wyciek memów.

Możesz sprofilować swoją aplikację, aby zobaczyć wykorzystanie jej pamięci i ustalić, że jeśli zarządza dużą ilością pamięci, a nie powinno, możesz powiedzieć, że ma przeciek.

Jeśli chodzi o zarządzanie, postawię szyję na krawędzi, aby powiedzieć, że zniknie, gdy proces zostanie zabity / usunięty.

Kod niezarządzany to jego własna bestia i jeśli pojawi się w nim wyciek, będzie postępować zgodnie ze standardową pamięcią. definicja wycieku.

Poklepać
źródło
7

Należy również pamiętać, że .NET ma dwie sterty, z których jedna jest stertą dużych obiektów. Wydaje mi się, że na tej stercie kładzie się przedmioty o wielkości około 85 tys. Lub więcej. Ta sterta ma inne reguły czasu życia niż zwykła sterta.

Jeśli tworzysz duże struktury pamięci (Słownik lub Lista), rozsądnie byłoby sprawdzić, jakie są dokładne reguły.

Jeśli chodzi o odzyskiwanie pamięci po zakończeniu procesu, o ile nie masz uruchomionego Win98 lub jego odpowiednika, wszystko jest zwracane do systemu operacyjnego po zakończeniu. Jedynymi wyjątkami są rzeczy, które są otwierane w różnych procesach, a zasób jest nadal otwarty w innym procesie.

Obiekty COM mogą być jednak trudne. Jeśli zawsze będziesz używać IDisposewzoru, będziesz bezpieczny. Ale natknąłem się na kilka zestawów międzyoperacyjnych, które implementują IDispose. Kluczem jest tutaj zadzwonić, Marshal.ReleaseCOMObjectkiedy skończysz. Obiekty COM nadal używają standardowego liczenia odwołań COM.

Joel Lucsy
źródło
6

Znalazłem .Net Memory Profiler jako bardzo dobrą pomoc przy znajdowaniu wycieków pamięci w .Net. Nie jest darmowy jak Microsoft CLR Profiler, ale moim zdaniem jest szybszy i bardziej konkretny. ZA

Lars Truijens
źródło