Stack and Heap: Var finns min kod i minnet?

📖 16min read

Fattigdom i överflöd, bortglömda minnen

Jag lärde mig ”minnesstruktur” under lektioner i operativsystem eller systemprogrammering i skolan. Jag minns också att termer som stack och heap användes som examensfrågor.

Men för att vara ärlig, ägnade jag sällan någon allvarlig uppmärksamhet åt minnet förrän jag tog examen. Dessa dagar är personlig laptop RAM i princip 16 GB eller 32 GB. Det var praktiskt taget ingen brist på minne när man gjorde uppgifter på grundnivå. Dessutom, på språk som Java, rensar Garbage Collector (GC) utrymmet automatiskt, så jag behövde inte oroa mig för minnesadressen.

Så, berusad av överflöd av hårdvara och bekvämligheten med språk, hoppade jag in i praktiskt arbete och glömde ”minneskänsla”, som är en utvecklares grundläggande färdighet.

Smärtsamma lärdomar från praktiken

När jag tränade för kodningstest i skolan kunde jag bara lägga några siffror i en array och snurra runt dem. Som mest översteg antalet indata inte 100 000. Men praktiken var annorlunda. Oavsett hur liten en startup var, ackumulerades tiotals eller miljoner delar av faktisk kunddata i databasen.

I praktiken används en teknik som kallas ’ORM (Object Relational Mapping)’ för att bekvämt hantera denna stora mängd data. Det är ett mycket användbart verktyg som låter dig hantera data som Java-objekt utan att behöva skriva SQL-frågor själv. (Hibernate är ett representativt exempel i Java-lägret.)

Problemet var att det här verktyget var ”för bekvämt”. Med bara en knapptryckning fördes all data i DB till listan, så jag kunde inte uppskatta vikten av data som gömdes bakom den. Bara genom att skriva en enda kodrad som sa ”Hämta kundinformation” laddades tiotusentals människors information och deras medföljande beställningsdetaljer in i minnet.

Resultaten var katastrofala. 32 GB RAM-minne fylldes på ett ögonblick. Servern led av ”extrem fördröjning” på grund av att GC (renare) försökte säkra minnet och slutade så småningom.

Bekant stack, obekant hög

När jag tittade på felloggen som servern spydde ut, fångades mina ögon av ett ord.

java.lang.OutOfMemoryError: Java-högutrymme

Egentligen var ordet ”stack” ganska bekant. Jag lärde mig det ad nauseam i datastrukturklassen, och det är också namnet på en webbplats (Stack Overflow) som utvecklare besöker en gång om dagen. Det var också allmänt känt att om en rekursiv funktion skrevs felaktigt, skulle stacken brista.

Men i praktiken, närhelst en server dör, är den skyldige inte stacken. Felloggen pekade alltid på ’Heap’.

”Det är inte så att högen är full, det är att det inte finns tillräckligt med högutrymme?”

I det ögonblicket kom en fråga i mitt huvud. Jag förstår stacken, men vad är högen? Varför exploderar det så ofta i praktiken? Är det samma som högen i datastrukturen? Varför stör min kod högen och inte stacken?

Den frågan ledde mig tillbaka till en värld av dammiga stora böcker och googling. Och så fick jag reda på det. Det faktum att ”Memory (RAM)”, huset där min kod bor, faktiskt inte är ett enkelrum, utan ett ”separat utrymme” som är grundligt uppdelat och drivs efter syfte.

Minne är inte ett enda utrymme. Den är uppdelad i en snabb och smal ’stack’ och en långsam och bred ’hög’.

Arbetsbänk och lager i digitalt distributionscenter

Låt oss återgå till vår världsbild av ”digitalt logistikcenter”. Här är ”RAM (minne)” utrymmet där arbetaren (CPU) sprider ut föremål för att utföra arbete. Detta utrymme drivs dock i två separata områden för effektivitet.

1. Stack: Arbetarens personliga arbetsbänk

2. Hög: Offentligt lager

Högen som försvinner när funktionen avslutas, och ”högen” som försvinner först när städaren städar upp den.

[Kodverifiering] Två världar bevisade genom fel

