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.

pdf39 trang | Chuyên mục: Java | Chia sẻ: dkS00TYs | Lượt xem: 2143 | Lượt tải: 1download
Tóm tắt nội dung Từ mã Java đến heap Java, để xem tài liệu hoàn chỉnh bạn click vào nút "TẢI VỀ" ở trên
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:

  • pdfTừ mã Java đến heap Java.pdf
Tài liệu liên quan