Từ mã Java đến heap Java

Hiểu và tối ưu hóa cách sử dụng bộ nhớ trong ứng dụng của bạn

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.

Chris Bailey, Kiến trúc sư dịch vụ Java, IBM

Chris BaileyChris Bailey là thành viên của nhóm Trung tâm Công nghệ Java của IBM tại Phòng thí nghiệm phát triển của Công viên Hursley ở Vương quốc Anh. Là kiến trúc sư kỹ thuật cho tổ chức hỗ trợ và dịch vụ Java của IBM, ông chịu trách nhiệm về tạo điều kiện cho những người dùng SDK Java của IBM có thể triển khai ứng dụng thành công. Chris cũng tham gia vào việc thu thập và đánh giá các yêu cầu mới, phân phối các khả năng và các công cụ gỡ lỗi mới, thực hiện các cải tiến về tài liệu hướng dẫn và cải thiện chất lượng của SDK Java của IBM.


Tác giả chuyên nghiệp của
        developerWorks

09 07 2013 (Xuất bản lần đầu tiên vào ngày 02 01 2013)

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.

Nền tảng: Cách sử dụng bộ nhớ của một quá trình Java

Sự hiểu biết

Để có một sự hiểu biết sâu sắc về cách sử dụng bộ nhớ-quá trình của ứng dụng Java, hãy đọc các bài "Cảm ơn bộ nhớ" của Andrew Hall trên developerWorks. Các bài này trình bày cách bố trí và vùng người dùng có sẵn trên Windows® và Linux® và trên AIX® và sự tương tác giữa vùng lưu trữ đặc biệt trong bộ nhớ của Java (Java heap - heap Java) và vùng lưu trữ đặc biệt trong bộ nhớ nguyên gốc (native heap - heap nguyên gốc).

Khi bạn chạy một ứng dụng Java bằng cách thực hiện lệnh java trên dòng lệnh hoặc bằng cách bắt đầu một số phần mềm trung gian dựa trên Java, thời gian chạy Java tạo ra một quá trình của hệ điều hành — cũng giống như bạn đang chạy một chương trình dựa trên-C. Trong thực tế, hầu hết các máy ảo Java (JVM) được viết chủ yếu bằng C hoặc C++. Là một quá trình (process) của hệ điều hành, Java runtime phải đối mặt với những hạn chế về bộ nhớ tương tự như bất kỳ quá trình khác nào: khả năng đánh địa chỉ được cung cấp bởi kiến trúc và vùng người dùng được cung cấp bởi hệ điều hành.

Khả năng đánh địa chỉ bộ nhớ được cung cấp bởi kiến trúc phụ thuộc vào kích cỡ bit của bộ vi xử lý — ví dụ, 32 hoặc 64 bit hoặc 31 bit trong trường hợp máy tính lớn. Số lượng các bit mà quá trình này có thể xử lý xác định phạm vi của bộ nhớ mà bộ xử lý có khả năng đánh địa chỉ: 32 bit cung cấp một phạm vi đánh địa chỉ là 2^32, bằng 4.294.967.296 bit, hoặc 4GB. Phạm vi đánh địa chỉ với một bộ xử lý 64-bit lớn hơn đáng kể: 2^64 bằng 18.446.744.073.709.551.616 hoặc 16 exabyte.

Một số phạm vi đánh địa chỉ do kiến trúc của bộ vi xử lý cung cấp được sử dụng bởi chính hệ điều hành với nhân của nó và (đối với các JVM được viết bằng C hoặc C++) với C runtime. Số lượng bộ nhớ được hệ điều hành và C runtime sử dụng phụ thuộc vào hệ điều hành đang được sử dụng, nhưng luôn quan trọng là: cách sử dụng mặc định của Windows là 2GB. Vùng đánh địa chỉ còn lại — được gọi là vùng người dùng — là bộ nhớ có sẵn cho quá trình thực tế đang chạy.

Tiếp theo, đối với các ứng dụng Java, vùng người dùng là bộ nhớ được sử dụng bởi quá trình Java, thực sự bao gồm hai nhóm: (các) heap Java và heap nguyên gốc (heap không phải của Java). Các giá trị thiết lập heap java của JVM kiểm soát kích cỡ của heap Java : -Xms-Xmx tương ứng thiết lập heap java tối thiểu và tối đa. heap nguyên gốc là vùng người dùng còn lại sau khi heap java đã được cấp phát ở giá trị thiết lập kích cỡ tối đa. Hình 1 cho thấy một ví dụ về những gì mà điều này có thể giống như với một quá trình Java 32-bit:

Hình 1. Ví dụ về cách bố trí bộ nhớ cho một quá trình (process) Java 32-bit
Ví dụ về cách bố trí bộ nhớ cho một quá trình Java 32-bit

