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.

pdf39 trang | Chia sẻ: lylyngoc | Lượt xem: 1455 | Lượt tải: 1download
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