
Cấp phát bộ nhớ Stack và Heap
Việc hiểu rõ bộ nhớ stack và heap là điều cần thiết để viết các chương trình hiệu quả và đáng tin cậy.
Trong ngôn ngữ C, C++ và Java, bộ nhớ có thể được cấp phát theo hai hình thức: Stack (ngăn xếp) hoặc Heap (vùng nhớ động). Việc cấp phát bộ nhớ Stack diễn ra trong ngăn xếp lời gọi hàm (Call Stack), trong đó mỗi hàm được cấp một vùng nhớ riêng để lưu trữ các biến. Trong C/C++, bộ nhớ Heap được quản lý bởi lập trình viên vì các ngôn ngữ này không có cơ chế thu gom rác tự động.
Cấp phát bộ nhớ Stack
Cấp phát bộ nhớ Stack (Stack Allocation) là quá trình cấp phát bộ nhớ cho các biến cục bộ và các lần gọi hàm trong Call Stack. Quá trình này diễn ra tự động khi một hàm được gọi và bộ nhớ sẽ được giải phóng ngay khi hàm kết thúc. Vì hệ thống tự quản lý bộ nhớ nên phương pháp này có tốc độ nhanh và hiệu quả, nhưng dung lượng lại hạn chế hơn so với cấp phát bộ nhớ Heap. Nếu số lượng lời gọi hàm vượt quá khả năng lưu trữ của Stack, lỗi Stack Overflow sẽ xảy ra.
Cách thức hoạt động của cấp phát bộ nhớ Stack
- Bộ nhớ được cấp phát thành các khối liên tiếp bên trong ngăn xếp lời gọi hàm (Call Stack).
- Kích thước bộ nhớ cần thiết được xác định trước khi chương trình bắt đầu thực thi.
- Khi một hàm được gọi, các biến cục bộ của hàm đó sẽ được cấp phát trên Stack.
- Khi hàm thực thi xong, vùng nhớ đã cấp phát sẽ tự động được giải phóng.
- Lập trình viên không cần tự quản lý quá trình cấp phát hoặc giải phóng bộ nhớ.
- Vì bộ nhớ Stack được giải phóng khi hàm kết thúc, phương pháp này còn được gọi là cấp phát bộ nhớ tạm thời.
Các đặc điểm chính của cấp phát bộ nhớ Stack
- Bộ nhớ chỉ khả dụng trong thời gian hàm đang thực thi.
- Việc giải phóng bộ nhớ diễn ra tự động khi hàm kết thúc.
- Nếu bộ nhớ Stack bị đầy, các lỗi như Java.lang.StackOverflowError trong Java và lỗi segmentation fault trong C++ có thể xảy ra.
- An toàn hơn bộ nhớ Heap vì dữ liệu có thể chỉ được truy cập bởi luồng (thread) sở hữu vùng nhớ đó.
- Tốc độ nhanh hơn so với cấp phát bộ nhớ Heap nhờ cơ chế quản lý bộ nhớ tự động.
Ví dụ mã nguồn:
int main() {
// All these variables get memory
// allocated on stack
int a;
int b[10];
int n = 20;
int c[n];
}Cấp phát bộ nhớ Heap
Bộ nhớ Heap được cấp phát động trong quá trình chương trình thực thi. Khác với bộ nhớ Stack, bộ nhớ Heap không được tự động giải phóng khi hàm kết thúc. Thay vào đó, bộ nhớ phải được giải phóng thủ công (trong C/C++) hoặc thông qua bộ thu gom rác (Garbage Collector) (trong Java hoặc Python) để thu hồi những vùng nhớ không còn được sử dụng.
Tên gọi "Heap" không liên quan đến cấu trúc dữ liệu Heap. Thuật ngữ này chỉ một vùng bộ nhớ lớn sẵn có để cấp phát động bất cứ khi nào một đối tượng được tạo ra. Khi một đối tượng được khởi tạo, đối tượng đó sẽ được lưu trữ trong bộ nhớ Heap, trong khi các tham chiếu đến đối tượng được lưu trữ trong bộ nhớ Stack. Cấp phát bộ nhớ Heap kém an toàn hơn cấp phát Stack vì dữ liệu trong Heap có thể được nhiều luồng truy cập, làm tăng nguy cơ hỏng dữ liệu và rò rỉ bộ nhớ nếu không được quản lý đúng cách.
Các loại bộ nhớ Heap
Bộ nhớ Heap được chia thành ba loại, giúp ưu tiên việc lưu trữ đối tượng và thu gom rác:
- Thế hệ trẻ (Young Generation) – Được sử dụng để lưu trữ các đối tượng mới. Khi vùng nhớ này đầy, các đối tượng không còn được sử dụng sẽ bị bộ thu gom rác loại bỏ, trong khi những đối tượng còn tồn tại sẽ được chuyển sang Thế hệ già (Old Generation).
- Thế hệ già (Old Generation hoặc Tenured Generation) – Lưu trữ các đối tượng đã tồn tại lâu và ít được sử dụng. Những đối tượng này tiếp tục nằm trong bộ nhớ cho đến khi được xóa hoặc thu hồi.
- Bộ nhớ thường trú (Permanent Generation – PermGen) – Được sử dụng để lưu trữ siêu dữ liệu (metadata) của JVM, chẳng hạn như các lớp trong thời gian chạy và phương thức của ứng dụng.
Các đặc điểm chính của cấp phát bộ nhớ Heap
- Nếu bộ nhớ Heap đầy, JVM sẽ phát sinh lỗi: java.lang.OutOfMemoryError.
- Khác với bộ nhớ Stack, bộ nhớ Heap không được giải phóng tự động ngay khi hàm kết thúc. Cần sử dụng bộ thu gom rác để thu hồi những vùng nhớ không còn được sử dụng.
- Tốc độ chậm hơn bộ nhớ Stack do quá trình cấp phát bộ nhớ và thu gom rác cần thêm thời gian xử lý.
- Khả năng đảm bảo an toàn trong môi trường đa luồng thấp hơn vì bộ nhớ Heap được chia sẻ giữa tất cả các luồng.
- Nhìn chung, bộ nhớ Heap có dung lượng lớn hơn so với bộ nhớ Stack.
- Bộ nhớ Heap có thể tồn tại trong suốt thời gian ứng dụng đang chạy.
Ví dụ mã nguồn:
int main()
{
// This memory for 10 integers
// is allocated on heap.
int *ptr = new int[10];
}Để hiểu rõ sự khác biệt giữa cấp phát bộ nhớ Stack và Heap, chúng ta hãy quan sát cách các đối tượng được tạo ra và quản lý trong cả hai trường hợp thông qua lớp Emp dùng để lưu trữ thông tin nhân viên.
Dưới đây là các ví dụ minh họa:
C++
#include <bits/stdc++.h>
using namespace std;
class Emp {
public:
int id;
string emp_name;
// Constructor to initialize employee details
Emp(int id, string emp_name) {
this->id = id;
this->emp_name = emp_name;
}
};
// Function to create and return an Emp object
Emp Emp_detail(int id, string emp_name) {
return Emp(id, emp_name);
}
int main() {
// Initializing employee details
int id = 21;
string name = "Maddy";
// Creating an Emp object using the function
Emp person_ = Emp_detail(id, name);
return 0;
}Java
class Emp {
int id;
String emp_name;
public Emp(int id, String emp_name) {
this.id = id;
this.emp_name = emp_name;
}
}
public class Emp_detail {
private static Emp Emp_detail(int id, String emp_name) {
return new Emp(id, emp_name);
}
public static void main(String[] args) {
int id = 21;
String name = "Maddy";
Emp person_ = null;
person_ = Emp_detail(id, name);
}
}Python
class Emp:
# Constructor to initialize employee details
def __init__(self, id, emp_name):
self.id = id
self.emp_name = emp_name
# Function to create and return an Emp object
def Emp_detail(id, emp_name):
return Emp(id, emp_name)
if __name__ == "__main__":
# Initializing employee details
id = 21
name = "Maddy"
# Creating an Emp object using the function
person_ = None
person_ = Emp_detail(id, name)JavaScript
class Emp {
// Constructor to initialize employee details
constructor(id, emp_name) {
this.id = id;
this.emp_name = emp_name;
}
}
// Function to create and return an Emp object
function Emp_detail(id, emp_name) {
return new Emp(id, emp_name);
}
// Initializing employee details
let id = 21;
let name = "Maddy";
// Creating an Emp object using the function
let person_ = null;
person_ = Emp_detail(id, name);Từ các ví dụ trên, chúng ta có thể rút ra những nhận xét sau:
Cấp phát bộ nhớ Heap:
Khi chương trình bắt đầu thực thi, các lớp cần thiết trong thời gian chạy sẽ được lưu trữ trong bộ nhớ Heap.
Cấp phát bộ nhớ Stack:
- Phương thức main được lưu trữ trong Stack cùng với các biến cục bộ và biến tham chiếu của nó.
- Biến tham chiếu Emp có kiểu Emp_detail được lưu trữ trong Stack và trỏ đến đối tượng tương ứng trong bộ nhớ Heap.
Lời gọi Constructor và cấp phát bộ nhớ:
- Constructor có tham số Emp(int, string) được gọi từ hàm main và khung ngăn xếp (Stack Frame) của nó được đặt ở vị trí trên cùng của Stack.
- Bên trong Constructor: Các tham chiếu đối tượng được lưu trữ trong bộ nhớ Stack. Biến số nguyên nguyên thủy id cũng được lưu trữ trong Stack, trong khi biến tham chiếu chuỗi emp_name được lưu trữ trong Stack nhưng trỏ đến chuỗi thực tế nằm trong String Pool (bộ nhớ Heap).
Lời gọi hàm và cấp phát bộ nhớ:
- Khi hàm Emp_detail() được gọi từ hàm main, một Stack Frame mới được tạo trên Stack Frame trước đó.
- Đối tượng Emp mới được tạo cùng với tất cả các biến thể hiện (instance variables) sẽ được lưu trữ trong bộ nhớ Heap.
Hình minh họa được thể hiện trong sơ đồ bên dưới:

