Từ mã Java đến heap Java
Bài này cung cấp cho bạn cái nhìn sâu sắc về cách sử dụng bộ nhớ khi
viết mã Java™, bao gồm chi phí sử dụng bộ nhớ trong việc đưa một giá trị int vào
một đối tượng Integer (Số nguyên), chi phí về ủy quyền đối tượng và hiệu quả bộ
nhớ của các kiểu Bộ sưu tập (collection) khác nhau. Bạn sẽ tìm hiểu cách xác định
xem những việc không hiệu quả xảy ra ở đâu trong ứng dụng của bạn và cách lựa
chọn đúng các bộ collection để cải thiện mã của mình.
Mặc dù việc tối ưu hóa bộ nhớ khi viết ứng dụng không phải là điều mới mẻ,
nhưng nó thường không được hiểu rõ. Bài này trình bày ngắn gọn cách sử dụng bộ
nhớ của một quá trình Java, sau đó đi sâu vào cách sử dụng bộ nhớ của mã Java mà
bạn viết. Cuối cùng, nó chỉ ra cách sử dụng bộ nhớ hiệu quả hơn khi viết mã ứng
dụng, đặc biệt là trong lĩnh vực sử dụng các bộ collection Java chẳng hạn như các
HashMap và các ArrayList.
số ký tự vào mảng ký tự — ví dụ "MY STRING" — thì bạn đang lưu trữ 9 ký tự trong mảng 16-ký tự. Hình 12 cho thấy cách sử dụng bộ nhớ của một StringBuffer có chứa "MY STRING" trên Java runtime 32-bit: Hình 12. Cách sử dụng bộ nhớ của một StringBuffer có chứa "MY STRING" trên Java runtime 32-bit Như Hình 12 cho thấy, 7 mục nhập ký tự bổ sung có sẵn trong mảng vẫn chưa được sử dụng nhưng đang tiêu thụ bộ nhớ — trong trường hợp này có một chi phí sử dụng bổ sung là 112 byte. Đối với collection này, bạn có 9 mục nhập trong một dung lượng là 16, điều đó cung cấp cho bạn một tỷ lệ lấp đầy (fill ratio) là 0,56. Tỷ lệ lấp đầy của một collection càng thấp thì chi phí sử dụng do dung lượng dự phòng gây ra càng lớn. Về đầu trang Sự mở rộng và việc định cỡ lại các collection Sau khi một collection sử dụng hết vùng dung lượng của mình nhưng lại có thêm yêu cầu nhập thêm thông tin vào collection đó thì collection sẽ được định cỡ lại và mở rộng để chứa các thông tin mới. Điều này làm tăng thêm dung lượng nhưng thường làm giảm tỷ lệ lấp đầy và làm tăng chi phí sử dụng bộ nhớ. Thuật toán mở rộng thường dùng khác nhau giữa các collection, nhưng một cách phổ biến là tăng gấp đôi dung lượng của collection. Cách này thường được dùng cho StringBuffer. Trong trường hợp của StringBuffer ở ví dụ trước, nếu bạn muốn nối thêm "OF TEXT" vào bộ đệm để tạo ra "MY STRING OF TEXT", bạn cần mở rộng collection, vì collection phải chứa tới 17 mục nhập (17 ký tự trong chuỗi) trong khi dung lượng hiện tại của nó là 16. Hình 13 cho thấy kết quả của cách sử dụng bộ nhớ: Hình 13. Cách sử dụng bộ nhớ của một StringBuffer có chứa "MY STRING OF TEXT" trên Java runtime 32-bit Bây giờ, như Hình 13 cho thấy, bạn có một mảng ký tự 32 mục nhập và 17 mục đã sử dụng, cung cấp cho bạn một tỷ lệ lấp đầy là 0,53. Tỷ lệ lấp đầy vẫn chưa giảm đáng kể, nhưng bây giờ bạn có một chi phí sử dụng là 240 byte cho dung lượng dự phòng. Trong trường hợp các chuỗi và các collection nhỏ, các chi phí sử dụng đối với tỷ lệ lấp đầy và dung lượng dự phòng thấp có thể không có vẻ là một vấn đề quá lớn, nhưng chúng trở nên thấy rõ và tốn kém hơn nhiều với các kích cỡ lớn hơn. Ví dụ, nếu bạn tạo ra một StringBuffer chỉ có 16MB dữ liệu, nó sẽ (theo mặc định) sử dụng một mảng ký tự được định cỡ để lưu giữ lên đến 32MB dữ liệu — tạo ra 16MB chi phí sử dụng bổ sung dưới dạng dung lượng dự phòng. Các collection Java: Tóm tắt Bảng 8 tóm tắt các thuộc tính của các collection: Bảng 8. Tóm tắt các thuộc tính của các collection Collection Hiệu năng Dung lượng mặc định Kích cỡ rỗng Chi phí sử dụng của mục nhập 10K Có định cỡ chính xác không? Thuật toán mở rộng HashSet O(1) 16 144 360K No x2 HashMap O(1) 16 128 360K No x2 Hashtable O(1) 11 104 360K No x2+1 LinkedList O(n) 1 48 240K Yes +1 ArrayList O(n) 10 88 40K No x1.5 StringBufferO(1) 16 72 24 No x2 Hiệu năng của các collection dạng Hash tốt hơn nhiều hơn so với hiệu năng của cả hai collection dạng List, nhưng chi phí sử dụng cho mỗi mục nhập lớn hơn nhiều. Vì hiệu năng truy cập, nếu bạn đang tạo ra các bộ collection (ví dụ, để thực hiện một bộ nhớ đệm), thì sử dụng một collection dạng Hash sẽ tốt hơn, không cần quan tâm tới chi phí sử dụng bổ sung. Đối với các collection nhỏ hơn mà hiệu năng truy cập với chúng có ít vấn đề hơn thì các collection dạng List là một tùy chọn. Hiệu năng của các collection ArrayList và LinkedList là xấp xỉ như nhau, nhưng vùng sử dụng bộ nhớ của chúng khác nhau: kích cỡ mỗi mục nhập của ArrayList nhỏ hơn nhiều so với LinkedList, nhưng nó không được định cỡ chính xác. Một ArrayList hay một LinkedList có là công cụ phù hợp của collection List để dùng hay không phụ thuộc vào chiều dài của collection List. Nếu không biết rõ về chiều dài này, một LinkedList có thể là tùy chọn đúng, vì collection sẽ có ít vùng rỗng. Nếu biết được kích cỡ, một ArrayList sẽ có chi phí sử dụng bộ nhớ thấp hơn nhiều. Việc chọn đúng kiểu collection cho phép bạn lựa chọn đúng sự cân bằng giữa hiệu năng và vùng sử dụng bộ nhớ của collection. Ngoài ra, bạn có thể giảm thiểu vùng sử dụng bộ nhớ bằng cách định cỡ chính xác bộ sưu tập để tối đa hóa tỷ lệ lấp đầy và để giảm thiểu vùng chưa sử dụng. Tìm kiếm các tỷ lệ lấp đầy thấp bằng Trình phân tích bộ nhớ (Memory Analyzer) Các công cụ Chẩn đoán và Giám sát của IBM cho Java - công cụ Trình phân tích bộ nhớ (Memory Analyzer), có sẵn là một phần của IBM Support Assistant (Trợ lý Hỗ trợ của IBM), có thể phân tích cách sử dụng bộ nhớ của các collection Java (xem phần Tài nguyên). Các khả năng của nó bao gồm việc phân tích các tỷ lệ lấp đầy và các kích cỡ của các bộ sưu tập. Bạn có thể sử dụng phân tích này để xác định bất kỳ các collection nào là các ứng viên để tối ưu hóa. Các khả năng phân tích-collection trong Trình phân tích bộ nhớ đều nằm trong trình đơn Open Query Browser -> Java Collections (Mở trình duyệt Query -> Các bộ sưu tập Java), như hiển thị trong Hình 14: Hình 14. Phân tích tỷ lệ lấp đầy của các collection Java trong Trình phân tích bộ nhớ Truy vấn tỷ lệ lấp đầy của bộ sưu tập (Collection Fill Ratio) được lựa chọn trong Hình 14 là có lợi nhất để xác định các collection lớn hơn nhiều so với các collection cần thiết hiện tại. Bạn có thể chỉ rõ một số các tùy chọn cho truy vấn này, bao gồm: Các đối tượng : Các kiểu của các đối tượng (các collection) mà bạn quan tâm Các đoạn : Các phạm vi của tỷ lệ lấp đầy để nhóm các đối tượng vào Việc chạy truy vấn với các tùy chọn các đối tượng được thiết lập là "java.util.Hashtable" và tùy chọn các đoạn được thiết lập là "10" tạo ra kết quả như trong Hình 15: Hình 15. Phân tích trong Trình phân tích bộ nhớ về tỷ lệ lấp đầy của các Hashtable Hình 15 cho thấy có 262.234 cá thể của java.util.Hashtable, 127.016 (48,4%) trong số đó là hoàn toàn rỗng và hầu như tất cả trong số đó chỉ có một số lượng nhỏ các mục nhập. Tiếp theo có khả năng xác định những collection này bằng cách chọn một hàng trong bảng các kết quả và nhấn phím chuột phải để chọn hoặc các đối tượng danh sách (list objects) -> với các tài liệu tham khảo gửi đến (with incoming references) để xem những đối tượng nào sở hữu các collection hoặc các đối tượng danh sách (list objects) -> với các tài liệu tham khảo gửi ra (with outgoing references) để xem những gì bên trong các collection đó. Hình 16 cho thấy các kết quả về xem xét các tài liệu tham khảo gửi đến dùng cho các Hashtable rỗng và mở rộng một số các mục nhập: Hình 16. Phân tích các tài liệu tham khảo gửi đến với các Hashtable rỗng trong Trình phân tích bộ nhớ Hình 16 cho thấy rằng một vài trong số các Hashtable rỗng thuộc sở hữu của mã javax.management.remote.rmi.NoCallStackClassLoader. Bằng cách xem xét khung nhìn Attributes (Các thuộc tính) trong ô cửa sổ bên trái của Trình phân tích bộ nhớ, bạn có thể xem các chi tiết cụ thể về chính Hashtable, như trong Hình 17: Hình 17. Kiểm tra Hashtable rỗng trong Trình phân tích bộ nhớ Hình 17 cho thấy rằng Hashtable có kích cỡ bằng 11 (kích cỡ mặc định) và nó là hoàn toàn rỗng. Đối với mã javax.management.remote.rmi.NoCallStackClassLoader, có khả năng tối ưu hóa việc sử dụng collection bằng: Cấp phát dần dần Hashtable: Nếu hầu hết với Hashtable là rỗng, thì với Hashtable có thể có nghĩa là được cấp phát chỉ khi có dữ liệu lưu trữ bên trong nó. Việc cấp phát Hashtable tới một kích cỡ chính xác: vì đã sử dụng kích cỡ mặc định, nên có khả năng là có thể sử dụng một kích cỡ ban đầu chính xác hơn. Liệu một hoặc cả hai sự tối ưu hóa này có thể dùng được hay không phụ thuộc vào cách sử dụng mã nói chung và dữ liệu nào thường được lưu trữ bên trong nó. Các collection rỗng trong ví dụ PlantsByWebSphere Bảng 10 cho thấy kết quả của việc phân tích các collection trong ví dụ PlantsByWebSphere để xác định xem các collection nào là rỗng: Bảng 10. Việc sử dụng collection-rỗng của PlantsByWebSphere trên WebSphere Application Server v7 Kiểu collectionSố lượng các cá thểCác cá thể rỗng % rỗng Hashtable 262,234 127,016 48.4 WeakHashMap 19,562 19,465 99.5 HashMap 10,600 7,599 71.7 ArrayList 9,530 4,588 48.1 HashSet 1,551 866 55.8 Vector 1,271 622 48.9 Tổng cộng 304,748 160,156 52.6 Bảng 10 cho thấy rằng tính trung bình, trên 50% các collection là rỗng, có nghĩa là có thể đạt được mức tiết kiệm vùng sử dụng bộ nhớ đáng kể bằng cách tối ưu hóa việc sử dụng collection. Nó có thể được áp dụng cho các mức ứng dụng khác nhau: trong mã ví dụ của PlantsByWebSphere, trong WebSphere Application Server và trong bản thân các lớp của các collcetion Java. Giữa phiên bản 7 và phiên bản 8 của WebSphere Application Server, có một số công việc đã được thực hiện để cải thiện hiệu quả bộ nhớ trong các collection Java và các lớp phần mềm trung gian. Ví dụ, một tỷ lệ phần trăm lớn về chi phí sử dụng của các cá thể của java.util.WeahHashMap là do thực tế là nó có chứa một cá thể của java.lang.ref.ReferenceQueue để xử lý các tài liệu tham khảo kém. Hình 18 cho thấy cách bố trí bộ nhớ của một WeakHashMap với Java runtime 32-bit: Hình 18. Cách bố trí bộ nhớ của một WeakHashMap cho Java runtime 32-bit Hình 18 cho thấy rằng đối tượng ReferenceQueue có trách nhiệm giữ lại số dữ liệu là 560 byte, ngay cả khi WeakHashMap rỗng và do đó ReferenceQueue không cần thiết. Đối với trường hợp ví dụ của PlantsByWebSphere với 19.465 đối tượng WeakHashMap rỗng, các đối tượng ReferenceQueue sẽ thêm vào 10.9MB dữ liệu bổ sung không cần thiết. Trong phiên bản 8 của WebSphere Application Server và bản phát hành Java 7 của các thời gian chạy Java của IBM, WeakHashMap đã trải qua một số tối ưu hóa: Nó chứa một ReferenceQueue, mà ReferenceQueue lại lần lượt chứa một mảng của các đối tượng Reference. Mảng đó đã được thay đổi để được cấp phát dần dần — tức là, chỉ khi đối tượng được thêm vào ReferenceQueue.
File đính kèm:
- Từ mã Java đến heap Java.pdf