Är minnet verkligen uppdelat så här? Existensen av dessa två mellanslag kan tydligt bevisas genom att avsiktligt orsaka ett fel i koden.

1. Stack OverflowError

Stacken kallas en ’arbetsbänk’. Arbetsbänken är smal. Om funktionen inte avslutas och fortsätter att kalla sig själv (rekursion), hopar sig dokument på arbetsbänken till taket och kollapsar så småningom.

public class StackTest {
    public static void recursiveCall(int depth) {
        // Oandlig rekursion: funktionen slutar aldrig och fortsatter att staplas pa Stacken
        System.out.println("Stack Depth: " + depth);
        recursiveCall(depth + 1);
    }

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

Resultat: Efter flera tusen omgångar spottar den ut StackOverflowError. Oavsett hur tomt högutrymmet är, om stapeln (arbetsbänken) är full, dör programmet.

2. Högexplosion (OutOfMemoryError)

Den här gången ska vi återskapa mardrömmen jag hade. I likhet med situationen där tiotusentals objekt laddas på en gång på grund av felaktig användning av ORM, kommer vi att fortsätta att skapa enorma listor och klämma in dem i högen.

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

public class HeapTest {
    public static void main(String[] args) {
        List<byte[]> warehouse = new ArrayList<>();
        
        while (true) {
            // Fortsatter skapa 1MB data och stapla i lagret (Heap)
            // Praktiskt exempel: intraffar nar tiotusentals poster hamtas fran DB utan paginering
            warehouse.add(new byte[1024 * 1024]);
        }
    }
}

Resultat: java.lang.OutOfMemoryError: Java-högutrymme. Detta är inte en stackfråga. Det här felet uppstår eftersom det inte finns mer utrymme för att ladda artiklar i lagret. Det här är ögonblicket då jag med egna ögon ser hur koden jag skrev stör högen.

Gorbage Collector (GC)

Det finns en viktig skillnad här. Högen töms ”automatiskt” när funktionen avslutas. Du behöver inte oroa dig. Men om någon inte städar upp högen, fortsätter soporna att samlas.

I gamla språk som C-språk, var utvecklare tvungna att rensa dem direkt med kommandot free(). Om du glömmer det kommer lagret att fyllas med skräp och explodera (minnesläcka). Å andra sidan använder moderna språk som Java, Python och JavaScript (JS) professionella städare som kallas ”GC (Garbage Collector)”.

”Ingen använder det här objektet längre?” GC går med jämna mellanrum genom högen, hittar oägda föremål och slänger dem. Tack vare detta behöver vi inte skriva minnesversionskod.

Men ingenting är gratis. I samma ögonblick som GC gör djuprengöringen stannar allt arbete i logistikcentret för ett ögonblick (Stop-the-world). Denna städningstid är anledningen till att spelet plötsligt släpar efter eller att servern fryser i ungefär en sekund.

Avslutande: Ögon som ser det osynliga

Efter att ha förstått stacken och högen började koden på min bildskärm att se annorlunda ut. Tidigare, när jag tittade på koden new Student(), tänkte jag helt enkelt: ”Jag skapade ett objekt.” Men nu kan jag se det.

”En ruta har nu kommit in i lagret som heter Heapen. Om jag inte tar bort den (eller om GC inte kommer), kommer den att fortsätta att äta upp minnet.”

Efter att ha fått ”ögon för att se det osynliga” kunde jag befria mig från vaga rädslor. Även om servern spottar ut OutOfMemory får jag inte panik och trycker på omstartsknappen som jag brukade. Slå istället på analysverktyget och fråga lugnt: ”Vilket objekt upptar högen?” Det beror på att om du känner till orsaken kan du lösa det.

Vi har nu erövrat utrymmet (minnet) där koden är lagrad. Lagret är fullt förberett. Så vilka är ”arbetarna” som faktiskt transporterar och monterar artiklar i detta lager? Vad är skillnaden mellan att arbeta ensam och att arbeta med flera personer samtidigt (multi-tasking)?

Nästa gång, låt oss gå till världen av ”Process & Thread”, operativsystemets blomma och backend-utvecklarnas eviga hemläxa.

Lämna en kommentar