Stack Memory và Heap Memory

1. Cấu trúc và cơ chế quản lý

  • Stack Memory:
    • Stack là một vùng bộ nhớ có cấu trúc lifo (last in, first out).
    • Khi một biến được khai báo (ví dụ: biến local hoặc biến trong hàm), nó được thêm trực tiếp vào stack. Khi biến không còn trong phạm vi (scope), nó sẽ tự động bị loại bỏ mà không cần sự can thiệp của trình quản lý bộ nhớ.
    • Việc cấp phát và giải phóng bộ nhớ trong stack rất nhanh, vì nó chỉ cần thay đổi con trỏ stack để quản lý các giá trị.
  • Heap Memory:
    • Heap là một vùng bộ nhớ lớn hơn và phức tạp hơn.
    • Khi một đối tượng được khởi tạo trên heap (thường bằng từ khóa new), bộ nhớ cần phải được quản lý thông qua Garbage Collector (GC), điều này làm chậm quá trình truy cập do GC cần thực hiện các bước thu thập, dọn dẹp, và tối ưu hóa bộ nhớ.

2. Thay đổi con trỏ stack là gì?

Trong Stack Memory, hệ điều hành sử dụng một con trỏ gọi là Stack Pointer (SP) để đánh dấu vị trí hiện tại của đỉnh stack. Mỗi khi một biến hoặc dữ liệu được thêm vào stack, con trỏ này dịch chuyển xuống dưới (tăng kích thước stack). Ngược lại, khi một biến ra khỏi phạm vi (scope) hoặc hàm hoàn thành, con trỏ dịch chuyển lên trên để loại bỏ dữ liệu.

Ví dụ: Cấp phát và giải phóng bộ nhớ trên stack

2.1. Phân tích với một method đơn giản:

void ExampleMethod()
{
    int a = 10;      // Biến local 1
    float b = 20.5f; // Biến local 2
    int c = a + 5;   // Biến local 3
}

Khi phương thức ExampleMethod() được gọi, các bước xảy ra như sau:

2.2. Quy trình cấp phát bộ nhớ trong Stack

  1. Khi hàm được gọi:
    • Stack Memory được chuẩn bị để lưu dữ liệu của hàm.
    • Bộ nhớ dành cho các biến local a, b, c sẽ được cấp phát tuần tự từ trên xuống.
  2. Dữ liệu lưu trữ:
    • Con trỏ stack bắt đầu tại một vị trí (giả sử SP = X).
    • Khi biến a được khai báo, stack cấp phát một phần bộ nhớ (4 bytes cho kiểu int).
      • SP được thay đổi: SP = X - 4.
    • Tiếp theo, biến b được khai báo (4 bytes cho kiểu float).
      • SP tiếp tục giảm: SP = X - 8.
    • Cuối cùng, c được tính toán và lưu trữ (4 bytes cho kiểu int).
      • SP giảm thêm: SP = X - 12.
  3. Bộ nhớ stack sau khi cấp phát:
(Stack bắt đầu từ địa chỉ cao và giảm dần)

|       4 bytes (c = 15)       | (SP=X-12)
|------------------------------|
|       4 bytes (b = 20.5)     | (SP=X-8)
|------------------------------|
|       4 bytes (a = 10)       | (SP=X-4)
|------------------------------| (SP=X) 

2.3. Giải phóng bộ nhớ khi hàm hoàn thành

Khi phương thức ExampleMethod() kết thúc:

  1. Toàn bộ các biến local (a, b, c) không còn cần thiết.
  2. Con trỏ stack (SP) trở về vị trí ban đầu (tăng lên lại SP = X), mà không cần xóa dữ liệu từng biến cụ thể.
  3. Phần bộ nhớ này sẵn sàng được sử dụng lại cho các hàm khác.

2.4. Quy trình cấp phát cho phương thức tiếp theo

Giả sử bạn có một phương thức mới như sau:

void AnotherMethod()
{
    short x = 5;   // 2 bytes
    int y = 10;    // 4 bytes
}
  • Khi AnotherMethod được gọi, nó cần 6 bytes (2 bytes cho x và 4 bytes cho y).
  • Stack pointer sẽ giảm thêm 6 bytes so với vị trí hiện tại của nó.
  • Nếu không có bất kỳ ghi đè nào xảy ra (do hành động của AnotherMethod), dữ liệu cũ (của ExampleMethod) có thể vẫn tồn tại trong stack nhưng sẽ không ảnh hưởng vì:
    • Các biến của ExampleMethod không còn trong phạm vi (scope).
    • Chỉ AnotherMethod có quyền truy cập tới vùng nhớ mới được cấp phát (6 bytes).

