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.
39 trang |
Chia sẻ: lylyngoc | Lượt xem: 1532 | Lượt tải: 1
Bạn đang xem trước 20 trang tài liệu Từ mã Java đến heap Java, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
Từ mã Java đến heap Java
Tóm tắt: 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.
Nền tảng: Cách sử dụng bộ nhớ của một quá trình Java
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 và -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
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
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 đó.
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
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, int
và long, 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
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, byte và
long — 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
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ường
Kích cỡ trường (bit)
Đối tượng Mảng
32-bit 64-bit 32-bit 64-bit
boolean 32 32 8 8
byte 32 32 8 8
char 32 32 16 16
short 32 32 16 16
int 32 32 32 32
float 32 32 32 32
long 64 64 64 64
double 64 64 64 64
Các trường đối tượng 32 64 (32*) 32 64 (32*)
Siêu dữ liệu đối tượng 32 64 (32*) 32 64 (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à:
HashSet
HashMap
Hashtable
LinkedList
ArrayList
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
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 định 16 mục nhập
Kích cỡ rỗng 144 bytes
Chi phí sử dụng 16 byte cộng với chi phí sử dụng của HashMap
Chi phí sử dụng cho một
collection 10 K
16 byte cộng với chi phí sử dụng của HashMap
Tìm kiếm/Chèn/Xóa hiệu
năng
O(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
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 định 16 mục nhập
Kích cỡ rỗng 128 bytes
Chi phí sử dụng 64 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ăng
O(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
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 định 11 mục nhập
Kích cỡ rỗng 104 bytes
Chi phí sử dụng 56 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ăng
O(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