Bộ nhớ Stack và Heap trong Java


99

Theo tôi hiểu, trong Java, bộ nhớ ngăn xếp giữ các nguyên hàm và các yêu cầu phương thức và bộ nhớ heap được sử dụng để lưu trữ các đối tượng.

Giả sử tôi có một lớp học

class A {
       int a ;
       String b;
       //getters and setters
}
  1. Trường hợp nguyên thủy atrong lớp Asẽ được lưu trữ ở đâu?

  2. Tại sao bộ nhớ heap tồn tại ở tất cả? Tại sao chúng ta không thể lưu trữ mọi thứ trên ngăn xếp?

  3. Khi đối tượng được thu gom rác, ngăn xếp có liên quan đến đối tượng bị phá hủy không?


1
stackoverflow.com/questions/1056444/is-it-on-the-stack-or-heap dường như trả lời câu hỏi của bạn.
S.Lott

@ S.Lott, ngoại trừ cái này là về Java, không phải C.
Péter Török

@ Péter Török: Đồng ý. Mặc dù mẫu mã là Java, không có thẻ nào cho biết đó chỉ là Java. Và nguyên tắc chung phải áp dụng cho Java như C. Hơn nữa, có rất nhiều câu trả lời cho câu hỏi này trên Stack Overflow.
S.Lott

9
@SteveHaigh: trên trang web này, mọi người đều quá quan tâm đến việc liệu cái gì đó thuộc về nơi này ... Tôi tự hỏi trang web này thực sự nhận được gì với tất cả sự kén chọn về việc câu hỏi có thuộc về nơi này hay không.
Sam Goldberg

Câu trả lời:


106

Sự khác biệt cơ bản giữa stack và heap là vòng đời của các giá trị.

Các giá trị ngăn xếp chỉ tồn tại trong phạm vi của hàm mà chúng được tạo. Một khi nó trả về, chúng sẽ bị loại bỏ.
Giá trị heap tuy nhiên tồn tại trên heap. Chúng được tạo tại một số thời điểm và bị phá hủy tại một thời điểm khác (bằng GC hoặc thủ công, tùy thuộc vào ngôn ngữ / thời gian chạy).

Bây giờ Java chỉ lưu trữ nguyên thủy trên ngăn xếp. Điều này giữ cho ngăn xếp nhỏ và giúp giữ các khung ngăn xếp riêng lẻ nhỏ, do đó cho phép các cuộc gọi lồng nhau hơn.
Các đối tượng được tạo trên heap và chỉ các tham chiếu (lần lượt là nguyên thủy) được truyền xung quanh trên ngăn xếp.

Vì vậy, nếu bạn tạo một đối tượng, nó được đặt vào heap, với tất cả các biến thuộc về nó, để nó có thể tồn tại sau khi hàm gọi trở lại.


2
"và chỉ các tài liệu tham khảo (lần lượt là nguyên thủy)" Tại sao nói rằng tài liệu tham khảo là nguyên thủy? Bạn có thể làm rõ?
Geek

5
@Geek: Bởi vì định nghĩa chung về các loại dữ liệu nguyên thủy được áp dụng: "một loại dữ liệu được cung cấp bởi ngôn ngữ lập trình như một khối xây dựng cơ bản" . Bạn cũng có thể nhận thấy rằng các tài liệu tham khảo được liệt kê trong số các ví dụ kinh điển trong bài viết .
back2dos

4
@Geek: Về mặt dữ liệu, bạn có thể xem bất kỳ loại dữ liệu nguyên thủy nào - bao gồm các tham chiếu - dưới dạng số. Ngay cả chars là số và có thể được sử dụng thay thế cho nhau. Các tham chiếu cũng chỉ là các số tham chiếu đến một địa chỉ bộ nhớ, dài 32 hoặc 64 bit (mặc dù chúng không thể được sử dụng như vậy - trừ khi bạn đang loay hoay với sun.misc.Unsafe).
Sune Rasmussen

3
Thuật ngữ của câu trả lời này là sai. Theo Đặc tả ngôn ngữ Java, tài liệu tham khảo KHÔNG phải là nguyên thủy. Ý chính của câu trả lời là đúng. (Trong khi bạn có thể làm cho một cuộc tranh cãi rằng tài liệu tham khảo là "theo nghĩa" nguyên thủy là do bởi JLS định nghĩa các thuật ngữ cho Java, và nó nói rằng loại nguyên thủy là. boolean, byte, short, char, int, long, floatdouble.)
Stephen C

