CodeGym /Các khóa học /JAVA 25 SELF /Tổ chức bộ nhớ trong JVM: stack, heap, PermGen/MetaSpace

Tổ chức bộ nhớ trong JVM: stack, heap, PermGen/MetaSpace

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Tổng quan bộ nhớ của tiến trình Java

Khi bạn chạy một chương trình Java, JVM (Java Virtual Machine) sẽ xin hệ điều hành một phần bộ nhớ. Đôi khi — rất khiêm tốn, đôi khi — khá đáng kể (đặc biệt nếu bạn chạy một Minecraft nào đó với cả đống mod). Bộ nhớ này được chia thành vài vùng chính, mỗi vùng đóng một vai trò riêng:

  • Stack — dành cho biến cục bộ và lời gọi phương thức.
  • Heap — dành cho mọi đối tượng bạn tạo bằng new.
  • Khu vực chuyên dụng (PermGen/MetaSpace) — dành cho metadata lớp, trường tĩnh và những thứ “ma thuật” khác.

Trông xấp xỉ như sau:

┌───────────────────────────────┐
│ Tiến trình JVM                │
│ ┌─────────────┐               │
│ │  Stack      │ ← Mỗi luồng có stack riêng!
│ └─────────────┘               │
│ ┌─────────────┐               │
│ │  Heap       │ ← Dùng chung cho mọi luồng
│ └─────────────┘               │
│ ┌───────────────┐             │
│ │ PermGen/      │ ← Metadata lớp
│ │ MetaSpace     │
│ └───────────────┘             │
└───────────────────────────────┘

Tại sao điều này quan trọng?

  • Hiểu cấu trúc bộ nhớ giúp bạn viết mã hiệu quả và an toàn hơn.
  • Dễ chẩn đoán các lỗi như StackOverflowError hoặc OutOfMemoryError.
  • Không còn sợ các thuật ngữ “bộ gom rác” và “rò rỉ bộ nhớ” — bạn biết cần tìm gì và ở đâu.

2. Stack: nhanh, cục bộ, nhưng không vĩnh viễn

Stack là vùng bộ nhớ đặc biệt được cấp phát cho từng luồng riêng biệt. Stack giống một chồng đĩa: cái đặt vào sau cùng sẽ được lấy ra đầu tiên sau khi bạn bỏ những cái ở trên. Nói cách khác, stack hoạt động theo nguyên tắc LIFO (Last In, First Out).

Stack dùng để làm gì?

Trong stack lưu trữ:

  • Các biến cục bộ của phương thức (ví dụ, int x = 5; bên trong phương thức).
  • Địa chỉ quay về sau lời gọi phương thức (để biết sau khi phương thức kết thúc sẽ quay lại đâu).

Mỗi lần bạn gọi một phương thức, một khung stack (stack frame) mới sẽ được thêm vào — giống như một chiếc hộp chứa tất cả biến cục bộ của phương thức đó và thông tin phục vụ. Khi phương thức kết thúc, khung của nó bị loại bỏ — mọi biến cục bộ của nó biến mất.

Ví dụ

public static void main(String[] args) {
    int a = 10;           // a nằm trên stack của main
    int b = sum(a, 5);    // gọi sum
}

public static int sum(int x, int y) {
    int result = x + y;   // x, y, result nằm trên stack của sum
    return result;
}
  • Khi gọi sum, một khung riêng cho nó được tạo trong stack.
  • Sau khi sum kết thúc, các biến của nó biến mất.

Vòng đời của biến

Biến cục bộ chỉ sống trong thời gian phương thức khai báo chúng đang thực thi. Ngay khi phương thức kết thúc — chúng không còn nữa, bộ nhớ được giải phóng ngay lập tức.

Tràn stack

Nếu bạn vô tình (hoặc cố ý) viết đệ quy vô tận, mỗi lần gọi phương thức sẽ thêm một khung mới vào stack. Đến một lúc nào đó stack sẽ hết, và bạn sẽ nhận được:

Exception in thread "main" java.lang.StackOverflowError

Ví dụ:

public static void main(String[] args) {
    recurse();
}
public static void recurse() {
    recurse(); // Đệ quy vô tận!
}

Kích thước stack

Kích thước stack bị giới hạn — thường là vài megabyte cho mỗi luồng (có thể đặt bằng tham số -Xss). Nếu stack cạn — chương trình sẽ lỗi.

3. Heap: nơi dành cho các đối tượng của bạn

Heap là vùng bộ nhớ dùng chung cho tất cả các luồng, nơi mọi đối tượng bạn tạo ra bằng new cũng như mảng sẽ “sống”. Chính trong heap diễn ra mọi “phép màu” của lập trình hướng đối tượng.

