Trong C, malloc()
phân bổ một vùng bộ nhớ trong heap và trả về một con trỏ tới nó. Đó là tất cả những gì bạn nhận được. Bộ nhớ không được khởi tạo và bạn không có gì đảm bảo rằng đó là tất cả số không hoặc bất cứ thứ gì khác.
Trong Java, việc gọi new
thực hiện phân bổ dựa trên heap giống như malloc()
, nhưng bạn cũng nhận được rất nhiều tiện ích bổ sung (hoặc chi phí chung, nếu bạn thích). Ví dụ: bạn không phải chỉ định rõ ràng số lượng byte sẽ được phân bổ. Trình biên dịch chỉ ra nó cho bạn dựa trên loại đối tượng bạn đang cố gắng phân bổ. Ngoài ra, các hàm tạo đối tượng được gọi (mà bạn có thể truyền đối số cho nếu bạn muốn kiểm soát cách khởi tạo xảy ra). Khi new
trả về, bạn được đảm bảo có một đối tượng được khởi tạo.
Nhưng có, vào cuối cuộc gọi cả kết quả malloc()
và new
chỉ đơn giản là con trỏ đến một số dữ liệu dựa trên heap.
Phần thứ hai của câu hỏi của bạn hỏi về sự khác biệt giữa một chồng và một đống. Câu trả lời toàn diện hơn có thể được tìm thấy bằng cách tham gia một khóa học về (hoặc đọc một cuốn sách về) thiết kế trình biên dịch. Một khóa học về hệ điều hành cũng sẽ hữu ích. Ngoài ra còn có rất nhiều câu hỏi và câu trả lời trên SO về các ngăn xếp và đống.
Phải nói rằng, tôi sẽ đưa ra một cái nhìn tổng quát, tôi hy vọng không quá dài dòng và nhằm mục đích giải thích sự khác biệt ở mức khá cao.
Về cơ bản, lý do chính để có hai hệ thống quản lý bộ nhớ, tức là một đống và một ngăn xếp, là vì hiệu quả . Một lý do thứ hai là mỗi loại tốt hơn ở một số loại vấn đề nhất định.
Ngăn xếp có phần dễ hiểu hơn đối với tôi như là một khái niệm, vì vậy tôi bắt đầu với ngăn xếp. Hãy xem xét chức năng này trong C ...
int add(int lhs, int rhs) {
int result = lhs + rhs;
return result;
}
Những điều trên có vẻ khá đơn giản. Chúng tôi xác định một chức năng được đặt tên add()
và vượt qua trong phần bổ sung bên trái và bên phải. Hàm thêm chúng và trả về một kết quả. Xin vui lòng bỏ qua tất cả các công cụ trường hợp cạnh như tràn ra có thể xảy ra, tại thời điểm này, nó không phải là nguyên nhân của cuộc thảo luận.
Các add()
mục đích của chức năng có vẻ đơn giản đẹp, nhưng những gì chúng ta có thể nói về vòng đời của nó? Đặc biệt là nhu cầu sử dụng bộ nhớ của nó?
Quan trọng nhất, trình biên dịch biết một tiên nghiệm (tức là tại thời điểm biên dịch) mức độ lớn của các loại dữ liệu và số lượng sẽ được sử dụng. Các đối số lhs
và rhs
là sizeof(int)
, 4 byte mỗi. Các biến result
cũng được sizeof(int)
. Trình biên dịch có thể nói rằng add()
hàm sử dụng 4 bytes * 3 ints
hoặc tổng cộng 12 byte bộ nhớ.
Khi add()
hàm được gọi, một thanh ghi phần cứng được gọi là con trỏ ngăn xếp sẽ có một địa chỉ trong đó trỏ đến đỉnh của ngăn xếp. Để phân bổ bộ nhớ mà add()
hàm cần chạy, tất cả các mã nhập chức năng cần làm là đưa ra một lệnh ngôn ngữ lắp ráp duy nhất để giảm giá trị thanh ghi con trỏ ngăn xếp lên 12. Trong khi đó, nó tạo lưu trữ trên ngăn xếp cho ba ints
, một trong mỗi cho lhs
, rhs
và result
. Có được không gian bộ nhớ mà bạn cần bằng cách thực hiện một lệnh đơn là một chiến thắng lớn về tốc độ vì các lệnh đơn có xu hướng thực thi trong một tích tắc đồng hồ (1 phần tỷ của giây một CPU 1 GHz).
Ngoài ra, từ chế độ xem của trình biên dịch, nó có thể tạo một bản đồ đến các biến trông rất khủng khiếp như lập chỉ mục một mảng:
lhs: ((int *)stack_pointer_register)[0]
rhs: ((int *)stack_pointer_register)[1]
result: ((int *)stack_pointer_register)[2]
Một lần nữa, tất cả điều này là rất nhanh.
Khi add()
chức năng thoát nó phải dọn sạch. Nó thực hiện điều này bằng cách trừ 12 byte khỏi thanh ghi con trỏ ngăn xếp. Nó tương tự như một cuộc gọi đến free()
nhưng nó chỉ sử dụng một lệnh CPU và nó chỉ mất một tích tắc. Nó rất, rất nhanh.
Bây giờ hãy xem xét một phân bổ dựa trên heap. Điều này xuất hiện khi chúng ta không biết một tiên nghiệm chúng ta sẽ cần bao nhiêu bộ nhớ (tức là chúng ta sẽ chỉ tìm hiểu về nó khi chạy).
Hãy xem xét chức năng này:
int addRandom(int count) {
int numberOfBytesToAllocate = sizeof(int) * count;
int *array = malloc(numberOfBytesToAllocate);
int result = 0;
if array != NULL {
for (i = 0; i < count; ++i) {
array[i] = (int) random();
result += array[i];
}
free(array);
}
return result;
}
Lưu ý rằng addRandom()
hàm không biết tại thời điểm biên dịch giá trị của count
đối số sẽ là gì. Vì điều này, sẽ không có ý nghĩa gì khi cố gắng xác định array
như chúng ta sẽ làm nếu chúng ta đặt nó lên ngăn xếp, như thế này:
int array[count];
Nếu quá count
lớn, nó có thể khiến ngăn xếp của chúng tôi phát triển quá lớn và ghi đè lên các phân đoạn chương trình khác. Khi tràn ngăn xếp này xảy ra chương trình của bạn gặp sự cố (hoặc tệ hơn).
Vì vậy, trong trường hợp chúng tôi không biết mình sẽ cần bao nhiêu bộ nhớ cho đến khi chạy, chúng tôi sẽ sử dụng malloc()
. Sau đó, chúng ta có thể yêu cầu số byte chúng ta cần khi chúng ta cần và malloc()
sẽ kiểm tra xem nó có thể trả được nhiều byte không. Nếu có thể, thật tuyệt, chúng tôi sẽ lấy lại, nếu không, chúng tôi nhận được một con trỏ NULL cho chúng tôi biết cuộc gọi malloc()
thất bại. Đáng chú ý là, chương trình không sụp đổ! Tất nhiên, với tư cách là lập trình viên, bạn có thể quyết định rằng chương trình của bạn không được phép chạy nếu phân bổ tài nguyên không thành công, nhưng việc chấm dứt do lập trình viên khởi tạo khác với một sự cố giả.
Vì vậy, bây giờ chúng tôi phải quay lại để xem xét hiệu quả. Bộ cấp phát ngăn xếp cực nhanh - một lệnh để phân bổ, một lệnh để phân bổ và nó được thực hiện bởi trình biên dịch, nhưng hãy nhớ rằng ngăn xếp có nghĩa là cho những thứ như các biến cục bộ có kích thước đã biết nên nó có xu hướng khá nhỏ.
Mặt khác, bộ phân bổ heap là một số đơn đặt hàng có cường độ chậm hơn. Nó phải thực hiện tra cứu trong các bảng để xem nó có đủ bộ nhớ trống để có thể trả lại số lượng bộ nhớ mà người dùng muốn hay không. Nó phải cập nhật các bảng đó sau khi nó lưu bộ nhớ để đảm bảo không ai khác có thể sử dụng khối đó (sổ sách này có thể yêu cầu người cấp phát dự trữ bộ nhớ cho chính nó ngoài những gì nó dự định trả lại). Bộ cấp phát phải sử dụng các chiến lược khóa để đảm bảo rằng nó bán bộ nhớ theo cách an toàn cho chuỗi. Và khi ký ức cuối cùngfree()
d, xảy ra ở các thời điểm khác nhau và không theo thứ tự dự đoán thông thường, người cấp phát phải tìm các khối liền kề và khâu chúng lại với nhau để sửa chữa phân mảnh đống. Nếu điều đó nghe có vẻ như sẽ mất nhiều hơn một lệnh CPU để hoàn thành tất cả điều đó, thì bạn đã đúng! Nó rất phức tạp và phải mất một thời gian.
Nhưng đống là lớn. Lớn hơn nhiều so với ngăn xếp. Chúng ta có thể nhận được rất nhiều bộ nhớ từ họ và họ thật tuyệt vời khi chúng ta không biết tại thời điểm biên dịch chúng ta sẽ cần bao nhiêu bộ nhớ. Vì vậy, chúng tôi đánh đổi tốc độ cho một hệ thống bộ nhớ được quản lý từ chối chúng tôi một cách lịch sự thay vì sụp đổ khi chúng tôi cố gắng phân bổ một cái gì đó quá lớn.
Tôi hy vọng điều đó sẽ giúp trả lời một số câu hỏi của bạn. Xin vui lòng cho tôi biết nếu bạn muốn làm rõ về bất kỳ điều nào ở trên.