2.5. Không ghi đè dữ liệu nếu không dùng

Khi stack pointer di chuyển để cấp phát cho AnotherMethod, các khu vực không sử dụng của stack sẽ chỉ được ghi đè khi:

  1. Biến mới được khởi tạo và ghi giá trị vào bộ nhớ.
  2. Nếu vùng nhớ đó không được ghi đè ngay, dữ liệu cũ (như từ ExampleMethod) vẫn còn nhưng không được sử dụng vì không có cách nào tham chiếu đến nó.

Dưới đây là cách stack hoạt động:

Ban đầu: Khi ExampleMethod chạy

|   4 bytes (c = 15)      |
|--------------------------|
|   4 bytes (b = 20.5)    |
|--------------------------|
|   4 bytes (a = 10)      |
|--------------------------|

Khi ExampleMethod kết thúc

  • Stack pointer trở về vị trí trước khi ExampleMethod được gọi.
  • Bộ nhớ của a, b, c vẫn tồn tại nhưng được đánh dấu là không hợp lệ.

Khi AnotherMethod chạy

|   4 bytes (y = 10)      |   <- `AnotherMethod` sử dụng 4 bytes cho `y`
|--------------------------|
|   2 bytes (x = 5)       |   <- `AnotherMethod` sử dụng 2 bytes cho `x`
|--------------------------|
|  (Dữ liệu cũ, không dùng)|
|--------------------------|
  • xy ghi đè lên một phần bộ nhớ của ExampleMethod, nhưng chỉ ở những nơi mà chúng được cấp phát.

3. Ví dụ minh họa 2

Giải thích mã nguồn

a. Khai báo và khởi tạo Person p

Person p = new Person();
  • Bước 1: Person p được khai báo
    • Biến p được khai báo trong phương thức Main.
    • Biến này là một tham chiếu (reference), được lưu trữ trong Stack Memory.
    • Ban đầu, p không trỏ đến bất kỳ đối tượng nào trên Heap.
  • Bước 2: new Person()
    • Lệnh new Person() khởi tạo một đối tượng mới thuộc lớp Person.
    • Đối tượng này được lưu trong Heap Memory.
    • Mặc định:
      • Thuộc tính name (kiểu string) được khởi tạo với giá trị null.
      • Thuộc tính age (kiểu int) được khởi tạo với giá trị 0.
  • Bước 3: Gán giá trị cho p
    • Giá trị trả về từ new Person() là địa chỉ của vùng nhớ trên Heap, nơi đối tượng Person được lưu trữ.
    • Địa chỉ này được gán cho biến p trong Stack.
    • Kết quả:
      • p lưu địa chỉ của đối tượng trên Heap.

b. Kết quả lưu trữ trong bộ nhớ

Sau khi thực hiện lệnh Person p = new Person();, trạng thái bộ nhớ sẽ như sau:

Stack Memory:

  • Biến p được lưu trên Stack.
  • Giá trị của pđịa chỉ tham chiếu (ví dụ: 0x123456) trỏ đến đối tượng trên Heap.

Heap Memory:

  • Đối tượng Person được lưu trữ trong Heap, với các thuộc tính:
    • name = null (kiểu string ban đầu là null vì chưa được khởi tạo).
    • age = 0 (kiểu int mặc định là 0).

c. Diễn giải bộ nhớ qua hình minh họa

Dựa trên hình ảnh và mã nguồn:

Stack:

  • Lưu trữ biến cục bộ p:
p -> 0x123456  (địa chỉ trỏ tới đối tượng trên Heap)

Heap:

  • Lưu trữ đối tượng Person:
Person
-------
name = null
age  = 0
  • Đối tượng này được lưu tại địa chỉ 0x123456 trên Heap, và biến p trên Stack trỏ tới địa chỉ này.