4
Như tôi đã tìm thấy trong bài viết này , Java có thể lưu trữ các đối tượng trên ngăn xếp (hoặc thậm chí trong các thanh ghi cho các đối tượng có thời gian tồn tại ngắn). JVM có thể thực hiện khá một chút dưới vỏ bọc. Nó không chính xác để nói "Bây giờ Java chỉ lưu trữ nguyên thủy trên ngăn xếp."

49

Trường nguyên thủy được lưu trữ ở đâu?

Các trường nguyên thủy được lưu trữ như một phần của đối tượng được khởi tạo ở đâu đó . Cách dễ nhất để nghĩ về nơi này - là đống. Tuy nhiên , điều này không phải lúc nào cũng đúng. Như được mô tả trong lý thuyết và thực tiễn Java: Truyền thuyết hiệu suất đô thị, được xem xét lại :

Các JVM có thể sử dụng một kỹ thuật gọi là phân tích thoát, qua đó họ có thể nói rằng các đối tượng nhất định vẫn bị giới hạn trong một luồng duy nhất trong suốt vòng đời của chúng và thời gian tồn tại đó bị giới hạn bởi thời gian tồn tại của một khung ngăn xếp nhất định. Các đối tượng như vậy có thể được phân bổ an toàn trên ngăn xếp thay vì đống. Thậm chí tốt hơn, đối với các đối tượng nhỏ, JVM có thể tối ưu hóa hoàn toàn việc phân bổ và chỉ cần đưa các trường của đối tượng vào các thanh ghi.

Do đó, ngoài việc nói "đối tượng được tạo ra và trường cũng ở đó", người ta không thể nói nếu một cái gì đó nằm trên đống hoặc trên ngăn xếp. Lưu ý rằng đối với các đối tượng nhỏ, tồn tại ngắn, có thể 'đối tượng' sẽ không tồn tại trong bộ nhớ như vậy và thay vào đó có thể có các trường của nó được đặt trực tiếp trong các thanh ghi.

Bài viết kết luận với:

Các JVM rất tốt trong việc tìm ra những thứ mà chúng ta từng cho rằng chỉ có nhà phát triển mới có thể biết. Bằng cách để JVM chọn giữa phân bổ ngăn xếp và phân bổ heap trên cơ sở từng trường hợp cụ thể, chúng ta có thể nhận được các lợi ích hiệu năng của phân bổ ngăn xếp mà không khiến lập trình viên thống nhất về việc phân bổ trên ngăn xếp hay trên heap.

Vì vậy, nếu bạn có mã trông giống như:

void foo(int arg) {
    Bar qux = new Bar(arg);
    ...
}

trong trường hợp ...không cho phép quxrời khỏi phạm vi đó, qux có thể được phân bổ trên ngăn xếp thay thế. Đây thực sự là một chiến thắng cho VM vì điều đó có nghĩa là nó không cần phải được thu gom rác - nó sẽ biến mất khi rời khỏi phạm vi.

Thêm về phân tích thoát tại Wikipedia. Đối với những người sẵn sàng đi sâu vào các bài báo, Phân tích thoát cho Java từ IBM. Đối với những người đến từ thế giới C #, bạn có thể thấy Ngăn xếp là chi tiết triển khaiSự thật về các loại giá trị của Eric Lippert đọc tốt (chúng cũng hữu ích cho các loại Java vì nhiều khái niệm và khía cạnh giống nhau hoặc tương tự nhau) . Tại sao sách .Net nói về stack vs phân bổ bộ nhớ heap? cũng đi vào đây

Trên các đống của đống và đống

Trên đống

Vì vậy, tại sao có ngăn xếp hoặc đống ở tất cả? Đối với những thứ rời khỏi phạm vi, ngăn xếp có thể tốn kém. Hãy xem xét mã:

void foo(String arg) {
    bar(arg);
    ...
}

void bar(String arg) {
    qux(arg);
    ...
}

void qux(String arg) {
    ...
}

Các tham số là một phần của ngăn xếp quá. Trong trường hợp bạn không có một đống, bạn sẽ chuyển toàn bộ giá trị trên ngăn xếp. Điều này tốt cho "foo"các chuỗi nhỏ ... nhưng điều gì sẽ xảy ra nếu ai đó đặt một tệp XML lớn trong chuỗi đó. Mỗi cuộc gọi sẽ sao chép toàn bộ chuỗi lớn vào ngăn xếp - và điều đó sẽ khá lãng phí.

