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.
Odpowiedzi:
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.
źródło
@Transactional(propagation = Propagation.NOT_SUPPORTED)
. Zostało to rozwiązane przez zmianę propagacji naPropagation.REQUIRED
i wywołanie em.flush / em.clear ().Czy możesz uruchomić skrzynkę produkcyjną z włączonym JMX?
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)
)źródło
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 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.
źródło
Szukałbym bezpośrednio przydzielonego ByteBuffera.
Z pliku javadoc.
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.
źródło
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.
źródło
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.
źródło
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,
persistenceInterceptor
wystąpieńAggregatePersistenceContextInterceptor
, a on miał listęHibernatePersistenceContextInterceptor
. Po jednym dla każdego źródła danych.Każda operacja
AggregatePersistenceContextInterceptor
przechodzi do HibernatePersistence, bez żadnych modyfikacji ani zabiegów.Kiedy wzywa
init()
onHibernatePersistenceContextInterceptor
on zwiększamy zmienną statyczną poniżejprivate 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
AggregatePersistence
implementacji.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ż
HibernatePersistence
wykonaj jedną walidację przed zamknięciem sesji hibernacji ... Sprawdza,nestingCount
czy 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.
źródło