Śledzenie wycieku pamięci / problemu z usuwaniem elementów bezużytecznych w Javie

79

Jest to problem, który od kilku miesięcy próbuję znaleźć. Mam uruchomioną aplikację Java, która przetwarza źródła XML i zapisuje wynik w bazie danych. Występowały sporadyczne problemy z zasobami, które są bardzo trudne do wyśledzenia.

Tło: Na pudełku produkcyjnym (gdzie problem jest najbardziej zauważalny) nie mam szczególnie dobrego dostępu do pudełka i nie mogę uruchomić Jprofiler. To pudełko to 64-bitowa czterordzeniowa maszyna o pojemności 8 GB z systemem Centos 5.2, tomcat6 i java 1.6.0.11. Zaczyna się od tych java-opts

JAVA_OPTS="-server -Xmx5g -Xms4g -Xss256k -XX:MaxPermSize=256m -XX:+PrintGCDetails -
XX:+PrintGCTimeStamps -XX:+UseConcMarkSweepGC -XX:+PrintTenuringDistribution -XX:+UseParNewGC"

Stos technologii jest następujący:

  • Centos 64-bitowy 5.2.0
  • Java 6u11
  • Tomcat 6
  • Wiosna / WebMVC 2.5
  • Hibernacja 3
  • Kwarc 1.6.1
  • DBCP 1.2.1
  • MySQL 5.0.45
  • Ehcache 1.5.0
  • (i oczywiście wiele innych zależności, w szczególności biblioteki dżakarta-commons)

Najbliżej odtworzenia problemu jest 32-bitowa maszyna z mniejszymi wymaganiami dotyczącymi pamięci. Nad czym mam kontrolę. Sondowałem to na śmierć za pomocą JProfilera i naprawiłem wiele problemów z wydajnością (problemy z synchronizacją, prekompilacją / buforowaniem zapytań xpath, zmniejszeniem puli wątków i usunięciem niepotrzebnego pobierania wstępnego hibernacji i nadgorliwego „podgrzewania pamięci podręcznej” podczas przetwarzania).

W każdym przypadku program profilujący wykazał, że pochłaniają one ogromne ilości zasobów z tego czy innego powodu i że po wprowadzeniu zmian nie były to już podstawowe zasoby.

Problem: maszyna JVM wydaje się całkowicie ignorować ustawienia użycia pamięci, wypełnia całą pamięć i przestaje odpowiadać. Jest to problem dla klienta stojącego przed końcem, który oczekuje regularnej ankiety (co 5 minut i 1 minuta ponowienia), a także dla naszych zespołów operacyjnych, które są stale powiadamiane, że skrzynka przestała odpowiadać i muszą ją ponownie uruchomić. Na tym pudełku nie ma nic znaczącego.

Wygląda na to, że problem dotyczy czyszczenia pamięci. Używamy modułu zbierającego ConcurrentMarkSweep (jak wspomniano powyżej), ponieważ oryginalny moduł zbierający STW powodował przekroczenia limitów czasu JDBC i stawał się coraz wolniejszy. Dzienniki pokazują, że wraz ze wzrostem użycia pamięci zaczyna rzucać awarie cms i powraca do oryginalnego kolektora stop-the-world, który wydaje się nie gromadzić prawidłowo.

Jednak działając z jprofilerem, przycisk "Uruchom GC" wydaje się ładnie czyścić pamięć, zamiast pokazywać rosnący ślad, ale ponieważ nie mogę podłączyć jprofilera bezpośrednio do skrzynki produkcyjnej, a rozwiązywanie sprawdzonych hotspotów wydaje się nie działać. po lewej stronie z voodoo strojenia Garbage Collection w ciemno.

Co próbowałem:

  • Profilowanie i naprawianie hotspotów.
  • Używanie garbage collectorów STW, Parallel i CMS.
  • Praca z minimalnymi / maksymalnymi rozmiarami sterty w krokach 1 / 2,2 / 4,4 / 5,6 / 6.
  • Działa z przestrzenią permgen w przyrostach 256 MB do 1 Gb.
  • Wiele kombinacji powyższych.
  • Skonsultowałem się również z JVM [odniesienie do strojenia] (http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html), ale tak naprawdę nie mogę znaleźć nic wyjaśniającego to zachowanie ani żadnych przykładów _which_ tuning parametry do użycia w takiej sytuacji.
  • Próbowałem również (bez powodzenia) jprofilera w trybie offline, łącząc się z jconsole, visualvm, ale nie mogę znaleźć niczego, co zinterpretuje moje dane dziennika gc.

Niestety problem też pojawia się sporadycznie, wydaje się być nieprzewidywalny, może trwać dni lub nawet tydzień bez żadnych problemów, albo może zawieść 40 razy w ciągu dnia, a jedyne, co zdaje się łapać konsekwentnie, to to wyrzucanie śmieci działa.

Czy ktoś może doradzić, jak:
a) Dlaczego JVM używa 8 fizycznych gigów i 2 GB przestrzeni wymiany, kiedy jest skonfigurowana tak, aby maksymalnie wynosić mniej niż 6.
b) Odniesienie do strojenia GC, które faktycznie wyjaśnia lub podaje rozsądne przykłady kiedy i jakiego rodzaju ustawienia używać zaawansowanych kolekcji.
c) Odniesienie do najczęstszych wycieków pamięci w Javie (rozumiem odwołania nieodebrane, ale mam na myśli poziom biblioteki / frameworka lub coś bardziej nieodłącznego w strukturach danych, takich jak hashmapy).

Dziękuję za wszelkie uwagi, których możesz udzielić.

EDYTUJ
Emil H:
1) Tak, mój klaster programistyczny jest lustrem danych produkcyjnych, aż do serwera multimediów. Podstawową różnicą jest 32/64-bitowy i ilość dostępnej pamięci RAM, której nie mogę łatwo odtworzyć, ale kod, zapytania i ustawienia są identyczne.

2) Istnieje jakiś starszy kod, który opiera się na JaxB, ale podczas zmiany kolejności zadań, aby uniknąć konfliktów planowania, generalnie wyeliminowałem to wykonanie, ponieważ jest ono uruchamiane raz dziennie. Podstawowy parser używa zapytań XPath, które wywołują pakiet java.xml.xpath. To było źródło kilku punktów aktywnych, ponieważ jeden z zapytań nie był wstępnie kompilowany, a dwa odniesienia do nich były zapisane na sztywno. Utworzyłem pamięć podręczną z ochroną wątków (hashmap) i rozważyłem odwołania do zapytań xpath, aby były ostatecznymi statycznymi ciągami znaków, co znacznie zmniejszyło zużycie zasobów. Zapytanie nadal stanowi dużą część przetwarzania, ale powinno tak być, ponieważ jest to główna odpowiedzialność aplikacji.

3) Dodatkowa uwaga, drugim głównym konsumentem są operacje na obrazach z JAI (ponowne przetwarzanie obrazów z kanału). Nie znam bibliotek graficznych Java, ale z tego, co odkryłem, nie są one szczególnie nieszczelne.

(dzięki za dotychczasowe odpowiedzi, ludzie!)

AKTUALIZACJA:
Mogłem połączyć się z instancją produkcyjną za pomocą VisualVM, ale wyłączyłem opcję wizualizacji GC / run-GC (chociaż mogłem to wyświetlić lokalnie). Interesująca rzecz: alokacja sterty maszyny wirtualnej jest zgodna z JAVA_OPTS, a rzeczywista przydzielona sterta mieści się wygodnie przy 1-1,5 giga i nie wydaje się przeciekać, ale monitorowanie na poziomie skrzynki nadal pokazuje wzór wycieku, ale tak jest nie ma odzwierciedlenia w monitorowaniu maszyny wirtualnej. Na tym pudełku nie ma nic innego, więc jestem zaskoczony.