Trong Hình 1, việc sử dụng hệ điều hành và C runtime chiếm khoảng 1 GB trong số 4GB vùng địa chỉ, Java heap sử dụng gần 2GB và heap nguyên gốc sử dụng phần còn lại. Lưu ý rằng bản thân JVM cũng sử dụng bộ nhớ — theo cùng cách mà nhân của hệ điều hành và C runtime làm — và cũng lưu ý rằng bộ nhớ mà JVM sử dụng là một tập hợp con của heap nguyên gốc.


Cấu tạo của một đối tượng Java

Khi mã Java của bạn sử dụng toán tử new để tạo ra một thể hiện của đối tượng Java, có nhiều dữ liệu được cấp phát hơn là bạn có thể mong đợi. Ví dụ, bạn có thể ngạc nhiên khi biết rằng tỷ lệ kích cỡ của một giá trị int trên một đối tượng Integer— đối tượng nhỏ nhất có thể lưu giữ một giá trị int— thường là 1:4. Chi phí sử dụng bổ sung chính là siêu dữ liệu mà JVM sử dụng để mô tả đối tượng Java, trong trường hợp này là một Integer.

Số lượng của siêu dữ liệu đối tượng thay đổi theo từng phiên bản và nhà cung cấp JVM, nhưng thường có:

  • Lớp (Class): Một con trỏ trỏ tới thông tin lớp, mô tả kiểu đối tượng. Ví dụ, trong trường hợp của một đối tượng java.lang.Integer, đây là một con trỏ đến lớp java.lang.Integer.
  • Cờ : (Flags): Một bộ sưu tập các cờ mô tả trạng thái của đối tượng, gồm có mã băm (hash code) cho đối tượng nếu nó chỉ có một và hình dạng (shape) của đối tượng (tức là, dù đối tượng có là một mảng hay không).
  • Khóa (Lock): Thông tin đồng bộ hóa cho đối tượng — đó là, liệu đối tượng có được đồng bộ hóa không.

Sau đó siêu dữ liệu đối tượng được tiếp theo bởi chính dữ liệu đối tượng, bao gồm các trường được lưu trữ trong cá thể đối tượng. Trong trường hợp của một đối tượng java.lang.Integer, đây là một int đơn.

Vì vậy, khi bạn tạo một thể hiện của một đối tượng java.lang.Integer khi chạy một JVM 32-bit, cách bố trí của đối tượng có thể trông như Hình 2:

Hình 2. Ví dụ về cách bố trí của một đối tượng java.lang.Integer cho một quá trình Java 32-bit
Ví dụ về cách bố trí của một đối tượng java.lang.Integer cho một quá trình Java 32-bit

Như Hình 2 cho thấy, 128 bit dữ liệu được sử dụng để lưu 32 bit dữ liệu trong giá trị int, vì siêu dữ liệu đối tượng sử dụng phần còn lại của 128 bit đó.


Cấu tạo của một đối tượng mảng Java

Hình dạng và cấu trúc của một đối tượng mảng, chẳng hạn như là một mảng của các giá trị int, tương tự như hình dạng và cấu trúc của một đối tượng Java tiêu chuẩn. Sự khác biệt chính là đối tượng mảng có thêm một mảnh siêu dữ liệu bổ sung để biểu thị kích cỡ của mảng. Vậy một siêu dữ liệu của đối tượng mảng gồm có:

  • Lớp : Một con trỏ trỏ tới thông tin lớp, mô tả kiểu đối tượng. Trong trường hợp của một mảng của các trường int, thì con trỏ này sẽ trỏ đến lớp int[].
  • Cờ : Một bộ sưu tập các cờ mô tả trạng thái của đối tượng, gồm có mã băm cho đối tượng (nếu có) và hình dạng của đối tượng (tức là, dù đối tượng có là một mảng hay không).
  • Khóa : Thông tin đồng bộ hóa cho đối tượng — đó là, liệu đối tượng có được đồng bộ hóa không.
  • Kích cỡ : Kích cỡ của mảng.

Hình 3 cho thấy một ví dụ về cách bố trí cho một đối tượng mảng int:

Hình 3. Ví dụ về cách bố trí của một đối tượng mảng int cho một quá trình Java 32-bit
Ví dụ về cách bố trí của một đối tượng mảng int cho một quá trình Java 32-bit

Trong Hình 3, 160 bit dữ liệu lưu 32 bit dữ liệu trong giá trị int, vì siêu dữ liệu mảng sử dụng phần còn lại của 160 bit đó. Đối với các nguyên hàm như byte, intlong, một mảng chỉ có một mục nhập tốn kém về bộ nhớ hơn so với đối tượng trình bao bọc tương ứng (Byte, Integer hay Long) với trường đơn lẻ.


Cấu tạo của các cấu trúc dữ liệu phức tạp hơn

