
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á