liam
źródło
Czy używasz danych rzeczywistych i bazy danych świata rzeczywistego do testowania? Preferujesz kopię danych produkcyjnych?
Emil H
4
+1 - to jedno z najlepszych pytań, jakie kiedykolwiek czytałem. Chciałbym mieć więcej do zaoferowania w zakresie pomocy. Wrócę do tego, żeby zobaczyć, czy ktoś ma coś mądrego do powiedzenia.
duffymo
Ponadto jakiego parsera XML używasz?
Emil H
Czy sprawdziłeś liczbę przydzielonych ByteBufferów i kto je przydziela?
Sean McCauliff
Sprawdź tę odpowiedź: stackoverflow.com/a/35610063 , zawiera szczegółowe informacje na temat wycieków pamięci natywnej Java.
Lari Hotari

Odpowiedzi:

92

W końcu znalazłem problem, który to powodował, i zamieszczam szczegółową odpowiedź na wypadek, gdyby ktoś inny miał te problemy.

Próbowałem jmap podczas działania procesu, ale zwykle powodowało to dalsze zawieszanie się jvm i musiałem go uruchomić z opcją --force. Skutkowało to zrzutami sterty, w których brakowało wielu danych lub przynajmniej brakowało odniesień między nimi. Do analizy użyłem jhat, który przedstawia dużo danych, ale niewiele w sposobie ich interpretacji. Po drugie, wypróbowałem narzędzie do analizy pamięci oparte na eclipse ( http://www.eclipse.org/mat/ ), które pokazało, że sterta składała się głównie z klas związanych z tomcat.

Problem polegał na tym, że jmap nie raportował aktualnego stanu aplikacji, a jedynie przechwytywał klasy podczas zamykania, które były głównie klasami tomcat.

Próbowałem kilka razy i zauważyłem, że było bardzo dużo obiektów modelu (właściwie 2-3 razy więcej niż zaznaczono publicznie w bazie danych).

Korzystając z tego, przeanalizowałem powolne dzienniki zapytań i kilka niepowiązanych problemów z wydajnością. Próbowałem bardzo leniwego ładowania ( http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html ), a także zastąpienia kilku operacji hibernacji bezpośrednimi zapytaniami jdbc (głównie tam, gdzie zajmował się ładowaniem i operowaniem na dużych kolekcjach - zamienniki jdbc działały bezpośrednio na tabelach złączeń) i zastąpiły kilka innych nieefektywnych zapytań, które rejestrował mysql.

Te kroki poprawiły wydajność frontendu, ale nadal nie rozwiązały problemu wycieku, aplikacja nadal była niestabilna i działała nieprzewidywalnie.

Wreszcie znalazłem opcję: -XX: + HeapDumpOnOutOfMemoryError. Ostatecznie powstał bardzo duży (~ 6,5 GB) plik hprof, który dokładnie przedstawiał stan aplikacji. Jak na ironię, plik był tak duży, że nie mógł go zanalizować, nawet na pudełku z 16 GB pamięci RAM. Na szczęście MAT był w stanie wygenerować kilka ładnie wyglądających wykresów i pokazał lepsze dane.

Tym razem jeden wątek kwarcowy zajmował 4,5 GB z 6 GB sterty, a większość z tego stanowiła hibernacja StatefulPersistenceContext ( https://www.hibernate.org/hib_docs/v3/api/org/hibernate /engine/StatefulPersistenceContext.html ). Ta klasa jest używana przez hibernację wewnętrznie jako jej główna pamięć podręczna (wyłączyłem pamięć podręczną drugiego poziomu i pamięci podręczne zapytań obsługiwane przez EHCache).

Ta klasa jest używana do włączania większości funkcji hibernacji, więc nie można jej bezpośrednio wyłączyć (można to obejść bezpośrednio, ale wiosna nie obsługuje sesji bezstanowej) i byłbym bardzo zaskoczony, gdyby miał taki poważny wyciek pamięci w dojrzałym produkcie. Więc dlaczego teraz przeciekał?

Cóż, była to kombinacja rzeczy: pula wątków kwarcowych tworzy instancje, gdy pewne rzeczy są wątkiem Lokalne, wiosna wprowadzała fabrykę sesji, która tworzyła sesję na początku cyklu życia nici kwarcowych, która była następnie ponownie używana do uruchamiania różne zadania kwarcowe, które korzystały z sesji hibernacji. Hibernate następnie buforował sesję, co jest jego oczekiwanym zachowaniem.

Problem polega na tym, że pula wątków nigdy nie zwalniała sesji, więc hibernacja pozostawała rezydentna i utrzymywała pamięć podręczną przez cały cykl życia sesji. Ponieważ używano obsługi szablonów hibernacji sprężyn, nie było jawnego użycia sesji (używamy hierarchii dao -> manager -> driver -> quartz-job, dao jest wstrzykiwany z konfiguracjami hibernacji do wiosny, więc operacje są wykonane bezpośrednio na szablonach).

Więc sesja nigdy nie była zamykana, hibernacja utrzymywała odniesienia do obiektów pamięci podręcznej, więc nigdy nie były zbierane jako śmieci, więc za każdym razem, gdy uruchamiano nowe zadanie, po prostu zapełniało pamięć podręczną lokalną dla wątku, więc nie było nawet jakikolwiek podział między różnymi miejscami pracy. Ponieważ jest to praca wymagająca intensywnego zapisu (bardzo mało odczytu), pamięć podręczna została w większości zmarnowana, więc obiekty były tworzone.

Rozwiązanie: utwórz metodę dao, która jawnie wywoła session.flush () i session.clear (), i wywołaj tę metodę na początku każdego zadania.

Aplikacja działa już od kilku dni bez problemów z monitorowaniem, błędów pamięci czy restartów.

Dziękuję wszystkim za pomoc w tej sprawie, był to dość trudny błąd do wyśledzenia, ponieważ wszystko działało dokładnie tak, jak powinno, ale ostatecznie metoda 3-liniowa zdołała naprawić wszystkie problemy.

liam
źródło
13
Niezłe podsumowanie procesu debugowania i dziękuję za śledzenie i przesłanie rozwiązania.
Boris Terzic
1
Dzięki za miłe wyjaśnienie. Miałem podobny problem w scenariuszu odczytu wsadowego (SELECT), co spowodowało, że StatefulPersistenceContext stało się tak duże. Nie mogłem uruchomić em.clear () lub em.flush (), jak miała moja główna metoda zapętlania @Transactional(propagation = Propagation.NOT_SUPPORTED). Zostało to rozwiązane przez zmianę propagacji na Propagation.REQUIREDi wywołanie em.flush / em.clear ().
Mohsen
3
Jednej rzeczy nie rozumiem: jeśli sesja nigdy nie została opróżniona, oznacza to, że żadne rzeczywiste dane nie zostały zapisane w DB. Czy te dane nie zostały pobrane z innego miejsca w Twojej aplikacji, abyś mógł zobaczyć, że ich brakuje?
yair
1
Podany link do StatefulPersistenceContext jest uszkodzony. Czy to docs.jboss.org/hibernate/orm/4.3/javadocs/org/hibernate/engine/ ... teraz?
Victor Stafusa,
1
Liam, wielkie dzięki. Uprzejmie mam ten sam problem i MAT wskazuje na hibernację statefulPersistentContext. Myślę, że czytając twój artykuł mam wystarczająco dużo wskazówek. Dzięki za tak wspaniałe informacje.
Reddymails
4

Czy możesz uruchomić skrzynkę produkcyjną z włączonym JMX?

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=<port>
...

Monitorowanie i zarządzanie za pomocą JMX

A potem dołączyć za pomocą JConsole, VisualVM ?

Czy można zrobić zrzut sterty za pomocą jmap ?

Jeśli tak, możesz przeanalizować zrzut sterty pod kątem wycieków za pomocą JProfiler (już masz), jhat , VisualVM, Eclipse MAT . Porównaj również zrzuty sterty, które mogą pomóc w znalezieniu przecieków / wzorców.

I jak wspomniałeś jakarta-commons. Podczas używania jakarta-commons-logging występuje problem związany z utrzymywaniem modułu ładującego klasy. Żeby dobrze przeczytać ten czek

Dzień z życia łowcy wycieków pamięci ( release(Classloader))

drganie
źródło
1) Próbowałem dzisiaj Visualvm i kilku innych narzędzi, ale muszę poprawnie otworzyć porty. 2) Widziałem problem z c-logowaniem w mojej ostatniej pracy i ten problem mi o tym przypomniał. Usługa obejmująca całą firmę regularnie ulegała awariom i została wyśledzona do znanego wycieku w miejscach publicznych. Wydaje mi się, że było to coś podobnego do tego, co podałeś. Próbowałem zachować większość rejestrowania jako log4j, ale nie mam dużego wyboru w przypadku projektów zależnych, które wymagają pakietu commons. Mamy również kilka klas używających simpleFacade, szukam teraz, czy mogę uczynić rzeczy bardziej spójnymi.
liam
4

