Phân bổ bộ nhớ: Stack vs Heap?


83

Tôi đang bối rối với những điều cơ bản về phân bổ bộ nhớ giữa Stack và Heap . Theo định nghĩa tiêu chuẩn (những điều mà mọi người đều nói), tất cả các Loại giá trị sẽ được phân bổ vào một Ngăn xếp và Các loại tham chiếu sẽ đi vào Heap .

Bây giờ hãy xem xét ví dụ sau:

class MyClass
{
    int myInt = 0;    
    string myString = "Something";
}

class Program
{
    static void Main(string[] args)
    {
       MyClass m = new MyClass();
    }
}

Bây giờ, việc cấp phát bộ nhớ sẽ diễn ra như thế nào trong c #? Liệu đối tượng của MyClass(nghĩa là, m) sẽ được cấp phát hoàn toàn cho Heap? Đó là, int myIntstring myStringcả hai sẽ đi đến đống?

Hoặc, đối tượng sẽ được chia thành hai phần và sẽ được cấp phát cho cả hai vị trí bộ nhớ đó là Stack và Heap?


Tôi đã bỏ phiếu đơn giản vì mặc dù những câu nói ngớ ngẩn bấy lâu nay là sai, tôi tin rằng sẽ có một số câu trả lời hay. Tuy nhiên, khá dễ dàng để đưa ra các đối số tầm thường chống lại "phân bổ kép" được đề xuất (gợi ý: một đối tượng lớp có thể - và nhiều người thường làm - tồn tại qua ranh giới gọi hàm).

Điều này có trả lời câu hỏi của bạn không? Stack và heap là gì và ở đâu?
Olivier Rogier

Câu trả lời:


55

mđược phân bổ trên heap và bao gồm myInt. Các tình huống mà các kiểu nguyên thủy (và cấu trúc) được cấp phát trên ngăn xếp là trong quá trình gọi phương thức, điều này cấp phát chỗ cho các biến cục bộ trên ngăn xếp (vì nó nhanh hơn). Ví dụ:

class MyClass
{
    int myInt = 0;

    string myString = "Something";

    void Foo(int x, int y) {
       int rv = x + y + myInt;
       myInt = 2^rv;
    }
}

rv, x, yTất cả sẽ được trên stack. myIntlà một nơi nào đó trên heap (và phải được truy cập thông qua thiscon trỏ).


7
Một phụ lục quan trọng là hãy nhớ rằng "ngăn xếp" và "đống" thực sự là chi tiết triển khai trong .NET. Hoàn toàn có thể tạo một triển khai hợp pháp của C # mà hoàn toàn không sử dụng phân bổ dựa trên ngăn xếp.
JSB ձոգչ