Đối tượng vào heap như thế nào?

String s = new String("Hello");
int[] arr = new int[10];
  • Biến s — là tham chiếu, nó nằm trong stack.
  • Bản thân đối tượng String và mảng arr — nằm trong heap.

Vòng đời của đối tượng

Đối tượng sống trong heap miễn là còn ít nhất một tham chiếu mạnh (strong reference) tới nó. Khi không còn ai tham chiếu — nó trở thành “rác” và có thể bị loại bỏ bởi bộ gom rác (GC).

Quản lý bộ nhớ

Khác với C/C++, nơi bạn phải tự giải phóng bộ nhớ (free, delete), trong Java việc này do GC đảm nhiệm. Bạn không thể giải phóng đối tượng một cách tường minh, nhưng có thể đặt mọi tham chiếu tới nó về null — khi đó nó trở thành ứng viên để xóa.

Sơ đồ: cái gì nằm ở đâu?

Stack (main)
  └─ s ─┬────────────┐
         │           │
         ▼           │
      Heap           │
   ┌─────────────┐   │
   │ String "Hello"◄──┘
   └─────────────┘

Đặc điểm của heap

  • Chỉ có một heap cho toàn bộ tiến trình JVM.
  • Có thể đặt kích thước heap khi chạy (-Xmx, -Xms).
  • Nếu trong heap không còn chỗ trống và GC không thể giải phóng thêm — chương trình sẽ lỗi OutOfMemoryError.

4. PermGen và MetaSpace: nơi các lớp “sống”?

Khi bạn viết class MyClass { ... } và chạy chương trình, JVM cần nơi để lưu mọi thứ liên quan đến lớp đó — phương thức, trường, bytecode, biến tĩnh, hằng và cả literal chuỗi. Vì vậy trong JVM có một vùng bộ nhớ đặc biệt nơi “các lớp sống”.

Trước đây, trước Java 8, vùng này gọi là PermGen (Permanent Generation). Nhưng nó có không ít vấn đề — ví dụ kích thước cố định, và nếu không đủ chỗ, ứng dụng sẽ sập với lỗi OutOfMemoryError: PermGen space.

Kể từ Java 8 có một vùng mới linh hoạt hơn — MetaSpace. Nó thay thế PermGen cũ và giờ có thể tự động mở rộng, dùng nhiều bộ nhớ như hệ thống cần (trong giới hạn bộ nhớ vật lý sẵn có).

PermGen (trước Java 8)

  • Trong PermGen lưu metadata lớp, trường tĩnh, literal chuỗi.
  • Kích thước PermGen bị giới hạn (mặc định nhỏ), có thể tăng bằng tham số -XX:MaxPermSize=256m.
  • Nếu ứng dụng nạp động nhiều lớp (ví dụ trên máy chủ web), PermGen có thể “hết chỗ”, và bạn nhận lỗi:
java.lang.OutOfMemoryError: PermGen space
  • Vấn đề: việc dọn dẹp PermGen không phải lúc nào cũng chính xác khi các lớp được dỡ nạp động (ví dụ khi reload ứng dụng web).

MetaSpace (Java 8+)

  • Từ Java 8 PermGen biến mất và xuất hiện MetaSpace.
  • MetaSpace lưu metadata lớp, nhưng nay trong bộ nhớ native (ngoài Java Heap).
  • Kích thước MetaSpace mặc định không giới hạn (chỉ bị giới hạn bởi bộ nhớ hệ thống), nhưng có thể đặt giới hạn qua -XX:MaxMetaspaceSize=512m.
  • Lỗi khi thiếu bộ nhớ giờ trông như sau:
java.lang.OutOfMemoryError: Metaspace
  • Trong MetaSpace cũng có các trường tĩnh, phương thức và thông tin về lớp.

Sơ đồ: mọi thứ được tổ chức thế nào

┌───────────────────────────────┐
│ Tiến trình JVM                │
│ ┌─────────────┐               │
│ │  Stack      │ ← Biến cục bộ, lời gọi phương thức
│ └─────────────┘               │
│ ┌─────────────┐               │
│ │  Heap       │ ← Đối tượng, mảng, mọi thứ tạo bằng new
│ └─────────────┘               │
│ ┌───────────────┐             │
│ │ MetaSpace     │ ← Metadata lớp, trường tĩnh
│ └───────────────┘             │
└───────────────────────────────┘

Tại sao điều này quan trọng?

