1. Przegląd pamięci procesu Java
Gdy uruchamiasz program w Javie, JVM (Java Virtual Machine) prosi system operacyjny o kawałek pamięci. Czasem — skromny, czasem — całkiem pokaźny (zwłaszcza jeśli uruchamiasz jakiegoś Minecrafta z masą modów). Ta pamięć dzieli się na kilka kluczowych obszarów, z których każdy pełni swoją rolę:
- Stos (Stack) — dla zmiennych lokalnych i wywołań metod.
- Sterta (Heap) — dla wszystkich obiektów tworzonych przez new.
- Obszary pomocnicze (PermGen/MetaSpace) — dla metadanych klas, pól statycznych i innych „magicznych” rzeczy.
Wygląda to mniej więcej tak:
┌───────────────────────────────┐
│ Proces JVM │
│ ┌─────────────┐ │
│ │ Stack │ ← Każdy wątek — ma własny stos!
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Heap │ ← Wspólna dla wszystkich wątków
│ └─────────────┘ │
│ ┌───────────────┐ │
│ │ PermGen/ │ ← Metadane klas
│ │ MetaSpace │
│ └───────────────┘ │
└───────────────────────────────┘
Dlaczego to ważne?
- Zrozumienie organizacji pamięci pomaga pisać bardziej wydajny i bezpieczny kod.
- Łatwiej diagnozować błędy typu StackOverflowError lub OutOfMemoryError.
- Nie straszne stają się słowa „garbage collector” i „wyciek pamięci” — wiesz, gdzie i czego szukać.
2. Stos (Stack): szybko, lokalnie, ale nie na zawsze
Stos to specjalny obszar pamięci, który jest przydzielany dla każdego wątku oddzielnie. Stos przypomina stos talerzy: ostatnio odłożony — zdejmiesz jako pierwszy dopiero po zdjęciu wszystkich leżących na nim. Innymi słowy, stos działa zgodnie z zasadą LIFO (Last In, First Out).
Po co jest stos?
W stosie przechowywane są:
- Zmienne lokalne metod (na przykład int x = 5; wewnątrz metody).
- Adres powrotu po wywołaniu metody (aby wiedzieć, dokąd wrócić po zakończeniu jej działania).
Za każdym razem, gdy wywołujesz metodę, na stos dodawana jest nowa ramka (stack frame) — coś w rodzaju pudełka, w którym znajdują się wszystkie zmienne lokalne tej metody oraz informacje pomocnicze. Gdy metoda kończy działanie, jej ramka jest usuwana — wszystkie jej zmienne lokalne znikają.
Przykład
public static void main(String[] args) {
int a = 10; // a jest na stosie main
int b = sum(a, 5); // wywołujemy sum
}
public static int sum(int x, int y) {
int result = x + y; // x, y, result leżą na stosie sum
return result;
}
- Gdy wywoływana jest metoda sum, tworzona jest dla niej osobna ramka na stosie.
- Po zakończeniu działania sum jej zmienne znikają.
Cykl życia zmiennej
Zmienne lokalne żyją tylko tak długo, jak wykonywana jest metoda, w której zostały zadeklarowane. Gdy tylko metoda się kończy — już ich nie ma, pamięć jest zwalniana natychmiast.
Przepełnienie stosu
Jeśli przypadkowo (lub celowo) napiszesz nieskończoną rekurencję, każde wywołanie metody będzie dodawać nową ramkę na stos. W pewnym momencie stos się skończy i otrzymasz:
Exception in thread "main" java.lang.StackOverflowError
Przykład:
public static void main(String[] args) {
recurse();
}
public static void recurse() {
recurse(); // Nieskończona rekurencja!
}
Rozmiar stosu
Rozmiar stosu jest ograniczony — zwykle to kilka megabajtów na wątek (można ustawić parametrem -Xss). Jeśli stos się skończy — program kończy się błędem.
3. Sterta (Heap): miejsce na obiekty
Sterta (Heap) to wspólny obszar pamięci dla wszystkich wątków, w którym żyją wszystkie obiekty tworzone za pomocą new, a także tablice. To właśnie na stercie dzieje się cała magia programowania obiektowego.
Jak obiekty trafiają na stertę?
String s = new String("Hello");
int[] arr = new int[10];
- Zmienna s to referencja, leży na stosie.
- Sam obiekt String i tablica arr — leżą na stercie.
Cykl życia obiektu
Obiekt żyje na stercie tak długo, jak istnieje co najmniej jedno silne odwołanie (strong reference) do niego. Gdy już nikt się do obiektu nie odwołuje — staje się „śmieciem” i może zostać usunięty przez garbage collector (GC).
Zarządzanie pamięcią
W przeciwieństwie do C/C++, gdzie samodzielnie dbasz o zwalnianie pamięci (free, delete), w Javie robi to GC. Nie możesz jawnie zwolnić obiektu, ale możesz wyzerować wszystkie referencje do niego — wtedy stanie się kandydatem do usunięcia.
Schemat: gdzie co leży?
Stack (main)
└─ s ─┬────────────┐
│ │
▼ │
Heap │
┌─────────────┐ │
│ String "Hello"◄──┘
└─────────────┘
Cechy sterty
- Sterta jest jedna na cały proces JVM.
- Rozmiar sterty można ustawić przy uruchamianiu (-Xmx, -Xms).
- Jeśli na stercie nie ma już wolnego miejsca, a GC nie może go odzyskać — program kończy się błędem OutOfMemoryError.
4. PermGen i MetaSpace: gdzie „mieszkają” klasy?
Gdy piszesz class MyClass { ... }, a następnie uruchamiasz program, JVM musi gdzieś przechowywać wszystko, co związane z tą klasą — metody, pola, bytecode, zmienne statyczne, stałe, a nawet literały łańcuchowe. Do tego w JVM istnieje specjalny obszar pamięci, gdzie „mieszkają” klasy.
Wcześniej, przed Java 8, ten obszar nazywano PermGen (Permanent Generation). Jednak miał on sporo problemów — na przykład miał stały rozmiar i jeśli brakowało miejsca, aplikacja po prostu kończyła się błędem OutOfMemoryError: PermGen space.
Począwszy od Java 8 pojawił się nowy, bardziej elastyczny obszar — MetaSpace. Zastąpił on stary PermGen i może automatycznie się rozszerzać, zajmując tyle pamięci, ile jest potrzebne systemowi (w granicach dostępnej fizycznej pamięci).
PermGen (przed Java 8)
- W PermGen przechowywano metadane klas, pola statyczne, literały łańcuchowe.
- Rozmiar PermGen był ograniczony (domyślnie niewielki), można go było zwiększyć parametrem -XX:MaxPermSize=256m.
- Jeśli do aplikacji dynamicznie ładowano wiele klas (np. na serwerach WWW), PermGen mógł się „skończyć” i pojawiał się błąd:
java.lang.OutOfMemoryError: PermGen space
- Problem: czyszczenie PermGen nie zawsze następowało poprawnie, gdy klasy były dynamicznie wyładowywane (np. przy przeładowaniu aplikacji webowych).
MetaSpace (Java 8+)
- Od Java 8 PermGen zniknął, a pojawił się MetaSpace.
- MetaSpace przechowuje metadane klas, ale teraz w pamięci natywnej (poza Java Heap).
- Rozmiar MetaSpace domyślnie nie jest ograniczony (ograniczony jedynie pamięcią systemu), ale można ustawić limit przez -XX:MaxMetaspaceSize=512m.
- Błąd przy braku pamięci wygląda teraz tak:
java.lang.OutOfMemoryError: Metaspace
- Do MetaSpace trafiają także pola statyczne, metody, informacje o klasach.
Schemat: jak to wszystko jest zorganizowane
┌───────────────────────────────┐
│ Proces JVM │
│ ┌─────────────┐ │
│ │ Stack │ ← Zmienne lokalne, wywołania metod
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Heap │ ← Obiekty, tablice, wszystko tworzone przez new
│ └─────────────┘ │
│ ┌───────────────┐ │
│ │ MetaSpace │ ← Metadane klas, pola statyczne
│ └───────────────┘ │
└───────────────────────────────┘
Dlaczego to ważne?
Jeśli piszesz zwykłe aplikacje desktopowe lub serwerowe, najpewniej nigdy nie trafisz na błędy PermGen ani MetaSpace. Ale jeśli pracujesz z dynamicznym ładowaniem klas (np. wtyczki, aplikacje webowe, frameworki typu Spring, które mogą dociągać i wyładowywać mnóstwo klas), to wiedza o MetaSpace to must-have!
5. Ilustracja: schemat pamięci JVM
flowchart TD
subgraph JVM
direction TB
Stack1["Stack (Thread 1)"]
Stack2["Stack (Thread 2)"]
Heap[Heap]
MetaSpace[MetaSpace]
end
Stack1 --odwołuje się do--> Heap
Stack2 --odwołuje się do--> Heap
Heap --wykorzystuje klasy z--> MetaSpace
- Każdy wątek — własny stos.
- Wszystkie stosy mogą odwoływać się do obiektów na stercie.
- Obiekty na stercie „znają” swoją klasę, której informacje leżą w MetaSpace.
6. Przykład: jak to wygląda w rzeczywistym kodzie
public class MemoryDemo {
public static void main(String[] args) {
int x = 42; // x jest na stosie main
String s = "Hello!"; // s — referencja na stosie, obiekt String na stercie, literał "Hello!" w MetaSpace
Person p = new Person("Alice"); // p — referencja na stosie, obiekt Person na stercie
// Wywołajmy metodę, żeby utworzyć nową ramkę stosu
printPerson(p);
}
public static void printPerson(Person person) {
// person — referencja na stosie printPerson
System.out.println(person.getName());
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() { return name; }
}
Omówienie:
- x — zmienna lokalna, żyje na stosie metody main.
- s — referencja na stosie, obiekt String na stercie, a literał łańcuchowy "Hello!" — w MetaSpace.
- p — referencja na stosie, obiekt Person na stercie.
- Klasa Person i wszystkie jej metody/pola — w MetaSpace (metadane klas).
- Wywołanie printPerson(p) tworzy nową ramkę stosu; wewnątrz niej lokalna referencja person wskazuje na ten sam obiekt na stercie.
7. Jak JVM zarządza pamięcią: krótkie FAQ
Czy mogę zarządzać stosem?
Nie, stos jest całkowicie pod kontrolą JVM. Możesz jedynie ustawić jego rozmiar przy uruchomieniu (-Xss).
Czy mogę zarządzać stertą?
Częściowo: rozmiar sterty ustawia się przy uruchomieniu (-Xmx, -Xms). Za czyszczenie odpowiada garbage collector (GC).
Czy mogę zarządzać MetaSpace?
Można ograniczyć rozmiar (-XX:MaxMetaspaceSize), ale zwykle nie jest to potrzebne.
Co się dzieje przy braku pamięci?
— Jeśli skończy się stos — StackOverflowError.
— Jeśli skończy się sterta — OutOfMemoryError: Java heap space.
— Jeśli skończy się MetaSpace — OutOfMemoryError: Metaspace.
8. Typowe błędy związane z pamięcią
Błąd nr 1: StackOverflowError z powodu nieskończonej rekurencji. Najczęstsza przyczyna — brak warunku zakończenia rekurencji. Na przykład metoda wywołuje samą siebie bez zatrzymania. JVM nie może rozszerzać stosu w nieskończoność i program „padnie”.
Błąd nr 2: OutOfMemoryError z powodu przepełnienia sterty. Jeśli tworzysz zbyt wiele obiektów, do których wciąż istnieją referencje (np. dodajesz elementy do listy, ale nigdy ich nie usuwasz), sterta może się skończyć.
Błąd nr 3: OutOfMemoryError: PermGen space / Metaspace. Jeśli używasz wtyczek lub dynamicznie ładujesz mnóstwo klas, a MetaSpace nie jest czyszczone (np. z powodu nieprawidłowego wyładowywania klas), może zabraknąć miejsca w MetaSpace.
Błąd nr 4: Mylenie referencji z obiektem. Wielu początkujących myli: zmienna typu Person na stosie — to tylko referencja, a sam obiekt — na stercie.
Błąd nr 5: Oczekiwanie, że garbage collector usunie wszystko natychmiast. GC działa „według własnego uznania” (w rzeczywistości — według wewnętrznych algorytmów i przy braku pamięci), a nie od razu po wyzerowaniu referencji. Nie należy liczyć na natychmiastowe zwolnienie pamięci.
GO TO FULL VERSION