W tej sekcji opisujemy typowe terminy używane w analizie pamięci. Sekcja ta ma zastosowanie do różnych narzędzi do profilowania pamięci w różnych językach.
Terminy i pojęcia opisane tutaj odnoszą się do profilowania stosu w Narzędziach deweloperskich w Chrome. Jeśli pracowałeś(-aś) już z profilowaniem pamięci w Java, .NET lub innym narzędziu, ten artykuł może być dla Ciebie przydatny.
Rozmiary obiektów
Pamiętaj, że pamięć to graf z typami prymitywnymi (np. liczbami i ciągiem znaków) oraz obiektami (tablicami asocjacyjnymi). Może on być wizualnie reprezentowany jako wykres z liczbą połączonych punktów, jak w tym przykładzie:
Obiekt może przechowywać pamięć na 2 sposoby:
- bezpośrednio przez obiekt;
- Domyślnie przez przechowywanie odwołań do innych obiektów, co uniemożliwia ich automatyczne usuwanie przez zbieracza pamięci (w skrócie GC).
Podczas pracy z profilowaniem sterty w DevTools (narzędzie do badania problemów z pamięcią w panelu Pamięć) prawdopodobnie zobaczysz kilka różnych kolumn informacji. Wyróżniają się Shallow Size (Mała objętość) i Retained Size (Zatrzymana objętość), ale co one oznaczają?
Płytki rozmiar
Jest to rozmiar pamięci używanej przez sam obiekt.
Typowe obiekty JavaScript mają pewną ilość pamięci zarezerwowaną na swój opis i przechowywanie wartości bezpośrednich. Zwykle tylko tablice i ciągi znaków mogą mieć znaczącą małą wielkość. Jednak ciągi tekstowe i tablice zewnętrzne są często przechowywane głównie w pamięci renderowania, a w pamięci zbiorczej JavaScriptu jest tylko mały obiekt opakowania.
Pamięć renderowania to cała pamięć procesu, w którym renderowana jest sprawdzana strona: pamięć natywnych zasobów + pamięć stosu JS strony + pamięć stosu JS wszystkich dedykowanych instancji roboczych uruchomionych przez stronę. Mimo to nawet mały obiekt może pośrednio zajmować dużo pamięci, uniemożliwiając usuwanie innych obiektów przez automatyczny proces usuwania elementów.
Zachowany rozmiar
Jest to rozmiar pamięci, która zostaje zwolniona po usunięciu obiektu wraz z zależnymi obiektami, które stały się niedostępne z poziomu korzenia GC.
GC roots to uchwyty tworzone (lokalnie lub globalnie) podczas tworzenia odwołania z kodu natywnego do obiektu JavaScript poza V8. Wszystkie takie uchwyty można znaleźć w migawce stosu w sekcji GC roots > Handle scope i GC roots > Global handles. Opisy uchwytów w tej dokumentacji bez szczegółowego omówienia implementacji w przeglądarce mogą być mylące. Zarówno korzenie GC, jak i uchwyty nie są czymś, czym musisz się martwić.
Istnieje wiele wewnętrznych rdzeni GC, z których większość nie jest interesująca dla użytkowników. Z punktu widzenia aplikacji istnieją te typy rdzeni:
- globalny obiekt okna (w każdym iframe). W migawkach stosu jest pole odległości, które jest liczbą odwołań do właściwości na najkrótszej ścieżce z zachowaniem z poziomu okna.
- Drzewo dokumentu DOM składające się ze wszystkich dostępnych węzłów DOM, do których można dotrzeć, przechodząc przez dokument. Nie wszystkie z nich mogą mieć obudowy JS, ale jeśli je mają, będą aktywne, dopóki dokument jest aktywny.
- Czasami obiekty mogą być zachowane przez kontekst debugera i konsolę Narzędzi deweloperskich (np. po ocenie konsoli). Tworzenie zrzutów stosu z czystą konsolą i bez aktywnych punktów przerwania w debugerze.
Graf pamięci zaczyna się od elementu rdzennego, którym może być obiekt window
przeglądarki lub obiekt Global
modułu Node.js. Nie masz kontroli nad tym, jak ten obiekt rdzeniowy jest usuwany.
Wszystko, czego nie można osiągnąć z poziomu głównego, jest usuwane.
Drzewo obiektów
Stos to sieć powiązanych ze sobą obiektów. W świecie matematyki ta struktura jest nazywana grafem lub grafem pamięci. Graf jest budowany z węzłów połączonych krawędziami, które mają przypisane etykiety.
- Węzły (lub obiekty) są oznaczane za pomocą nazwy funkcji konstruktora, która została użyta do ich utworzenia.
- Krawędzie są oznaczane za pomocą nazw właściwości.
Dowiedz się, jak nagrywać profil za pomocą narzędzia Heap Profiler. W tym nagraniu Heap Profiler możemy zobaczyć kilka ciekawych rzeczy, takich jak odległość od głównego korzenia GC. Jeśli prawie wszystkie obiekty tego samego typu znajdują się w tej samej odległości, a kilka z nich jest dalej, warto to zbadać.
Dominatory
Obiekty dominatora są tworzone w strukturze drzewa, ponieważ każdy obiekt ma dokładnie jednego dominatora. Dominujący element może nie mieć bezpośrednich odwołań do obiektu, który dominuje, czyli drzewo dominującego elementu nie jest drzewem rozpinającym grafu.
Na poniższym diagramie:
- Węzeł 1 dominuje nad węzłem 2
- Węzeł 2 dominuje nad węzłami 3, 4 i 6
- Węzeł 3 dominuje nad węzłem 5
- Węzeł 5 dominuje nad węzłem 8
- Węzeł 6 dominuje nad węzłem 7
W tym przykładzie węzeł #3
jest dominatorem węzła #10
, ale węzeł #7
występuje też na każdej prostej ścieżce z GC do węzła #10
. Dlatego obiekt B jest dominatorem obiektu A, jeśli istnieje na każdej prostej ścieżce od korzenia do obiektu A.
Szczegóły dotyczące V8
Podczas profilowania pamięci warto wiedzieć, dlaczego migawki stosu mają taki, a nie inny wygląd. W tej sekcji omawiamy niektóre tematy związane z pamięcią, które odnoszą się konkretnie do wirtualnej maszyny JavaScript V8 (VM V8 lub VM).
Reprezentacja obiektu JavaScript
Istnieją 3 typy prymitywne:
- Liczby (np. 3,14159..)
- Wartości logiczne (prawda lub fałsz)
- ciągi tekstowe (np. 'Werner Heisenberg')
Nie mogą się one odwoływać do innych wartości i zawsze są liśćmi lub węzłami końcowymi.
Liczby mogą być przechowywane w postaci:
- wartości całkowite 31-bitowe zwane małe liczby całkowite (SMIs),
- obiekty stosu, zwane liczbami stosu; Liczby w pamięci podręcznej służą do przechowywania wartości, które nie pasują do formatu SMI, np. doubles, lub gdy wartość musi być opakowana, np. w przypadku właściwości.
Ciągi znaków mogą być przechowywane w tych miejscach:
- pamieci podręcznej maszyny wirtualnej,
- zewnętrznie w pamięci procesora graficznego. Tworzony jest obiekt owijający, który służy do uzyskiwania dostępu do pamięci zewnętrznej, w której przechowywane są źródła skryptów i inne treści pochodzące z Internetu, a nie kopiowane na stos maszyny wirtualnej.
Pamięć dla nowych obiektów JavaScript jest przydzielana z osobnej sterty JavaScriptu (lub sterty maszyny wirtualnej). Tymi obiektami zarządza zbieracz śmieci V8, dlatego będą one aktywne, dopóki istnieje co najmniej 1 silne odwołanie do nich.
Obiekty natywne to wszystko inne, co nie znajduje się w kupce JavaScriptu. Obiekt natywny, w odróżnieniu od obiektu stosu, nie jest zarządzany przez zbieracz śmieci V8 przez cały czas jego istnienia i można uzyskać do niego dostęp tylko z JavaScriptu za pomocą obiektu opakowania JavaScriptu.
Konkatenowany ciąg tekstowy to obiekt składający się z par ciągów tekstowych, które są przechowywane, a następnie łączone. Jest to wynik konkatenacji. Łączenie zawartości cons string występuje tylko w razie potrzeby. Przykładem może być tworzenie podciągu ciągu połączonego.
Jeśli na przykład złączasz ciągi znaków a i b, otrzymujesz ciąg znaków (a, b), który jest wynikiem konkatenacji. Jeśli później złączesz d z tym wynikiem, otrzymasz kolejną łańcuchową tablicę znaków ((a, b), d).
Tablice – tablica to obiekt z kluczami liczbowymi. Są one szeroko wykorzystywane w wirtualnej maszynie V8 do przechowywania dużych ilości danych. Zbiory par klucz-wartość używane jak słowniki są zabezpieczane za pomocą tablic.
Typowy obiekt JavaScript może być jednym z 2 typów tablic służących do przechowywania:
- właściwości o nazwie,
- elementy liczbowe
W przypadku bardzo małej liczby właściwości można je przechowywać wewnętrznie w samym obiekcie JavaScriptu.
Map – obiekt opisujący rodzaj obiektu i jego układ. Na przykład mapy są używane do opisywania domyślnych hierarchii obiektów na potrzeby szybkiego dostępu do właściwości.
Grupy obiektów
Każda grupa obiektów natywnych składa się z obiektów, które zawierają wzajemne odwołania. Rozważ na przykład poddrzewo DOM, w którym każdy węzeł ma link do swojego rodzica oraz do następnego elementu podrzędnego i następnego elementu siostrzanego, tworząc w ten sposób połączony graf. Pamiętaj, że obiekty natywne nie są reprezentowane w steku JavaScriptu, dlatego mają rozmiar zerowy. Zamiast tego tworzone są obiekty opakowujące.
Każdy obiekt opakowania zawiera odwołanie do odpowiadającego mu obiektu natywnego, aby można było do niego przekierowywać polecenia. Grupa obiektów zawiera obiekty opakowania. Nie powoduje to jednak cyklu niemożnego do zebrania, ponieważ GC jest na tyle inteligentny, że może zwalniać grupy obiektów, których opakowania nie są już referowane. Jeśli jednak zapomnisz uwolnić pojedynczy element opakowania, cała grupa i powiązane z nią elementy opakowania zostaną zablokowane.