Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00113 012134 20613112 na godz. na dobę w sumie
Wydajność Javy. Szczegółowe porady dotyczące programowania i strojenia aplikacji w Javie. Wydanie II - ebook/pdf
Wydajność Javy. Szczegółowe porady dotyczące programowania i strojenia aplikacji w Javie. Wydanie II - ebook/pdf
Autor: Liczba stron: 384
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-283-7033-3 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> java - programowanie
Porównaj ceny (książka, ebook (-20%), audiobook).

Istnieją dwie strategie rozwiązywania problemów wydajnościowych aplikacji w Javie. Z jednej strony można wykorzystać potężne komputery i przydzielić JVM ogromne zasoby pamięci, z drugiej - w czasach ekspansji rozwiązań opartych na chmurach obliczeniowych nowe znaczenie zyskują małe, jednoprocesorowe komputery. Firmy takie jak Oracle czy Amazon udostępniają tanie serwery, na których można uruchamiać proste aplikacje. Łatwo się przekonać, jak ważne jest właściwe zarządzanie niewielką ilością pamięci w tego rodzaju środowiskach. Każdy, kto programuje w Javie, powinien dokładnie wiedzieć, jak maszyna JVM wykonuje kod i jak należy ją dostrajać, aby osiągała możliwie największą wydajność.

W tej książce opisano wiele funkcjonalności, narzędzi i procedur, dzięki którym można poprawić efektywność kodu napisanego w Javie 8 i 11 LTS. Główny nacisk położono na zagadnienia istotne dla środowisk produkcyjnych, ale przedstawiono również ciekawe nowe technologie, takie jak kompilacja z wyprzedzeniem i eksperymentalne kolektory. Znalazło się tu także omówienie nowości w mechanizmie porządkowania pamięci i rejestratorze Java Flight Recorder, zaprezentowano kwestie funkcjonowania Javy w środowiskach kontenerowych, udoskonalone narzędzie JMH, kompilatory JIT, współdzielone klasy danych, narzędzia do monitorowania wydajności i wiele innych. Książkę doceni każdy inżynier zajmujący się JVM, który chce poradzić sobie z nietypowym działaniem systemu, wyciekami pamięci i problemami z jej porządkowaniem.

Najciekawsze zagadnienia:

'To najbardziej szczegółowy i praktyczny podręcznik na temat wydajności i strojenia maszyny JVM. Powinien go przeczytać każdy inżynier zajmujący się JVM, który kiedykolwiek zmagał się z nietypowym działaniem systemu, wyciekami pamięci i problemami z jej porządkowaniem'

Rod Hilton

Dostrojenie JVM: oto sekret wydajności kodu Javy!

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

Tytuł oryginału: Java Performance: In-Depth Advice for Tuning and Programming Java 8, 11, and Beyond, 2nd Edition Tłumaczenie: Andrzej Watrak ISBN: 978-83-283-7031-9 © 2021 Helion SA Authorized Polish translation of the English edition of Java Performance 2E ISBN 9781492056119 ©2020 Scott Oaks This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz Helion SA dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Helion SA nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Helion SA ul. Kościuszki 1c, 44-100 Gliwice tel. 32 231 22 19, 32 230 98 63 e-mail: helion@helion.pl WWW: http://helion.pl (księgarnia internetowa, katalog książek) Pliki z przykładami omawianymi w książce można znaleźć pod adresem: ftp://ftp.helion.pl/przyklady/wydja2.zip Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/wydja2 Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję. Printed in Poland. • Kup książkę • Poleć książkę • Oceń książkę • Księgarnia internetowa • Lubię to! » Nasza społeczność Spis treści Wstęp .......................................................................................................................................................9 Pełny obraz wydajności Platformy Javy Platformy sprzętowe Ogólny zarys książki Platformy i konwencje 1. Wprowadzenie ............................................................................................................................15 16 16 16 18 21 21 22 22 24 25 25 Pisz lepsze algorytmy Pisz mniej kodu Śmiało, przedwcześnie optymalizuj Rozglądaj się wokoło: baza danych jest zawsze słabym punktem Optymalizuj pod kątem typowego użycia Podsumowanie Testy rzeczywistej aplikacji Testy przepustowości, operacji wsadowych i czasu odpowiedzi aplikacji Mikrotesty porównawcze Makrotesty porównawcze Mezotesty porównawcze 2. Testowanie wydajności................................................................................................................27 27 27 31 32 34 34 35 36 38 42 45 45 52 55 Analiza zmienności wyników Zasada wczesnego i częstego testowania Przykłady testów porównawczych Czas wykonania operacji wsadowej Przepustowość Czas odpowiedzi Java Microbenchmark Harness Przykładowe kody Podsumowanie 3 Poleć książkęKup książkę Narzędzia do monitorowania Javy Monitorowanie i analiza wydajności systemu operacyjnego Wykorzystanie procesora Kolejka procesora Wykorzystanie dysku Wykorzystanie sieci Podstawowe informacje o maszynie wirtualnej Informacje o wątkach Informacje o klasach Bieżąca analiza mechanizmu porządkowania pamięci Przetwarzanie zrzutu sterty 3. Narzędzia wydajnościowe ........................................................................................................... 57 57 58 61 62 64 65 66 69 69 69 70 70 70 74 75 77 77 79 79 86 89 91 Profilery próbujące Profilery instrumentalizujące Metody blokujące i szeregi czasowe wątków Natywne profilery Java Mission Control Ogólne informacje o JFR Włączenie funkcjonalności JFR Wybieranie zdarzeń Profilowanie maszyny JVM Java Flight Recorder Podsumowanie Kompilacja HotSpot Ogólne informacje o kompilatorze Kompilacja etapowa Popularne flagi kompilatora Strojenie pamięci podręcznej kodu Monitorowanie procesu kompilacji Poziomy kompilacji etapowej Deoptymalizacja 4. Kompilator JIT ............................................................................................................................ 93 93 95 96 97 97 99 102 103 106 106 107 109 110 111 Wartości progowe Wątki kompilatora Rozwijanie metod Analiza ucieczki Kod procesora Zaawansowane flagi kompilatora 4  Spis treści Poleć książkęKup książkę Kompromisy kompilacji etapowej Maszyna GraalVM Prekompilacja Kompilacja z wyprzedzeniem Natywna kompilacja w maszynie GraalVM Podsumowanie 112 114 115 115 118 119 Podstawy strojenia kolektora Ogólne informacje o porządkowaniu pamięci Porządkowanie generacji obiektów Algorytmy porządkowania pamięci Wybór kolektora 5. Wprowadzenie do porządkowania pamięci.................................................................................121 121 123 125 127 135 135 138 140 142 143 143 144 147 Wielkość sterty Dobór wielkości generacji Dobór wielkości metaprzestrzeni Sterowanie wielowątkowością Włączanie dzienników kolektorów w pakiecie JDK 8 Włączanie dzienników kolektorów w pakiecie JDK 11 Narzędzia do monitorowania porządkowania pamięci Podsumowanie Kolektor równoległy Adaptywne i statyczne skalowanie sterty 6. Algorytmy porządkowania pamięci ............................................................................................149 149 152 156 165 168 Strojenie kolektora G1 Kolektor CMS Kolektor G1 Strojenie kolektora w celu uniknięcia awarii trybu współbieżnego porządkowania pamięci Zaawansowane strojenie kolektorów Okres dojrzewania obiektów i obszar obiektów ocalonych Alokowanie dużych obiektów Flaga AggressiveHeap Pełna kontrola wielkości sterty Kolektory eksperymentalne Współbieżne scalanie sterty: kolektory ZGC i Shenandoah Bez porządkowania: kolektor Epsilon Podsumowanie 173 176 176 180 186 187 189 189 191 192 Spis treści  5 Poleć książkęKup książkę Analiza sterty Zmniejszenie zajętości pamięci Histogram sterty Zrzut sterty Problem przepełnienia pamięci 7. Dobre praktyki używania sterty ................................................................................................. 195 195 196 197 201 205 206 208 212 214 214 219 232 233 Zarządzanie cyklem życia obiektów Obiekty wielokrotnego użytku Odwołania miękkie, słabe i inne Skompresowane wskaźniki Zmniejszanie wielkości obiektów Leniwe inicjowanie obiektu Obiekty niemutowalne i kanoniczne Podsumowanie Obciążenie pamięci Monitorowanie obciążenia pamięci Minimalizacja obciążenia pamięci Monitorowanie ilości pamięci natywnej Natywna pamięć i współdzielone biblioteki 8. Dobre praktyki używania natywnej pamięci ............................................................................... 235 235 236 237 238 241 244 245 249 Strojenie maszyny JVM pod kątem systemu operacyjnego Duże strony pamięci Podsumowanie 9. Pule wątków i klasa ThreadPoolExecutors Dobór maksymalnej liczby wątków Dobór minimalnej liczby wątków Wielkość kolejki zadań Klasa ThreadPoolExecutor Synchronizacja wątków i wydajność aplikacji ............................................................................. 251 251 Wątki i sprzęt 252 252 256 258 258 260 265 266 268 269 272 274 Cena synchronizacji Zapobieganie synchronizacji Fałszywe współdzielenie danych Wykradanie pracy Automatyczne zrównoleglanie operacji Synchronizacja wątków Klasa ForkJoinPool 6  Spis treści Poleć książkęKup książkę Strojenie wątków maszyny JVM Strojenie wielkości stosów wątków Preferencje blokowania Priorytety wątków Monitorowanie wątków i blokad Monitorowanie wątków Monitorowanie zablokowanych wątków Podsumowanie 278 278 279 280 280 280 280 284 10. Strojenie puli wątków serwera Asynchroniczne serwery REST Asynchroniczne zapytania wychodzące Asynchroniczne zapytania HTTP Serwery Java .............................................................................................................................285 285 Przegląd operacji NIO w Javie Kontenery serwerowe 287 287 289 291 291 298 298 299 301 302 Przetwarzanie danych JSON Przetwarzanie danych Obiekty JSON Parsowanie danych JSON Podsumowanie Przykładowa baza danych Interfejs JDBC Sterowniki JDBC Pule połączeń JDBC Preparowane zapytania i pule zapytań Transakcje Przetwarzanie zestawu wyników 11. Wydajność baz danych — dobre praktyki...................................................................................303 303 304 304 306 307 309 316 318 318 319 323 329 330 Optymalizacja zapisu danych w platformie JPA Optymalizacja odczytu danych w platformie JPA Bufor platformy JPA Spring Data Podsumowanie Platforma JPA Spis treści  7 Poleć książkęKup książkę Buforowanie operacji wejścia/wyjścia Ładowanie klas Współdzielenie danych klas Liczby losowe Interfejs JNI Wyjątki Dzienniki Kolekcje Kompaktowe ciągi znaków Duplikowanie i internowanie ciągów Łączenie ciągów znaków Java SE API — porady............................................................................................................... 331 Ciągi znaków 331 331 332 338 341 343 343 346 349 351 354 356 356 357 358 360 362 362 364 364 365 367 369 371 Serializacja obiektów Pola przejściowe Przesłanianie domyślnej serializacji Kompresja danych Śledzenie duplikatów obiektów Kolekcje synchroniczne i asynchroniczne Wielkość kolekcji Kolekcje i wykorzystanie pamięci Funkcje lambda i klasy anonimowe Wydajność strumieni i filtrów Leniwe przetwarzanie danych 12. Podsumowanie Dodatek. Flagi maszyny JVM............................................................................................................. 373 8  Spis treści Poleć książkęKup książkę ROZDZIAŁ 7. Dobre praktyki używania sterty W rozdziałach 5. i 6. szczegółowo opisałem, jak dostroić kolektor, aby jego wpływ na działanie aplikacji był jak najmniejszy. Strojenie kolektora to ważna operacja, ale zazwyczaj jeszcze lepszy efekt daje stosowanie dobrych praktyk programistycznych. W tym rozdziale opisuję kilka takich prak- tyk dotyczących wykorzystania sterty. W tym kontekście pojawiają się dwa sprzeczne podejścia. Podstawową zasadą jest oszczędne korzy- stanie z obiektów i usuwanie ich tak szybko, jak jest to możliwe. Im mniej pamięci wykorzystuje aplikacja, tym efektywniej działa kolektor. Z drugiej strony, częste tworzenie i usuwanie obiektów pogarsza ogólną wydajność aplikacji, nawet jeżeli kolektor działa sprawniej. Jeżeli natomiast obiekty są używane wielokrotnie, aplikacja działa znacznie efektywniej. Obiekty można wielokrotnie wyko- rzystywać na kilka sposobów, m.in. stosując lokalne zmienne wątkowe, specjalne odwołania i pule obiektów. Tego rodzaju obiekty są długotrwałe i wpływają na działanie kolektora; umiejętnie wyko- rzystywane, poprawiają ogólną wydajność aplikacji. W tym rozdziale opisuję różne podejścia i ich konsekwencje. Najpierw jednak przyjrzyjmy się kilku narzędziom, dzięki którym można się dowiedzieć, co się dzieje wewnątrz sterty. Analiza sterty Dziennik kolektora i narzędzia opisane w rozdziale 5. są doskonałymi źródłami informacji o wpływie porządkowania pamięci na działanie aplikacji. Jednak aby uzyskać jeszcze dokładniejszy obraz, trzeba zajrzeć do samej sterty. Narzędzia opisane w tym podrozdziale dostarczają informacji o obiektach aktualnie wykorzystywanych przez aplikację. Większość narzędzi uwzględnia tylko aktywnie wykorzystywane obiekty. Te, które zostaną usunięte podczas najbliższego pełnego porządkowania pamięci, nie są uwzględniane w prezentowanych wynikach. W pewnych sytuacjach narzędzia te inicjują pełne porządkowanie pamięci, co może mieć wpływ na działanie aplikacji. Oprócz tego analizują całą stertę i zbierają informacje o wykorzy- stywanych obiektach, ale nie usuwają tych, które nie są już potrzebne. W obu przypadkach narzędzia wykorzystują zasoby komputera przez pewien czas, dlatego nie należy ich stosować do mierzenia wydajności aplikacji. 195 Poleć książkęKup książkę Histogram sterty Ograniczanie zajętości pamięci jest bardzo ważne, ale tak jak w każdej kwestii dotyczącej wydaj- ności, należy skupić się na maksymalizacji możliwych do uzyskania korzyści. W dalszej części roz- działu poznasz przykład leniwego inicjowania obiektu typu Calendar. Jest to operacja, dzięki której można zaoszczędzić 640 bajtów pamięci. Jednak jeżeli ten sam obiekt będzie inicjowany wielokrotnie, różnica w wydajności aplikacji będzie niezauważalna. Celem analizy sterty jest uzyskanie informacji o obiektach zajmujących duże ilości pamięci. Najłatwiej można to osiągnąć, tworząc histogram sterty. Jest to szybki sposób dający wgląd w liczbę obiektów bez konieczności zrzucania całej sterty (zrzut zajmuje sporo miejsca na dysku, a jego analiza trwa dłuższą chwilę). Jeżeli za brak pamięci są odpo- wiedzialne obiekty kilku określonych typów, z pomocą histogramu można je szybko zidentyfikować. Histogram sterty można utworzyć za pomocą narzędzia jcmd. Ilustruje to poniższy przykład. Proces analizowanej aplikacji miał w tym przypadku identyfikator 8898: jcmd 8998 GC.class_histogram 8898: num #instances #bytes class name ---------------------------------------------- 1: 789087 31563480 java.math.BigDecimal 2: 172361 14548968 [C 3: 13224 13857704 [B 4: 184570 5906240 java.util.HashMap$Node 5: 14848 4188296 [I 6: 172720 4145280 java.lang.String 7: 34217 3127184 [Ljava.util.HashMap$Node; 8: 38555 2131640 [Ljava.lang.Object; 9: 41753 2004144 java.util.HashMap 10: 16213 1816472 java.lang.Class Na początku histogramu zazwyczaj znajdują się informacje o tablicy znaków ([C) i obiektach typu String, ponieważ są to najczęściej wykorzystywane obiekty. Często pojawiają się też tablice bajtów ([B) i obiektów ([Ljava.lang.Object;), ponieważ w tego typu strukturach przechowują swoje dane obiekty ładujące klasy (ang. classloaders). Jeżeli zawartość histogramu nie jest dla Ciebie zrozumiała, zapoznaj się z dokumentacją do interfejsu JNI. W tym przykładzie należy wyjaśnić obecność obiektów typu BigDecimal. Wiadomo, że aplikacja tworzy wiele krótkotrwałych obiektów tego typu, ale tak duża ich liczba jest czymś nietypowym. Histogram zawiera tylko obiekty wykorzystywane w aplikacji, ponieważ polecenie użyte do jego utworzenia wymusza pełne porządkowanie pamięci. Jeżeli użyje się parametru -all, porządkowanie nie zostanie wykonane i histogram będzie uwzględniał wszystkie obiekty, również te niepotrzebne (bez odwołań). Podobny wynik można uzyskać za pomocą następującego polecenia: jmap -histo identyfikator_procesu Wynik powyższego polecenia uwzględnia obiekty przeznaczone do usunięcia. Aby wymusić pełne porządkowanie pamięci, należy użyć następującego polecenia: jmap -histo:live identyfikator_procesu 196  Rozdział 7. Dobre praktyki używania sterty Poleć książkęKup książkę Histogram jest prostym raportem, więc warto go tworzyć w zautomatyzowanym systemie testowania aplikacji. Nie należy go jednak generować podczas mierzenia wydajności ustabilizowanej aplikacji, ponieważ zajmuje to kilka sekund, a ponadto inicjuje pełne porządkowanie pamięci. Zrzut sterty Histogram doskonale nadaje się do diagnozowania problemów wynikających z alokowania zbyt wielu instancji jednej lub kilku określonych klas. Jednak do przeprowadzenia głębszej analizy potrzebny jest zrzut sterty. Dostępnych jest wiele narzędzi do wykonywania tego rodzaju analiz. Większość z nich pozwala podłączyć się pod działającą aplikację i tworzyć zrzut. Najłatwiej robi się to za pomocą następujących poleceń: jcmd identyfikator_procesu GC.heap_dump /ścieżka/zrzut.hprof lub jmap -dump:live,file=/ścieżka/zrzut.hprof identyfikator_procesu Opcja live w powyższym poleceniu powoduje, że narzędzie przed zrzuceniem sterty przeprowa- dza jej pełne porządkowanie. Jest to domyślne działanie narzędzia. Jeżeli jednak z jakiegoś powodu zrzut powinien zawierać niewykorzystywane obiekty, należy na końcu polecenia użyć parametru -all. Narzędzie, wykonując pełne porządkowania pamięci, wprowadza oczywiście długą pauzę w działaniu aplikacji. Jednak nawet bez porządkowania aplikacja jest wstrzymywana na dość długi czas, niezbędny do zapisania sterty w pliku. Oba powyższe polecenia tworzą plik o nazwie zrzut.hprof we wskazanym katalogu. Do analizowania zrzutu służy wiele różnych narzędzi. Poniżej opisane są najczęściej stosowane. jvisualvm W zakładce Monitor powyższego narzędzia można zrzucić stertę działającej aplikacji lub otworzyć zrzut wygenerowany wcześniej. Oprócz tego można przeglądać zawartość sterty, wyszukiwać największe obiekty i przetwarzać stertę za pomocą specjalnych zapytań. mat Do bezpłatnego narzędzia EclipseLink Memory Analyzer Tool (mat) można załadować jeden lub kilka zrzutów i analizować je. Narzędzie tworzy raporty zawierające informacje o źródłach poten- cjalnych problemów. Umożliwia też przeglądanie sterty za pomocą zapytań podobnych do SQL. Analiza sterty obejmuje przede wszystkim zachowaną pamięć obiektów (ang. retained memory), tj. taką, która byłaby dostępna, gdyby można było te obiekty usunąć. Na rysunku 7.1 jest to pamięć zajmowana przez obiekt Smyczki Trio, jak również obiekty Sylwia i Dawid. Nie jest to jednak pamięć zajmowana przez obiekt Marek, ponieważ odwołuje się do niego inny obiekt. Dlatego nie można go usunąć podczas porządkowania pamięci wraz z obiektem Smyczki Trio. Obiekty zajmujące duże ilości pamięci zachowanej są nazywane dominatorami sterty. Jeżeli narzę- dzie analityczne pokazuje, że sterta jest zdominowana przez kilka obiektów, problem jest prosty. Aby go rozwiązać, wystarczy zmniejszyć ich liczbę, używać ich przez krótszy czas, uprościć ich strukturę lub pomniejszyć je. Wprawdzie łatwiej to powiedzieć niż zrobić, ale przynajmniej analiza jest prosta. Analiza sterty 197  Poleć książkęKup książkę Rysunek 7.1. Struktura zachowanej pamięci Wielkości płytkich, zachowanych i głębokich obiektów Dwa inne często stosowane w analizie sterty terminy to obiekty płytkie (ang. shallow) i głębokie (ang. deep). Określenie wielkość obiektu płytkiego oznacza wielkość samego obiektu. Jeżeli dany obiekt zawiera odwołania do innych obiektów, powyższy termin uwzględnia dodatkowe 4 bajty (lub 8 bajtów) zajmo- wane przez każde odwołanie, ale nie obejmuje wielkości obiektów docelowych. Termin wielkość głębokiego obiektu obejmuje również wielkości obiektów, do których odwołuje się dany obiekt. Różnica pomiędzy wielkością głębokiego obiektu a zachowaną pamięcią to suma wielkości obiektów współdzielonych. Na rysunku 7.1 pamięć zajmowana przez obiekt Marek jest uwzględniona w wielkości głębokiego obiektu Flety Duet, ale nie w jego pamięci zachowanej. Zazwyczaj jednak w celu zdiagnozowania problemu trzeba przeprowadzać dochodzenie detektywi- styczne, ponieważ często obiekty są współdzielone. Wielkość tego rodzaju obiektów, jak np. Marek na powyższym rysunku, nie jest uwzględniana w zachowanej pamięci, ponieważ usunięcie nadrzęd- nego obiektu nie powoduje usunięcia obiektu współdzielonego. Ponadto najwięcej zachowanej pamięci zajmują obiekty ładujące klasy, nad którymi nie ma się praktycznie żadnej kontroli. Rysunek 7.2 przedstawia ekstremalny przypadek serwera danych giełdowych. Na początku listy znajdują się obiekty zajmujące najwięcej pamięci zachowanej. Serwer umieszcza obiekty z silnymi i słabymi odwołaniami odpowiednio w pamięci podręcznej i w globalnej tablicy mieszającej. Zatem w pamięci podręcznej znajdują się obiekty zawierające wiele odwołań. Obiekty zajmują 1,4 GB sterty (ta wartość nie jest widoczna w narzędziu). Jednak największy zbiór obiektów posiadających jedno odwołanie ma wielkość tylko 6 MB (nie powinno dziwić, że jest to część platformy ładującej klasy). Przeglądanie obiektów zajmujących najwięcej pamięci zachowanej nie ułatwia diagnostyki problemów z pamięcią. W tym przykładzie widocznych jest wiele instancji klasy StockPriceHistoryImpl, z których każda zajmuje sporo pamięci zachowanej. Na podstawie ilości pamięci zajmowanej przez te obiekty można wywnioskować, że tu leży problem. Jednak zazwyczaj obiekty mogą być współdzielone, więc anali- zując zachowaną pamięć, nie da się wyciągnąć jednoznacznych wniosków. 198  Rozdział 7. Dobre praktyki używania sterty Poleć książkęKup książkę Rysunek 7.2. Widok pamięci zachowanej w narzędziu Memory Analyzer Kolejnym ważnym krokiem może być utworzenie histogramu obiektów, przedstawionego na rysunku 7.3. Rysunek 7.3. Histogram obiektów w narzędziu Memory Analyzer Histogram zawiera podsumowanie informacji o obiektach tych samych typów. W tym przypadku widać wyraźnie, że źródłem problemu jest 7 milionów obiektów typu TreeMap$Entry, zajmujących przestrzeń o wielkości 1,4 GB. Za pomocą narzędzia Memory Analyzer można łatwo wyśledzić tego rodzaju obiekty i sprawdzić ich zawartość, nawet gdy nie wiadomo, co się dzieje wewnątrz aplikacji. Za pomocą narzędzia do analizy sterty można łatwo znaleźć główne obiekty (lub ich zbiór, jak w tym przykładzie), od których zaczyna się porządkowanie pamięci. Jednak zazwyczaj nie jest to pomocna informacja. Obiekty główne zawierają statyczne, globalne odwołania do poszukiwanych obiektów, prowadzące zazwyczaj przez długi łańcuch innych obiektów. Najczęściej są to statyczne Analiza sterty 199  Poleć książkęKup książkę zmienne w klasie znajdującej się w ścieżce systemowej lub ścieżce bootstrap. Jest to m.in. klasa Thread wraz ze wszystkimi aktywnymi wątkami. Wątki zapisują obiekty za pośrednictwem lokalnych zmiennych lub odwołań do docelowego obiektu Runnable (ewentualnie do klasy pochodnej od Thread i wszystkich zawartych w niej odwołań, jak w tym przykładzie). Czasami informacja o głównych obiektach jest pomocna. Jednak jeżeli obiekt zawiera wiele odwołań, to jest połączony z wieloma obiektami głównymi. Odwołania tworzą strukturę odwróconego drzewa. Załóżmy, że dwa obiekty odwołują się do obiektu TreeMap$Entry. Do każdego z nich mogą odwoływać się dwa inne obiekty, z kolei do każdego z tych obiektów mogą odwoływać się trzy inne itd. Liczba odwołań, gwałtownie rosnąca w miarę zbliżania się obiektów głównych, oznacza, że do każdego obiektu odwołuje się wiele obiektów głównych. Dlatego bardziej pomocne jest wcielenie się w detektywa i znalezienie współdzielonego obiektu znajdującego się na najniższym poziomie struktury. Można to osiągnąć analizując obiekty i odwołania do nich do momentu znalezienia powielonych ścieżek. W tym przykładzie do obiektów Stock PriceHistoryImpl odwołują się dwa inne obiekty: ConcurrentHashMap, zawierający atrybuty sesji, oraz WeakHashMap, będący globalną pamięcią podręczną. Na rysunku 7.4 rozwinięte są tylko początki ścieżek dwóch obiektów. Aby się przekonać, że zawie- rają one dane sesji, należy dalej rozwijać ścieżkę obiektu ConcurrentHashMap, aż stanie się to oczywiste. Podobnie należy potraktować obiekt WeakHashMap. Rysunek 7.4. Wsteczne ścieżki odwołań widoczne w narzędziu Memory Analyzer W tym przykładzie, z racji zastosowanych typów obiektów, analiza była nieco prostsza niż zazwyczaj bywa w praktyce. Jeżeli główne dane wykorzystywane w aplikacji są przechowywane w obiektach typu String, a nie BigDecimal, lub w strukturach typu HashMap, a nie TreeMap, wtedy zadanie staje się trudniejsze. Zrzut sterty może zawierać setki tysięcy obiektów typu String i HashMap, więc znalezienie tych właściwych jest żmudną czynnością. Zgodnie z niepisaną zasadą, należy wtedy zacząć od obiektów reprezentujących kolekcje, np. HashMap, a nie wejścia, jak HashMap$Entry, i poszukać największych kolekcji. 200  Rozdział 7. Dobre praktyki używania sterty Poleć książkęKup książkę Krótkie podsumowanie  Znalezienie obiektów zajmujących najwięcej pamięci jest pierwszym krokiem do optymalizacji kodu.  Za pomocą histogramu można szybko i łatwo diagnozować problemy w pamięcią, których źródłem jest duża liczba obiektów określonego typu.  Analiza zrzutu sterty jest jedną z najskuteczniejszych technik diagnozowania problemów z zajętością pamięci. Wymaga jednak cierpliwości i wysiłku. Problem przepełnienia pamięci Maszyna JVM zgłasza błąd przepełnienia pamięci w następujących przypadkach:  braku wystarczającej ilości natywnej pamięci,  zapełnienia metaprzestrzeni,  zapełnienia sterty (aplikacja nie jest w stanie tworzyć nowych obiektów),  zbyt długiego czasu porządkowania pamięci. Najczęściej występują dwa ostatnie przypadki, które są związane ze stertą. Jednak przepełnienie pamięci nie oznacza od razu, że problem dotyczy sterty. W takiej sytuacji trzeba sprawdzić, co jest przyczyną błędu (zazwyczaj zawiera ją opis zgłoszonego wyjątku). Brak wystarczającej ilości natywnej pamięci Pierwsza wymieniona na powyższej liście przyczyna, brak wystarczającej ilości fizycznej pamięci, nie jest związana ze stertą. Maszyna JVM w wersji 32-bitowej jest w stanie obsłużyć pamięć o wielkości 4 GB (w starszych wersjach systemu Windows było to 3 GB, a Linux — 3,5 GB). Skonfigurowanie bardzo dużej sterty, np. 3,8 GB, powoduje niebezpieczne zbliżenie się do tej granicy. Nawet dla 64-bitowej maszyny JVM system operacyjny może nie mieć wystarczającej ilości wirtualnej pamięci. Ten temat będzie dokładniej opisany w rozdziale 8. Należy jednak pamiętać, że gdy pojawi się komunikat o braku wystarczającej ilości natywnej pamięci, strojenie sterty nic nie da. Zamiast tego należy dokładniej przyjrzeć się komunikatowi. Na przykład poniższy informuje, że przepełniła się natywna pamięć przeznaczona dla stosu wątków: Exception in thread main java.lang.OutOfMemoryError: unable to create new native thread Czasami zdarza się, że maszyna JVM zgłasza problem z powodu zupełnie niezwiązanego z natywną pamięcią. Aplikacje zazwyczaj mogą uruchamiać ograniczoną liczbę wątków, określoną przez system operacyjny lub kontener. Na przykład w systemie Linux aplikacja może utworzyć 1024 wątki (wartość tę można sprawdzić za pomocą polecenia ulimit -u). Przy próbie uruchomienia 1025. wątku pojawi się błąd OutOfMemoryError i wygenerowany zostanie komunikat o niewystarczającej ilości natywnej pamięci, choć rzeczywistą przyczyną problemu jest przekroczenie określonej przez system operacyjny liczby wątków. Analiza sterty 201  Poleć książkęKup książkę Zapełnienie metaprzestrzeni Błąd zapełnienia metaprzestrzeni również nie jest związany ze stertą. Pojawia się wtedy, gdy zapełni się natywna pamięć zajmowana przez metaprzestrzeń. Ponieważ domyślnie maksymalna wielkość metaprzestrzeni nie jest określona, błąd ten jest zazwyczaj efektem zdefiniowania tej granicy (powód, dla którego się to robi, jest opisany w dalszej części rozdziału). Są dwie główne przyczyny tego błędu. Pierwsza jest prosta — aplikacja wykorzystuje zbyt dużo klas, które nie mieszczą się w zdefiniowanej metaprzestrzeni (patrz rozdział 5., „Dobór wielkości meta- przestrzeni”). Druga przyczyna jest mniej oczywista: wyciek pamięci podczas ładowania klas. Naj- częściej zdarza się to w serwerach dynamicznie ładujących klasy. Jednym z przykładów jest serwer aplikacji Java EE. Każda zainstalowana w nim aplikacja wykorzystuje własny obiekt ładujący klasy, dzięki czemu aplikacje są od siebie odizolowane, nie współdzielą klas i nie interferują ze sobą. W środowisku programistycznym, za każdym razem, gdy aplikacja jest modyfikowana, jest ponow- nie instalowana. Tworzony jest nowy obiekt ładujący klasy, a stary wychodzi poza zakres widocz- ności. Gdy tak się stanie, metadane klas mogą zostać usunięte. Jeżeli jednak obiekt ładujący klasy nie wyjdzie poza zakres widoczności, nie można usunąć metadanych klas. W efekcie metaprzestrzeń zapełnia się i pojawia się błąd. W takim przypadku można powiększyć metaprzestrzeń, choć w rzeczywistości spowoduje to tylko odłożenie problemu w czasie. Jeżeli tego typu sytuacja ma miejsce w serwerze aplikacji, jedyne, co można zrobić, to poinformo- wanie dostawcy serwera o konieczności opracowania poprawki zapobiegającej wyciekowi pamięci. Jeżeli tworzona aplikacja wykorzystuje dużo obiektów ładujących klasy, należy upewnić się, czy je poprawnie usuwa, a w szczególności, czy kontekst żadnego wątku nie obejmuje tymczasowego obiektu ładującego klasy. Aby zdiagnozować tego typu przypadek, trzeba wykonać opisaną wcze- śniej analizę zrzutu sterty. W histogramie należy odszukać wszystkie instancje klasy ClassLoader, znaleźć ich obiekty główne i sprawdzić ich zawartość. Kluczowe znaczenie w diagnostyce tego rodzaju przypadków ma — jak poprzednio — dokładna analiza komunikatu o przepełnieniu pamięci. Jeżeli metaprzestrzeń zostanie zapełniona, pojawi się następująca informacja: Exception in thread main java.lang.OutOfMemoryError: Metaspace Między innymi z powodu wycieku pamięci podczas ładowania klas należy określać maksymalną wielkość metaprzestrzeni. Jeżeli się tego nie zrobi, wtedy z powodu wycieku obiektów ładujących klasy zostanie zapełniona cała pamięć komputera. Zapełnienie sterty Gdy zapełni się sterta, pojawia się następujący komunikat: Exception in thread main java.lang.OutOfMemoryError: Java heap space Zazwyczaj przyczyna zapełnienia sterty jest taka sama, jak opisana wcześniej przyczyna zapełnienia metaprzestrzeni. Aplikacja po prostu potrzebuje większej sterty, ponieważ na aktualnie zdefiniowanej nie mieszczą się wszystkie wykorzystywane obiekty. Innym powodem może być wyciek pamięci, gdy aplikacja tworzy kolejne obiekty, a stare nie wychodzą poza zakres widoczności. W pierwszym przypadku powiększenie sterty może rozwiązać problem, natomiast w drugim tylko go odsunie 202  Rozdział 7. Dobre praktyki używania sterty Poleć książkęKup książkę w czasie. W obu przypadkach jednak należy wykonać analizę zrzutu sterty w celu znalezienia obiek- tów, które zajmują najwięcej pamięci, natomiast rozwiązaniem jest zmniejszenie liczby lub wielkości tworzonych obiektów. Jeżeli w aplikacji ma miejsce wyciek pamięci, należy wykonać kilka zrzutów pamięci w kilkuminutowych odstępach i porównać je ze sobą. W tym celu mona wykorzystać wbudowaną funkcjonalność narzędzia Memory Analyzer. Po załadowaniu dwóch zrzutów można utworzyć histogramy i wyliczyć różnice między nimi. Automatyczne zrzucanie pamięci Błędy przepełnienia pamięci pojawiają się niespodziewanie, więc nigdy nie wiadomo, kiedy należy utwo- rzyć zrzut sterty. Dlatego pojawiły się następujące flagi: -XX:+HeapDumpOnOutOfMemoryError Włączenie tej flagi (o domyślnej wartości false) powoduje, że maszyna JVM będzie tworzyła zrzut sterty za każdym razem, gdy pojawi się błąd przepełnienia pamięci. -XX:HeapDumpPath= ścieżka Za pomocą tej flagi określa się miejsce, w którym ma być utworzony zrzut. Domyślnie flaga zawiera nazwę bieżącego katalogu aplikacji. Ścieżka może być nazwą katalogu lub pliku. W pierwszym przypadku plik otrzymuje domyślną nazwę java_pid pid .hprof. -XX:+HeapDumpAfterFullGC Flaga powodująca utworzenie zrzutu po pełnym uporządkowaniu pamięci. -XX:+HeapDumpBeforeFullGC Flaga powodująca utworzenie zrzutu przed pełnym uporządkowaniem pamięci. Jeżeli tworzonych jest kilka zrzutów, np. z powodu wielokrotnego pełnego porządkowania pamięci, nazwy plików są uzupełniane o kolejne numery. Powyższych flag należy używać w sytuacjach, gdy aplikacja niespodziewanie wyświetla komunikaty o przepełnieniu pamięci i w celu ustalenia przyczyny problemu niezbędna jest analiza sterty. Należy pamiętać, że tworzenie zrzutu wprowadza pauzę w działaniu aplikacji, ponieważ zawartość sterty jest zapisywana na dysku. Rysunek 7.5 przedstawia typowy przypadek wycieku pamięci spowodowany klasą kolekcji, tutaj HashMap. Tego rodzaju klasy przyczyniają się do przepełnienia pamięci najczęściej. Aplikacja umieszcza elementy w kolekcji, ale ich nie usuwa. Rysunek przedstawia histogram porównawczy, zawierający różnice w liczbach poszczególnych obiektów w dwóch zrzutach. Na przykład drugi zrzut zawiera 19 744 obiektów typu Integer więcej niż pierwszy. Najskuteczniejszym sposobem rozwiązania problemu jest taka zmiana kodu aplikacji, aby elementy kolekcji były sukcesywnie usuwane, gdy przestaną być potrzebne. Ewentualnie można stosować kolekcje wykorzystujące słabe lub miękkie odwołania. Elementy takich kolekcji są automatycznie usuwane, gdy żadne obiekty się do nich nie odwołują. Kolekcje te mają jednak swoją cenę, o czym będzie mowa w dalszej części rozdziału. Często zdarza się, że maszyna JVM po zgłoszeniu wyjątku kontynuuje działanie, ponieważ problem dotyczy tylko jednego wątku. Przeanalizujmy przykład aplikacji wykonującej obliczenia za pomocą dwóch wątków. W pewnym momencie jeden z nich zgłasza błąd OutOfMemoryError. Domyślnie procedura obsługi wątku wyświetla stos wywołań, a sam wątek kończy działanie. Analiza sterty 203  Poleć książkęKup książkę Rysunek 7.5. Histogram porównawczy Jednak drugi wątek pozostaje aktywny, więc maszyna JVM działa dalej. Ponieważ pierwszy wątek, w którym wystąpił błąd, zakończył działanie, prawdopodobnie przy następnym porządkowaniu pamięci duża jej ilość zostanie zwolniona. Będą to wszystkie obiekty wykorzystywane przez zakoń- czony wątek, do których nie odwołuje się drugi, aktywny wątek. Zatem drugi wątek będzie działał dalej i będzie dysponował wystarczająco dużą stertą, aby dokończyć swoje zadania. W opisany wyżej sposób funkcjonują platformy serwerowe wykorzystujące pule wątków do obsługi zapytań. Przechwytują błędy i zapobiegają dzięki temu przerywaniu działania wątków. Nie dotyczy to jednak opisywanego tu problemu, ponieważ pamięć, którą wątek wykorzystuje w celu obsłużenia zapytania, jest zwalniana podczas jej porządkowania. Zatem błąd przepełnienia pamięci ma fatalne skutki jedynie wtedy, gdy dotyczy ostatniego wątku, innego niż wykorzystywany wewnętrznie przez maszynę JVM. W przypadku platformy serwerowej tak się jednak nie zdarza nigdy, a bardzo rzadko ma to miejsce w przypadku zwykłej aplikacji wyko- rzystującej kilka wątków. Zazwyczaj mechanizm obsługi błędów działa dobrze, a pamięć zajęta na potrzeby obsługi aktywnego zapytania jest często zwalniania podczas jej kolejnego porządkowania. Jeżeli maszyna JVM ma kończyć działanie po przepełnieniu sterty, należy użyć flagi -XX:+ExitOnOut OfMemoryError, która domyślnie ma wartość false. Zbyt długi czas porządkowania pamięci Opisany w poprzednim podrozdziale mechanizm obsługi błędów opiera się na założeniu, że gdy zostanie zgłoszony błąd przepełnienia pamięci, cała wykorzystywana przez niego pamięć będzie zwolniona przy jej najbliższym porządkowaniu. Okazuje się, że założenie to nie zawsze jest słuszne. 204  Rozdział 7. Dobre praktyki używania sterty Poleć książkęKup książkę Dochodzimy w ten sposób do ostatniej przyczyny błędu, tj. zbyt długiego czasu porządkowania pamięci, sygnalizowanego następującym komunikatem: Exception in thread main java.lang.OutOfMemoryError: GC overhead limit exceeded Błąd pojawia się wtedy, gdy są spełnione wszystkie poniższe warunki:  Procentowy udział czasu poświęcanego na porządkowanie pamięci przekracza wartość okre- śloną za pomocą flagi -XX:GCTimeLimit=N. Domyślnie flaga ta ma wartość 98, co oznacza, że pamięć jest porządkowana przez 98 czasu.  Ilość pamięci zwalnianej podczas pełnego porządkowania pamięci jest mniejsza od wartości określonej za pomocą flagi -XX:GCHeapFreeLimit=N. Domyślnie flaga ta ma wartość 2 oznaczającą, że warunek jest spełniony, jeżeli zwalniane jest mniej niż 2 wielkości sterty.  Powyższe warunki są spełnione po pięciu kolejnych porządkowaniach pamięci. Nie ma moż- liwości zmiany tej liczby.  Flaga -XX:+UseGCOverheadLimit ma wartość true (domyślną). Pamiętaj, że wszystkie powyższe warunki muszą być spełnione. Często zdarza się, że po pięciu pełnych porządkowaniach nie jest zgłaszany błąd przepełnienia pamięci, ponieważ mimo że aplikacja spędza ponad 98 czasu na porządkowaniu pamięci, za każdym razem odzyskuje więcej niż 2 wielkości sterty. W takim wypadku należy zwiększyć wartość flagi GCHeapFreeLimit. Warto również wiedzieć, że jeżeli pierwsze dwa warunki będą spełnione po czterech kolejnych porządkowaniach, maszyna JVM w desperackiej próbie zwolnienia pamięci podczas piątego porząd- kowania usunie wszystkie miękkie odwołania do obiektów (o ile aplikacja je wykorzystuje). Zapo- biega w ten sposób pojawieniu się błędu, ponieważ w ostatnim cyklu zwalnia więcej pamięci niż 2 wielkości sterty. Krótkie podsumowanie  Przyczyny błędu przepełnienia pamięci mogą być różne, dlatego nie można zakładać, że jest on oznaką problemu ze stertą.  Najczęstszą przyczyną przepełnienia zarówno sterty, jak i metaprzestrzeni jest wyciek pamięci, który można zdiagnozować za pomocą narzędzi do analizy sterty. Zmniejszenie zajętości pamięci Pierwszym krokiem w ograniczeniu ilości wykorzystywanej pamięci jest zmniejszenie zajętości sterty. Stwierdzenie to nie powinno dziwić, bo mniejsze wykorzystanie sterty oznacza, że zapełnia się ona wolniej i rzadziej trzeba ją porządkować. Ponadto efekty mogą się kumulować. Jeżeli cykle porządkowania młodej generacji są rzadsze, obiekty dojrzewają wolniej, co z kolei oznacza mniejsze prawdopodobieństwo przeniesienia ich do starej generacji. Rzadziej są też wykonywane cykle pełnego porządkowania pamięci. Co więcej, mogą być wykonywane rzadziej, jeżeli podczas każdego z nich zwalniane jest więcej pamięci. W tym podrozdziale opisane są trzy techniki ograniczania zajętości pamięci: zmniejszanie obiektów, ich leniwe inicjowanie i stosowanie kanonicznych wersji. Zmniejszenie zajętości pamięci 205  Poleć książkęKup książkę Zmniejszanie wielkości obiektów Obiekty zajmują określoną część sterty, zatem najprostszym sposobem ograniczenia wykorzystania pamięci jest ich pomniejszenie. Zwiększenie sterty o 10 nie zawsze jest możliwe, ale ten sam efekt można osiągnąć pomniejszając połowę obiektów o 20 . Jak się dowiesz w rozdziale 12., wersja Java 11 optymalizuje obiekty typu String, dzięki czemu maksymalna wielkość sterty w porównaniu z wersją Java 8 jest nierzadko mniejsza o 25 . Optymalizacja nie wpływa na proces porządkowania pamięci ani na wydajności aplikacji. Ilość zajmowanej przez obiekty pamięci można najprościej ograniczyć poprzez zmniejszenie ich liczby lub — co już nie jest takie proste — przez pomniejszenie samych obiektów. Tabela 7.1 przed- stawia wielkości obiektów różnych typów. Tabela 7.1. Wielkości (w bajtach) obiektów różnych typów Typ byte char short int float long double Odwołanie Wielkość 1 2 2 4 4 8 8 8 (lub 4 w 32-bitowej maszynie JVM dla systemu Windows)a a Więcej szczegółowych informacji znajdziesz w podrozdziale „Skompresowane wskaźniki”. Termin „odwołanie” w ostatnim wierszu dotyczy dowolnego obiektu, np. tablicy lub instancji klasy. Podana liczba dotyczy pamięci zajmowanej tylko przez odwołanie. Wielkość obiektu zawierającego odwołania do innych obiektów zależy od tego, czy w analizie uwzględniane są płytkie, głębokie, czy zachowane obiekty. Ponadto obiekt zawiera niewidoczne pola nagłówków. W zwykłym obiekcie takie pole ma wielkość 8 bajtów w przypadku 32-bitowej maszyny na systemie Windows lub 16 bajtów w maszynie 64-bitowej, niezależnie od wielkości sterty. W tablicy pole nagłówka zajmuje 16 bajtów w maszynie 32-bitowej lub 64-bitowej ze stertą mniejszą niż 32 GB, lub 24 bajty w pozo- stałych przypadkach. Przeanalizujmy poniższe definicje klas: public class A { private int i; } public class B { private int i; private Locale l = Locale.US; } public class C { private int i; private ConcurrentHashMap chm = new ConcurrentHashMap(); } 206  Rozdział 7. Dobre praktyki używania sterty Poleć książkęKup książkę Tabela 7.2 przedstawia wielkości pojedynczych instancji tych klas w przypadku 64-bitowej maszyny JVM ze stertą mniejszą niż 32 GB. Tabela 7.2. Wielkości (w bajtach) prostych obiektów Płytki Głęboki Zachowany 16 24 24 16 216 200 16 24 200 A B C Odwołanie typu Locale w klasie B powiększa obiekt o 8 bajtów. Jednak w tym przykładzie obiekt typu Locale jest współdzielony przez inne obiekty. Jeżeli obiekt ten nie jest nigdzie wykorzysty- wany, oznacza to, że wraz z odwołaniem niepotrzebnie zajmuje pamięć. Jeżeli aplikacja tworzy wiele obiektów typu B, niewykorzystane bajty sumują się. Następnym przykładem jest odwołanie typu ConcurrentHashMap, zajmujące określoną liczbę bajtów wraz z odwołaniami do innych obiektów. Jeżeli instancje klasy C nie są wykorzystywane, niepo- trzebnie zajmują mnóstwo miejsca w pamięci. Jednym ze sposobów pomniejszania obiektów jest definiowanie tylko niezbędnych klas. Mniej oczywisty sposób polega na korzystaniu z mniejszych typów danych. Jeżeli w klasie trzeba np. przechowywać informacje o jednym z ośmiu możliwych stanów, należy użyć typu byte, a nie int, dzięki czemu zaoszczędzi się 3 bajty. Stosując typ float zamiast double lub int zamiast long można zaoszczędzić sporo pamięci, szczególnie jeżeli tworzonych jest wiele instancji danej klasy. Jak się dowiesz z rozdziału 12., podobne oszczędności uzyskuje się stosując kolekcje o odpowiedniej wielkości lub zwykłe instancje zamiast kolekcji. Wyrównanie i wielkość obiektów Wszystkie klasy przedstawione w tabeli 7.2 zawierają pole typu int, o którym do tej pory nie wspomi- nałem. Po co ono jest? Tylko po to, aby uprościć opis poszczególnych klas, np. że klasa B zajmuje 8 bajtów więcej niż A (jest to zgodne z oczekiwaniami, a konkluzja jest bardziej zrozumiała). Pojawia się tu pewien ważny szczegół: każdy obiekt jest powiększany o dodatkowe bajty, aby jego wiel- kość była wielokrotnością liczby 8. Gdyby klasa A nie zawierała pola i, jej instancja i tak zajmowałaby 16 bajtów. Dodatkowe 4 bajty stanowiłyby uzupełnienie obiektu, aby jego wielkość była wielokrotnością liczby 8. Gdyby klasa B nie zawierała pola i, jej instancja również zajmowałaby 16 bajtów, tj. tyle samo, co A. Uzupełnienie to powoduje, że instancja klasy B jest o 8 bajtów większa od instancji klasy A, mimo że B zawiera tylko jedno dodatkowe, 4-bajtowe odwołanie. Maszyna JVM uzupełnia w ten sposób również obiekty zajmujące nieparzystą liczbę bajtów, dzięki czemu tablice złożone z takich obiektów są dopasowane do granic segmentów pamięci wykorzystywanego komputera. Podsumowując, usunięcie niektórych pól lub zmniejszenie obiektu może, ale nie musi przysparzać oszczędności, ale nie ma powodu, aby tego nie robić. Ze strony projektu OpenJDK (https://oreil.ly/cSPvd) można pobrać narzędzie do obliczania wielkości obiektów. Zmniejszenie zajętości pamięci 207  Poleć książkęKup książkę Usuwając pola z obiektu można go pomniejszyć, ale wciąż pozostaje dylemat: czy należy stosować pola zawierające przetworzone dane? Jest to typowy w programowaniu kompromis czas-przestrzeń: lepiej poświęcić więcej pamięci (przestrzeni), aby przechowywać przetworzone dane, czy więcej cykli procesora (czasu) na przetwarzanie danych za każdym razem, gdy będzie zachodziła taka potrzeba? W przypadku Javy przestrzeń jest związana z czasem, ponieważ porządkowanie dodatkowej pamięci zajmuje więcej cykli procesora. Na przykład kod mieszający obiektu typu String wylicza się sumując w określony sposób kody wszystkich znaków, co trwa dłuższą chwilę. Dlatego wartość tę wylicza się tylko raz i zapisuje w zmiennej instancji. Dzięki temu niemal zawsze, gdy wykorzystywana jest ta wartość, wzrost wydajności aplikacji jest na tyle duży, że warto jest poświęcić dodatkową pamięć. Z drugiej strony metoda toString() w większości klas nie zapisuje tekstowej reprezentacji obiektu w zmiennej instancji, ponieważ zajmowana byłaby dodatkowa pamięć zarówno przez zmienną, jak i ciąg znaków. W takim przypadku zazwyczaj większą wydajność uzyskuje się generując za każdym razem ciąg znaków od nowa niż zapisując ciąg w pamięci. Należy też pamiętać, że kod mieszający obiektu typu String jest wykorzystywany często, a wynik metody toString() dość rzadko. Wszystko oczywiście zależy od sytuacji, a decyzja o rezygnacji z zapisywania wyników w pamięci na rzecz ich wielokrotnego wyliczania lub odwrotnie jest bardzo płynna i uzależniona od wielu czynników. Jeżeli celem jest skrócenie czasu porządkowania pamięci, bardziej korzystne okazuje się wyliczanie wyników. Krótkie podsumowanie  Często wydajność porządkowania pamięci można poprawić zmniejszając wielkości obiektów.  Wielkość obiektów nie zawsze jest oczywista, ponieważ są one uzupełniane o dodat- kowe bajty tak, aby były dopasowane do 8-bajtowych segmentów pamięci. Ponadto odwołania do obiektów w przypadku 32-bitowej maszyny JVM mają inną wielkość niż w maszynie 64-bitowej.  Pamięć zajmuje nawet pusta zmienna instancji. Leniwe inicjowanie obiektu Zazwyczaj decyzja o utworzeniu zmiennej instancji nie jest tak jednoznaczna, jak sugeruje poprzedni podrozdział. Jeżeli na przykład klasa wykorzystuje obiekt typu Calendar tylko przez 10 czasu, powinna go zapisać w pamięci, a nie tworzyć za każdym razem na nowo, ponieważ jest to bardzo czasochłonna operacja. W takich sytuacjach można wykorzystać leniwe inicjowanie obiektów (ang. lazy initialization). We wszystkich dotychczasowych opisach przyjąłem założenie, że zmienne instancji są inicjowanie aktywnie. Klasa wykorzystująca obiekt typu Calendar, która nie musi być wątkowo bezpieczna, może wyglądać następująco: public class CalDateInitialization { private Calendar calendar = Calendar.getInstance(); private DateFormat df = DateFormat.getDateInstance(); 208  Rozdział 7. Dobre praktyki używania sterty Poleć książkęKup książkę private void report(Writer w) { w.write( Godzina + df.format(calendar.getTime()) + : + this); } } Leniwe inicjowanie obiektu pogarsza nieznacznie wydajność aplikacji, ponieważ przed każdym użyciem obiektu trzeba sprawdzać jego stan. Poniżej przedstawiony jest odpowiednio zmieniony kod klasy: public class CalDateInitialization { private Calendar calendar; private DateFormat df; private void report(Writer w) { if (calendar == null) { calendar = Calendar.getInstance(); df = DateFormat.getDateInstance(); } w.write( Godzina + df.format(calendar.getTime()) + : + this); } } Leniwe inicjowanie obiektu przynosi najwięcej korzyści wtedy, gdy operacje na obiekcie są wyko- nywane rzadko. W przeciwnym wypadku oszczędność pamięci będzie żadna, ponieważ obiekt będzie nieustannie alokowany, co dodatkowo, choć nieznacznie, pogorszy wydajność aplikacji. Jeżeli kod musi być bezpieczny wątkowo, leniwe inicjowanie zaczyna się komplikować. W pierw- szym kroku najprościej jest zastosować tradycyjne synchronizowanie wątków: public class CalDateInitialization { private Calendar calendar; private DateFormat df; private synchronized void report(Writer w) { if (calendar == null) { calendar = Calendar.getInstance(); df = DateFormat.getDateInstance(); } w.write( Godzina + df.format(calendar.getTime()) + : + this); } } Wprowadzenie synchronizacji może pogorszyć wydajność aplikacji, lecz jest to mało prawdopo- dobne. Leniwe inicjowanie obiektów przynosi korzyści tylko wtedy, gdy obiekty są wykorzystywane rzadko. Jeżeli jednak są zawsze inicjowane, nie ma żadnych oszczędności pamięci. Zatem synchro- nizacja wraz z leniwym inicjowaniem obiektów pogarszają wydajność aplikacji, jeżeli obiekty te są wykorzystywane przez wiele wątków jednocześnie. Takie sytuacje zdarzają się rzadko, ale nie można ich wykluczać. Problemu z synchronizacją nie ma jedynie wtedy, gdy leniwie inicjowane obiekty są bezpieczne wątkowo. Klasa DateFormat nie spełnia tego warunku, więc w poniższym przykładzie nie ma znaczenia, czy blokada obejmuje obiekt typu Calendar, czy nie. Jeżeli leniwie inicjowany obiekt jest intensywnie wykorzystywany, niezbędna synchronizacja dostępu do obiektu typu DateFormat i tak jest proble- mem. Kod bezpieczny wątkowo może wyglądać następująco: Zmniejszenie zajętości pamięci 209  Poleć książkęKup książkę Leniwe inicjowanie obiektów a wydajność aplikacji Nie zawsze leniwe inicjowanie obiektów pogarsza wydajność aplikacji. Przeanalizujmy przykład zawartej w pakiecie JDK klasy ArrayList, która obsługuje zawartą w niej tablicę obiektów. W starszych wersjach Javy pseudokod tej klasy wyglądał następująco: public class ArrayList { private Object[] elementData = new Object[16]; int index = 0; public void add(Object o) { ensureCapacity(); elementData[index++] = o; } private void ensureCapacity() { if (index == elementData.length) { ...Ponowna alokacja tablicy i kopiowanie danych... } } } Kilka lat temu klasa ta została zmieniona i teraz tablica elementData jest leniwie inicjowana. Ponieważ metoda ensureCapacity() sprawdza wcześniej wielkość tablicy, wydajność innych metod nie pogorszyła się. Podczas inicjowania i powiększania tablicy wykorzystywany jest ten sam kod. Zmieniona klasa wykorzystuje statyczną tablicę o zerowej wielkości, dzięki czemu jej wydajność nie zmieniła się: public class ArrayList { private static final Object[] EMPTY_ELEMENTDATA = {} ; private Object[] elementData = EMPTY_ELEMENTDATA; } Oznacza to, że metodę ensureCapacity() można w zasadzie pozostawić bez zmian, ponieważ na początku zarówno zmienna index, jak i elementData.length zawierają wartość 0. public class CalDateInitialization { private Calendar calendar; private DateFormat df; private void report(Writer w) { unsychronizedCalendarInit(); synchronized(df) { w.write( Goczina + df.format(calendar.getTime()) + : + this); } } } Jeżeli leniwie inicjowany obiekt nie jest bezpieczny wątkowo, można synchronizować dostęp do zmiennej np. stosując opisaną wcześniej metodę z modyfikatorem synchronized. Przeanalizujmy nieco inny przykład, w którym leniwie inicjowany jest duży obiekt typu Concurrent HashMap: public class CHMInitialization { private ConcurrentHashMap chm; public void doOperation() { 210  Rozdział 7. Dobre praktyki używania sterty Poleć książkęKup książkę synchronized(this) { if (chm == null) { chm = new ConcurrentHashMap(); ... Kod wypełniający mapę ... } } ... Kod wykorzystujący zmienną chm ... } } Do obiektu typu ConcurrentHashMap może się odwoływać kilka wątków, więc jest to jeden z niewielu przypadków poprawnego leniwego inicjowania obiektu, w którym wprowadzenie dodatkowej synchronizacji może pogorszyć wydajność aplikacji. Jest to jednak rzadki przypadek i jeżeli obiekt nie jest często wykorzystywany, warto się zastanowić, czy w ogóle trzeba go leniwie inicjować. Problem można rozwiązać dwukrotnie sprawdzając blokadę: public class CHMInitialization { private volatile ConcurrentHashMap instanceChm; public void doOperation() { ConcurrentHashMap chm = instanceChm; if (chm == null) { synchronized(this) { chm = instanceChm; if (chm == null) { chm = new ConcurrentHashMap(); ... Kod wypełniający mapę ... instanceChm = chm; } } ... Kod wykorzystujący zmienną chm ... } } } Pojawia się tu ważny problem związany z wątkami. Zmienna instancji musi być zadeklarowana jako volatile. Wydajność można nieznacznie poprawić przypisując zmienną instancji lokalnej zmiennej. Więcej szczegółowych informacji na ten temat będzie podanych w rozdziale 9. W rzad- kich przypadkach, w których uzasadnione jest leniwe inicjowanie obiektów w wielowątkowym kodzie, należy stosować opisaną technikę. Aktywne zamykanie obiektów Przeciwieństwem leniwego inicjowania obiektu jest jego aktywne zamykanie (ang. eager deini- tialization) poprzez przypisanie zmiennej wartości null. Dzięki temu obiekt może być szybciej usunięty przez kolektor. W teorii brzmi to nieźle, ale w praktyce sprawdza się w nielicznych, szcze- gólnych przypadkach. Mogłoby się wydawać, że obiekt, który można leniwie inicjować, można również aktywnie zamykać. Użytym w poprzednim przykładzie zmiennym typów Calendar i DateFormat można przypisać wartość null na końcu metody report(). Jeżeli jednak zmienne te nie będą wykorzystywane w kolej- nych wywołaniach metody lub w innych miejscach klasy, nie ma powodu, aby w ogóle były to zmienne instancji. Zamiast nich można wewnątrz metody użyć lokalnych zmiennych, które po zakończeniu wykonywania kodu wyjdą poza zakres widoczności i zostaną usunięte przez kolektor. Zmniejszenie zajętości pamięci 211  Poleć książkęKup książkę Najczęściej wyjątki od zasady unikania aktywnego zamykania obiektów zdarzają się w klasach, które przez długi czas utrzymują odwołania do obiektów, po czym otrzymują sygnał, że obiekty te nie są już potrzebne. Przeanalizujmy przedstawioną niżej implementację metody remove() w klasie ArrayList zawartej w pakiecie JDK (część kodu została uproszczona): public E remove(int index) { E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // Zamknięcie obiektu, aby kolektor mógł go usunąć. return oldValue; } W kodzie źródłowym pakietu JDK, generalnie opatrzonego bardzo nielicznymi komentarzami, znajduje się komentarz do wiersza, w którym zmiennej jest przypisywana wartość null. Jest to tak nietypowy przypadek, że zasługuje na specjalny opis. Prześledźmy, co się dzieje, gdy usuwany jest ostatni element tablicy. Zmniejsza się liczba jej elementów zapisana w zmiennej instancji size. Załóżmy, że zmienna ta zmniejszyła swoją wartość z 5 do 4. Po tej operacji nie można się odwołać do elementu elementData[4], ponieważ znajduje się on poza granicą tablicy. Zatem odwołanie elementData[4] przestaje być nieaktualne. Sama tablica elementData będzie prawdopodobnie aktywna jeszcze przez jakiś czas, więc odwołania do wszystkich obiektów, które nie są potrzebne, należy aktywnie ustawić na null. Kluczowe znaczenie ma pojęcie nieaktualnego odwołania. Jeżeli tworzona klasa będzie przez dłuższy czas tworzyła i usuwała obiekty, należy zwrócić szczególną uwagę, aby nie pojawiały się w niej nieaktualne odwołania. Jawne przypisanie odwołaniu wartości null pozwoli również nieznacznie poprawić wydajność aplikacji. Krótkie podsumowanie  Leniwe inicjowanie obiektów należy stosować tylko wtedy, gdy często wykorzystywany kod nie odwołuje się do tych obiektów.  W bezpiecznym wątkowo kodzie rzadko pojawia się potrzeba leniwego inicjowania obiektów i zazwyczaj można wykorzystać istniejącą synchronizację.  W kodzie bezpiecznym wątkowo należy dwukrotnie sprawdzać blokadę leniwie inicjo- wanego obiektu. Obiekty niemutowalne i kanoniczne W Javie wiele typów jest niemutowalnych. Dotyczy to typów prymitywnych, takich jak Integer, Double czy Boolean, jak również bardziej złożonych, np. BigDecimal. Najczęściej używanym, niemu- towalnym typem jest oczywiście String. Z programistycznego punktu widzenia dobrą praktyką jest definiowanie własnych, niemutowalnych klas. Jeżeli obiekty wyżej wymienionych typów są często tworzone i natychmiast usuwane, ich wpływ na porządkowanie obszaru młodej generacji jest niewielki, o czym się przekonałeś w rozdziale 5. Jednak jeżeli obiekty niemutowalne są przenoszone do starej generacji, wydajność aplikacji może się pogorszyć. 212  Rozdział 7. Dobre praktyki używania sterty Poleć książkęKup książkę Nie ma powodów, aby unikać stosowania typów niemutowalnych, nawet jeżeli wydaje się to nie- logiczne, że obiektów tych nie można zmieniać i trzeba je wciąż tworzyć na nowo. Jedną z technik optymalizacji stosowania tego rodzaju obiektów jest zapobieganie tworzeniu wielokrotnych kopii tego samego obiektu. Najlepszym przykładem jest klasa Boolean. Każda aplikacja potrzebuje tylko dwóch instancji tej klasy, reprezentujących odpowiednio wartości true i false. Niestety, klasa ta została źle zaprojek- towana. Ponieważ posiada publiczny konstruktor, można tworzyć dowolną liczbę jej instancji, mimo że każda z nich może być tylko jednym z dwóch kanonicznych obiektów. Lepiej byłoby, gdyby klasa ta posiadała prywatny konstruktor i statyczne metody zwracające w zależności od para- metru wartości Boolean.TRUE lub Boolean.FALSE (a jeszcze lepiej, gdyby w ogóle nie trzeba było two- rzyć obiektów typu Boolean). Własne niemutowalne klasy funkcjonujące w ten sposób nie zajmo- wałyby sterty. Tego rodzaju niemutowalne obiekty reprezentujące skończoną liczbę wartości są nazywane obiek- tami kanonicznymi. Tworzenie obiektów kanonicznych Pamięć można oszczędzić stosując obiekty kanoniczne. Pakiet JDK oferuje specjalnie przeznaczoną do tego celu funkcjonalność. Za pomocą metody intern() można znaleźć kanoniczną wersję ciągu znaków. Więcej szczegółowych informacji na ten temat znajdziesz w rozdziale 12., natomiast teraz dowiesz się, jak można ten efekt osiągnąć w przypadku własnych klas. Aby utworzyć obiekt kanoniczny, należy zdefiniować mapę zawierającą jego kanoniczne wersje. Aby zapobiec wyciekowi pamięci, odwołania do tych obiektów muszą być słabe. Poniżej przedstawiony jest szkielet takiej klasy: public class ImmutableObject { private static WeakHashMap ImmutableObject, ImmutableObject map = new WeakHashMap(); public ImmutableObject canonicalVersion(ImmutableObject io) { synchronized(map) { ImmutableObject canonicalVersion = map.get(io); if (canonicalVersion == null) { map.put(io, new WeakReference(io)); canonicalVersion = io; } return canonicalVersion; } } } W aplikacji wielowątkowej może pojawić się problem z synchronizacją dostępu, którego nie można rozwiązać w prosty sposób, jeżeli wykorzystywane są klasy zawarte w pakiecie JDK. Pakiet ten nie oferuje mapy ze słabymi odwołaniami, do której kilka wątków mogłoby się odwoływać jednocześnie. Pojawiły się jednak sugestie, m.in. w żądaniu JSR (ang. Java Specification Request — żądanie zmiany specyfikacji Javy) nr 166, aby rozszerzyć pakiet JDK o klasę CustomConcurrentHashMap. W internecie można znaleźć kilka niezależnych implementacji tej klasy. Zmniejszenie zajętości pamięci 213  Poleć książkęKup książkę Krótkie podsumowanie  Obiekty niemutowalne można kanonizować i zarządzać w specjalny sposób ich cyklem życia.  Kanonizując obiekty można wyeliminować ich wielokrotne niemutowalne kopie i w ten sposób znacznie ograniczyć wykorzystanie sterty. Zarządzanie cyklem życia obiektów Drugim obszernym tematem związanym z wykorzystaniem pamięci jest zarządzanie cyklem życia obiektów. Java w dużej mierze zwalnia programistę z tego obowiązku. Gdy utworzone w aplikacji obiekty przestają być potrzebne, wychodzą poza zakres widoczności i kolektor usuwa je z pamięci. Czasami jednak taki cykl życia obiektów nie jest optymalny. Tworzenie obiektów niektórych typów jest czasochłonne i samodzielnie zarządzając ich cyklami życia można zwiększyć wydajność aplikacji, nawet jeżeli kolektor będzie musiał wykonywać dodatkowe operacje. W tym rozdziale opisuję, kiedy należy zmieniać cykl życia obiektów poprzez ich wielokrotne wykorzystywanie lub odwoływanie się do nich w specjalny sposób. Obiekty wielokrotnego użytku Obiekty można wykorzystywać wielokrotnie na dwa sposoby: tworząc pule obiektów lub lokalne zmienne wątkowe. Inżynierowie zajmujący się porządkowaniem pamięci mogą w tym momencie zaprotestować, ponieważ obie techniki obniżają skuteczność tego procesu. Szczególnie niepopularna jest pierwsza technika, która z kilku innych powodów nie jest też lubiana przez innych programistów, niezwiązanych z porządkowaniem pamięci. Jeden z powodów tej niechęci wydaje się oczywisty: obiekty wielokrotnego użytku przez długi czas zajmują stertę. Jeżeli jest ich dużo, mniej miejsca pozostaje dla nowych obiektów i częściej trzeba porządkować stertę. Ale to dopiero początek problemów. Jak pamiętasz z rozdziału 6., obiekt jest tworzony w edenie. Podczas kilku pierwszych cykli porząd- kowania obszaru młodej generacji obiekt jest przenoszony z jednego do drugiego obszaru obiektów ocalonych, po czym ostatecznie ląduje w obszarze starej generacji. Za każdym razem, gdy na nowym lub ostatnio utworzonym obiekcie w puli są wykonywane jakieś operacje, obiekt ten jest kopiowany i aktualizowane są odwołania do niego, do chwili ostatecznego umieszczenia go w starej generacji. Przeniesienie obiektu do starej generacji pozornie oznacza koniec zamieszania, ale w rzeczywistości pojawiają się wtedy problemy z wydajnością aplikacji. Czas potrzebny na pełne uporządkowanie pamięci jest proporcjonalny do liczby aktywnych obiektów starej generacji. Większe znacznie od wielkości sterty ma ilość danych. Krócej trwa porządkowanie obszaru starej generacji o wielkości 3 GB zajmowanego przez kilka aktywnych obiektów niż obszaru o wielkości 1 GB, w którym aktyw- nych jest 75 wszystkich obiektów. Stosowanie kolektora współbieżnego i zapobieganie pełnemu porządkowaniu pamięci nie poprawia sytuacji, ponieważ czas potrzebny na oznakowanie obiektów jest podobnie proporcjonalny do ilości aktywnych danych. Jeżeli wykorzystywany jest kolektor CMS, obiekty znajdujące się w puli mogą 214  Rozdział 7. Dobre praktyki używania sterty Poleć książkęKup książkę Skuteczność procesu porządkowania pamięci Jaki dokładnie wpływ ma wielkość aktywnych obiektów na czas porządkowania pamięci? Najprostsza odpowiedź brzmi: ogromny. Poniżej przedstawiony jest fragment dziennika utworzonego przez kolektor podczas testu aplikacji. Test był wykonany za pomocą typowego komputera wyposażonego w system Linux i czterordzeniowy procesor. Sterta miała wielkość 4 GB, z czego obszar młodej generacji miał stałą wielkość równą 1 GB. [Full GC [PSYoungGen: 786432K- 786431K(917504K)] [ParOldGen: 3145727K- 3145727K(3145728K)] 3932159K- 3932159K(4063232K) [PSPermGen: 2349K- 2349K(21248K)], 0.5432730 secs] [Times: user=1.72 sys=0.01, real=0.54 secs] ... [Full GC [PSYoungGen: 786432K- 0K(917504K)] [ParOldGen: 3145727K- 210K(3145728K)] 3932159K- 210K(4063232K) [PSPermGen:
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Wydajność Javy. Szczegółowe porady dotyczące programowania i strojenia aplikacji w Javie. Wydanie II
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: