1. Quản lý bộ nhớ trong Java
Trong Java, quản lý bộ nhớ (memory management) là quá trình cấp phát và thu hồi bộ nhớ cho các đối tượng trong chương trình. JVM và Garbage Collection trong Java sẽ tự động thực hiện nhiệm vụ này giúp lập trình viên.
Tuy nhiên, hiểu về cơ chế quản lý bộ nhớ cũng là một việc giúp lập trình viên am hiểu, tự tin hơn khi lập trình với Java. Hơn nữa, một số trường hợp về ngoại lệ (exception) liên quan đến bộ nhớ trong Java sẽ được nắm bắt và khắc phục tốt hơn. Để tìm hiểu về quản lý bộ nhớ trong Java, chúng ta cần tìm hiểu 2 vấn đề chính:
- Kiến trúc bộ nhớ trong Java
- Hoạt động của Garbage Collection
1.1. Kiến trúc bộ nhớ trong Java
Khi chạy chương trình Java, JVM sẽ yêu cầu hệ điều hành (Windows, Linux, Mac,..) cấp phát một không gian bộ nhớ trong RAM. JVM sẽ phân chia không gian bộ nhớ này được cấp phát thành các vùng nhớ khác nhau để lưu những dữ liệu cần thiết cho việc chạy chương trình.
Vùng nhớ Class (Method) Area
Giúp lưu trữ dữ liệu cho mỗi lớp (class). Mỗi lớp thì có dữ liệu gì? Đó có thể là tên lớp, lớp đó có access modifier gì, các thuộc tính và phương thức của lớp, các đoạn code của phương thức,…Lưu ý, vùng nhớ này lưu thông tin về lớp (class), chứ không phải đối tượng (object) của lớp (class).
Vùng nhớ Heap
Là vùng nhớ được sử dụng trong khi đang chạy chương trình (runtime). Vùng nhớ heap giúp lưu trữ tất các đối tượng (object) và mảng (array) được tạo ra với từ khóa new trong Java. Dung lượng sử dụng của heap sẽ tăng giảm phụ thuộc vào các object được tạo ra.
Thời gian tồn tại của object phụ thuộc vào Garbage Collector (GC) của Java. Khi một object bị null hoặc không còn được sử dụng thì GC sẽ xóa object khỏi bộ nhớ.
Dung lượng vùng heap thường lớn hơn vùng nhớ stack.
Vùng nhớ Stack
Bộ nhớ để lưu các biến cục bộ (local variable), các lời gọi hàm, các đối số được truyền vào hàm hay các biến tham chiếu. Các biến cục bộ ở đây bao gồm các biến có kiểu dữ liệu nguyên thuỷ (primitive). Biến tham chiếu là các biến đại diện cho một đối tượng (object) được lưu trữ trong vùng nhớ heap.
Khi hàm được gọi thì một vùng nhớ được tạo ra trong stack. Vùng nhớ này lưu lời gọi hàm, các biến trong hàm đó. Khi hàm thực hiện xong, vùng nhớ cho hàm trong stack sẽ được giải phóng.
Bộ nhớ stack thường có dung lượng nhỏ.
Vùng nhớ Program Counter Register
Là vùng nhớ được tạo ra khi khởi tạo một Thread và dành riêng cho Thread đó.
Vùng nhớ Native Method Stack
Còn được gọi là C stack, native method stack không lưu chương trình Java mà lưu các hàm của ngôn ngữ C gọi các hàm của Java. Bộ nhớ này được cấp phát cho mỗi Thread khi nó được tạo.
Ví dụ bộ nhớ Heap và Stack khi chạy chương trình Java
Giả sử có chương trình Java đơn giản như bên dưới:
public class Main {
//thread chạy hàm main() và lưu trong stack
public static void main(String[] args) {
//biến i có kiểu dữ liệu primitive trong hàm main() lưu trong stack
int i=1;
//Đối tượng obj lưu trong heap và biến tham chiếu obj lưu trong stack
Object obj = new Object();
//Đối tượng ojbnew lưu trong heap và biến tham chiếu objnew lưu trong stack
Main objnew = new Main();
//Hàm foo() được gọi và lưu trong stack
objnew.foo(obj);
}
private void foo(Object p) {
//Đối tượng str được lưu trong String Pool của heap và biến tham chiếu str lưu trong stack
String str = p.toString();
System.out.println(str);
}
}
Dữ liệu được lưu trong vùng nhớ Heap và Stack như sau:
– Khi chạy chương trình, một Thread sẽ khởi tạo và sẽ gọi hàm main()
. Một vùng nhớ được tạo trong stack cho hàm main()
.
int i=1;
một biến local được tạo ra có kiểu dữ liệu primitive được lưu trong cùng vùng nhớ của hàm main()
.
Object obj = new Object();
một đối tượng được tạo sẽ được lưu trong bộ nhớ Heap và biến tham chiếu obj được lưu trong Stack của hàm main()
.
Main objnew = new Main();
một đối tượng được tạo sẽ được lưu trong bộ nhớ Heap và biến tham chiếu objnew được lưu trong Stack của hàm main()
.
– Khi hàm foo()
được gọi, một vùng nhớ trong stack được cấp cho hàm foo()
.
Tham số Object p của hàm foo()
: một biến tham chiếu cục bộ p được tạo và lưu trong cùng vùng nhớ của hàm foo()
.
String str=p.toString();
một biến String được tạo ra và lưu trong String Pool của vùng nhớ Heap. Biến tham chiếu str được lưu trong cùng vùng nhớ của hàm foo()
.
– Khi hàm foo()
thực hiện xong, vùng nhớ lưu trữ của nó trong Stack sẽ được giải phóng. Sau đó, hàm main()
kết thúc và bộ nhớ trong Stack cho hàm main()
cũng được giải phóng.
1.2. Garbage Collection (GC) trong Java hoạt động như thế nào?
Máy ảo JVM trong Java được sử dụng phổ biến là Java HotSpot. Java HotSpot có nhiều chương trình Garbage Collection (GC) chạy nền trong nó. GC có nhiệm vụ theo dõi toàn bộ các object trong bộ nhớ Heap và tìm ra những object nào không được sử dụng nữa để xóa và thu hồi vùng nhớ của chúng.
Một câu hỏi đặt ra là “Một đối tượng như thế nào là không được sử dụng nữa?”. Đó là các đối tượng không được tham chiếu nữa. Các trường hợp sau là đối tượng không được tham chiếu nữa:
Trường hợp 1: Đối tượng được gán là null
Employee e=new Employee();
e=null;
Trường hợp 2: Gán đối tượng đến một tham chiếu khác
Employee e1=new Employee();
Employee e2=new Employee();
e1=e2;
Trường hợp 3: Một đối tượng anonymous, đối tượng này không có biến tham chiếu đến nó
new Employee();
Quá trình thu gom rác của GC gồm 3 bước cơ bản là:
Bước 1: Marking
Là bước đánh dấu những object còn sử dụng và những object không còn sử dụng.
Bước 2: Normal deletion
Trình Garbage Collection sẽ xóa các object không còn sử dụng.
Bước 3: Deletion with Compacting
Sau khi những object không còn được sử dụng bị xóa, những object còn được sử dụng sẽ được gom lại gần nhau. Điều này giúp làm tăng hiệu suất sử dụng bộ nhớ trống để cấp phát cho những object mới.
2. Thiết lập dung lượng bộ nhớ Heap và Stack trong Java
Đầu tiên, các bạn hãy chắc chắn đã cài đặt JDK trên Windows 10. Các bước thiết lập dung lượng bộ nhớ Heap và Stack của Java trong Windows 10 như sau:
Bước 1: Vào Control Panel, chọn Programs rồi chọn Java.
Bước 2: Hộp thoại Java Control Panel xuất hiện, chuyển qua tab Java rồi chọn View…
Bước 3: Thiết lập dung lượng vùng nhớ Heap và Stack
Chúng ta có thể sử dụng các cờ điều chỉnh tham số (tuning flag) để thiết lập dung lượng vùng nhớ Heap và Stack:
- Sử dụng -Xms để thiết lập kích thước bộ nhớ Heap lúc khởi tạo
- Sử dụng -Xmx để xác định kích thước tối đa của bộ nhớ Heap
- Sử dụng -Xss để xác định kích thước bộ nhớ Stack
Ví dụ:
-Xms64m hoặc -Xms64M //64 Megabyte
-Xmx2g hoặc -Xmx2G //1 Gigabyte
-Xss512m hoặc -Xss512M //512 Megabyte
Với hệ thống 32bit thì có thể thiết lập kích thước vùng nhớ tối đa 4GB, còn hệ thống 64bit thì có thể tăng kích thước lớn hơn. Để hiểu tại sao lại như vậy, các bạn có thể đọc lại kiến thức về Độ rộng thanh ghi của CPU.
Lưu ý: Khi bộ nhớ stack đầy, Java runtime sẽ ném ra ngoại lệ java.lang.StackOverFlowError
. Khi bộ nhớ heap đầy, Java runtime sẽ ném ra ngoại lệ java.lang.OutOfMemoryError
.