d. Khi phương thức Main kết thúc

  • Stack:
    • Biến p trên Stack bị xóa, vì phạm vi của phương thức Main đã kết thúc.
  • Heap:
    • Đối tượng Person trên Heap không được giải phóng ngay lập tức.
    • Nó sẽ trở thành “rác” nếu không còn tham chiếu nào trỏ đến nó.
    • Garbage Collector (GC) sẽ thu gom bộ nhớ của đối tượng này trong một chu kỳ thu gom rác sau đó.

4. Cấp phát bộ nhớ

Ví dụ minh họa

Mã nguồn:

int[] arrs = new int[1000];
Phân tích cấp phát bộ nhớ:
  1. int[] arrs (biến tham chiếu):
    • Biến arrs là một reference type, được lưu trên Stack.
    • Nó lưu địa chỉ của mảng int[1000] trên Heap.
  2. new int[1000] (khởi tạo mảng):
    • Mảng int[1000] được tạo trên Heap Memory.
    • Bộ nhớ trên Heap được cấp phát để lưu trữ 1000 phần tử, mỗi phần tử là một int (4 byte trên hệ thống 32-bit hoặc 64-bit).
  3. Tổng kích thước bộ nhớ cấp phát:
    • Stack Memory:
      • Biến arrs lưu trữ một tham chiếu (địa chỉ) trỏ tới mảng trên Heap.
      • Ví dụ: arrs -> 0x123456 (địa chỉ của mảng trên Heap).
    • Heap Memory:
      • Mảng int[1000] chiếm: 1000 × 4 byte = 4000 byte (4 KB).
      • Bộ nhớ này được quản lý bởi Garbage Collector.
Cách lưu trữ bộ nhớ:
  • Stack:arrs -> 0x123456 (Biến arrs trỏ tới địa chỉ của mảng trên Heap.)
  • Heap:0x123456: [int(0)][int(0)][int(0)] ... [int(0)]
    (Heap lưu mảng int[1000], mỗi phần tử được khởi tạo với giá trị mặc định là 0.)

Hành vi bộ nhớ khi kết thúc phạm vi

  • Khi phạm vi sử dụng biến arrs kết thúc (ví dụ: khi phương thức chứa dòng lệnh trên kết thúc):
    • Stack:
      • Biến arrs bị xóa khỏi Stack.
    • Heap:
      • Mảng int[1000] vẫn nằm trên Heap, nhưng không còn tham chiếu nào trỏ đến nó.
      • Garbage Collector (GC) sẽ thu gom mảng này trong một chu kỳ thu gom rác tiếp theo.

5. Vị trí trong bộ nhớ và khả năng truy cập

  • Stack Memory có vị trí liền mạch trong RAM, do đó CPU có thể truy cập nhanh hơn thông qua địa chỉ trực tiếp.
  • Heap Memory thường bị phân mảnh do việc cấp phát và giải phóng bộ nhớ không đồng đều. Điều này dẫn đến thời gian tìm kiếm và truy cập lâu hơn so với stack.

    Dung lượng Stack Memory
  • Kích thước stack được giới hạn cố định bởi hệ điều hành hoặc môi trường runtime:
    • Windows: Thông thường, mỗi luồng (thread) được cấp khoảng 1 MB stack memory.
    • Unity (C#): Kích thước mặc định của stack là 1 MB cho mỗi luồng (thread) chính.
  • Nếu bộ nhớ stack bị sử dụng vượt quá giới hạn này, sẽ xảy ra Stack Overflow.

6. Chi phí quản lý bộ nhớ

  • Stack Memory:
    • Không có chi phí quản lý lớn, vì bộ nhớ được tự động cấp phát và giải phóng dựa trên phạm vi của hàm hoặc khối lệnh.
    • Do đó, hiệu năng cao hơn khi truy cập.
  • Heap Memory:
    • Yêu cầu dynamic memory allocation (cấp phát bộ nhớ động) và cần Garbage Collector để quản lý việc thu hồi bộ nhớ.
    • Quá trình này làm tăng chi phí và ảnh hưởng đến hiệu năng.

7. Ứng dụng thực tế

  • Stack Memory thường được dùng cho các biến giá trị (value type) hoặc dữ liệu tạm thời như các biến trong hàm.
  • Heap Memory phù hợp với các đối tượng phức tạp hoặc dữ liệu cần tồn tại trong thời gian dài, chẳng hạn như reference type (object, string, array…).

Để lại một bình luận 0

Your email address will not be published. Required fields are marked *