Những khác biệt chính giữa cấp phát bộ nhớ Stack và Heap
- Đối với cấu trúc dữ liệu Stack, quá trình cấp phát và giải phóng bộ nhớ được trình biên dịch quản lý tự động, trong khi đối với Heap, lập trình viên phải thực hiện các thao tác này thủ công.
- Chi phí quản lý bộ nhớ Heap cao hơn so với quản lý Stack Frame.
- Tình trạng thiếu hụt bộ nhớ thường xảy ra đối với bộ nhớ Stack, trong khi vấn đề chính của bộ nhớ Heap là hiện tượng phân mảnh bộ nhớ (Memory Fragmentation).
- Việc truy cập Stack Frame dễ dàng hơn Heap Frame vì Stack sử dụng một vùng bộ nhớ nhỏ và có khả năng tận dụng bộ nhớ đệm (Cache) tốt hơn. Ngược lại, các vùng nhớ Heap phân bố rải rác trong bộ nhớ nên dễ gây ra tình trạng trượt bộ nhớ đệm (Cache Miss) hơn.
- Cấu trúc dữ liệu Stack kém linh hoạt vì kích thước vùng nhớ đã cấp phát không thể thay đổi, trong khi Heap linh hoạt hơn và kích thước vùng nhớ đã cấp phát có thể được thay đổi.
- Thời gian truy cập bộ nhớ Heap thường lâu hơn so với bộ nhớ Stack.