Thay vào đó, tốt hơn là đặt các đối tượng có một số cuộc sống bên ngoài phạm vi ngay lập tức (được chuyển sang phạm vi khác, bị mắc kẹt trong một cấu trúc mà người khác đang duy trì, v.v ...) vào một khu vực khác được gọi là đống.

Trên ngăn xếp

Bạn không cần ngăn xếp. Theo giả thuyết, người ta có thể viết một ngôn ngữ không sử dụng ngăn xếp (độ sâu tùy ý). Một BASIC cũ mà tôi học được khi còn trẻ đã làm như vậy, một người chỉ có thể thực hiện 8 cấp độ gosubcuộc gọi và tất cả các biến là toàn cầu - không có ngăn xếp.

Ưu điểm của ngăn xếp là khi bạn có một biến tồn tại với một phạm vi, khi bạn rời khỏi phạm vi đó, khung ngăn xếp đó được bật lên. Nó thực sự đơn giản hóa những gì ở đó và những gì không có ở đó. Chương trình chuyển sang thủ tục khác, khung stack mới; chương trình trở lại quy trình và bạn đã quay lại chương trình thấy phạm vi hiện tại của mình; chương trình rời khỏi thủ tục và tất cả các mục trên ngăn xếp được sắp xếp lại.

Điều này thực sự làm cho cuộc sống dễ dàng cho người viết thời gian chạy cho mã để sử dụng một ngăn xếp và một đống. Họ chỉ đơn giản là nhiều khái niệm và cách làm việc với mã cho phép người viết mã bằng ngôn ngữ được giải phóng khỏi suy nghĩ của họ một cách rõ ràng.

Bản chất của ngăn xếp cũng có nghĩa là nó không thể bị phân mảnh. Phân mảnh bộ nhớ là một vấn đề thực sự với heap. Bạn phân bổ một vài đối tượng, sau đó rác thu thập một đối tượng ở giữa, và sau đó thử tìm không gian cho đối tượng lớn tiếp theo được phân bổ. Đó là một mớ hỗn độn. Thay vào đó, có thể đặt mọi thứ lên ngăn xếp có nghĩa là bạn không phải đối phó với điều đó.

Khi một cái gì đó là rác được thu thập

Khi một cái gì đó là rác được thu thập, nó biến mất. Nhưng nó chỉ là rác được thu thập vì nó đã bị quên - không có thêm tài liệu tham khảo nào cho đối tượng trong chương trình có thể được truy cập từ trạng thái hiện tại của chương trình.

