Không gian địa đàng
Vì vậy, câu hỏi của tôi là bất kỳ điều này có thể thực sự đúng không, và nếu vậy tại sao phân bổ heap của java lại nhanh hơn nhiều.
Tôi đã nghiên cứu một chút về cách thức hoạt động của Java GC vì nó rất thú vị đối với tôi. Tôi luôn cố gắng mở rộng bộ sưu tập các chiến lược phân bổ bộ nhớ của mình trong C và C ++ (quan tâm đến việc cố gắng thực hiện một cái gì đó tương tự trong C), và đó là một cách rất, rất nhanh để phân bổ nhiều đối tượng theo kiểu bùng nổ từ quan điểm thực tế nhưng chủ yếu là do đa luồng.
Cách phân bổ Java GC hoạt động là sử dụng chiến lược phân bổ cực kỳ rẻ để ban đầu phân bổ các đối tượng vào không gian "Eden". Từ những gì tôi có thể nói, đó là sử dụng bộ phân bổ nhóm tuần tự.
Điều đó nhanh hơn rất nhiều về mặt thuật toán và giảm các lỗi trang bắt buộc so với mục đích chung malloc
trong C hoặc mặc định, ném operator new
vào C ++.
Nhưng các bộ cấp phát tuần tự có một điểm yếu rõ ràng: chúng có thể phân bổ các khối có kích thước thay đổi, nhưng chúng không thể giải phóng bất kỳ khối riêng lẻ nào. Họ chỉ phân bổ theo kiểu tuần tự thẳng với phần đệm để căn chỉnh và chỉ có thể lọc tất cả bộ nhớ mà họ đã phân bổ cùng một lúc. Chúng thường hữu ích trong C và C ++ để xây dựng cấu trúc dữ liệu chỉ cần chèn và không loại bỏ các phần tử, như cây tìm kiếm chỉ cần được xây dựng một lần khi chương trình bắt đầu và sau đó được tìm kiếm nhiều lần hoặc chỉ thêm khóa mới ( không có phím nào bị xóa).
Chúng cũng có thể được sử dụng ngay cả đối với các cấu trúc dữ liệu cho phép loại bỏ các phần tử, nhưng các phần tử đó thực sự sẽ không được giải phóng khỏi bộ nhớ vì chúng ta không thể phân bổ chúng riêng lẻ. Cấu trúc như vậy sử dụng bộ cấp phát tuần tự sẽ chỉ tiêu tốn nhiều bộ nhớ hơn, trừ khi nó có một số bị hoãn trong đó dữ liệu được sao chép sang một bản sao mới, được nén bằng cách sử dụng một bộ cấp phát tuần tự riêng biệt (và đôi khi đó là một kỹ thuật rất hiệu quả nếu bộ phân bổ cố định giành chiến thắng Vì lý do nào đó - chỉ cần thẳng lên phân bổ tuần tự một bản sao mới của cấu trúc dữ liệu và kết xuất tất cả bộ nhớ của cái cũ).
Bộ sưu tập
Như trong ví dụ về cấu trúc dữ liệu / nhóm tuần tự ở trên, sẽ là một vấn đề lớn nếu Java GC chỉ phân bổ theo cách này mặc dù nó cực nhanh để phân bổ nhiều khối riêng lẻ. Nó sẽ không thể giải phóng bất cứ thứ gì cho đến khi phần mềm bị tắt, tại thời điểm đó, nó có thể giải phóng tất cả các nhóm bộ nhớ cùng một lúc.
Vì vậy, thay vào đó, sau một chu trình GC duy nhất, một đường chuyền được thực hiện thông qua các đối tượng hiện có trong không gian "Eden" (được phân bổ tuần tự) và những đối tượng vẫn được tham chiếu sau đó được phân bổ bằng cách sử dụng một cấp phát đa mục đích có khả năng giải phóng các khối riêng lẻ. Những người không còn được tham chiếu sẽ chỉ đơn giản là bị xử lý trong quá trình thanh trừng. Vì vậy, về cơ bản, đó là "sao chép các đối tượng ra khỏi không gian Eden nếu chúng vẫn được tham chiếu và sau đó thanh lọc".
Điều này thường sẽ khá tốn kém, vì vậy nó được thực hiện trong một luồng nền riêng biệt để tránh làm chậm đáng kể các luồng ban đầu được phân bổ tất cả bộ nhớ.
Khi bộ nhớ được sao chép ra khỏi không gian Eden và được phân bổ bằng cách sử dụng lược đồ đắt tiền hơn này có thể giải phóng các khối riêng lẻ sau một chu kỳ GC ban đầu, các đối tượng sẽ chuyển sang vùng nhớ liên tục hơn. Các khối riêng lẻ đó sau đó được giải phóng trong các chu kỳ GC tiếp theo nếu chúng không còn được tham chiếu.
Tốc độ
Vì vậy, nói một cách thô thiển, lý do Java GC rất có thể vượt trội so với C hoặc C ++ khi phân bổ heap thẳng là vì nó sử dụng chiến lược phân bổ hoàn toàn thoái hóa, rẻ nhất trong luồng yêu cầu phân bổ bộ nhớ. Sau đó, nó tiết kiệm công việc đắt tiền hơn mà chúng ta thường cần phải làm khi sử dụng một công cụ phân bổ tổng quát hơn như thẳng malloc
cho một luồng khác.
Vì vậy, về mặt khái niệm, GC thực sự phải thực hiện nhiều công việc tổng thể hơn, nhưng nó phân phối nó qua các luồng để toàn bộ chi phí không được trả trước bởi một luồng. Nó cho phép luồng phân bổ bộ nhớ thực hiện nó với giá siêu rẻ, và sau đó hoãn lại chi phí thực sự cần thiết để thực hiện mọi thứ một cách hợp lý để các đối tượng riêng lẻ thực sự có thể được giải phóng sang luồng khác. Trong C hoặc C ++ khi chúng tôi malloc
hoặc gọi operator new
, chúng tôi phải trả toàn bộ chi phí trả trước trong cùng một chuỗi.
Đây là sự khác biệt chính và tại sao Java rất có thể vượt trội so với C hoặc C ++ khi chỉ sử dụng các cuộc gọi ngây thơ đến malloc
hoặc operator new
phân bổ một nhóm các khối tuổi teen riêng lẻ. Tất nhiên, thông thường sẽ có một số hoạt động nguyên tử và một số khóa tiềm năng khi chu trình GC khởi động, nhưng có lẽ nó được tối ưu hóa khá nhiều.
Về cơ bản, lời giải thích đơn giản tập trung vào việc trả chi phí nặng hơn trong một luồng ( malloc
) so với trả chi phí rẻ hơn trong một luồng và sau đó trả chi phí nặng hơn trong một luồng khác có thể chạy song song ( GC
). Như một nhược điểm khi thực hiện theo cách này ngụ ý rằng bạn yêu cầu hai lần chuyển hướng từ tham chiếu đối tượng sang đối tượng theo yêu cầu để cho phép người cấp phát sao chép / di chuyển bộ nhớ xung quanh mà không làm mất hiệu lực các tham chiếu đối tượng hiện có và bạn cũng có thể mất vị trí không gian khi bộ nhớ đối tượng là di chuyển ra khỏi không gian "Eden".
Cuối cùng nhưng không kém phần quan trọng, việc so sánh là một chút không công bằng vì mã C ++ thường không phân bổ một lượng thuyền các đối tượng riêng lẻ trên heap. Mã C ++ Decent có xu hướng phân bổ bộ nhớ cho nhiều phần tử trong các khối liền kề hoặc trên ngăn xếp. Nếu nó phân bổ một lượng thuyền của các vật thể nhỏ một lần trên cửa hàng miễn phí, mã sẽ rất tệ.