Nếu bạn viết ứng dụng desktop hoặc server thông thường, nhiều khả năng bạn sẽ không bao giờ gặp lỗi PermGen hay MetaSpace. Nhưng nếu làm việc với tải/nạp lớp động (ví dụ plugin, ứng dụng web, các framework như Spring có thể nạp và dỡ rất nhiều lớp), thì hiểu biết về MetaSpace là điều must-have!

5. Minh họa: Sơ đồ bộ nhớ JVM

flowchart TD
    subgraph JVM
        direction TB
        Stack1["Stack (Thread 1)"]
        Stack2["Stack (Thread 2)"]
        Heap[Heap]
        MetaSpace[MetaSpace]
    end
    Stack1 --tham chiếu đến--> Heap
    Stack2 --tham chiếu đến--> Heap
    Heap --sử dụng các lớp từ--> MetaSpace
  • Mỗi luồng có stack riêng.
  • Tất cả các stack có thể tham chiếu tới đối tượng trong heap.
  • Các đối tượng trong heap “biết” lớp của chúng; thông tin lớp nằm trong MetaSpace.

6. Ví dụ: trông như thế nào trong mã thực tế

public class MemoryDemo {
    public static void main(String[] args) {
        int x = 42; // x nằm trên stack của main
        String s = "Hello!"; // s là tham chiếu trên stack; đối tượng String ở heap; literal "Hello!" ở MetaSpace
        Person p = new Person("Alice"); // p là tham chiếu trên stack; đối tượng Person ở heap

        // Gọi phương thức để tạo stack frame mới
        printPerson(p);
    }

    public static void printPerson(Person person) {
        // person là tham chiếu trên stack của printPerson
        System.out.println(person.getName());
    }
}

class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() { return name; }
}

Phân tích:

  • x — biến cục bộ, sống trong stack của phương thức main.
  • s — tham chiếu trong stack, đối tượng String ở heap, và literal chuỗi "Hello!" — ở MetaSpace.
  • p — tham chiếu trong stack, đối tượng Person ở heap.
  • Lớp Person và mọi phương thức/trường của nó — ở MetaSpace (metadata lớp).
  • Lời gọi printPerson(p) tạo một stack frame mới; bên trong nó, tham chiếu cục bộ person trỏ tới cùng một đối tượng trong heap.

7. JVM quản lý bộ nhớ như thế nào: FAQ ngắn

Tôi có thể quản lý stack không?
Không, stack hoàn toàn do JVM kiểm soát. Bạn chỉ có thể đặt kích thước khi chạy (-Xss).

Tôi có thể quản lý heap không?
Một phần: kích thước heap được đặt khi chạy (-Xmx, -Xms). Việc dọn dẹp do bộ gom rác (GC) đảm nhiệm.

Tôi có thể quản lý MetaSpace không?
Có thể giới hạn kích thước (-XX:MaxMetaspaceSize), nhưng thường không cần.

Điều gì xảy ra khi thiếu bộ nhớ?
— Nếu hết stack — StackOverflowError.
— Nếu hết heap — OutOfMemoryError: Java heap space.
— Nếu hết MetaSpace — OutOfMemoryError: Metaspace.

8. Lỗi thường gặp khi làm việc với bộ nhớ

Lỗi số 1: StackOverflowError do đệ quy vô tận. Nguyên nhân phổ biến nhất — quên điều kiện thoát khỏi đệ quy. Ví dụ, phương thức tự gọi chính nó không điểm dừng. JVM không thể mở rộng stack vô hạn, và chương trình sẽ “ngã”.

Lỗi số 2: OutOfMemoryError do tràn heap. Nếu bạn tạo quá nhiều đối tượng mà vẫn còn biến/bộ sưu tập tham chiếu đến chúng (ví dụ thêm phần tử vào danh sách nhưng không bao giờ xóa), heap có thể cạn.

Lỗi số 3: OutOfMemoryError: PermGen space / Metaspace. Nếu bạn dùng plugin hoặc nạp động rất nhiều lớp, và MetaSpace không được dọn (ví dụ do dỡ lớp không đúng cách), MetaSpace có thể hết chỗ.

Lỗi số 4: Nhầm lẫn giữa tham chiếu và đối tượng. Nhiều người mới nhầm: biến kiểu Person trong stack — chỉ là tham chiếu, còn bản thân đối tượng — ở heap.

Lỗi số 5: Kỳ vọng bộ gom rác xóa mọi thứ ngay lập tức. GC hoạt động “tùy tâm trạng” (thực ra — theo thuật toán nội bộ và khi thiếu bộ nhớ), chứ không phải ngay khi bạn đặt tham chiếu về null. Đừng trông chờ giải phóng bộ nhớ tức thì.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION