Các khởi tạo đối tượng trong Java Mười Foo f = new Foo () cơ bản giống như sử dụng malloc cho một con trỏ trong C?


9

Tôi đang cố gắng tìm hiểu quy trình thực tế đằng sau các sáng tạo đối tượng trong Java - và tôi cho rằng các ngôn ngữ lập trình khác.

Sẽ là sai lầm khi cho rằng khởi tạo đối tượng trong Java giống như khi bạn sử dụng malloc cho một cấu trúc trong C?

Thí dụ:

Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));

Đây có phải là lý do tại sao các đối tượng được cho là trên đống chứ không phải là ngăn xếp? Bởi vì về cơ bản chúng chỉ là con trỏ đến dữ liệu?


Các đối tượng được tạo trên heap cho các ngôn ngữ được quản lý như c # / java. Trong cpp bạn có thể tạo các đối tượng trên stack chỉ là tốt
Bas

Tại sao những người tạo Java / C # quyết định lưu trữ độc quyền các đối tượng trên heap?
Jules

Tôi nghĩ vì mục đích đơn giản. Lưu trữ các đối tượng trên ngăn xếp và chuyển cho chúng một mức độ sâu hơn liên quan đến việc sao chép đối tượng trên ngăn xếp, liên quan đến các nhà xây dựng sao chép. Tôi không google cho một câu trả lời chính xác, nhưng tôi chắc chắn rằng bạn có thể tìm một câu trả lời thỏa mãn hơn mình (hoặc người khác sẽ xây dựng trên câu hỏi bên này)
bas

Các đối tượng @Jules trong java vẫn có thể được "dịch ngược" tại thời gian chạy (được gọi scalar-replacement) thành các trường đơn thuần chỉ sống trên ngăn xếp; nhưng đó là một cái gì đó JITkhông, không javac.
Eugene

Heap heap chỉ là một tên cho một tập hợp các thuộc tính được liên kết với các đối tượng / bộ nhớ được phân bổ. Trong C / C. ngụ ý rằng các thuộc tính này giống như đối với heap C / C ++, trên thực tế, chúng không có. Điều này không có nghĩa là việc triển khai không thể có các chiến lược khác nhau để quản lý các đối tượng, nó ngụ ý rằng các chiến lược đó không liên quan đến logic ứng dụng.
Holger

Câu trả lời:


5

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 newthự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 newtrả 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()newchỉ đơ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ố lhsrhssizeof(int), 4 byte mỗi. Các biến resultcũ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 intshoặ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, rhsresult. 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 arraynhư 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á countlớ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.


intkhông phải là 8 byte trên nền tảng 64 bit. Vẫn là 4. Cùng với đó, trình biên dịch rất có khả năng tối ưu hóa phần thứ ba intcủa ngăn xếp vào thanh ghi trả về. Trên thực tế, hai đối số cũng có khả năng nằm trong các thanh ghi trên bất kỳ nền tảng 64 bit nào.
SS Anne

Tôi đã chỉnh sửa câu trả lời của mình để xóa câu lệnh về 8 byte inttrên nền tảng 64 bit. Bạn đúng là intvẫn còn 4 byte trong Java. Tuy nhiên, tôi đã để lại phần còn lại của câu trả lời vì tôi tin rằng việc tối ưu hóa trình biên dịch sẽ đặt giỏ hàng trước ngựa. Vâng, bạn cũng đúng về những điểm này, nhưng câu hỏi yêu cầu làm rõ về ngăn xếp so với đống. RVO, đối số thông qua các thanh ghi, cuộc bầu chọn mã, v.v. làm quá tải các khái niệm cơ bản và cản trở việc hiểu các nguyên tắc cơ bản.
mệnh
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.