Wygląda na to, że przecieka pamięć inna niż sterta. Wspomniałeś, że sterta pozostaje stabilna. Klasycznym kandydatem jest permgen (trwałe generowanie), które składa się z 2 rzeczy: załadowanych obiektów klas i wewnętrznych ciągów. Ponieważ zgłaszasz, że masz połączenie z VisualVM, powinieneś być w stanie wyświetlić liczbę załadowanych klas, jeśli następuje ciągły wzrost załadowanych klas (ważne, visualvm pokazuje również całkowitą liczbę załadowanych klas, jest w porządku, jeśli wzrasta, ale ilość załadowanych klas powinna ustabilizować się po pewnym czasie).

Jeśli okaże się, że jest to wyciek permgen, debugowanie staje się trudniejsze, ponieważ brakuje narzędzi do analizy permgen w porównaniu ze stertą. Najlepszym rozwiązaniem jest uruchomienie małego skryptu na serwerze, który wielokrotnie (co godzinę?) Wywołuje:

jmap -permstat <pid> > somefile<timestamp>.txt

jmap z tym parametrem wygeneruje przegląd załadowanych klas wraz z oszacowaniem ich rozmiaru w bajtach, ten raport może pomóc w zidentyfikowaniu, czy niektóre klasy nie zostaną rozładowane. (uwaga: mam na myśli identyfikator procesu i powinien to być jakiś wygenerowany znacznik czasu, aby rozróżnić pliki)