5
Tôi đồng ý rằng chúng nên được đối xử theo cách đó, nhưng không hoàn toàn đúng khi chúng hoàn toàn là chi tiết triển khai. Nó được ghi chú rõ ràng trong tài liệu API công khai và trong tiêu chuẩn ngôn ngữ (EMCA-334, ISO / IEC 23270: 2006) (nghĩa là "Các giá trị cấu trúc được lưu trữ 'trên ngăn xếp". Những người lập trình cẩn thận đôi khi có thể nâng cao hiệu suất thông qua việc sử dụng hợp lý các cấu trúc. ") Nhưng, vâng, nếu tốc độ phân bổ heap là một nút thắt cổ chai cho ứng dụng của bạn, có thể bạn đang làm sai (hoặc sử dụng sai ngôn ngữ).
Bùn

65

Bạn nên xem xét câu hỏi về nơi các đối tượng được phân bổ như một chi tiết triển khai. Đối với bạn chính xác nơi lưu trữ các bit của một đối tượng không quan trọng. Có thể quan trọng liệu một đối tượng là kiểu tham chiếu hay kiểu giá trị, nhưng bạn không phải lo lắng về nơi nó sẽ được lưu trữ cho đến khi bạn bắt đầu phải tối ưu hóa hành vi thu gom rác.

Trong khi các loại tham chiếu luôn được phân bổ trên heap trong các triển khai hiện tại, các loại giá trị có thể được phân bổ trên ngăn xếp - nhưng không nhất thiết. Một kiểu giá trị chỉ được cấp phát trên ngăn xếp khi nó là biến cục bộ hoặc biến tạm thời không được đóng hộp không có hộp đựng trong kiểu tham chiếu và không được cấp phát trong thanh ghi.

  • Nếu một kiểu giá trị là một phần của một lớp (như trong ví dụ của bạn), nó sẽ kết thúc trên heap.
  • Nếu nó được đóng hộp, nó sẽ kết thúc trên đống.
  • Nếu nó nằm trong một mảng, nó sẽ kết thúc trên heap.
  • Nếu đó là một biến tĩnh, nó sẽ kết thúc trên heap.
  • Nếu nó được nắm bắt bằng cách đóng, nó sẽ kết thúc trên đống.
  • Nếu nó được sử dụng trong khối lặp hoặc khối không đồng bộ, nó sẽ kết thúc trên heap.
  • Nếu nó được tạo bởi mã không an toàn hoặc không được quản lý, nó có thể được cấp phát trong bất kỳ loại cấu trúc dữ liệu nào (không nhất thiết phải là ngăn xếp hoặc một đống).

Có điều gì tôi đã bỏ lỡ?

Tất nhiên, tôi sẽ rất tiếc nếu tôi không liên kết đến các bài đăng của Eric Lippert về chủ đề:


1
Ed: Chính xác thì nó quan trọng khi nào?
Gabe

1
@Gabe: Nó quan trọng ở đâu các bit được lưu trữ. Ví dụ: nếu bạn đang gỡ lỗi một kết xuất sự cố, bạn sẽ không đi được xa trừ khi bạn biết nơi tìm kiếm các đối tượng / dữ liệu.
Brian Rasmussen

14
Các tình huống bạn đã bỏ qua là: nếu loại giá trị là từ mã không được quản lý được truy cập thông qua một con trỏ không an toàn thì nó có thể không nằm trên ngăn xếp hoặc vùng được quản lý. Nó có thể nằm trên heap không được quản lý hoặc trong một số cấu trúc dữ liệu thậm chí không phải là heap. Toàn bộ ý tưởng rằng có "đống" cũng là một huyền thoại. Có thể có hàng chục đống. Ngoài ra, nếu jitter chọn đăng ký giá trị thì nó không nằm trên ngăn xếp hoặc đống, nó nằm trong một thanh ghi.
Eric Lippert

1
Phần Hai của Eric Lippert là một bài đọc tuyệt vời, cảm ơn bạn đã liên kết!
Dan Bechard

1
Điều này quan trọng vì nó được hỏi trong các cuộc phỏng vấn chứ không phải trong cuộc sống thực. :)
Mayank

22

"Tất cả các loại GIÁ TRỊ sẽ được phân bổ cho Ngăn xếp" là rất, rất sai; các biến cấu trúc có thể sống trên ngăn xếp, như các biến phương thức. Tuy nhiên, các trường trên một kiểu sống với kiểu đó . Nếu kiểu khai báo của một trường là một lớp, các giá trị nằm trên heap như một phần của đối tượng đó. Nếu kiểu khai báo của một trường là một cấu trúc, thì các trường là một phần của cấu trúc đó ở bất kỳ nơi đâu mà cấu trúc đó tồn tại.

Ngay cả các biến phương thức cũng có thể nằm trên heap, nếu chúng được nắm bắt (lambda / anon-method) hoặc một phần của (ví dụ) khối trình lặp.


1
Và đừng quên quyền anh: nếu bạn có object x = 12;trong một phương thức, 12 sẽ được lưu trữ trên heap mặc dù đó là một số nguyên (một kiểu giá trị).
Gabe