Việc thiết kế và việc lập trình hướng đối tượng khuyến khích sử dụng tính bao đóng - encapsulation (cung cấp các lớp giao diện interface kiểm soát quyền truy cập vào dữ liệu bên trong) và ủy thác - delegation (việc sử dụng các đối tượng của trình trợ giúp để thực hiện các nhiệm vụ). Tính đóng gói (encapsulation) và sự ủy thác (delegation) là nguyên nhân đòi hỏi phải có nhiều đối tượng khi muốn biểu diễn cấu trúc dữ liệu. Một ví dụ đơn giản là một đối tượng java.lang.String. Dữ liệu trong một đối tượng java.lang.String là một mảng các ký tự được đóng gói bởi một đối tượng java.lang.String có thể quản lý và kiểm soát quyền truy cập vào mảng ký tự đó. Cách bố trí của một đối tượng java.lang.String cho một quá trình Java 32-bit có thể như Hình 4:

Hình 4. Ví dụ về cách bố trí của một đối tượng java.lang.String cho một quá trình Java 32-bit
Ví dụ về cách bố trí của một đối tượng java.lang.String cho một quá trình Java 32-bit

Như Hình 4 cho thấy, một đối tượng java.lang.String có chứa — ngoài siêu dữ liệu đối tượng tiêu chuẩn — một số trường để quản lý dữ liệu chuỗi. Thông thường, các trường này là một giá trị băm, một tổng số kích cỡ của chuỗi đếm được, giá trị bù vào các dữ liệu chuỗi và một tài liệu tham khảo đối tượng cho chính mảng ký tự đó.

Điều này có nghĩa là để có một chuỗi 8 ký tự (128 bit dữ liệu char), 256 bit dữ liệu dùng cho mảng ký tự và 224 bit dữ liệu dùng cho đối tượng java.lang.String quản lý nó, thì sẽ tạo ra một tổng là 480 bit (60 byte) để biểu diễn cho 128 bit (16 byte) dữ liệu. Đây là một tỷ lệ chi phí sử dụng 3.75:1.

Nói chung, một cấu trúc dữ liệu trở nên càng phức tạp hơn thì chi phí sử dụng của nó càng lớn hơn. Điều này được thảo luận chi tiết hơn trong phần tiếp theo.


Các đối tượng Java 32-bit và 64-bit

Các kích cỡ và chi phí đối với các đối tượng trong các ví dụ trước áp dụng cho một quá trình (process) Java 32-bit. Như bạn đã biết từ phần Thông tin cơ bản: Cách sử dụng bộ nhớ của một quá trình Java, một bộ xử lý 64-bit có một mức đánh địa chỉ bộ nhớ cao hơn nhiều hơn so với một bộ xử lý 32-bit. Với một quá trình 64-bit, kích cỡ của một số các trường dữ liệu trong đối tượng Java — đặc biệt, các siêu dữ liệu đối tượng và bất kỳ trường nào có liên quan đến một đối tượng khác — cũng cần tăng lên đến 64 bit. Các kiểu dữ liệu trường khác — chẳng hạn như int, bytelong — không thay đổi về kích cỡ. Hình 5 cho thấy cách bố trí cho một đối tượng Integer 64-bit và cho một mảng int:

Hình 5. Ví dụ về cách bố trí của một đối tượng java.lang.Integer và một mảng int cho một quá trình Java 64-bit
Ví dụ về cách bố trí của một đối tượng java.lang.Integer và một mảng int cho một quá trình Java 64-bit

Hình 5 cho thấy rằng đối với một đối tượng Integer 64-bit, hiện tại đang sử dụng 224 bit dữ liệu để lưu trữ 32 bit cho trường int— một tỷ lệ chi phí sử dụng là 7:1. Đối với một mảng int chỉ có một phần tử 64-bit, sử dụng 288 bit dữ liệu để lưu trữ mục nhập int 32-bit — một chi phí sử dụng là 9:1. Hiệu quả của điều này trên các ứng dụng thực tế là ở chỗ việc sử dụng bộ nhớ heap java của một ứng dụng mà trước đây đã chạy trên một thời gian chạy Java 32-bit tăng lên đáng kể khi nó được chuyển sang một thời gian chạy Java 64-bit. Thông thường, mức tăng vào khoảng 70% kích cỡ của heap ban đầu. Ví dụ, đối với một ứng dụng java chạy trên Java runtime 32-bit mà sử dụng 1GB java heap thì khi chạy trên Java runtime 64-bit sẽ sử dụng đến 1,7GB java heap.

Lưu ý rằng sự tăng thêm bộ nhớ này không bị hạn chế theo head java. Cách sử dụng vùng bộ nhớ heap nguyên gốc cũng sẽ tăng thêm, đôi khi bằng 90%.

Bảng 1 cho thấy các kích cỡ trường cho các đối tượng và các mảng khi một ứng dụng chạy ở chế độ 32-bit và 64-bit:

Bảng 1. Các kích cỡ trường trong các đối tượng với các thời gian chạy Java 32-bit và 64-bit
Kiểu trườngKích cỡ trường (bit)
Đối tượngMảng
32-bit64-bit32-bit64-bit
boolean323288
byte323288
char32321616
short32321616
int32323232
float32323232
long64646464
double64646464
Các trường đối tượng3264 (32*)3264 (32*)
Siêu dữ liệu đối tượng3264 (32*)3264 (32*)

*Kích cỡ của các trường đối tượng và của dữ liệu được sử dụng cho từng mục nhập siêu dữ liệu đối tượng có thể được giảm đến 32 bit thông qua các công nghệ OOP nén hoặc Các tài liệu tham khảo nén.

Các con trỏ đối tượng nén thông thường (OOPs - Compressed Ordinary Object Pointers) và Các tài liệu tham khảo nén

Các JVM của IBM và Oracle đều cung cấp các khả năng nén tài liệu tham khảo-đối tượng thông qua các tùy chọn tương ứng là Các tài liệu tham khảo nén (-Xcompressedrefs) và Các OOPs nén (-XX:+UseCompressedOops). Việc sử dụng các tùy chọn này cho phép các giá trị của các trường đối tượng và siêu dữ liệu đối tượng được lưu trữ bằng 32 bit thay vì 64 bit. Điều này có tác dụng vô hiệu hóa sự tăng thêm 70% của bộ nhớ heap java khi một ứng dụng được dịch chuyển từ một Java runtime 32-bit sang Java runtime 64-bit. Lưu ý rằng các tùy chọn này không có tác dụng gì đối với cách sử dụng bộ nhớ heap nguyên gốc; với Java runtime 64-bit nó vẫn còn cao hơn so với Java runtime 32-bit.


Cách sử dụng bộ nhớ của các bộ collection Java

Trong hầu hết các ứng dụng, một số lượng lớn dữ liệu được lưu trữ và quản lý bằng cách sử dụng các lớp collection Java tiêu chuẩn được cung cấp như là một phần cốt lõi của Java API. Nếu việc tối ưu hóa vùng nhớ là quan trọng cho ứng dụng của bạn, thì việc hiểu chức năng mà mỗi bộ collection cung cấp và chi phí sử dụng bộ nhớ liên quan là đặc biệt có ích. Nói chung, mức độ về các khả năng chức năng của một bộ collection càng cao thì chi phí sử dụng bộ nhớ của nó càng cao — do đó, việc sử dụng các kiểu bộ collection có nhiều chức năng hơn bạn mong muốn, thì sẽ phải chịu chi phí sử dụng bộ nhớ bổ sung không cần thiết.

Một số bộ collection thường dùng là:

Trừ HashSet, danh sách này theo thứ tự giảm dần về cả chức năng lẫn chi phí sử dụng bộ nhớ. (Một HashSet, là một trình bao bọc xung quanh một đối tượng HashMap, thực sự cung cấp ít chức năng hơn so với HashMap trong khi chi phí sử dụng bộ nhớ lại lớn hơn một chút).

Bộ collection Java: HashSet

HashSet là một dẫn xuất từ interface Set. Tài liệu hướng dẫn API của nền tảng Java SE 6 mô tả HashSet như sau:

Một collcetion không chứa các phần tử trùng lặp nào. Chính thức hơn, các tập hợp không chứa cặp các phần tử e1 và e2 nào như là e1.equals (e2) và nhiều nhất có một phần tử rỗng (null). Như tên gọi của mình, giao diện này mô hình hóa sự trừu tượng hóa của tập hợp toán học.

Một HashSet có ít các khả năng hơn một HashMap ở chỗ nó không thể chứa nhiều hơn một mục nhập rỗng và không thể có các mục nhập trùng lặp. Việc thực hiện này là một trình bao bọc xung quanh một HashMap, với đối tượng HashSet quản lý những gì được phép đưa vào đối tượng HashMap. Chức năng bổ sung để hạn chế các khả năng của một HashMap có nghĩa là các HashSet có một chi phí sử dụng bộ nhớ cao hơn một chút.

Hình 6 cho thấy cách bố trí và cách sử dụng bộ nhớ của một HashSet trên Java runtime 32-bit:

Hình 6. Cách bố trí và cách sử dụng bộ nhớ của một HashSet trên Java runtime 32-bit
Cách bố trí và cách sử dụng bộ nhớ của một HashSet trên thời gian chạy Java 32-bit

Hình 6 cho thấy heap nông (shallow heap) (cách sử dụng bộ nhớ của các đối tượng riêng lẻ) tính theo byte, cùng với heap lưu giữ (retained heap) (cách sử dụng bộ nhớ của đối tượng riêng lẻ và các đối tượng con của nó) tính theo byte cho một đối tượng java.util.HashSet. Kích cỡ của shallow heap là 16 byte và kích cỡ của retained heap là 144 byte. Khi một HashSet được tạo ra, dung lượng mặc định (default capacity) của nó — số lượng các mục nhập có thể được đưa vào tập hợp đó — là 16 mục nhập. Khi một HashSet được tạo ra với dung lượng mặc định và không có mục nhập nào được đưa vào tập hợp đó, nó chiếm 144 byte. Đây là 16 byte bổ sung thêm so với cách sử dụng bộ nhớ của một HashMap. Bảng 2 cho thấy các thuộc tính của một HashSet:

Bảng 2. Các thuộc tính của một HashSet
Dung lượng mặc định16 mục nhập
Kích cỡ rỗng144 bytes
Chi phí sử dụng16 byte cộng với chi phí sử dụng của HashMap
Chi phí sử dụng cho một collection 10 K16 byte cộng với chi phí sử dụng của HashMap
Tìm kiếm/Chèn/Xóa hiệu năngO(1) — Thời gian thực hiện là thời gian cố định, bất kể số lượng các phần tử (giả sử không có va chạm hàm băm nào)

Bộ collection Java: HashMap

HashMap là một dẫn xuất từ interface Map. Tài liệu hướng dẫn API của nền tảng Java SE 6 mô tả HashMap như sau:

Một đối tượng để ánh xạ các khóa tới các giá trị. Một ánh xạ không thể chứa các khóa trùng lặp; mỗi khóa có thể ánh xạ nhiều nhất tới một giá trị.

HashMap cung cấp một cách để lưu trữ các cặp giá trị/khóa, sử dụng một hàm băm để chuyển đổi khóa thành một chỉ mục trong collection, nơi lưu trữ cặp giá trị/khóa. Điều này cho phép truy cập nhanh đến vị trí dữ liệu. Cho phép sử dụng các mục nhập rỗng và các mục nhập trùng lặp; như vậy, một HashMap là một sự đơn giản hóa của một HashSet.

Việc thực hiện của một HashMap như là một mảng của các đối tượng HashMap$Entry. Hình 7 cho thấy cách bố trí và cách sử dụng bộ nhớ của một HashMap trên Java runtime 32-bit:

Hình 7. Cách bố trí và cách sử dụng bộ nhớ của một HashMap trên Java runtime 32-bit
Cách bố trí và cách sử dụng bộ nhớ của một HashMap trên một thời gian chạy Java 32-bit

Như Hình 7 cho thấy, khi một HashMap được tạo ra, kết quả là một đối tượng HashMap và một mảng của đối tượng HashMap$Entry với dung lượng mặc định là 16 mục nhập. Điều này cung cấp cho một HashMap một kích cỡ 128 byte khi nó hoàn toàn rỗng. Bất kỳ các cặp giá trị/khóa nào được chèn vào HashMap đều được bao bọc bởi một đối tượng HashMap$Entry, mà bản thân nó có một số chi phí sử dụng.

Hầu hết các triển khai thực hiện của các đối tượng HashMap$Entry đều có chứa các trường sau:

  • int KeyHash
  • Object next (đối tượng tiếp theo)
  • Object key (khóa của đối tượng)
  • Object value (giá trị đối tượng)

Một đối tượng HashMap$Entry 32-byte quản lý các cặp giá trị/khóa của dữ liệu được đưa vào collection. Điều này có nghĩa là tổng chi phí sử dụng cần thiết của một HashMap gồm có đối tượng HashMap, một mục nhập của mảng HashMap$Entry và một đối tượng HashMap$Entry cho mỗi mục nhập. Điều này có thể được thể hiện theo công thức sau:

Đối tượng HashMap + chi phí sử dụng của đối tượng Mảng + (số các mục nhập * (mục nhập mảng HashMap$Entry + đối tượng HashMap$Entry))

Đối với một HashMap có 10.000 mục nhập, chi phí sử dụng của HashMap, mảng HashMap$Entry và đối tượng HashMap$Entry xấp xỉ 360K. Chi phí sử dụng này có trước khi tính đến kích cỡ của các giá trị và các khóa đang được lưu trữ.

Bảng 3 cho thấy các thuộc tính của HashMap:

Bảng 3. Các thuộc tính của HashMap
Dung lượng mặc định16 mục nhập
Kích cỡ rỗng128 bytes
Chi phí sử dụng64 byte cộng 36 byte cho mỗi mục nhập
Chi phí sử dụng cho một collection 10 K~360K
Tìm kiếm/Chèn/Xóa hiệu năngO(1) — Thời gian thực hiện là thời gian cố định, bất kể số lượng các phần tử (giả sử không có va chạm hàm băm nào)

Bộ collection Java: Hashtable

Hashtable, giống như HashMap, là một dẫn xuất của interface Map. Tài liệu hướng dẫn API của nền tảng Java SE 6 mô tả Hashtable như sau:

Lớp này thực hiện một Hashtable, ánh xạ các khóa tới các giá trị. Có thể sử dụng bất kỳ đối tượng không rỗng (null) nào làm một khóa hoặc một giá trị.

Hashtable rất giống với HashMap, nhưng nó có hai hạn chế. Nó không chấp nhận các giá trị rỗng (null) cho khóa hoặc mục nhập có. Ngược lại, HashMap có thể nhận giá trị rỗng và không đồng bộ nhưng có thể được thực hiện đồng bộ khi sử dụng phương thức Collections.synchronizedMap().

Việc triển khai Hashtable — cũng tương tự như của HashMap— như là một mảng của các đối tượng mục nhập, trong trường hợp này là các đối tượng Hashtable$Entry. Hình 8 cho thấy cách bố trí và cách sử dụng bộ nhớ của một Hashtable trên Java runtime 32-bit:

Hình 8. Cách bố trí và cách sử dụng bộ nhớ của một Hashtable trên Java runtime 32-bit
Cách bố trí và cách sử dụng bộ nhớ của một Hashtable trên một thời gian chạy Java 32-bit

Hình 8 Hình 8 cho thấy rằng khi một Hashtable được tạo ra, kết quả là một đối tượng Hashtable đang sử dụng 40 byte bộ nhớ cùng với một mảng của các Hashtable$entry có dung lượng mặc định là 11 mục nhập, tổng số một kích cỡ lên tới 104 byte cho một Hashtable rỗng.

Hashtable$Entry thực sự lưu dữ liệu tương tự như HashMap:

  • int KeyHash
  • Object next
  • Object key
  • Object value

Điều này có nghĩa là đối tượng Hashtable$Entry cũng dùng 32 byte cho mục nhập giá trị/khóa trong Hashtable và tính toán chi phí sử dụng của Hashtable và kích cỡ của 10K mục nhập của bộ sư tập (khoảng 360K) là giống như của HashMap.

Bảng 4 cho thấy các thuộc tính của một Hashtable:

Bảng 4. Các thuộc tính của một Hashtable
Dung lượng mặc định11 mục nhập
Kích cỡ rỗng104 bytes
Chi phí sử dụng56 byte cộng 36 byte cho mỗi mục nhập
Chi phí sử dụng cho một collection 10 K~360K
Tìm kiếm/Chèn/Xóa hiệu năngO(1) — Thời gian thực hiện là thời gian cố định, bất kể số lượng các phần tử (giả sử không có va chạm hàm băm nào)

Như bạn có thể thấy, Hashtable có dung lượng mặc định hơi nhỏ hơn so với HashMap (11 so với 16). Mặt khác, sự khác biệt chính là khả năng của Hashtable để nhận các giá trị và các khóa rỗng và sự đồng bộ hóa mặc định của nó có thể không cần thiết và làm giảm hiệu năng của collection.

Các collection Java: LinkedList

LinkedList là một dẫn xuất của interface List. Tài liệu hướng dẫn API của nền tảng Java SE 6 mô tả LinkedList như sau:

Một collection theo thứ tự (còn được gọi là một chuỗi). Người dùng giao diện này có quyền điều khiển chính xác ở nơi mỗi phần tử được chèn vào trong danh sách. Người dùng có thể truy cập các phần tử theo chỉ mục số nguyên (vị trí trong danh sách) và tìm kiếm các phần tử trong danh sách đó. Không giống như các tập hợp, các danh sách thường cho phép dùng các phần tử trùng lặp.

Việc thực hiện là một danh sách liên kết của các đối tượng LinkedList$Entry. Hình 9 cho thấy cách bố trí và cách sử dụng bộ nhớ của LinkedList trên Java runtime 32-bit:

Hình 9. Cách bố trí và cách sử dụng bộ nhớ của một LinkedList trên Java runtime 32-bit
Cách bố trí và cách sử dụng bộ nhớ của một LinkedList trên một thời gian chạy Java 32-bit

Hình 9 cho thấy rằng khi một đối tượng LinkedList được tạo ra, kết quả là một đối tượng LinkedList đang sử dụng 24 byte bộ nhớ cùng với một đối tượng LinkedList$Entry duy nhất, tổng số lên tới 48 byte bộ nhớ cho một LinkedList rỗng.

Một trong những lợi thế của các danh sách liên kết là chúng được định cỡ chính xác và không cần phải định cỡ lại. Dung lượng mặc định thực tế chỉ có một mục nhập và điều này tăng lên và co lại một cách linh động khi thêm vào hoặc gỡ bỏ các mục nhập. Hiện vẫn còn một chi phí sử dụng cho từng đối tượng LinkedList$Entry, với các trường dữ liệu của chúng như sau:

  • Object previous
  • Object next
  • Object value

Nhưng chi phí sử dụng này nhỏ hơn so với chi phí sử dụng của các HashMap và các Hashtable, bởi vì các danh sách liên kết chỉ lưu trữ một mục nhập duy nhất chứ không phải là một cặp giá trị/khóa và không cần lưu trữ một giá trị băm (hash value) vì không sử dụng các việc dò tìm dựa trên mảng. Về mặt tiêu cực, các việc dò tìm trong một danh sách liên kết có thể chậm hơn nhiều, vì phải duyệt qua danh sách liên kết để tìm ra mục nhập đúng. Đối với các danh sách liên kết lớn, điều đó có thể làm cho thời gian dò tìm kéo dài.

Bảng 5 cho thấy các thuộc tính của một LinkedList:

Bảng 5. Các thuộc tính của một LinkedList
Dung lượng mặc định1 mục nhập
Kích cỡ rỗng48 bytes
Chi phí sử dụng24 byte, cộng 24 byte cho mỗi mục nhập
Chi phí sử dụng cho một collection 10 K~240K
Tìm kiếm/Chèn/Xóa hiệu năngO(n) — Thời gian thực hiện phụ thuộc tuyến tính vào số lượng các phần tử