Po zidentyfikowaniu określonych klas jako załadowanych, a nie wyładowanych, możesz mentalnie dowiedzieć się, gdzie mogą one zostać wygenerowane, w przeciwnym razie możesz użyć jhat do analizy zrzutów wygenerowanych za pomocą jmap -dump. Zachowam to dla przyszłej aktualizacji, jeśli będziesz potrzebować tych informacji.

Boris Terzic
źródło
Dobry pomysł. Spróbuję tego dziś po południu.
liam
jmap nie pomogło, ale było blisko. zobacz pełną odpowiedź dla wyjaśnienia.
liam
2

Szukałbym bezpośrednio przydzielonego ByteBuffera.

Z pliku javadoc.

Bezpośredni bufor bajtów można utworzyć, wywołując metodę fabryki assignateDirect tej klasy. Bufory zwracane przez tę metodę mają zwykle nieco wyższe koszty alokacji i zwalniania alokacji niż bufory niebezpośrednie. Zawartość buforów bezpośrednich może znajdować się poza zwykłą stertą zbieraną elementów bezużytecznych, więc ich wpływ na zużycie pamięci aplikacji może nie być oczywisty. Dlatego zaleca się, aby bufory bezpośrednie były przydzielane przede wszystkim dla dużych, długotrwałych buforów, które podlegają natywnym operacjom we / wy systemu bazowego. Ogólnie najlepiej jest przydzielać bufory bezpośrednie tylko wtedy, gdy dają one wymierny wzrost wydajności programu.