@Gabe: Vị trí lưu trữ kiểu giá trị tự chứa các trường (công khai và riêng tư) của kiểu giá trị. Các vị trí lưu trữ kiểu tham chiếu giữ nullhoặc tham chiếu đến một đối tượng heap thuộc loại thích hợp. Đối với mọi kiểu giá trị có một kiểu đối tượng đống tương ứng; cố gắng lưu trữ một kiểu giá trị trong vị trí lưu trữ kiểu tham chiếu sẽ tạo ra một đối tượng mới thuộc kiểu đối tượng đống tương ứng của nó, sao chép tất cả các trường vào đối tượng mới đó và lưu trữ một tham chiếu đến đối tượng trong vị trí lưu trữ kiểu tham chiếu. C # giả vờ kiểu giá trị và các loại đối tượng đều giống nhau, nhưng ...
supercat

... một quan điểm như vậy thêm sự nhầm lẫn hơn là sự hiểu biết. Một unboxed List<T>.Enumeratorđược lưu trữ trong một biến kiểu đó sẽ thể hiện ngữ nghĩa giá trị, vì đó là một kiểu giá trị. Một List<T>.Enumeratorđược lưu trữ trong một biến kiểu IEnumerator<T>, tuy nhiên, sẽ cư xử giống như một loại tài liệu tham khảo. Nếu người ta coi cái sau là một kiểu khác với kiểu trước, thì sự khác biệt về hành vi có thể dễ dàng giải thích được. Giả vờ họ là cùng một loại khiến việc suy luận về họ khó hơn nhiều.
supercat

12

2

Cây rơm

Đây stacklà một khối bộ nhớ để lưu trữ local variablesparameters. Ngăn xếp phát triển và thu hẹp một cách hợp lý khi một hàm được nhập và thoát.

Hãy xem xét phương pháp sau:

public static int Factorial (int x)
{
    if (x == 0) 
    {
        return 1;
    }

    return x * Factorial (x - 1);
}

Phương thức này là đệ quy, nghĩa là nó gọi chính nó. Mỗi khi phương thức được nhập, một int mới sẽ được cấp phát trên ngăn xếpmỗi khi phương thức thoát ra, int sẽ được phân bổ .


Đống

  • Heap là một khối bộ nhớ trong đó objects(tức là reference-type instances) cư trú. Bất cứ khi nào một đối tượng mới được tạo, nó sẽ được cấp phát trên heap và một tham chiếu đến đối tượng đó được trả về. Trong quá trình thực thi chương trình, heap bắt đầu lấp đầy khi các đối tượng mới được tạo. Thời gian chạy có bộ thu gom rác định kỳ xử lý các đối tượng từ đống, vì vậy chương trình của bạn không chạy Out Of Memory. Một đối tượng đủ điều kiện để phân bổ giao dịch ngay khi nó không được tham chiếu bởi bất kỳ thứ gì đó chính nó alive.
  • Đống cũng lưu trữ static fields. Không giống như các đối tượng được phân bổ trên heap (có thể được thu thập rác) , these live until the application domain is torn down.

Hãy xem xét phương pháp sau:

using System;
using System.Text;

class Test
{
    public static void Main()
    {
        StringBuilder ref1 = new StringBuilder ("object1");
        Console.WriteLine (ref1);
        // The StringBuilder referenced by ref1 is now eligible for GC.

        StringBuilder ref2 = new StringBuilder ("object2");
        StringBuilder ref3 = ref2;
        // The StringBuilder referenced by ref2 is NOT yet eligible for GC.
        Console.WriteLine (ref3); // object2
    }
}    

