Trước đây, chúng tôi đã có một vài bài giải thích về garbage collection và cơ chế truyền giá trị (pass by value) trong Java. Sau đó, chúng tôi nhận được nhiều bình luận yêu cầu giải thích thêm về bộ nhớ heap, stack, và việc cấp phát bộ nhớ nói chung trong Java.
Có rất nhiều tài liệu tham khảo về vấn đề này, tuy nhiên trong bài này chúng tôi sẽ giải thích một cách đầy đủ hơn về khác biệt giữa heap và stack trong Java với một chương trình ví dụ cụ thể.
Bộ nhớ Heap
Heap được Java runtime sử dụng để cấp phát bộ nhớ cho các đối tượng và các class của JRE. Bất cứ khi nào chúng ta tạo một object, nó luôn được tạo trong bộ nhớ heap.
Garbage collection (GC) chạy trên vùng này để giải phóng bộ nhớ được sử dụng bởi các object không còn tham chiếu nào. Mọi object được tạo trong heap đều có quyền truy cập global và có thể được tham chiếu từ bất kỳ đâu trong ứng dụng.
Bộ nhớ Stack
Bộ nhớ stack được sử dụng cho việc thực thi của một thread. Stack chứa các giá trị của các method (vốn có thời gian tồn tại ngắn) và các tham chiếu đến những object khác trong heap mà method đó đang tham chiếu tới.
Bộ nhớ stack luôn được tham chiếu theo thứ tự LIFO (Last-In-First-Out). Mỗi khi một method được gọi, một block bộ nhớ mới được tạo trong stack dành cho method đó để giữ các giá trị primitive cục bộ và tham chiếu đến các object khác trong method.
Ngay khi method dừng, block đó đực giải phóng và có thể được dùng cho method mới. Kích thước stack nhỏ hơn nhiều so với bộ nhớ heap.
Bộ nhớ Heap và Stack trong chương trình Java
Hãy cùng tìm hiểu việc sử dụng bộ nhớ heap và stack qua một chương trình đơn giản như sau.
package com.journaldev.test;
public class Memory {
public static void main(String[] args) { // Line 1
int i=1; // Line 2
Object obj = new Object(); // Line 3
Memory mem = new Memory(); // Line 4
mem.foo(obj); // Line 5
} // Line 9
private void foo(Object param) { // Line 6
String str = param.toString(); //// Line 7
System.out.println(str);
} // Line 8
}
Sơ đồ sau minh họa cách bộ nhớ heap và stack và cách chúng lưu trữ các giá trị primitive, object và biến tham chiếu.
Đây là các bước thực thi của chương trình trên:
- Khi chương trình bắt đầu, nó sẽ load tất cả các class Runtime vào heap. Khi tới hàm main() ở dòng 1, Java runtime tạo bộ nhớ stack để sử dụng cho thread của main().
- Đoạn code tạo biến cục bộ primitive ở dòng 2 (int i=1), vì vậy nó được tạo và lưu trữ trong bộ nhớ stack của main().
- Vì chúng ta đang tạo một object ở dòng 3, nó được tạo trong heap trong khi bộ nhớ stack chứa tham chiếu đến nó. Quá trình tương tự xảy ra khi chúng ta tạo object Memory ở dòng 4.
- Bây giờ khi chúng ta gọi foo() ở dòng 5, một block ở đầu stack được tạo ra để sử dụng cho hàm này. Vì Java là ngôn ngữ truyền giá trị (pass-by-value), một tham chiếu mới đến object được tạo trong khối stack của foo() ở dòng 6.
- Một string được tạo ở dòng 7, nó được đưa vào string pool trong heap và một tham chiếu tới nó được tạo trong không gian stack của foo().
- Hàm foo() kết thúc ở dòng 8, lúc này khối bộ nhớ được cấp phát cho foo() trong stack sẽ được giải phóng.
- Ở dòng 9, main() kết thúc và bộ nhớ stack được tạo cho main() được giải phóng. Chương trình cũng kết thúc ở dòng này, do đó Java runtime cũng giải phóng toàn bộ bộ nhớ và kết thúc việc thực thi chương trình.
Sự khác biệt giữa bộ nhớ Heap và Stack trong Java
Từ những giải thích ở trên, chúng ta có thể dễ dàng kết luận điểm khác biệt sau đây giữa bộ nhớ heap và stack.
- Bộ nhớ heap được sử dụng bởi tất cả các phần của ứng dụng, trong khi mỗi bộ nhớ stack chỉ được sử dụng bởi một luồng (thread) thực thi nhất định.
- Bất cứ khi nào một object được tạo, nó luôn được lưu trữ trong heap còn bộ nhớ stack chứa tham chiếu đến nó. Stack chỉ chứa các biến cục bộ primitive và các biến tham chiếu đến các object trong heap.
- Các object được lưu trữ trong heap có thể truy cập từ mọi nơi trong chương trình, trong khi bộ nhớ stack không thể được truy cập bởi các thread khác.
- Việc quản lý bộ nhớ trong stack được thực hiện theo kiểu LIFO, trong khi ở bộ nhớ heap thì phức tạp hơn vì nó được sử dụng toàn cục. Heap được chia thành Young-Generation, Old-Generation, v.v.
- Bộ nhớ stack có thời gian tồn tại ngắn, trong khi bộ nhớ heap tồn tại từ khi bắt đầu cho đến khi kết thúc thực thi ứng dụng.
- Chúng ta có thể sử dụng tùy chọn -Xms và -Xmx của JVM để thiết lập kích thước lúc khởi động và kích thước tối đa của bộ nhớ heap. Chúng ta có thể sử dụng -Xss để đặt kích thước bộ nhớ stack.
- Khi bộ nhớ stack đầy, Java sẽ gặp lỗi java.lang.StackOverFlowError, trong khi nếu bộ nhớ heap đầy, sẽ có lỗi java.lang.OutOfMemoryError: Java Heap Space.
- Kích thước bộ nhớ stack nhỏ hơn rất nhiều so với bộ nhớ Heap. Do sự đơn giản trong cơ cấu cấp phát bộ nhớ (LIFO), stack hoạt động nhanh hơn nhiều so với bộ nhớ heap.
Tổng kết
Vậy là chúng ta đã cùng nhau tìm hiểu về sự khác biệt giữa Java Heap Space và Stack Memory trong Java. Hy vọng bài viết này sẽ giúp bạn giải đáp những thắc mắc về việc cấp phát bộ nhớ trong quá trình chạy của một chương trình Java điển hình.