Stos i sterta: gdzie mój kod znajduje się w pamięci?

📖 17min read

Ubóstwo w obfitości, zapomniane wspomnienia

„Struktury pamięci” nauczyłem się w szkole na zajęciach z systemów operacyjnych lub programowania systemów. Pamiętam też terminy takie jak stos i sterta używane w pytaniach egzaminacyjnych.

Ale szczerze mówiąc, rzadko przywiązywałem większą wagę do pamięci, dopóki nie ukończyłem szkoły. Obecnie pamięć RAM osobistego laptopa wynosi w zasadzie 16 GB lub 32 GB. Podczas wykonywania zadań na poziomie licencjackim praktycznie nie brakowało pamięci. Co więcej, w językach takich jak Java moduł Garbage Collector (GC) automatycznie czyści przestrzeń, więc nie musiałem się martwić o adres pamięci.

Tak więc odurzony obfitością sprzętu i wygodą języka rzuciłem się do pracy praktycznej, zapominając o „zmyśle pamięci”, który jest podstawową umiejętnością programisty.

Bolesne wnioski wyciągnięte z praktyki

Podczas ćwiczeń w szkole do testów z kodowania jedyne, co mogłem zrobić, to umieścić kilka liczb w tablicy i obracać nimi. Co najwyżej liczba danych wejściowych nie przekraczała 100 000. Ale praktyka była inna. Niezależnie od tego, jak mały był startup, w bazie danych zgromadzono dziesiątki lub miliony rzeczywistych danych klientów.

W praktyce do wygodnej obsługi tak dużej ilości danych wykorzystywana jest technologia zwana „ORM (Object Relational Mapping)”. Jest to bardzo przydatne narzędzie, które pozwala na obsługę danych takich jak obiekty Java, bez konieczności samodzielnego pisania zapytań SQL. (Hibernacja jest reprezentatywnym przykładem w obozie Java.)

Problem polegał na tym, że to narzędzie było „zbyt wygodne”. Wystarczy jedno naciśnięcie przycisku, aby wszystkie dane z bazy danych zostały przeniesione na listę, więc nie mogłem oszacować wagi danych ukrytych za nim. Wystarczy napisać jedną linijkę kodu z informacją „Uzyskaj informacje o kliencie”, a do pamięci zostaną załadowane informacje o dziesiątkach tysięcy osób wraz ze szczegółami zamówień.

Rezultaty były katastrofalne. 32 GB RAM zapełniło się w mgnieniu oka. Serwer cierpiał z powodu „ekstremalnych opóźnień” spowodowanych próbą zabezpieczenia pamięci przez GC (program czyszczący) i ostatecznie się zatrzymał.

Znany stos, nieznana sterta

Kiedy przeglądałem dziennik błędów wyrzucony przez serwer, mój wzrok przykuło jedno słowo.

java.lang.OutOfMemoryError: Miejsce na stercie Java

Właściwie słowo „stos” było dość znajome. Nauczyłem się tego do znudzenia na zajęciach ze struktury danych i jest to także nazwa witryny (Stack Overflow), którą programiści odwiedzają raz dziennie. Powszechnie było również wiadome, że niepoprawne zapisanie funkcji rekurencyjnej powoduje pęknięcie stosu.

Jednak w praktyce za każdym razem, gdy serwer umiera, winowajcą nie jest stos. Dziennik błędów zawsze wskazywał „Stertę”.

„To nie jest tak, że stos jest pełny, tylko że nie ma wystarczającej ilości miejsca na stercie?”

W tym momencie przyszło mi do głowy pytanie. Rozumiem stos, ale co to jest sterta? Dlaczego w praktyce tak często eksploduje? Czy to to samo, co sterta w strukturze danych? Dlaczego mój kod przeszkadza stercie, a nie stosowi?

To pytanie zaprowadziło mnie z powrotem do świata zakurzonych głównych książek i Googlingu. I wtedy się dowiedziałem. Fakt, że „Pamięć (RAM)”, dom, w którym mieszka mój kod, nie jest tak naprawdę pojedynczym pomieszczeniem, ale „odrębną przestrzenią”, która jest dokładnie podzielona i obsługiwana zgodnie z przeznaczeniem.

Pamięć to nie pojedyncza przestrzeń. Dzieli się na szybki i wąski „stos” oraz wolny i szeroki „stos”.

Stół warsztatowy i magazyn w cyfrowym centrum dystrybucji

Powróćmy do naszego światopoglądu „cyfrowego centrum logistycznego”. W tym przypadku „RAM (pamięć)” to przestrzeń, w której pracownik (CPU) rozkłada elementy w celu wykonania pracy. Jednak ze względu na efektywność przestrzeń ta jest obsługiwana w dwóch oddzielnych obszarach.

1. Stos: osobisty stół warsztatowy pracownika

2. Sterta: Magazyn publiczny

„Stos” znikający po zakończeniu funkcji i „stos” znikający dopiero po jego wyczyszczeniu przez osobę czyszczącą.

[Weryfikacja kodu] Dwa światy sprawdzone błędami

Czy pamięć naprawdę jest tak podzielona? Istnienie tych dwóch spacji można jednoznacznie udowodnić, celowo powodując błąd w kodzie.

1. Błąd przepełnienia stosu

Stos nazywany jest „stółem warsztatowym”. Stół warsztatowy jest wąski. Jeśli funkcja nie zakończy się i nadal będzie się wywoływać (rekurencja), dokumenty piętrzą się na stole warsztatowym aż do sufitu i ostatecznie zapadają się.

public class StackTest {
    public static void recursiveCall(int depth) {
        // Nieskonczona rekurencja: funkcja nie konczy sie i wciaz laduje sie na Stos
        System.out.println("Stack Depth: " + depth);
        recursiveCall(depth + 1);
    }

    public static void main(String[] args) {
        recursiveCall(1);
    }
}

Wynik: po kilku tysiącach rund wypluwa StackOverflowError. Nieważne, jak pusta jest przestrzeń na stercie, jeśli stos (stół warsztatowy) jest pełny, program umiera.

2. Eksplozja sterty (OutOfMemoryError)

Tym razem odtwórzmy mój koszmar. Podobnie jak w przypadku, gdy z powodu nieprawidłowego użycia ORM ładowane są dziesiątki tysięcy obiektów na raz, nadal będziemy tworzyć ogromne listy i upychać je na stercie.

import java.util.ArrayList;
import java.util.List;

public class HeapTest {
    public static void main(String[] args) {
        List<byte[]> warehouse = new ArrayList<>();
        
        while (true) {
            // Ciagke tworzenie danych 1MB i ladowanie ich do magazynu (Heap)
            // Przyklad praktyczny: wystepuje przy ladowaniu dziesiatek tysiecy rekordow z DB bez paginacji
            warehouse.add(new byte[1024 * 1024]);
        }
    }
}

Wynik: java.lang.OutOfMemoryError: miejsce na stercie Java. To nie jest problem ze stosem. Ten błąd występuje, ponieważ w magazynie nie ma już miejsca na załadowanie towarów. To jest moment, kiedy na własne oczy widzę, jak napisany przeze mnie kod przeszkadza stercie.

Kolektor śmieci (GC)

Jest tu istotna różnica. Stos jest „automatycznie” opróżniany po zakończeniu funkcji. Nie musisz się martwić. Jeśli jednak ktoś nie uprzątnie sterty, śmieci będą się gromadzić dalej.

W starych językach, takich jak język C, programiści musieli je wyczyścić bezpośrednio za pomocą polecenia free(). Jeśli zapomnisz, magazyn zapełni się śmieciami i eksploduje (wyciek pamięci). Z drugiej strony, współczesne języki, takie jak Java, Python i JavaScript (JS), wykorzystują profesjonalne narzędzia czyszczące zwane „GC (Garbage Collector)”.

„Nikt już nie używa tego obiektu?” GC okresowo przegląda stertę, znajduje niepotrzebne obiekty i odrzuca je. Dzięki temu nie musimy pisać kodu zwalniającego pamięć.

Ale nic nie jest darmowe. W momencie, gdy GC dokona głębokiego sprzątania, cała praca w centrum logistycznym zostaje na chwilę zatrzymana (Stop-the-world). Ten czas czyszczenia powoduje, że gra nagle się opóźnia lub serwer zawiesza się na około sekundę.

Podsumowanie: oczy, które widzą niewidzialne

Po zrozumieniu stosu i sterty kod na moim monitorze zaczął wyglądać inaczej. W przeszłości, gdy patrzyłem na kod new Student(), po prostu myślałem: „Utworzyłem obiekt”. Ale teraz to widzę.

„Do magazynu weszło pudełko zwane Stertą. Jeśli go nie usunę (lub jeśli GC nie przyjdzie), będzie nadal pochłaniać pamięć.”

Po zyskaniu „oczu umożliwiających widzenie niewidzialnego” udało mi się uwolnić od niejasnych lęków. Nawet jeśli serwer wypluje OutOfMemory, nie wpadam w panikę i naciskam przycisk restartu, tak jak to robiłem wcześniej. Zamiast tego włącz narzędzie analityczne i spokojnie zapytaj: „Który obiekt zajmuje stertę?” Dzieje się tak dlatego, że jeśli znasz przyczynę, możesz ją rozwiązać.

Podbiliśmy teraz przestrzeń (pamięć), w której przechowywany jest kod. Magazyn jest w pełni przygotowany. Kim więc są „pracownicy”, którzy faktycznie transportują i montują towary w tym magazynie? Jaka jest różnica między pracą w pojedynkę a pracą z kilkoma osobami jednocześnie (wielozadaniowość)?

Następnym razem przejdźmy do świata „Process & Thread”, kwiatu systemu operacyjnego i odwiecznej pracy domowej programistów back-end.

Dodaj komentarz