niedziela, 16 listopada 2014

Introduction to increase performance application based on JVM - part 2 - basic elements

Dziś kontynuujemy wprowadzenie do zwiększenia wydajności w aplikacjach opartych na JVM. Dzisiejszy post będzie skupiał się na VM runtime - czyli uruchomieniowym środowisku JVM stanowiącym pomost pomiedzy JIT a GC oraz dostarczającym funkcjonalości oraz API dla powyższych.
HotSpot VM można podzielić na trzy elementy :
   - VM runtime
   - JIT (client/server)
   - GC 
       - serial (szeregowy) 
       - throughput  (przepustowościowy)
       - CMS  (równoczesny)
       -  G1



Poprzedni post sygnalizował potrzebę monitoringu na warstwie systemu operacyjnego. Wymienione zostały podstawowe narzędzia wystarczające do zbierania danych i statystyk z dostępnych zasobów jak :
  - CPU
  - Memory
  - I/O
  - Network

Rysunek przedstawia poszczególne obszary możliwego tuningu wydajnościowego. Przez kolejne posty będziemy przechodzić przez poszczególne obszary.
W niektórych firmach admin DBA będzie miał odpowiedzialność dostrajania bazy, w innych będzie to musiał robić administrator odpowiedzialny za sprzęt czy serwery jak database server czy application server. Mogą występować też permutacje tych zależności. Wszystko zależy od profilu firmy, więc nie wykluczone, że w niektórych przypadkach będzie odpowiadał za wszystko programista/projektant/architekt


Powiedzieliśmy o możliwych problemach, które możesz napotkać jak : 
 - wolno działająca aplikacja
 - znaczący spadek wydajności postępujący w czasie (przyrost danych, rosnące zużycie pamięci itd)
 -  małe wycieki pamięci - impakt na wydajność
 - przestoje GC
 - duże wycieki pamięci generujące OutOfMemoryException a tym samym wydalenie się całego serwera
 - czasowe skoki procesora powodujące , że Twoja aplikacja przestaje odpowiadać na jakiekolwiek bodźce
 - inne zachowanie się aplikacja pod znaczącym obciążeniem a inne w przypadku małego obciążenia (response time)
 - problemy, które uaktywniają się na produkcji a na środowisku testowym były niezauważalne.

Poniższy wykres pamiętam, ze starej już książki pt :
Pro Java EE 5 Performance Management and Optimalization
obrazuje on zależność pomiędzy :
  - wykorzystaniem zasobów
  - przepustowością
  - czasem odpowiedzi dla końcowego użytkownika
  - zwiększeniem użycia aplikacji przez użytkowników

Chciałem go tutaj umieścić bo obrazuje bardzo fajnie te zależności, oraz pozwala zrozumieć jak Twoja aplikacja będzie reagowała na zmianę obciążenia.






















źródło : http://taas.inapp.com/?q=performance-testing

Ok, gdzie popełniliśmy błąd ? -  Zrozumienie JVM to dobra podstawa do eliminacji części tym problemów.


HotSpot JVM architecure :





























źródło : http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html


 Co nam daje JVM ?
  - przenośność między platformami dzięki kodowi bajtowemu
  - optymalizację w czasie wykonywania -  JIT
    - analiza ucieczki (escape analysis)
    - method inlining
    - wybór pomiędzy stertą a stosem (on stack replacement)
    - dynamiczna deoptymalizacja (dynamic deoptymalization)
   - zarządzanie pamięcią
   - security

Jeśli patrzymy na aplikację pod pryzmatem wydajności i skalowalności powinniśmy wiedzieć jak działa HotSpot VM.


VM Runtime zapewnia wspólny interfejs oraz usługi zarówno dla JIT jak i GC oraz :
  - zarządzanie wątkami
  - JNI
  - kontrola cyklu życia
  - analiza argumentów wywołania
  - obsługa wyjątków
  - class loader - ładowanie klas
  - byte code interpeter 
  - obsługa fatal errors

W VM Runtime możemy ustawić szereg ustawień lub zmienić  ich wartości domyślne.

-X - wersja standardowa dla opcji/ustawień
-XX - wersja deweloperska dla opcji/ustawień

( - ) - wyłącza daną opcję
( + ) - włącza daną opcję 

przykład : JAVA_OPTS=-verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

  Flagi zostały podzielone na cztery grupy  : 
     - behavioral options - podstawowe zachodzania dla VM
     - GC options - zachowania w obrębie GC
     - Performance tuning options - użyteczne dla konfiguracja pod kontem  wydajności
     - debugging options - umożliwiające śledzenie, wyświetlanie oraz zbieranie informacji o działaniu HotSpot VM 

Główne flagi dla jvm runtime znajdziesz :  tutaj

Kontrola cyklu życia - to kontrola nad uruchamianiem (launcher) oraz wyłączaniem VM.

> java - launcher
    Czynności wykonywane są w kolejności :
       - analiza wiersza poleceń (argumenty)
       - określenie rozmiaru stery i stosu jeśli nie zostały wyszczególnione użyte zostaną ustawienia domyślne. Określenie trybu JIT (server/client)
      - utworzenie classpath
      - wyodrębnienie Main-Class z jara
      - szereg zdarzeń w ramach wywołania metody JNI_CreateJavaVM
      - załadowanie Main-Class w działającej już instancji VM
     
> javaws - sieciowy laucher dla apletów

Class Loading - VM ładuje klasy, mapuje klasy lub interfejsy na obiektu klas.
Ładowanie klas umożliwia API JAVY jak :
 - ClassLoader.loadClass()
 - Class.forName()
 - reflection API
 - JNI_FindClass

HotSpot może również sam inicjalizować ładowanie klas. Dzieje się to podczas fazy uruchamiania. Ładownie klasy wymaga wcześniejszego załadowania wszystkich nadklas oraz interfejsów.
Podczas weryfikacji pliku .class może okazać się , że wymagane będzie załadowanie jakiegoś zbioru klas.

Jak to działa ?
Plik .class  definiuje typ dla JVM określając pola, metody, dziedziczone informację,adnotacje itd. Klasa jest najmniejszą jednostką, która może być załadowana do JVM.
Korzystając z nazwy klasy lub jej interfejsu JVM szuka kodu binarnego w formie pliku .class, definiuje nazwę klasy oraz tworzy jej obiekt.

Cykl w jakim to wszystko następuje jest następujący : 

Class loadnig - > loaded - > linked -> verified

Po tych operacjach obiekt new Class jest dostępny dla systemu i nowa instancja obiektu może zostać stworzona.

Faza loaded pobiera za  pomocą strumieni danych kod danego pliku .class.
Obiekt klasy po tej fazie nie jest w pełni rozwinięty, a klasa nie jest gotowa do użytku.

Faza linked dzieli się na 3 fazy :
   - verification - potwierdzenie, że plik klasy jest zgodny z oczekiwaniami i że nie spowoduje błędów w czasie wykonywania programu. Sprawdzane też są lokalizacje innych typów referencyjnych występujących w klasie, aby upewnić się, że klasa jest gotowa do użycia. Testujemy symantykę pliku klasowego, symbole ze stałej puli, wykonywana jest też kontrola typów
Sprawdzamy czy np nie chcemy dziedziczyć po klasie finalnej czy metody statyczne są napisane przez klasy pochodne itd.
   Weryfikacja jest złożonym procesem i składają się na nią poszczególne etapy :
     - podstawowe sprawdzenie integralności (constants, static , final , etc )
     - sprawdzenie wszystkich metod pod kątem kontroli dostępu
    -  sprawdzenie czy te metody wywoływane są z prawidłową ilością parametrów i typów

Jeśli kod binarny reprezentujący interfejs lub klasę  nie może być odnaleziony dostajemy znany już wyjątek NoClassDefFound.

ClassLoader sprawdza również format dla pliku .class co może wprowadzić do :
 - ClassFormatError - jeśli uzna,że plik jest uszkodzony lub nie może być poprawnie zinterpretowany
 - UnsupportedClassVersionError - jeśli np klasy były skompilowane w kompilatorze dla innej wersji javy a odpalane już w innej wersji javy.
- ClassCircularityError - jeśli wystąpił błąd związany z hierarchią klas
- IncompatibleClassChangeError - jeśli bezpośredni interfejs nie jest interfejsem, lub nadklasa jest interfejsem.
- VerifyError - np gdy podczas kompilacji użyto innej biblioteki niż w czasie wykonania. 

  - upewnienie się czy bytecode nie próbuje manipulować stosem w sposób zagrażający bezpieczeństwu
  - upewnienie się czy zmienne są zainicjalizowane przed swoim użyciem
  - sprawdzenie czy zmienne mają przypisane odpowiednie typy i wartości
Kontrole te są wykonywane z powodów wydajnościowych, ponieważ umożliwiają pominięcie sprawdzeń w trakcie wykonywania, tworząc w ten sposób interpretowany kod mogący szybciej się uruchomić. Upraszczają także kompilację bytecode do kodu maszynowego w czasie wykonywania (JIT)

   -  preparation - polega na przydzielaniu pamięci i przygotowując zmienne statyczne do zainicjalizowania - ustawiane są ich wartości domyślne.

   -  resolution - sprawdzenie każdego typu referencyjnego znajdującego się w klasie. Nawet gdy typy nie są znane, mogą zostać załadowane.
W finalnej fazie wszystkie zmienne statyczne mogą być zainicjalizowane to samo dotyczy bloków statycznych w kodzie. Na samym końcu klasa jest gotowa do użycia.


Class loader delegation - przerzucenie odpowiedzialności za ładowanie klasy na inny class loader.  Ładowarki są zdefiniowane hierarchicznie. Główny class loader wyszukuje innych ładowarek w następującej kolejności :
  - bootstap class loader (ładuje klasy BOOTCLASSPATH, cześć VM ładuje podstawowe jary jak rt.jar i nie przeprowadza przy tym żadnej weryfikacji)
  - extension class loader (używana do ładowania standardowych jarów, zwykle uzupełniona o moduł security)
  - system class loader (domyślna ładowarka klas dla aplikacji, tak następuje ładowanie klas w większości środowisk SE)
  - custom class loader  - (użycie swojego własnego rozwiązania)

Obsługa błędów krytycznych

Często spotykanym błędem VM jest OutOfMemoryException.
Kiedy już on wystąpi konieczne jest zrozumienie przyczyny jego wystąpienia.
W przypadku awarii JVM tworzony jest dziennik błędów (error log file ) , o nazwie hs_err_pid{pid}.log. Domyślnie plik ten jest tworzony w katalogu w którym została uruchomiona maszyna wirtualna.
Plik ten na potrzeby analizy zawiera mapę pamięci.
Dzięki opcji -XX:ErrorFile możemy zdefiniować lokację tworzenia się pliku.
-XX:+HeapDumpOnOutOfMemoryError - włączenie generowania zrzutu sterty
-XX:HeapDumpPath=<path_to_heap_dump> - lokacja do pliku z zrzutem sterty

Przyczyny wystąpiena tego rozdzaju wyjątku mogą być następujące : 

  - brak fizycznej pamięci dostępnej dla JVM
  (Exception : java.lang.OutOfMemoryError: Unable to create new native thread)
 
  - brak pamięci w przestrzeni PermGen w wersjach od javy 7 w dół , lub metaspace dla javy 8 (może być spowodowany przez classloader)
   (Exception : java.lang.OutOfMemoryError: Metaspace)  - za wiele klas lub zbyt duże klasy ładowane do obszaru Metaspace
   (Exception : java.lang.OutOfMemoryError: PermGen space) - za wiele klas lub zbyt duże klasy ładowane do PermGen space
    więcej o obszarach PermGen i MetaSpace w następnym poście... 

  - aplikacja tworzy za dużo żywych obiektów by zmieściły się w stercie o danym rozmiarze (możliwe wycieki pamięci : kolekcje ! )
 (Exception : java.lang.OutOfMemoryError: Java heap space)

- JVM spędził za dużo czasu na wykonywanie GC  
 (Exception : java.lang.OutOfMemoryError: GC overhead limit exceeded)

Fajny opis wyjątków OutOfMemoryException znajdziesz  w cylku postów na tej stronie - polecam.

Jak wykrywać i jak przeciwdziałać tego typu sytuacją dowiesz się w następnych postach.

GC działanie i możliwe strategie - cdn ->

Brak komentarzy:

Prześlij komentarz