Bộ collection Java: ArrayList

ArrayList là một mảng dẫn xuất từ interface List, có khả năng thay đổi kích cỡ. Tài liệu hướng dẫn API của nền tảng Java SE 6 mô tả ArrayList như sau:

Một collection theo thứ tự (còn được gọi là một chuỗi). Người dùng có thể điều khiển chính xác nơi mà mỗi phần tử được chèn vào trong danh sách. Người dùng có thể truy cập các phần tử theo chỉ mục số nguyên (vị trí trong danh sách) và tìm kiếm các phần tử trong danh sách đó. Không giống như các tập hợp, các danh sách thường cho phép dùng các phần tử trùng lặp.

Không giống như LinkedList, ArrayList được thực hiện bằng cách sử dụng một mảng các Object. Hình 10 cho thấy cách bố trí và cách sử dụng bộ nhớ của ArrayList với Java runtime 32-bit:

Hình 10. Cách bố trí và cách sử dụng bộ nhớ của ArrayList trên Java runtime 32-bit
Cách bố trí và cách sử dụng bộ nhớ của ArrayList trên một thời gian chạy Java 32-bit

Hình 10 cho thấy khi một ArrayList được tạo ra, kết quả là một đối tượng ArrayList đang sử dụng 32 byte bộ nhớ, cùng với một mảng Object đang có một kích thước mặc định là 10, tổng số lên tới 88 byte bộ nhớ dùng cho một ArrayList rỗng. Điều này có nghĩa là ArrayList không phải định cỡ chính xác và do đó có một dung lượng mặc định ngẫu nhiên là 10 mục nhập.

Bảng 6 cho thấy các thuộc tính của một ArrayList:

Bảng 6. Các thuộc tính của một ArrayList
Dung lượng mặc định10
Kích cỡ rỗng88 bytes
Chi phí sử dụng48 bytes plus 4 bytes per entry
Chi phí sử dụng cho một collection 10 K~40K
Tìm kiếm/Chèn/Xóa hiệu năngO(n) — Thời gian thực hiện phụ thuộc tuyến tính vào số lượng các phần tử

Các kiểu "collection" khác

Ngoài các collection tiêu chuẩn, cũng có thể coi, StringBuffer là một collection trong đó nó quản lý dữ liệu ký tự và là giống về cấu trúc và các khả năng với các collection khác. Tài liệu hướng dẫn API của nền tảng Java SE 6 mô tả StringBuffer như sau:

Một chuỗi các ký tự hay thay đổi, có thể hoạt động trong môi trường đa luồng.... Mỗi bộ đệm chuỗi có một khả năng. Miễn là chiều dài của chuỗi ký tự có trong bộ đệm chuỗi không vượt quá khả năng đó, không cần thiết phải cấp phát một mảng bộ đệm bên trong mới. Nếu có các lỗi tràn bộ đệm bên trong, nó sẽ tự động mở rộng lớn hơn.

Việc triển khai StringBuffer giống như triển khai một mảng các ký tự char. Hình 11 cho thấy cách bố trí và cách sử dụng bộ nhớ của một StringBuffer trên Java runtime 32-bit:

Hình 11. Cách bố trí và cách sử dụng bộ nhớ của một StringBuffer trên Java runtime 32-bit
Cách bố trí và cách sử dụng bộ nhớ của một StringBuffer trên một thời gian chạy Java 32-bit

Hình 11 cho thấy rằng khi một StringBuffer được tạo ra, kết quả là một đối tượng StringBuffer đang sử dụng 24 byte bộ nhớ, cùng với một mảng ký tự có kích thước mặc định là 16, tổng số lên đến 72 byte dữ liệu cho một StringBuffer rỗng.

Giống như bộ collection, StringBuffer có một dung lượng mặc định và một cơ chế để định cỡ lại. Bảng 7 cho thấy các thuộc tính của StringBuffer:

Bảng 7. Các thuộc tính của một StringBuffer
Dung lượng mặc định16
Kích cỡ rỗng72 bytes
Chi phí sử dụng24 bytes
Chi phí sử dụng cho một collection 10 K24 bytes
Tìm kiếm/Chèn/Xóa hiệu năngNA


Vùng rỗng trong các collection

Chi phí sử dụng của các collection khác nhau với một số lượng các đối tượng đã cho không phải là toàn bộ câu chuyện về chi phí sử dụng bộ nhớ. Các phép đo trong các ví dụ trên giả định rằng các collection đã được định cỡ chính xác. Nhưng đối với hầu hết các collection, điều này không chắc đúng. Hầu hết các collection được tạo ra với dung lượng ban đầu đã cho và dữ liệu được đưa vào collection đó. Điều này có nghĩa rằng hầu hết các collection có một dung lượng lớn hơn so với dữ liệu được lưu trữ trong nó, mà điều đó đưa vào chi phí sử dụng bổ sung.

Hãy xem xét ví dụ về một StringBuffer. Dung lượng mặc định của nó là 16 mục nhập ký tự, với kích cỡ là 72 byte. Ban đầu, không có dữ liệu nào được lưu trữ trong 72 byte đó. Nếu bạn đặt một 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
Cách sử dụng bộ nhớ của một StringBuffer có chứa 'MY STRING' trên một thời gian chạy Java 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.


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
Các sử dụng bộ nhớ của một StringBuffer có chứa 'MY STRING OF TEXT' trên một thời gian chạy Java 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
CollectionHiệu năngDung lượng mặc địnhKích cỡ rỗngChi phí sử dụng của mục nhập 10KCó định cỡ chính xác không?Thuật toán mở rộng
HashSetO(1)16144360KNox2
HashMapO(1)16128360KNox2
HashtableO(1)11104360KNox2+1
LinkedListO(n)148240KYes+1
ArrayListO(n)108840KNox1.5
StringBufferO(1)167224Nox2

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 ArrayListLinkedList 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.


Các collection đang sử dụng: PlantsByWebSphere và Phiên bản 7 của WebSphere Application Server (máy chủ ứng dụng WebSphere)

Trong Bảng 8, chi phí sử dụng để tạo ra một collection dạng Hash có 10.000 mục nhập được hiển thị là 360K. Căn cứ vào đó, đối với các ứng dụng Java phức tạp hầu hết đều chạy với các heap java có kích cỡ tính bằng gigabyte, điều này có vẻ không giống như chi phí sử dụng lớn — tất nhiên, trừ khi đang sử dụng một số lượng lớn các collection.

Bảng 9 cho thấy việc sử dụng đối tượng-collection như là một phần của 206MB của việc sử dụng heap java khi ứng dụng mẫu PlantsByWebSphere được cung cấp với Phiên bản 7 của WebSphere® Application Server chạy thông qua thử nghiệm của năm người dùng:

Bảng 9. Cách sử dụng collection của PlantsByWebSphere trên WebSphere Application Server v7
Kiểu collectionSố các cá thểTổng chi phí sử dụng của collection (MB)
Hashtable262,23426.5
WeakHashMap19,56212.6
HashMap10,6002.3
ArrayList9,5300.3
HashSet1,5511.0
Vector1,2710.04
LinkedList1,1480.1
TreeMap2990.03
Tổng cộng306,19542.9

Bạn có thể thấy từ Bảng 9 là nhiều hơn 300.000 collection khác nhau đang được sử dụng — và bản thân các collection, không kể các dữ liệu mà chúng chứa, chiếm 42,9MB (21%) trong việc sử dụng heap java 206MB. Điều này có nghĩa là mức tiết kiệm bộ nhớ tiềm năng đáng kể có sẵn nếu bạn hoặc thay đổi các kiểu collectoin hoặc bảo đảm rằng kích cỡ của các collection chính xác hơn.


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ớ
Phân tích tỷ lệ lấp đầy của các bộ sưu tập 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
Phân tích tỷ lệ lấp đầy của các Hashtable trong Trình phân tích bộ nhớ

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ớ
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ớ
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
Hashtable262,234127,01648.4
WeakHashMap19,56219,46599.5
HashMap10,6007,59971.7
ArrayList9,5304,58848.1
HashSet1,55186655.8
Vector1,27162248.9
Tổng cộng304,748160,15652.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
Cách bố trí bộ nhớ của một WeakHashMap cho một thời gian chạy Java 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.


Kết luận

Một số lượng lớn và có lẽ đáng ngạc nhiên của các collection tồn tại trong bất kỳ ứng dụng bình thường nào và còn nhiều hơn thế nữa đối với các ứng dụng phức tạp. Việc sử dụng một số lượng lớn các collection thường cung cấp cơ hội để đôi khi đạt được mức tiết kiệm vùng sử dụng bộ nhớ đáng kể bằng cách chọn đúng collection, định cỡ nó một cách chính xác và có thể bằng cách cấp phát nó dần dần. Những quyết định này tốt nhất được tạo ra trong quá trình thiết kế và phát triển, nhưng bạn cũng có thể sử dụng công cụ Trình phân tích bộ nhớ để phân tích các ứng dụng hiện có của mình cho việc tối ưu hóa vùng sử dụng bộ nhớ tiềm năng.

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

Thảo luận

  • IBM về xử lý sự cố các ứng dụng Java: Đọc blog này, do Chris Bailey và các đồng nghiệp của ông viết ra, để biết các tin tức và thông tin về việc tạo công cụ của IBM để xử lý sự cố các ứng dụng Java của bạn.
  • Hãy tham gia vào cộng đồng developerWorks. Kết nối với những người dùng developerWorks khác trong khi khám phá các blog, các diễn đàn, các nhóm và các wiki theo hướng nhà phát triển.

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java, Nguồn mở
ArticleID=853796
ArticleTitle=Từ mã Java đến heap Java
publish-date=07092013