Zbieramy wymagania, kodujemy, refaktorujemy, testujemy i puszczamy nasz nowy greenfieldowy projekt na produkcję 🚀. Dziś stworzyć działający mikroseriws jest bardzo łatwo, na co pozwala chociażby Sping Boot, Micronaut lub Quarkus.
Podczas przygotowywania aplikacji przed wyjściem na produkcję dobrze jest zwrócić uwagę na to, w jaki sposób jest uruchamiana:
- gdzie i jak loguje,
- jakie metryki eksponuje i gdzie je raportuje,
- jakie javaopts włączamy.
Tym ostatnim poświęcam dzisiejszy wpis na blogu. JVM opts owiane są nutą tajemniczości i kojarzą się raczej z tuneowaniem GC (i bynajmniej do tego nie zachęcam w tym wpisie), ale kilka z nich warto znać przed wyjściem na produkcję.
Z tego artykułu dowiesz się o opcjach:
-Xms
i-Xmx
oraz pamięcią w kontekście kontenerów Dockerowych-XX:+HeapDumpOnOutOfMemoryError
-Duser.timezone
-Djava.security.egd
-server
-Xloggc
-Xms i -Xmx
Xms i Xmx odpowiadają za kontrolę zużycia pamięci. Obiekty w Java są tworzone na stercie zarządzanej przez JVM poprzez alokowanie obiektów, a gdy są już nieużywane, ich usuwanie za pomocą Garbage Collector.
Więcej o pamięci i GC dowiesz się z artykułów:
- Obszary pamięci Maszyny Wirtualnej Javy (JVM)
- Jak działa Garbage Collector – zarządzanie pamięcią JVM
Xmx to maksymalny rozmiar zaalokowanego obszaru pamięci w systemie operacyjnym, którego aplikacja nie przekroczy. Gdy pamięć się skończy, próba alokacji nowego obiektu zakończy się wyjątkiem typu Error: java.lang.OutOfMemoryError
. Xmx daje Ci to kontrolę nad procesami, aby nie zaalokowały zbyt dużo pamięci i nie zagłodziły innych procesów. Przykładowe użycia:
-Xmx1G, -Xmx512M
Xms to minimalna ilość pamięci alokowana na start JVM. Można by zapytać – po co, skoro pamięc jest alokowana w miarę potrzeb? Otóż alokowanie pamięci zajmuje czas oraz powoduje co jakiś czas uruchamianie Garbage Collector.
Jeżeli Twoja aplikacja na start ładuje pewne dane do pamięci, na przykład dane do Cache, bazując na empirycznych obserwacjach i oszacowaniu ile jest danych, można przewidzieć ile pamięci aplikacja potrzebuje „na start”. Ustawiając Xms GC będzie wiedział, że nie należy sprzątać obiektów, a sama pamięć zostanie zaalokowana z góry, co z pewnością przyspieszy czas startu aplikacji*.
* Oczywiście nie wykluczam, że na start aplikacji może mieć większy wpływ inny czynnik niż GC i czas alokacji pamięci.
Maksymalna pamięć w kontenerze Docker
Coraz częściej jednak aplikacje Java są konteneryzowane za pomocą Docker’a. Przy uruchamianiu obrazu można wyspecifikować limit pamięci dla uruchomionego obrazu. Wtedy definiowanie -Xmx wydaje się być nadmiarowe, bo ustawienia są już zdefiniowane.
- Jeżeli w projekcie używasz Java 8 (wtedy upewnij się, że masz JRE 8u131+) lub Java 9, użyj opcji:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
. - Od Java 10 (JDK-8146115) maszyna wirtualna respektuje ustawienia kontenerów dockerowych – nie musisz niczego ustawiać.
-XX:+HeapDumpOnOutOfMemoryError
Pozostając w temacie pamięci. Istnieją sytuacje, kiedy aplikacji kończy się pamięć. Najczęsciej przyczyną jest:
- Brak zwalniania zasobów metodą close()
Takich jak wszelkiej maści połączenia (baza danych, JMS, messaging, I/O), uchwyty do plików. Coraz częściej dostęp do zasobów zapewniają biblioteki lub frameowrki (np. Spring Messaging, Spring Data), ale wciąż należy uważać korzystając z zasobów. - Przepełnienie buforów
Przy okazji np. wczytywanie dużej ilości stringów, plików XML. - Przepełnienie kolejek
Kolejki (te in-memory) to struktury danych, które wykorzystywane do zakolejkowania zadań przed zasobem o skończonej przepustowości. Przykładem są Executor. Gdy do pamięci akceptowane są dania pomimo tego, że nie mają szansy się zrealizować, z czasem może dojść do sytuacji, że kolejka się po prosu przepełni. Wtedy lepiej ograniczyć bufor do znanej wartości i po prostu odrzucić żądanie niż doprowadzić do katastrofy aplikacji.
Gdy już jednak dojdzie do sytuacji, że pamięć się wyczerpie i otrzymamy błąd java.lang.OutOfMemoryError
, dobrze jest, zanim aplikacja całkowicie się wyłoży, zrobić zrzut pamięci (heap dump) sprzed awarii. Pozwoli nam to na diagnozę problemu po ewentualnej awarii procesu. Opcję właczamy następującymi przełącznikami:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/jakas/sciezka
Analizując pamięć nie otrzymasz jednoznacznej odpowiedzi na pytanie: alokacja którego obiektu spowodowała błąd? Ale jesteś w stanie przeanalizować stertę (np. patrząc na rozmiar obiektów, liczbę wystąpień lub skąd pochodzą) i dojść do źródła problemu.
-Duser.timezone
Wprowadzone API Date and Time w Java 8 dość mocno wymusza na wyspecifikowanie strefy czasowej, gdy tworzona jest data i czas. Wciąż w Java istnieje stare API oraz wiele miejsc, gdzie nie jest jednoznaczne, jaka strefa czasowa jest używana, np.:
- data i czas w bibliotekach logujących
- stare API daty i czasu w Java
- wyciąganie danych z bazy danych (standard SQL: TIMESTAMP WITH TIME ZONE) i dana typu
java.sql.Timestamp
lubjava.time.OffsetDateTime
.
Domyślną strefą czasową jest strefa systemu operacyjnego, która dostępna jest później w klasie TimeZone.getDefault()
.
Dobrą praktyką jest po prostu ustawić strefę czasową JVM za pomocą przełączki:
-Duser.timezone=UTC
Strefa UTC to dobry wybór z kilka powodów:
- Wpisy w logach są logowane zawsze w jednej strefie czasowej i przy globalnych deploymentach wiadomo, kiedy wystąpiło zdarzenie (można łatwo przeliczyć do czasu lokalnego).
- Dane w bazie danych są zapisywane w UTC, odczytywane w UTC, a zmiana strefy następuje dopiero podczas prezentacji użytkownikowi. Mamy spójny zapis daty i czasu dla różnych użytkowników i na serwerach w różnych lokalizacjach.
Żyje się prościej.
-Djava.security.egd
Gdy potrzebujemy generować pseudolosowe liczby, Java korzysta z generatora liczb. Domyślnie jest to /dev/random
czyli wirtualne urządzenie w Unix, pełniące funkcję generatora losowych liczb z losowością pochodzącą ze sterowników urządzeń i innych źródeł. Brzmi jak dokładne źródło „losowości”, ale jego problemem jest to, że jest wolne.
Generowanie liczb jest potrzebne np. do celów kryptograficznych, np. generowanie certyfikatów, kluczy, seedów sesji potrzebnych do bezpiecznych połączeń. Może się okazać, że przy częstej wymianie danych sesji generowanie nowych liczb jest zbyt wolne.
Wtedy warto skorzystać z alternatywy jaką jest unblocking random, czyli: /dev/urandom. Aby ustawić źródło liczb losowych w Java należy skorzystać z przełącznika:
-Djava.security.egd=file:/dev/urandom
-server
Teraz to rzadkość, ale Java może być używana po stronie serwera oraz klienta (jako uruchamiane aplikacje). Wymagania serwera i klienta są inne: serwer chce szybko obsługiwać żadania, a klient zaalokować możliwie mało pamięci (mając na uwadze inne działające aplikacje).
Choć patrząc po statystykach niektórych przeglądarek internetowych, to mam mieszane spostrzeżenia, co jest priorytetem.
Dlatego dla aplikacji serwetowych warto skorzystać z przełączki -server, aby jawnie wyspecyfikować, jak ma się zachować JVM i Garbage Collector.
Ustawienie -server spowoduje m.in.:
- Bardziej agresywne optymalizacje kodu (doc)
- Więcej zaalokowanej pamięci przy domyślnych ustawieniach
-XX:+DisableExplicitGC
Włączenie opcji -XX:+DisableExplicitGC
zapobiega uruchamianiu GC „z zewnątrz”, np po API JMX z konsoli lub przez inne aplikacje. Podczas podłączania się narzędziami diagnostycznymi (JConsole, Java Mission Control), mogą one wywoływać proces GC, np podczas inspekcji sterty. Przydatne, aby zagwarantować sobie deterministyczne wyniki.
-Xloggc
Gdy aktywnie nie monitorujemy aplikacji i działa ona w trybie ciągłym, w razie jakichś problemów z opóźnieniem może okazać się przydatnym logowanie pauz GC do dalszej diagnostyki. Aby włączyć logowanie zdarzeń z GC użyj przełączek:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -Xloggc:/jakas/sciezka/gc.log
Opcje kolejno odpowiadają za:
- -XX:+PrintGCDetails – włącza logowanie GC z większą liczbą szczegółów w przeciwieństwie do -XX:+PrintGC.
- -XX:+PrintGCDateStamps – loguje czasy zdarzenia. Uwaga na -XX:+PrintGCTimeStamps, który loguje czas relatywnie do uruchomienia maszyny wirtualnej – ciężko wtedy określić, kiedy GC miało miejsce.
- -Xloggc – definiuje ścieżkę pliku, w którym umieszczane będą logi, zamiast logować wpisy na standardowe wyjście
- -XX:+PrintTenuringDistribution – wypisuje informacje na temat generacji obiektów.
Podsumowanie
JVM to tysiące parametrów. Włączenie kluczowych z nich pozwala na spełnienie wymagań niefunkcjonalnych, takich jak stabilność systemu, szybkość działania (inicjalizacji), diagnostyka.
Dobrą inspiracją na wybór parametrów jest przeglądanie projektów Open Source i sprawdzenie jakich przełączek używają i zapoznawanie się z nimi. Uważaj natomiast, żeby zachować rozsądek i nie używać przełączek, które tuneują JVM i stawiać hipotez bez testów.
Gdybyś potrzebował jeszcze więcej:
Jesteś Java Developerem?
Przejdź na wyższy poziom wiedzy
„Droga do Seniora” 🔥💪
Jesteś Team Leaderem? Masz zespół?
Podnieś efektywność i wiedzę swojego zespołu 👌
FAQ
-Xmx służy do ustawienia maksymalnej ilości pamięci, którą zaalokuje maszyna wirtualna Java. Odpowiednio -Xms wskazuje, jak duży obszar jest przydzielany „na start”.
Włącza zrzucenie pamięci JVM do pliku w przypadku przekroczenia maksymalnego dostępnego limitu zaraz przed zakończeniem działania JVM. Pozwala na późniejszą diagnostykę po awarii.
Włącza logowanie zdarzeń procesu Garbage Collectora, a -Xloggc wskazuje ścieżkę do plików logu.
Obraz: Technologia zdjęcie utworzone przez pvproductions – pl.freepik.com