Być może kod Tomcat używa tego do we / wy; skonfiguruj serwer Tomcat do korzystania z innego łącznika.

W przeciwnym razie możesz mieć wątek, który okresowo wykonuje System.gc (). „-XX: + ExplicitGCInvokesConcurrent” może być interesującą opcją do wypróbowania.

Sean McCauliff
źródło
1) Kiedy mówisz złącze, masz na myśli złącze DB, czy inną klasę związaną z We / Wy? Osobiście wolałbym nie podejmować wysiłków zmierzających do wprowadzenia nowej puli połączeń, nawet jeśli c3p0 jest zbliżony, ale nie umieściłbym tego jako możliwości. 2) Nie natrafiłem na jednoznaczną flagę GC, ale na pewno to rozważę. Jednak wydaje się to trochę hakerskie, a przy starszej bazie kodów tej wielkości próbuję odejść od tego podejścia. (np. kilka miesięcy temu musiałem wyśledzić kilka miejsc, w których pojawiły się wątki jako efekty uboczne. Wątki są teraz skonsolidowane).
liam
1) Minęło trochę czasu, odkąd skonfigurowałem tomcat. Miał koncepcję zwaną łącznikiem, więc można było skonfigurować go do nasłuchiwania żądań z serwera httpd Apache lub bezpośredniego nasłuchiwania HTTP. W pewnym momencie było złącze HTTP NIO i podstawowe złącze HTTP. Możesz zobaczyć, jakie opcje konfiguracji są dostępne dla łącznika NIO HTTP lub sprawdzić, czy dostępny jest jedyny podstawowy łącznik. 2) Potrzebujesz tylko wątku, który okresowo wywołuje System.gc () lub możesz ponownie użyć wątku czasu. Tak, to totalnie hackerskie.
Sean McCauliff
Zobacz stackoverflow.com/questions/26041117/… w celu debugowania przecieków pamięci natywnej.
Lari Hotari,
1

Jakikolwiek JAXB? Uważam, że JAXB to wypełniacz do trwałej przestrzeni.

Ponadto uważam, że visualgc , teraz dostarczany z JDK 6, to świetny sposób, aby zobaczyć, co dzieje się w pamięci. Pięknie pokazuje eden, przestrzenie pokoleniowe i perm oraz przejściowe zachowanie GC. Wszystko czego potrzebujesz to PID procesu. Może to pomoże podczas pracy nad JProfile.

A co z aspektami śledzenia / rejestrowania Springa? Może potrafisz napisać prosty aspekt, zastosować go deklaratywnie iw ten sposób zrobić profil dla biednego człowieka.

duffymo
źródło
1) Pracuję z SA, aby spróbować otworzyć zdalny port i mam zamiar wypróbować natywne narzędzia oparte na java / jmx (wypróbowałem kilka, w tym jprofiler - świetne narzędzie! - ale było to zbyt trudne odpowiednie biblioteki na poziomie systemu). 2) Nieufnie podchodzę do wszystkiego, co jest zorientowane na aspekt, nawet od wiosny. Z mojego doświadczenia wynika, że ​​nawet zależność od tego sprawia, że ​​rzeczy są bardziej zagmatwane i trudniejsze do skonfigurowania. Jednak będę o tym pamiętać, jeśli nic innego nie zadziała.
liam
1

„Niestety problem pojawia się również sporadycznie, wydaje się być nieprzewidywalny, może trwać kilka dni, a nawet tydzień bez żadnych problemów, lub może zawieść 40 razy w ciągu dnia, a jedyna rzecz, którą potrafię łapać konsekwentnie jest to, że wyrzucanie elementów bezużytecznych działa w górę ”.

Wygląda na to, że jest to związane z przypadkiem użycia, który jest wykonywany do 40 razy dziennie, a potem już nie przez kilka dni. Mam nadzieję, że nie śledzisz tylko objawów. To musi być coś, co możesz zawęzić, śledząc działania aktorów aplikacji (użytkowników, ofert pracy, usług).