Tôi sẽ chỉ ra rằng đây là một sự đơn giản hóa rất lớn của bộ sưu tập rác. Có nhiều trình thu gom rác (ngay cả trong Java - bạn có thể điều chỉnh trình thu gom rác bằng cách sử dụng các cờ ( tài liệu ) khác nhau. Chúng hoạt động khác nhau và các sắc thái về cách mỗi người làm mọi thứ quá sâu cho câu trả lời này. Bạn có thể muốn đọc Khái niệm cơ bản về bộ sưu tập rác Java để hiểu rõ hơn về cách thức hoạt động của một số thứ đó.

Điều đó nói rằng, nếu một cái gì đó được phân bổ trên ngăn xếp, thì đó không phải là rác được thu thập như một phần của System.gc()- nó được xử lý khi khung ngăn xếp bật lên. Nếu một cái gì đó trên đống, và được tham chiếu từ một cái gì đó trên ngăn xếp, nó sẽ không phải là rác được thu thập tại thời điểm đó.

Vì sao vấn đề này?

Đối với hầu hết các phần, truyền thống của nó. Các sách văn bản được viết và các lớp trình biên dịch và các tài liệu bit khác nhau tạo ra một vấn đề lớn về heap và stack.

Tuy nhiên, các máy ảo ngày nay (JVM và tương tự) đã cố gắng hết sức để cố gắng giấu điều này khỏi lập trình viên. Trừ khi bạn đang hết cái này hay cái kia và cần biết tại sao (thay vì chỉ tăng không gian lưu trữ một cách thích hợp), điều đó không thành vấn đề quá nhiều.

Đối tượng ở đâu đó và ở nơi nó có thể được truy cập chính xác và nhanh chóng trong khoảng thời gian thích hợp mà nó tồn tại. Nếu nó nằm trên chồng hoặc đống - nó không thực sự quan trọng.


7
  1. Trong heap, là một phần của đối tượng, được tham chiếu bởi một con trỏ trong ngăn xếp. I E. a và b sẽ được lưu trữ liền kề nhau.
  2. Bởi vì nếu tất cả bộ nhớ là ngăn xếp bộ nhớ, nó sẽ không còn hiệu quả nữa. Thật tốt khi có một khu vực nhỏ, truy cập nhanh nơi chúng tôi bắt đầu và có các mục tham chiếu đó trong vùng bộ nhớ lớn hơn nhiều. Tuy nhiên, điều này là quá mức khi một đối tượng chỉ đơn giản là một nguyên thủy duy nhất sẽ chiếm khoảng cùng dung lượng trên ngăn xếp như con trỏ tới nó.
  3. Đúng.

1
Tôi sẽ thêm vào điểm số 2 của bạn rằng nếu bạn lưu trữ các đối tượng trên ngăn xếp (hãy tưởng tượng một đối tượng từ điển với hàng trăm ngàn mục nhập) sau đó để chuyển nó đến hoặc trả lại từ một chức năng bạn cần sao chép đối tượng mỗi thời gian. Bằng cách sử dụng một con trỏ hoặc tham chiếu đến một đối tượng trong heap, chúng ta chỉ truyền tham chiếu (nhỏ).
Scott Whitlock

1
Tôi nghĩ 3 sẽ là 'không' bởi vì nếu đối tượng đang được thu gom rác, thì không có tham chiếu nào trong ngăn xếp trỏ đến nó.
Luciano

@Luciano - Tôi thấy quan điểm của bạn. Tôi đọc câu hỏi 3 khác nhau. "Cùng một lúc" hoặc "tại thời điểm đó" là ẩn. :: nhún ::
pdr

3
  1. Trên heap trừ khi Java phân bổ thể hiện lớp trên ngăn xếp dưới dạng tối ưu hóa sau khi chứng minh thông qua phân tích thoát rằng điều này sẽ không ảnh hưởng đến ngữ nghĩa. Tuy nhiên, đây là một chi tiết triển khai, vì vậy, cho tất cả các mục đích thực tế ngoại trừ tối ưu hóa vi mô, câu trả lời là "trên đống".

  2. Bộ nhớ ngăn xếp phải được phân bổ và giải quyết cuối cùng theo thứ tự đầu tiên. Bộ nhớ heap có thể được phân bổ và giải quyết theo bất kỳ thứ tự nào.

  3. Khi đối tượng là rác được thu thập, không có thêm các tham chiếu trỏ đến nó từ ngăn xếp. Nếu có, họ sẽ giữ cho đối tượng còn sống. Nguyên thủy ngăn xếp không phải là rác được thu thập bởi vì chúng tự động bị hủy khi hàm trả về.


3

Bộ nhớ ngăn xếp được sử dụng để lưu trữ các biến cục bộ và gọi hàm.

Trong khi bộ nhớ heap được sử dụng để lưu trữ các đối tượng trong Java. Không có vấn đề, nơi đối tượng được tạo trong mã.

Nguyên thủy sẽ ở đâu atrong class Ađược lưu trữ?

Trong trường hợp này nguyên thủy a được liên kết với đối tượng lớp A. Vì vậy, nó tạo ra trong bộ nhớ Heap.

Tại sao bộ nhớ heap tồn tại ở tất cả? Tại sao chúng ta không thể lưu trữ mọi thứ trên ngăn xếp?

  • Các biến được tạo trên ngăn xếp sẽ đi ra khỏi phạm vi và tự động bị hủy.
  • Stack nhanh hơn nhiều để phân bổ so với các biến trên heap.
  • Các biến trên đống phải được phá hủy bởi Garbage Collector.
  • Chậm hơn để phân bổ so với các biến trên ngăn xếp.
  • Bạn sẽ sử dụng ngăn xếp nếu bạn biết chính xác lượng dữ liệu bạn cần phân bổ trước thời gian biên dịch và nó không quá lớn. (Biến cục bộ nguyên thủy lưu trữ trong ngăn xếp)
  • Bạn sẽ sử dụng heap nếu bạn không biết chính xác mình sẽ cần bao nhiêu dữ liệu khi chạy hoặc nếu bạn cần phân bổ nhiều dữ liệu.

Khi đối tượng được thu gom rác, ngăn xếp có liên quan đến đối tượng bị phá hủy không?

Garbage Collector hoạt động theo phạm vi của bộ nhớ Heap, vì vậy nó phá hủy các đối tượng không có chuỗi tham chiếu từ gốc đến gốc.


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.