Trong ví dụ trên, chúng ta bắt đầu bằng cách tạo một đối tượng StringBuilder được tham chiếu bởi biến ref1, và sau đó viết ra nội dung của nó. Đối tượng StringBuilder đó ngay lập tức đủ điều kiện để thu gom rác, vì sau đó không có gì sử dụng nó. Sau đó, chúng tôi tạo một StringBuilder khác được tham chiếu bởi biến ref2 và sao chép tham chiếu đó vào ref3. Mặc dù ref2 không được sử dụng sau thời điểm đó, ref3 vẫn giữ nguyên đối tượng StringBuilder giống nhau — đảm bảo rằng nó không đủ điều kiện để thu thập cho đến khi chúng ta sử dụng xong ref3.

Các cá thể kiểu giá trị (và các tham chiếu đối tượng) sống ở bất cứ nơi nào biến được khai báo. Nếu cá thể được khai báo dưới dạng một trường trong một kiểu lớp hoặc dưới dạng một phần tử mảng, thì cá thể đó sống trên heap.


1

các biện pháp đơn giản

Loại giá trị có thể được lưu trữ trên THE STACK, nó là chi tiết vùng triển khai mà nó có thể được phân bổ cho một số cấu trúc dữ liệu theo chủ nghĩa tương lai.

vì vậy, tốt hơn nên hiểu cách giá trị và kiểu tham chiếu hoạt động, Kiểu giá trị sẽ được sao chép theo giá trị, nghĩa là khi bạn chuyển một kiểu giá trị làm tham số cho một FUNCTION hơn là nó sẽ được sao chép về bản chất có nghĩa là bạn sẽ có một bản sao hoàn toàn mới .

Các kiểu tham chiếu được thông qua bằng tham chiếu (vui mừng không coi tham chiếu sẽ lưu trữ lại một địa chỉ trong một số phiên bản trong tương lai, nó có thể được lưu trữ trên một số cấu trúc dữ liệu khác.)

vậy trong trường hợp của bạn

myInt là một int được đóng gói trong một lớp mà bản chất là một kiểu tham chiếu, vì vậy nó sẽ được gắn với thể hiện của lớp sẽ được lưu trữ trên 'THE HEAP'.

tôi muốn đề nghị, bạn có thể bắt đầu đọc các blog được viết bởi ERIC LIPPERTS.

Eric's Blog


1

Mỗi khi một đối tượng được tạo trong nó sẽ đi vào vùng bộ nhớ được gọi là heap. Các biến nguyên thủy như int và double được cấp phát trong ngăn xếp, nếu chúng là biến phương thức cục bộ và trong heap nếu chúng là biến thành viên. Trong các phương thức, các biến cục bộ được đẩy vào ngăn xếp khi một phương thức được gọi và con trỏ ngăn xếp được giảm khi một lệnh gọi phương thức hoàn thành. Trong một ứng dụng đa luồng, mỗi luồng sẽ có ngăn xếp riêng nhưng sẽ chia sẻ cùng một đống. Đây là lý do tại sao mã của bạn nên được cẩn thận để tránh bất kỳ sự cố truy cập đồng thời nào trong không gian heap. Ngăn xếp là luồng an toàn (mỗi luồng sẽ có ngăn xếp riêng) nhưng đống không an toàn cho luồng trừ khi được bảo vệ bằng đồng bộ hóa thông qua mã của bạn.

Liên kết này cũng hữu ích http://www.programmerinterview.com/index.php/data-structures/difference-between-stack-and-heap/


0

m là một tham chiếu đến một đối tượng của MyClass vì vậy m là lưu trữ trong ngăn xếp của luồng chính nhưng đối tượng của MyClass lưu trữ trong heap. Do đó myInt và myString lưu trữ trong heap. Lưu ý rằng m chỉ là một tham chiếu (một địa chỉ tới bộ nhớ) và nằm trên ngăn xếp chính. Khi m deallocated thì GC xóa đối tượng MyClass khỏi heap Để biết thêm chi tiết, hãy đọc cả bốn phần của bài viết này https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net- part-i /

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.