Jeśli dzieje się tak przez import XML, należy porównać dane XML z 40 dni awarii z danymi zaimportowanymi w dniu zerowej awarii. Może to jakiś logiczny problem, którego nie znajdujesz tylko w swoim kodzie.

cafebabe
źródło
1

Miałem ten sam problem, z kilkoma różnicami ...

Moja technologia jest następująca:

graile 2.2.4

tomcat7

Quartz-plugin 1.0

W mojej aplikacji używam dwóch źródeł danych. To jest szczególny wyznacznik przyczyn błędów.

Inną rzeczą do rozważenia jest to, że wtyczka kwarcowa wprowadza sesję hibernacji do wątków kwarcowych, tak jak mówi @liam, i wątki kwarcowe wciąż żyją, dopóki nie skończę aplikacji.

Moim problemem był błąd na ORM Grails w połączeniu ze sposobem, w jaki wtyczka obsługuje sesję i moje dwa źródła danych.

Wtyczka Quartz miała słuchacza do inicjowania i niszczenia sesji hibernacji

public class SessionBinderJobListener extends JobListenerSupport {

    public static final String NAME = "sessionBinderListener";

    private PersistenceContextInterceptor persistenceInterceptor;

    public String getName() {
        return NAME;
    }

    public PersistenceContextInterceptor getPersistenceInterceptor() {
        return persistenceInterceptor;
    }

    public void setPersistenceInterceptor(PersistenceContextInterceptor persistenceInterceptor) {
        this.persistenceInterceptor = persistenceInterceptor;
    }

    public void jobToBeExecuted(JobExecutionContext context) {
        if (persistenceInterceptor != null) {
            persistenceInterceptor.init();
        }
    }

    public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) {
        if (persistenceInterceptor != null) {
            persistenceInterceptor.flush();
            persistenceInterceptor.destroy();
        }
    }
}

W moim przypadku, persistenceInterceptorwystąpień AggregatePersistenceContextInterceptor, a on miał listę HibernatePersistenceContextInterceptor. Po jednym dla każdego źródła danych.

Każda operacja AggregatePersistenceContextInterceptorprzechodzi do HibernatePersistence, bez żadnych modyfikacji ani zabiegów.

Kiedy wzywa init()on HibernatePersistenceContextInterceptoron zwiększamy zmienną statyczną poniżej

private static ThreadLocal<Integer> nestingCount = new ThreadLocal<Integer>();

Nie wiem, jak wygląda ta statyczna liczba. Po prostu wiem, że jest on zwiększany dwa razy, po jednym na źródło danych, z powodu AggregatePersistenceimplementacji.

Do tego momentu wyjaśnię tylko cenario.

Problem pojawia się teraz ...

Kiedy moja kwarcowa praca zakończy się, wtyczka wywołuje słuchacza, aby opróżnił i zniszczył sesje hibernacji, jak widać w kodzie źródłowym SessionBinderJobListener.

Flush przebiega doskonale, ale nie niszczy, ponieważ HibernatePersistencewykonaj jedną walidację przed zamknięciem sesji hibernacji ... Sprawdza, nestingCountczy wartość jest większa niż 1. Jeśli odpowiedź brzmi tak, nie zamyka sesji.

Upraszczanie tego, co zrobił Hibernate:

if(--nestingCount.getValue() > 0)
    do nothing;
else
    close the session;

To podstawa mojego wycieku pamięci. Wątki kwarcowe wciąż żyją ze wszystkimi obiektami używanymi w sesji, ponieważ grails ORM nie zamyka sesji, z powodu błędu spowodowanego, ponieważ mam dwa źródła danych.

Aby rozwiązać ten problem, dostosowuję odbiornik, aby wywołać wyczyść przed zniszczeniem i wywołać zniszczenie dwa razy (po jednym dla każdego źródła danych). Upewniłem się, że moja sesja była czysta i zniszczona, a jeśli zniszczenie się nie powiedzie, przynajmniej był jasny.

jpozorio
źródło