Tải bản đầy đủ (.pdf) (48 trang)

Cảm ơn bộ nhớ Hiểu cách JVM sử dụng bộ nhớ riêng trên Windows và Linux như thế nào pps

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (7.27 MB, 48 trang )

Cảm ơn bộ nhớ
Hiểu cách JVM sử dụng bộ nhớ riêng trên Windows và Linux như thế nào
Andrew Hall, Kỹ sư phần mềm, IBM
Tóm tắt: Việc sử dụng hết heap (ND: heap là vùng lưu trữ đặc biệt trong bộ nhớ
được dùng để lưu giữ các tài liệu quan trọng như tài nguyên hệ thống và các loại
đối tượng khác nhau đang được sử dụng. Các heap đều được giới hạn trong phạm
vi 64k. Sau đây gọi là vùng heap) của Java™ không phải là nguyên nhân duy nhất
tạo ra một lỗi java.lang.OutOfMemoryError. Nếu bộ nhớ riêng (native memory)
dùng hết, có thể xảy ra các lỗi OutOfMemoryError (lỗi thiếu bộ nhớ) mà các kỹ
thuật gỡ lỗi thông thường của bạn sẽ không thể giải quyết được. Bài viết này giải
thích bộ nhớ riêng là gì, thời gian chạy của Java sử dụng nó như thế nào, việc
dùng hết nó sẽ như thế nào và làm thế nào để gỡ lỗi cho một lỗi
OutOfMemoryError trên Windows® và Linux®. Một bài viết khác của cùng tác
giả trình bày về các chủ đề tương tự nhưng dành cho các hệ thống AIX®.
Vùng heap của Java, nơi cấp phát không gian nhớ cho mọi đối tượng Java, là vùng
bộ nhớ gắn kết mật thiết nhất với bạn khi viết các ứng dụng Java. Máy ảo Java
(JVM) được thiết kế để cách ly chúng ta khỏi các điểm đặc thù của máy chủ, vì thế
hoàn toàn tự nhiên, có thể coi vùng heap như là một bộ nhớ. Chắc chắn là bạn đã
từng gặp phải một lỗi OutOfMemoryError của vùng heap của Java — gây ra bởi
một lỗ rò đối tượng hoặc do không tạo ra vùng heap đủ lớn để lưu trữ tất cả các dữ
liệu của bạn — và có lẽ bạn đã học được một vài thủ thuật để gỡ lỗi các kịch bản
này. Nhưng khi các ứng dụng Java của bạn xử lý nhiều dữ liệu hơn và nạp công
việc đồng thời nhiều hơn, bạn có thể bắt đầu nếm trải các lỗi OutOfMemoryError
không thể sửa chữa được khi sử dụng cả túi các thủ thuật thông thường của bạn —
đó là các kịch bản trong đó các lỗi xuất hiện ngay cả khi vùng heap của Java chưa
đầy. Khi điều này xảy ra, bạn cần phải hiểu những gì đang xảy ra bên trong Môi
trường thời gian chạy Java (Java Runtime Environment-JRE) của bạn.
Các ứng dụng Java chạy trong môi trường ảo hóa của thời gian chạy (runtime)
Java, nhưng thời gian chạy bản thân nó là một chương trình riêng được viết bằng
một ngôn ngữ (ví dụ như C), có tiêu dùng tài nguyên riêng, bao gồm cả bộ nhớ
riêng. Bộ nhớ riêng là bộ nhớ có sẵn dùng cho tiến trình thời gian chạy, để phân


biệt với bộ nhớ của vùng heap Java do một ứng dụng Java sử dụng. Mỗi tài
nguyên ảo — bao gồm cả vùng heap Java và các luồng (threads) Java — phải
được lưu trữ trong bộ nhớ riêng, cùng với các dữ liệu được các máy ảo sử dụng
khi nó chạy. Điều này có nghĩa rằng những hạn chế về bộ nhớ riêng, do phần cứng
của máy chủ và hệ điều hành (OS) áp đặt sẽ ảnh hưởng đến những gì bạn có thể
làm với ứng dụng Java của bạn.
Bài viết này là một trong hai bài trình bày cùng một chủ đề trên các nền tảng hệ
thống khác nhau. Trong cả hai bài, bạn sẽ tìm hiểu bộ nhớ riêng là gì, thời gian
chạy Java dùng nó như thế nào, việc sử dụng hết nó sẽ ra sao và làm thế nào để gỡ
lỗi một OutOfMemoryError riêng. Bài viết này trình bày Windows và Linux và
không tập trung vào bất kỳ thời gian chạy cụ thể nào. Bài viết của cùng một tác giả
trình bày về AIX và tập trung vào IBM® Developer Kit for Java (Bộ dụng cụ của
nhà phát triển của IBM cho Java). (Các thông tin trong bài viết đó về việc thực
hiện của IBM cũng đúng cho các nền tảng khác, không phải AIX, vì thế nếu bạn
sử dụng IBM Developer Kit cho Java trên Linux hay IBM 32-bit Runtime
Environment (Môi trường thời gian chạy 32-bit của IBM cho Windows), bạn có
thể nhận thấy bài viết đó cũng có ích).
Tóm tắt về bộ nhớ riêng
Tôi sẽ bắt đầu bằng cách giải thích những hạn chế về bộ nhớ riêng do hệ điều hành
và phần cứng nằm bên dưới áp đặt. Nếu bạn quen với việc quản lý bộ nhớ động
trong một ngôn ngữ như C, thì bạn có thể chuyển sang phần tiếp theo.
Những hạn chế về phần cứng
Rất nhiều hạn chế mà một tiến trình riêng phải trải qua là do phần cứng chứ không
phải do hệ điều hành áp đặt. Mỗi máy tính đều có một bộ xử lý và một số bộ nhớ
truy cập ngẫu nhiên (RAM), cũng được gọi là bộ nhớ vật lý. Một bộ xử lý dịch
dòng dữ liệu thành các lệnh để thực hiện; nó có một hoặc nhiều đơn vị xử lý để
thực hiện các phép tính số học số nguyên và dấu phẩy động cũng như nhiều phép
tính nâng cao hơn. Một bộ xử lý có một số thanh ghi — đó là các phần tử nhớ rất
nhanh được sử dụng làm nơi lưu trữ làm việc khi đang thực hiện các phép tính;
kích thước thanh ghi xác định số lượng lớn nhất mà một phép tính đơn lẻ có thể sử

dụng.
Bộ xử lý được kết nối với bộ nhớ vật lý bằng bus bộ nhớ. Độ lớn của địa chỉ vật lý
(địa chỉ được bộ xử lý sử dụng để lập chỉ số RAM vật lý) giới hạn dung lượng bộ
nhớ có thể được đánh địa chỉ. Ví dụ, một địa chỉ vật lý 16-bit có thể đánh địa chỉ
từ 0x0000 đến 0xFFFF, tạo ra 2^16 = 65536 vị trí nhớ duy nhất. Nếu mỗi địa chỉ
trỏ đến một byte của thiết bị lưu trữ, thì một địa chỉ vật lý 16-bit cho phép một bộ
xử lý đánh địa chỉ 64KB của bộ nhớ.
Các bộ xử lý được mô tả như là một số bit nhất định. Số này thường nói đến kích
thước của các thanh ghi, mặc dù có các trường hợp ngoại lệ — như 390 31-bit —
ở đây nó nói đến kích thước địa chỉ vật lý. Đối với các nền tảng hệ thống máy tính
để bàn và máy chủ, số này là 31, 32 hoặc 64; với thiết bị nhúng và các bộ vi xử lý,
nó có thể thấp tới mức bằng 4. Kích thước địa chỉ vật lý có thể giống như độ rộng
thanh ghi nhưng có thể lớn hơn hoặc nhỏ hơn. Hầu hết các bộ xử lý 64-bit có thể
chạy các chương trình 32-bit khi chạy một hệ điều hành phù hợp.
Bảng 1 liệt kê một số các kiến trúc Linux và Windows phổ biến với kích thước
thanh ghi và kích thước địa chỉ vật lý của chúng:

Bảng 1. Kích thước thanh ghi và địa chỉ vật lý của một số kiến trúc bộ xử lý
phổ biến
Kiến trúc
Độ rộng thanh
ghi (bits)
Kích thước địa chỉ vật lý (bits)
(Modern) Intel®
x86
32
32
36 nếu có phần mở rộng địa chỉ vật lý
(Pentium Pro và cao hơn)
x86 64

64 Hiện tại là 48-bit (có cơ hội để tăng lên sau)
PPC64
64 50-bit với POWER 5
390 31-bit
32 31
390 64-bit
64 64

Các hệ điều hành và bộ nhớ ảo
Nếu bạn đã viết các ứng dụng để chạy trực tiếp trên bộ xử lý mà không có một hệ
điều hành, bạn có thể sử dụng tất cả bộ nhớ mà bộ xử lý có thể đánh địa chỉ cho
chúng (giả sử có đủ RAM vật lý được đấu nối). Tuy nhiên, để tận hưởng các tính
năng như đa nhiệm và sự trừu tượng của phần cứng, gần như tất cả mọi người đều
sử dụng một hệ điều hành nào đó để chạy các chương trình của họ.
Trong các hệ điều hành (OS) đa nhiệm như Windows và Linux, có nhiều hơn một
chương trình sử dụng tài nguyên hệ thống, bao gồm cả bộ nhớ. Mỗi chương trình
cần phải được cấp phát các vùng nhớ vật lý để làm việc trong đó. Có thể thiết kế
một hệ điều hành sao cho mọi chương trình làm việc trực tiếp với bộ nhớ vật lý và
được tin tưởng sẽ chỉ sử dụng bộ nhớ mà nó đã được cấp. Một số hệ điều hành
nhúng làm việc giống như thế, nhưng sẽ là không thích hợp trong một môi trường
có nhiều chương trình không được thử nghiệm cùng với nhau vì bất kỳ chương
trình nào có thể làm hỏng bộ nhớ của các chương trình khác hoặc của chính hệ
điều hành.
Bộ nhớ ảo cho phép nhiều tiến trình xử lý dùng chung bộ nhớ vật lý mà không thể
làm hỏng dữ liệu của nhau. Trong một hệ điều hành có bộ nhớ ảo (như Windows,
Linux và nhiều hệ khác), mỗi chương trình có vùng địa chỉ ảo riêng của nó — một
vùng logic của các địa chỉ mà kích thước của nó do kích thước địa chỉ trên hệ
thống đó quyết định (như vậy là 31, 32 hoặc 64 bit cho các nền tảng máy tính để
bàn và máy chủ). Các vùng trong vùng địa chỉ ảo của một tiến trình có thể được
ánh xạ tới bộ nhớ vật lý, đến một tệp hoặc tới bất kỳ thiết bị lưu trữ có đánh địa

chỉ khác. Hệ điều hành có thể di chuyển dữ liệu được giữ trong bộ nhớ vật lý đến
và ra khỏi một vùng trao đổi (tệp trang (page file) trên Windows hay phân vùng
trao đổi (swap partition) trên Linux) khi nó không được sử dụng, để sử dụng bộ
nhớ vật lý một cách tốt nhất. Khi một chương trình cố gắng truy cập vào bộ nhớ
bằng cách sử dụng một địa chỉ ảo, hệ điều hành kết hợp với phần cứng trên chip
ánh xạ địa chỉ ảo đến vị trí vật lý. Vị trí đó có thể là RAM vật lý, một tệp hoặc tệp
trang/phân vùng trao đổi. Nếu một vùng bộ nhớ đã được di chuyển tới vùng trao
đổi, thì sau đó nó được nạp lại vào bộ nhớ vật lý trước khi được sử dụng. Hình 1
cho thấy bộ nhớ ảo làm việc như thế nào bằng cách ánh xạ các vùng của vùng địa
chỉ tiến trình xử lý đến các tài nguyên dùng chung:

Hình 1. Bộ nhớ ảo ánh xạ vùng địa chỉ tiến trình tới các tài nguyên vật lý

Mỗi cá thể của một chương trình chạy như một tiến trình. Một tiến trình trên
Linux và Windows là một tập hợp thông tin về tài nguyên do hệ điều hành kiểm
soát (như tệp và thông tin về trình cắm thêm), thường là một vùng địa chỉ ảo
(nhiều hơn một vùng trên một số kiến trúc) và ít nhất một luồng thi hành.
Kích thước của vùng địa chỉ ảo có thể nhỏ hơn kích thước địa chỉ vật lý của bộ xử
lý. Intel x86 32-bit ban đầu có một địa chỉ vật lý 32-bit, cho phép bộ xử lý đánh
địa chỉ 4GB của thiết bị lưu trữ. Sau đó, một đặc tính gọi là Physical Address
Extension (PAE-Phần mở rộng địa chỉ vật lý) đã được thêm vào để mở rộng kích
thước địa chỉ vật lý lên 36-bit — cho phép cài đặt và đánh địa chỉ RAM lên đến
64GB. PAE đã cho phép các hệ điều hành ánh xạ các vùng địa chỉ ảo 4GB 32-bit
lên trên một dải địa chỉ vật lý lớn, nhưng nó không cho phép mỗi tiến trình có một
vùng địa chỉ ảo 64GB. Điều này có nghĩa là nếu bạn đặt nhiều hơn 4GB bộ nhớ
trong một máy chủ Intel 32-bit, bạn không thể ánh xạ tất cả nó trực tiếp vào trong
một tiến trình đơn.
Tính năng Các phần mở rộng cửa sổ địa chỉ (Address Windowing Extensions) cho
phép một tiến trình Windows ánh xạ một phần vùng địa chỉ 32-bit của nó như là
một cửa sổ trượt vào trong một vùng bộ nhớ lớn hơn. Linux sử dụng các công

nghệ tương tự dựa vào việc ánh xạ các vùng vào trong vùng địa chỉ ảo. Điều này
có nghĩa rằng mặc dù bạn không thể trực tiếp tham chiếu nhiều hơn 4GB bộ nhớ,
bạn có thể làm việc với các vùng bộ nhớ lớn hơn.
Vùng nhân (kernel) và vùng người sử dụng
Mặc dù mỗi tiến trình có vùng địa chỉ riêng của mình, một chương trình thường
không thể sử dụng tất cả vùng ấy. Các vùng địa chỉ được chia thành vùng người
dùng (user space) và vùng nhân (kernel space). Nhân (kernel) là chương trình hệ
điều hành chính và chứa đựng logic để giao diện đến phần cứng máy tính, lập lịch
trình các chương trình và cung cấp các dịch vụ như làm việc trên mạng và bộ nhớ
ảo.
Là một phần của quá trình khởi động máy tính, nhân của hệ điều hành chạy và
khởi động phần cứng. Một khi nhân đã cấu hình phần cứng và trạng thái bên trong
riêng của mình, tiến trình đầu tiên của vùng người dùng mới khởi động. Nếu một
chương trình của người dùng cần một dịch vụ từ hệ điều hành, nó có thể thực hiện
một hoạt động — có tên là cuộc gọi hệ thống — để nhảy vào trong chương trình
nhân (kernel), sau đó chương trình nhân thực hiện yêu cầu. Các cuộc gọi hệ thống
thường cần thiết cho các hoạt động như đọc và viết các tệp, làm việc trên mạng và
bắt đầu các tiến trình mới.
Nhân yêu cầu truy cập vào bộ nhớ riêng của nó và bộ nhớ của tiến trình gọi khi thi
hành một cuộc gọi hệ thống. Vì bộ xử lý, đang thi hành luồng hiện tại, được cấu
hình để ánh xạ các địa chỉ ảo bằng cách sử dụng ánh xạ vùng địa chỉ cho tiến trình
hiện tại, hầu hết các hệ điều hành ánh xạ một phần của mỗi vùng địa chỉ tiến trình
tới một vùng bộ nhớ nhân chung. Phần của vùng địa chỉ được ánh xạ để sử dụng
bởi nhân được gọi là vùng nhân; phần còn lại, có thể được ứng dụng của người
dùng sử dụng, được gọi là vùng người dùng.
Sự cân bằng giữa vùng nhân và vùng người dùng khác nhau theo hệ điều hành và
thậm chí khác nhau cả trong các cá thể của cùng một hệ điều hành chạy trên kiến
trúc phần cứng khác nhau. Sự cân bằng thường cấu hình được và có thể được điều
chỉnh để cung cấp thêm vùng cho các ứng dụng của người dùng hay cho nhân.
Việc thu nhỏ vùng nhân có thể gây ra các vấn đề như hạn chế số lượng người sử

dụng có thể đăng nhập cùng lúc hoặc số các tiến trình có thể chạy; vùng người sử
dụng nhỏ hơn có nghĩa là lập trình viên ứng dụng có phạm vi làm việc nhỏ hơn
Theo mặc định, Windows 32-bit có một vùng người dùng 2GB và một vùng nhân
2GB. Sự cân bằng này có thể được thay đổi thành một vùng người sử dụng 3GB
và một vùng nhân 1GB trên một số phiên bản của Windows bằng cách thêm khóa
chuyển đổi /3GB vào cấu hình khởi động và liên kết lại các ứng dụng bằng khóa
chuyển đổi /LARGEADDRESSAWARE. Trong Linux 32-bit, giá trị mặc định là
vùng người sử dụng 3GB và vùng nhân 1GB. Một số bản phân phối Linux cung
cấp một nhân hugemem hỗ trợ một vùng người sử dụng 4GB. Để đạt được điều
này, nhân được cung cấp một vùng địa chỉ của riêng nó, được sử dụng khi một
cuộc gọi hệ thống được bắt đầu. Cái lợi về vùng người dùng phải trả giá bằng các
cuộc gọi hệ thống chậm hơn vì hệ điều hành phải sao chép dữ liệu giữa các vùng
địa chỉ và thiết lập lại các ánh xạ vùng-địa chỉ tiến trình mỗi khi một cuộc gọi hệ
thống bắt đầu. Hình 2 cho thấy bố trí vùng địa chỉ cho Windows 32-bit:

Hình 2. Bố trí vùng địa chỉ cho Windows 32-bit

Hình 3 cho thấy các bố trí vùng-địa chỉ cho Linux 32-bit:

Hình 3. Bố trí vùng-địa chỉ cho Linux 32-bit

Một vùng địa chỉ nhân riêng biệt cũng được sử dụng trên Linux 390 31-bit, trong
đó vùng địa chỉ nhỏ hơn 2GB làm cho việc phân chia một vùng địa chỉ duy nhất là
không nên, tuy nhiên, kiến trúc 390 có thể làm việc với nhiều vùng địa chỉ đồng
thời mà không làm hiệu năng giảm sút.
Vùng địa chỉ tiến trình phải có mọi thứ mà một chương trình đòi hỏi — bao gồm
chính chương trình đó và các thư viện dùng chung (các DLL trên Windows, các
tệp .so trên Linux) mà nó sử dụng. Các thư viện dùng chung không chỉ có thể
chiếm vùng mà một chương trình không thể sử dụng để lưu trữ dữ liệu vào nữa,
chúng cũng còn có thể phân mảnh vùng địa chỉ và giảm số lượng bộ nhớ có thể

được cấp phát như là một đoạn liên tục. Điều này là dễ nhận thấy trong các
chương trình chạy trên Windows x86 với một vùng người sử dụng 3GB. Các DLL
được xây dựng với một địa chỉ nạp vào ưa thích: khi một DLL được nạp, nó được
ánh xạ vào vùng địa chỉ tại một vị trí cụ thể trừ khi vị trí đó đã bị chiếm, trong
trường hợp này nó được bố trí lại và được nạp vào nơi khác. Với vùng người sử
dụng 2GB có sẵn khi Windows NT được thiết kế ban đầu, việc xây dựng các thư
viện hệ thống để nạp vào gần ranh giới 2GB là có ý nghĩa — làm như thế sẽ để lại
hầu hết vùng người sử dụng tự do cho ứng dụng sử dụng. Khi vùng người dùng
được mở rộng đến 3GB, các thư viện dùng chung hệ thống vẫn nạp ở gần 2GB —
bây giờ nằm ở giữa vùng người dùng. Mặc dù có một vùng người dùng tổng cộng
3GB, không thể cấp phát một khối 3GB của bộ nhớ vì các thư viện dùng chung đã
ở trong đó rồi.
Sử dụng khóa chuyển đổi /3GB trên Windows làm giảm vùng nhân tới một nửa so
với những gì nó đã được thiết kế ban đầu. Trong một số kịch bản có thể dùng hết
vùng nhân 1GB và nếm trải vào/ra (I/O) chậm hoặc các vấn đề khi tạo phiên người
dùng mới. Mặc dù khóa chuyển đổi /3GB có thể rất có giá trị cho một số ứng
dụng, bất cứ môi trường nào khi sử dụng nó cần được kiểm tra tải kỹ lưỡng trước
khi được triển khai. Xem Tài nguyên với các đường liên kết đến nhiều thông tin
hơn về khóa chuyển đổi /3GB và các lợi thế và các bất lợi của nó.
Lỗi lỗ rò bộ nhớ riêng hoặc sử dụng bộ nhớ riêng quá mức sẽ gây ra những vấn đề
khác nhau tùy thuộc vào việc bạn tận dụng hết vùng địa chỉ hay là chạy hết bộ nhớ
vật lý. Việc cạn kiệt vùng địa chỉ thường xảy ra chỉ với các tiến trình 32-bit — vì
tối đa 4GB dễ dàng cấp phát. Một tiến trình 64-bit có một vùng người sử dụng
bằng hàng trăm hoặc hàng ngàn gigabyte, rất khó để lấp đầy ngay cả khi bạn cố
gắng. Nếu bạn dùng hết vùng địa chỉ của một tiến trình Java, thì sau đó thời gian
chạy Java có thể bắt đầu hiển thị các triệu chứng kỳ lạ mà tôi sẽ mô tả sau trong
bài viết này. Khi chạy trên một hệ thống có nhiều vùng địa chỉ tiến trình hơn bộ
nhớ vật lý, một lỗ rò bộ nhớ hoặc việc sử dụng quá mức bộ nhớ riêng buộc hệ điều
hành trao đổi, đưa ra thiết bị lưu trữ hậu thuẫn một số vùng địa chỉ ảo của tiến
trình riêng. Truy cập vào một địa chỉ bộ nhớ đã được trao đổi đưa ra ngoài chậm

hơn nhiều so với đọc địa chỉ đang thường trú (trong bộ nhớ vật lý) vì hệ điều hành
phải lấy dữ liệu ra từ đĩa cứng. Có thể cấp phát đủ bộ nhớ để tận dụng hết tất cả bộ
nhớ vật lý và tất cả bộ nhớ trao đổi (vùng phân trang); trên Linux, điều này sẽ kích
hoạt trình sát thủ hết bộ nhớ (OOM) nhân, trình sát thủ này buộc phải giết tiến
trình thiếu bộ nhớ nhất. Trên Windows, việc cấp phát bắt đầu thất bại theo cùng
một cách như chúng đã xảy ra nếu vùng địa chỉ đã đầy.
Nếu bạn đồng thời cố gắng sử dụng nhiều bộ nhớ ảo hơn bộ nhớ vật lý hiện có, dĩ
nhiên vấn đề xuất hiện sớm hơn nhiều trước khi tiến trình này bị giết vì dùng quá
bộ nhớ. Hệ thống sẽ luẩn quẩn — dành phần lớn thời gian của nó sao chép bộ nhớ
quay đi quay lại từ vùng trao đổi. Khi điều này xảy ra, hiệu năng của máy tính và
các ứng dụng riêng lẻ sẽ trở nên tồi tệ đến mức người dùng không thể không nhận
thấy đã có vấn đề. Khi một vùng heap Java của JVM bị trao đổi ra, hiệu năng của
các bộ thu gom dữ liệu rác trở nên cực kỳ kém, đến mức mà ứng dụng có thể bị
treo. Nếu nhiều thời gian chạy Java đồng thời đang chạy trên một máy tính tại
cùng một thời điểm, thì bộ nhớ vật lý phải đủ để chứa hết tất cả các vùng heap của
Java.


Thời gian chạy của Java sử dụng bộ nhớ riêng như thế nào
Thời gian chạy của Java là một tiến trình hệ điều hành chịu các ràng buộc của
phần cứng và hệ điều hành mà tôi nêu trong phần trước. Các môi trường thời gian
chạy cung cấp các khả năng theo đòi hỏi của mã của người sử dụng còn chưa biết;
điều này làm cho không thể dự đoán môi trường thời gian chạy sẽ đòi hỏi tài
nguyên nào trong mỗi tình huống. Mỗi hành động mà một ứng dụng Java thực
hiện bên trong môi trường Java được quản lý đều có khả năng có thể ảnh hưởng
đến các yêu cầu tài nguyên của thời gian chạy cung cấp môi trường đó. Phần này
mô tả các ứng dụng Java tiêu dùng bộ nhớ riêng như thế nào và tại sao.
Vùng heap của Java và việc thu dọn dữ liệu rác
Vùng heap của Java là vùng bộ nhớ mà các đối tượng được cấp phát ở đó. Hầu hết
các triển khai thực hiện Java SE có một vùng heap lôgic, mặc dù một số thời gian

chạy Java chuyên biệt, ví dụ như là triển khai thực hiện Đặc tả thời gian thực cho
Java (Real Time Specification for Java -RTSJ) có nhiều vùng heap. Một vùng
heap vật lý có thể được chia một cách lô gic thành các phần tùy thuộc vào thuật
toán thu dọn dữ liệu rác (GC) được sử dụng để quản lý bộ nhớ của vùng heap.
Những phần này thường được triển khai thực hiện như các ô nhớ liền khối của bộ
nhớ riêng dưới sự kiểm soát của trình quản lý bộ nhớ Java (bao gồm các bộ thu
dọn dữ liệu rác).
Kích thước của vùng heap được điều khiển từ dòng lệnh Java bằng cách sử dụng
các tuỳ chọn -Xmx và -Xms (mx là kích thước tối đa của vùng heap, ms là kích
thước ban đầu). Mặc dù vùng heap lô-gic (vùng bộ nhớ được sử dụng thực sự) có
thể tăng lên và thu nhỏ theo số lượng các đối tượng trên vùng heap và thời gian
dành cho GC, dung lượng bộ nhớ riêng được sử dụng vẫn không đổi và được
quyết định bởi giá trị -Xmx: kích thước vùng heap tối đa. Hầu hết các thuật toán
GC dựa trên vùng heap đang được cấp phát như một dãy ô nhớ liền khối của bộ
nhớ, do đó không thể cấp phát thêm nhiều bộ nhớ riêng khi vùng heap cần mở
rộng. Tất cả bộ nhớ của vùng heap phải được dự trữ trước.
Việc dự trữ bộ nhớ riêng không giống như việc cấp phát nó. Khi bộ nhớ riêng
được dự trữ, nó không được hậu thuẫn bởi bộ nhớ vật lý hoặc thiết bị lưu trữ khác.
Mặc dù việc dự trữ các đoạn của vùng địa chỉ sẽ không làm cạn kiệt tài nguyên vật
lý, nhưng nó ngăn cản không cho bộ nhớ đó được sử dụng cho các mục đích khác.
Lỗ rò do việc dự trữ bộ nhớ gây ra vì không bao giờ được sử dụng cũng nghiêm
trọng không kém lỗ rò bộ nhớ được cấp phát.
Một số bộ thu gom dữ liệu rác giảm thiểu việc sử dụng bộ nhớ vật lý bằng cách
không chuyển giao (giải phóng thiết bị lưu trữ phía sau cho) các phần của vùng
heap khi mà vùng heap sử dụng bị thu nhỏ.
Thêm bộ nhớ riêng là cần thiết để duy trì trạng thái của hệ thống quản lý bộ nhớ
đang duy trì vùng heap của Java. Các cấu trúc dữ liệu phải được phân phối để theo
dõi thiết bị lưu trữ chưa sử dụng và ghi lại tiến trình khi thu dọn dữ liệu rác. Kích
thước chính xác và bản chất của các cấu trúc dữ liệu ấy thay đổi tùy từng triển
khai thực hiện, nhưng phần nhiều là tỉ lệ thuận với kích thước của vùng heap.

Trình biên dịch tức thời (JIT)
Trình biên dịch tức thời JIT biên dịch bytecode của Java thành mã thực thi riêng
được tối ưu hóa trong thời gian chạy. Điều này cải thiện rất nhiều tốc độ thời gian-
chạy của các thời gian chạy của Java và cho phép các ứng dụng Java chạy ở các
tốc độ so sánh được với mã riêng.
Việc biên dịch Bytecode sử dụng bộ nhớ riêng (giống như cách mà một trình biên
dịch tĩnh như là gcc đòi hỏi bộ nhớ để chạy), nhưng cả đầu vào (bytecode) lẫn đầu
ra (mã thực thi) từ JIT cũng phải được lưu trữ trong bộ nhớ riêng. Các ứng dụng
Java có chứa nhiều phương thức được biên dịch tức thời (JIT) sử dụng nhiều bộ
nhớ riêng hơn các ứng dụng nhỏ hơn.
Các lớp và các trình nạp lớp (classloader)
Các ứng dụng Java gồm có các lớp định nghĩa cấu trúc đối tượng và logic phương
thức. Chúng cũng sử dụng các lớp từ các thư viện lớp thời gian chạy Java (như
java.lang.String) và có thể sử dụng các thư viện của bên thứ ba. Các lớp này cần
phải được lưu trữ trong bộ nhớ khi mà chúng được sử dụng.
Theo cách triển khai thực hiện các lớp được lưu trữ thay đổi như thế nào. Sun JDK
sử dụng vùng heap được tạo ra cố định (PermGen). Việc thực hiện của IBM từ
Java 5 trở đi cấp phát dãy ô nhớ của bộ nhớ riêng cho mỗi một trình nạp lớp
(classloader) và lưu trữ dữ liệu lớp trong đó. Thời gian chạy Java hiện đại có các
công nghệ như việc dùng chung lớp có thể yêu cầu ánh xạ các vùng bộ nhớ dùng
chung vào trong vùng địa chỉ. Để hiểu cách các cơ chế cấp phát này ảnh hưởng
đến dấu vết riêng của thời gian chạy Java của bạn như thế nào, bạn cần phải đọc
tài liệu kỹ thuật về việc triển khai thực hiện đó. Tuy nhiên, một số sự kiện phổ
biến ảnh hưởng đến tất cả các việc thực hiện.
Ở mức độ cơ sở nhất, việc sử dụng càng nhiều lớp hơn thì càng sử dụng nhiều bộ
nhớ hơn. (Điều này có thể có nghĩa là việc sử dụng bộ nhớ riêng của bạn tăng lên
hoặc bạn phải thay đổi kích thước một vùng một cách rõ ràng — chẳng hạn như
PermGen hoặc bộ nhớ sẵn (cache) của lớp-dùng chung — để cho phép chứa hết tất
cả các lớp). Hãy nhớ rằng không chỉ cần chứa hết ứng dụng của bạn; mà các
khung công tác, các máy chủ ứng dụng, các thư viện của bên thứ ba và thời gian

chạy của Java đều chứa các lớp được nạp theo yêu cầu và chiếm vùng nhớ.
Thời gian chạy của Java có thể giải phóng các lớp để lấy lại vùng nhớ, nhưng chỉ
trong những điều kiện nghiêm ngặt. Không thể chỉ giải phóng một lớp đơn lẻ; thay
vào đó các trình nạp lớp được giải phóng và mang theo tất cả các lớp mà chúng đã
nạp. Một trình nạp lớp có thể được giải phóng chỉ khi:
 Vùng heap của Java không chứa tham chiếu tới đối tượng
java.lang.ClassLoader đại diện cho trình nạp lớp đó.
 Vùng heap của Java không chứa tham chiếu tới bất cứ các đối tượng
java.lang.Class đại diện cho các lớp được nạp bởi trình nạp lớp đó.
 Không có đối tượng nào của lớp bất kỳ được trình nạp lớp đó nạp vào đang
còn hoạt động (được tham chiếu) trên vùng heap của Java.
Cần lưu ý rằng trong ba trình nạp lớp mặc định do thời gian chạy Java tạo ra cho
tất cả các ứng dụng Java — bootstrap (tự mồi), extension (phần mở rộng) và
application (ứng dụng)— có thể không bao giờ đáp ứng các tiêu chí này; do vậy,
các lớp hệ thống bất kỳ (như java.lang.String) hoặc các lớp ứng dụng bất kỳ được
nạp qua trình nạp lớp của ứng dụng không thể được giải phóng trong thời gian
chạy.
Ngay cả khi một trình nạp lớp đủ điều kiện bị thu gom, thời gian chạy thu gom các
trình nạp lớp chỉ như là một phần của một chu kỳ GC. Một số triển khai thực hiện
chỉ giải phóng các trình nạp lớp trong một số chu kỳ GC nào đó.
Cũng có khả năng các lớp được tạo ra trong thời gian chạy, mà bạn không biết
điều đó. Nhiều ứng dụng JEE sử dụng công nghệ JavaServer Pages (JSP) để sản
xuất các trang Web. Việc sử dụng JSP sẽ tạo ra một lớp cho mỗi trang .jsp được
thi hành sẽ tồn tại suốt vòng đời của trình nạp lớp đã nạp chúng — vòng đời tiêu
biểu của ứng dụng Web.
Một cách phổ biến khác để tạo ra các lớp là sử dụng sự phản chiếu của Java. Cách
thức sự phản chiếu Java hoạt động thay đổi theo các việc triển khai thực hiện Java,
nhưng cả hai việc triển khai thực hiện của Sun và IBM đều sử dụng phương thức
mà tôi sẽ mô tả bây giờ.
Khi sử dụng API java.lang.reflect, thời gian chạy Java phải kết nối các phương

thức của một đối tượng phản chiếu java.lang.reflect.Field) đến đối tượng hoặc lớp
được phản chiếu tới. Điều này có thể được thực hiện bằng cách sử dụng trình truy
cập (accessor) của Giao diện riêng của Java (Java Native Interface-JNI), JNI đòi
hỏi phải thiết lập rất ít, nhưng lại rất chậm khi chạy, hoặc bằng cách xây dựng một
lớp động trong thời gian chạy cho từng kiểu đối tượng mà bạn muốn phản chiếu
tới. Phương thức sau thiết lập chậm hơn, nhưng lại chạy nhanh hơn, và là lý tưởng
cho các ứng dụng thường xuyên phải phản chiếu đến một lớp cụ thể.
Thời gian chạy Java sử dụng phương thức JNI vài lần đầu tiên khi một lớp được
phản chiếu, nhưng sau khi được sử dụng một số lần, trình truy cập lớn lên thành
một trình truy cập (accessor) bytecode, bao gồm việc xây dựng một lớp và nạp nó
nhờ một trình nạp lớp mới. Việc thực hiện nhiều sự phản chiếu có thể làm cho
phải sinh ra nhiều lớp của trình truy cập và trình nạp lớp. Việc duy trì các tham
chiếu đến các đối tượng phản chiếu làm cho các lớp này vẫn hoạt động và tiếp tục
chiếm vùng nhớ. Vì việc tạo ra các trình truy cập bytecode khá chậm, nên thời
gian chạy Java có thể ghi nhớ sẵn (cache) các trình truy cập này để sử dụng lại
sau. Một số ứng dụng và các khung công tác cũng ghi nhớ sẵn các đối tượng phản
chiếu, do đó làm tăng dấu vết riêng của chúng.
JNI
JNI cho phép mã riêng (các ứng dụng được viết bằng ngôn ngữ được biên dịch ban
đầu như C và C++) để gọi các phương thức Java và ngược lại. Thời gian chạy Java
tự nó dựa chủ yếu vào mã JNI để thực hiện các hàm thư viện-lớp như là tệp và
vào/ra (I/O) mạng. Một ứng dụng JNI có thể làm tăng dấu vết riêng của thời gian
chạy Java theo ba cách:
 Mã riêng cho một ứng dụng JNI được biên dịch thành một thư viện dùng
chung hoặc mã có thể chạy được rồi nạp vào vùng địa chỉ tiến trình. Các
ứng dụng riêng lớn có thể chiếm một đoạn đáng kể của vùng địa chỉ tiến
trình đơn giản ngay khi được nạp.
 Mã riêng phải dùng chung vùng địa chỉ với thời gian chạy Java. Bất kỳ các
việc cấp phát bộ nhớ riêng hay các việc ánh xạ bộ nhớ nào được thực hiện
bởi mã riêng đều lấy bộ nhớ từ thời gian chạy Java.

 Một số hàm JNI nhất định có thể sử dụng bộ nhớ riêng như là một phần
hoạt động bình thường của chúng. Các hàm GetTypeArrayElements và
GetTypeArrayRegion có thể sao chép dữ liệu của vùng heap của Java vào
các bộ đệm của bộ nhớ riêng để cho mã riêng làm việc với chúng. Việc có
tạo ra một bản sao chép hay không phụ thuộc vào việc triển khai thực hiện
thời gian chạy. (Bộ dụng cụ của nhà phát triển của IBM cho Java 5 – IBM
Developer for Java 5.0 - và cao hơn, có tạo ra một bản sao riêng). Việc truy
cập một số lượng lớn dữ liệu của vùng heap của Java theo cách này có thể
sử dụng một số lượng lớn của vùng heap riêng tương ứng.
NIO
Các lớp I/O mới (NIO) được bổ sung thêm vào Java 1.4 đã đưa vào một cách làm
mới để thực hiện I/O dựa trên các kênh và các bộ đệm. Giống như các bộ đệm I/O
được hậu thuẫn bởi bộ nhớ trên vùng heap Java, NIO bổ sung thêm sự hỗ trợ cho
các ByteBuffer trực tiếp (được cấp phát bằng cách sử dụng phương thức
java.nio.ByteBuffer.allocateDirect() được hậu thuẫn bởi bộ nhớ riêng chứ không
phải vùng heap Java. Các ByteBuffer trực tiếp có thể được chuyển trực tiếp tới các
hàm thư viện của hệ điều hành riêng để thực hiện I/O — làm cho chúng nhanh hơn
đáng kể trong một số kịch bản vì chúng có thể tránh việc sao chép dữ liệu giữa
vùng heap Java và vùng heap riêng.
Dễ bị lúng túng về dữ liệu ByteBuffer trực tiếp đang được lưu giữ ở đâu. Ứng
dụng này vẫn còn sử dụng một đối tượng trên vùng heap Java để hòa phối các hoạt
động I/O, nhưng bộ đệm chứa dữ liệu được tổ chức trong bộ nhớ riêng — đối
tượng của vùng heap Java chỉ chứa một tham chiếu đến bộ đệm của vùng heap
riêng. Một ByteBuffer không trực tiếp sẽ chứa dữ liệu của nó trong một mảng
byte[] trên vùng heap Java. Hình 4 cho thấy sự khác biệt giữa các đối tượng
ByteBuffer trực tiếp và không trực tiếp:

Hình 4. Hình trạng (tô pô) của bộ nhớ với các java.nio.ByteBuffer trực tiếp
và không trực tiếp


Các đối tượng ByteBuffer trực tiếp tự động xóa bộ đệm riêng của chúng nhưng chỉ
có thể làm như vậy như là một phần của GC của vùng heap Java — vì vậy chúng
không tự động đáp ứng với sức ép trên vùng heap riêng. GC xảy ra chỉ khi vùng
heap Java trở nên đầy đến nỗi nó không thể phục vụ một yêu cầu cấp phát- vùng
heap hoặc nếu ứng dụng Java yêu cầu thực hiện một cách rõ ràng (việc này không
được khuyến khích vì nó gây ra các vấn đề về hiệu năng).
Các trường hợp không hợp lý sẽ là vùng heap riêng trở nên đầy và một hoặc nhiều
ByteBuffers (các bộ đệm byte) trực tiếp có đủ điều kiện để thu dọn dữ liệu rác (và
có thể được giải phóng để tạo ra chỗ trống dành cho vùng heap riêng), nhưng vùng
heap Java chủ yếu trống rỗng nên việc thu dọn dữ liệu rác (GC) không xảy ra.
Các luồng
Mỗi luồng trong một ứng dụng đòi hỏi bộ nhớ để lưu trữ ngăn xếp của nó (vùng
bộ nhớ được sử dụng để chứa các biến tại chỗ và duy trì trạng thái khi gọi các
hàm). Mỗi luồng Java yêu cầu vùng ngăn xếp để chạy. Tùy thuộc vào việc triển
khai thực hiện, một luồng Java có thể có ngăn xếp riêng và ngăn xếp Java riêng
biệt. Ngoài vùng ngăn xếp, mỗi luồng yêu cầu một số bộ nhớ riêng để lưu trữ cục
bộ của luồng và các cấu trúc dữ liệu bên trong.
Kích thước ngăn xếp thay đổi theo việc triển khai thực hiện Java và theo kiến trúc.
Một số việc triển khai thực hiện cho phép bạn quy định kích thước ngăn xếp cho
các luồng Java. Điển hình là các giá trị giữa 256KB và 756KB.
Mặc dù số lượng bộ nhớ được sử dụng cho mỗi luồng là khá nhỏ, đối với một ứng
dụng có hàng trăm luồng, tổng bộ nhớ sử dụng cho các ngăn xếp luồng có thể lớn.
Việc chạy một ứng dụng với nhiều luồng hơn số các bộ xử lý có sẵn để chạy
chúng thường không hiệu quả và có thể dẫn đến hiệu năng kém cũng như việc sử
dụng bộ nhớ tăng lên.


Tôi có thể nói như thế nào nếu tôi đang dùng hết bộ nhớ riêng?
Một thời gian chạy Java đối phó hoàn toàn khác nhau với việc dùng hết vùng heap
Java so với việc dùng hết vùng heap riêng, mặc dù cả hai tình thế có thể làm xuất

hiện các dấu hiệu tương tự nhau. Một ứng dụng Java rất khó hoạt động khi vùng
heap Java bị cạn kiệt — bởi vì rất khó cho một ứng dụng Java để thực hiện bất cứ
việc gì mà không cấp phát các đối tượng. Hiệu năng GC kém và các lỗi
OutOfMemoryError báo hiệu một vùng heap đầy được tạo ra ngay khi vùng heap
tràn đầy.
Trái lại, ngay khi một thời gian chạy Java đã khởi động và ứng dụng ở trong trạng
thái ổn định, nó có thể tiếp tục hoạt động với vùng heap riêng đã cạn hết. Không
nhất thiết phải chỉ ra bất kỳ hành vi xấu nào, bởi vì các hành động đòi hỏi cấp phát
bộ nhớ riêng hiếm xảy ra hơn nhiều so với các hành động đòi hỏi cấp phát vùng
heap Java. Mặc dù các hành động yêu cầu bộ nhớ riêng thay đổi tùy theo việc triển
khai thực hiện JVM, nhưng sau đây là một số ví dụ phổ biến: bắt đầu một luồng,
nạp một lớp và thực hiện một số loại I/O mạng và tệp.
Hành vi thiếu bộ nhớ riêng cũng ít nhất quán hơn so với các hành vi thiếu bộ nhớ
của vùng heap Java, vì không có điểm kiểm soát duy nhất với các việc cấp phát
vùng heap riêng. Trong khi tất cả các việc cấp phát vùng heap Java nằm dưới sự
kiểm soát của hệ thống quản lý bộ nhớ Java, thì bất kỳ mã riêng nào — cho dù nó
ở bên trong JVM, các thư viện lớp Java hoặc mã ứng dụng — có thể thực hiện một
việc cấp phát bộ nhớ riêng và bị thất bại. Sau đó, mã cố gắng thực hiện việc cấp
phát ấy có thể xử lý nó như người thiết kế muốn: nó có thể đưa ra một lỗi
OutOfMemoryError thông qua giao diện JNI, in một thông báo trên màn hình, hay
âm thầm không chạy nữa và thử làm lại sau hoặc làm cái gì đó khác.
Việc thiếu một hành vị dự tính trước có nghĩa là không có một cách đơn giản nào
để nhận biết sự cạn kiệt bộ nhớ riêng. Thay vào đó, bạn cần phải sử dụng dữ liệu
từ hệ điều hành và từ thời gian chạy Java để xác định chẩn đoán đó.


Ví dụ về thiếu bộ nhớ riêng
Để giúp bạn thấy việc cạn kiệt bộ nhớ riêng ảnh hưởng đến việc triển khai thực
hiện Java mà bạn đang sử dụng như thế nào, mã mẫu của bài viết này (xem Tải về)
có chứa một số chương trình Java gây ra sự cạn kiệt vùng heap riêng theo nhiều

cách khác nhau. Các ví dụ này sử dụng một thư viện riêng được viết bằng C để
tiêu dùng tất cả các vùng địa chỉ riêng và sau đó cố gắng thực hiện một số hành
động có sử dụng bộ nhớ riêng. Các ví dụ được cung cấp đã được xây dựng (built)
sẵn, mặc dù các chỉ thị biên dịch chúng được cung cấp trong tệp README.html
trong thư mục cao nhất của gói ví dụ mẫu.
Lớp com.ibm.jtc.demos.NativeMemoryGlutton cung cấp phương thức
gobbleMemory(), gọi hàm malloc trong một vòng lặp cho đến khi hầu như tất cả
bộ nhớ riêng được sử dụng hết. Khi nó đã hoàn thành nhiệm vụ của mình, nó in số
byte được cấp phát thành lỗi tiêu chuẩn như sau:
Allocated 1953546736 bytes of native memory before running out

Kết quả đầu ra được thu giữ cho mỗi lần trình diễn (demo) với một thời gian chạy
Java của Sun và một thời gian chạy Java của IBM đang chạy trên Windows 32-bit.
Các tệp mã nhị phân được cung cấp đã được thử nghiệm trên:
 Linux x86
 Linux PPC 32
 Linux 390 31
 Windows x86
Phiên bản sau đây của thời gian chạy Java của Sun đã được sử dụng để thu kết quả
đầu ra:
java version "1.5.0_11" Java(TM) 2 Runtime Environment, Standard
Edition (build 1.5.0_11-b03) Java HotSpot(TM) Client VM (build
1.5.0_11-b03, mixed
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters
|
mode)

Phiên bản thời gian chạy Java của IBM được sử dụng là:
java version "1.5.0" Java(TM) 2 Runtime Environment, Standard

Edition (build pwi32devifx-20071025 (SR 6b)) IBM J9 VM (build 2.3,
J2RE 1.5.0 IBM J9
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters
|
2.3 Windows XP x86-32 j9vmwi3223-2007100 7 (JIT enabled) J9VM -
20071004_14218_lHdSMR JIT - 20070820_1846ifx1_r8 GC -
200708_10) JCL - 20071025
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters
|

Cố gắng để khởi động một luồng khi hết bộ nhớ riêng
Lớp com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation cố gắng để khởi
động một luồng khi vùng địa chỉ tiến trình đã dùng hết. Đây là một cách phổ biến
để phát hiện ra rằng tiến trình Java của bạn thiếu bộ nhớ vì nhiều ứng dụng bắt đầu
các luồng trong vòng đời của chúng.
Kết quả đầu ra từ trình diễn (demo) StartingAThreadUnderNativeStarvation khi
chạy trên thời gian chạy Java của IBM là:
Allocated 1019394912 bytes of native memory before running out
JVMDUMP006I Processing Dump Event "systhrow", detail
"java/lang/OutOfMemoryError" -
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters |
Please Wait. JVMDUMP007I JVM Requesting Snap Dump using
'C:\Snap0001.20080323.182114.5172.trc' JVMDUMP010I Snap Dump written to

C:\Snap0001.20080323.182114.5172.trc JVMDUMP007I JVM Requesting Heap
Dump using
| 10 20 30 40 50 60 70 80 9|

| XML error: The previous line is longer than the max of 90 characters |
'C:\heapdump.20080323.182114.5172.phd' JVMDUMP010I Heap Dump written
to
C:\heapdump.20080323.182114.5172.phd JVMDUMP007I JVM Requesting
Java Dump using
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters |
'C:\javacore.20080323.182114.5172.txt' JVMDUMP010I Java Dump written to
C:\javacore.20080323.182114.5172.txt JVMDUMP013I Processed Dump Event
"systhrow",
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters |
detail "java/lang/OutOfMemoryError". java.lang.OutOfMemoryError:
ZIP006:OutOfMemoryError, ENOMEM error in ZipFile.open at
java.util.zip.ZipFile.open(Native Method) at
java.util.zip.ZipFile.<init>(ZipFile.java:238) at
java.util.jar.JarFile.<init>(JarFile.java:169) at
java.util.jar.JarFile.<init>(JarFile.java:107) at
com.ibm.oti.vm.AbstractClassLoader.fillCache(AbstractClassLoader.java:69) at
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters |

com.ibm.oti.vm.AbstractClassLoader.getResourceAsStream(AbstractClassLoader.java:113)

| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters |
at java.util.ResourceBundle$1.run(ResourceBundle.java:1101) at
java.security.AccessController.doPrivileged(AccessController.java:197) at
java.util.ResourceBundle.loadBundle(ResourceBundle.java:1097) at
java.util.ResourceBundle.findBundle(ResourceBundle.java:942) at

java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:779) at
java.util.ResourceBundle.getBundle(ResourceBundle.java:716) at
com.ibm.oti.vm.MsgHelp.setLocale(MsgHelp.java:103) at
com.ibm.oti.util.Msg$1.run(Msg.java:44) at
java.security.AccessController.doPrivileged(AccessController.java:197) at
com.ibm.oti.util.Msg.<clinit>(Msg.java:41) at
java.lang.J9VMInternals.initializeImpl(Native Method) at
java.lang.J9VMInternals.initialize(J9VMInternals.java:194) at
java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:764) at
java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:758) at
java.lang.Thread.uncaughtException(Thread.java:1315)
K0319java.lang.OutOfMemoryError: Failed to fork OS thread at
java.lang.Thread.startImpl(Native Method) at
java.lang.Thread.start(Thread.java:979)

| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters |
at com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(
StartingAThreadUnderNativeStarvation.java:22)

Việc gọi java.lang.Thread.start() cố gắng để cấp phát bộ nhớ cho một luồng hệ
điều hành mới. Nỗ lực này không thành công và đưa ra lỗi OutOfMemoryError .
Các dòng JVMDUMP thông báo cho người dùng rằng thời gian chạy Java đã sinh
ra dữ liệu gỡ lỗi OutOfMemoryError tiêu chuẩn của nó.
Việc cố gắng xử lý lỗi OutOfMemoryError đầu tiên gây ra một lỗi — thứ hai
:OutOfMemoryError, ENOMEM error in ZipFile.open. Nhiều lỗi
OutOfMemoryError là dấu hiệu phổ biến khi bộ nhớ tiến trình riêng đã cạn kiệt.
Thông báo Failed to fork OS thread (bị thất bại khi phân nhánh luồng hệ điều
hành) có lẽ là dấu hiệu thường hay gặp nhất của việc thiếu bộ nhớ riêng.
Các ví dụ được cung cấp với bài viết này bắt đầu một nhóm các lỗi

OutOfMemoryError, nghiêm trọng hơn nhiều so với bất cứ những gì mà bạn có
thể thấy với các ứng dụng riêng của bạn. Điều này một phần là vì hầu như tất cả
các bộ nhớ riêng đã được sử dụng hết và không giống như trong một ứng dụng
thực, bộ nhớ ấy không được giải phóng sau đó. Trong một ứng dụng thực, khi lỗi
OutOfMemoryError được đưa ra, các luồng sẽ bị tắt và sức ép về bộ nhớ riêng có
nhiều khả năng giảm bớt một chút, mang lại cho thời gian chạy một cơ hội để xử
lý lỗi. Bản chất tầm thường của các bài thử nghiệm còn có nghĩa là toàn bộ các
phần của thư viện lớp (như các hệ thống an ninh) vẫn chưa được khởi động — và
việc khởi động chúng được điều khiển bởi thời gian chạy khi đang cố xử lý tình
trạng thiếu bộ nhớ. Trong một ứng dụng thực, bạn có thể thấy một số lỗi được chỉ
ra ở đây, nhưng ít khả năng bạn sẽ thấy tất cả các lỗi đồng thời.
Khi thực hiện cùng một bài thử nghiệm trên thời gian chạy Java của Sun, kết quả
đầu ra trên cửa sổ màn hình như sau:
Allocated 1953546736 bytes of native memory before running out
Exception in thread "main" java.lang.OutOfMemoryError: unable to
create new native
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters
|
thread at java.lang.Thread.start0(Native Method) at
java.lang.Thread.start(Thread.java:574) at
com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(
StartingAThreadUnderNativeStarvation.java:22)

Mặc dù vết ngăn xếp và thông báo lỗi là hơi khác nhau, về bản chất các hành vi là
giống nhau: việc cấp phát bộ nhớ riêng thất bại và một
java.lang.OutOfMemoryError được đưa ra. Điều duy nhất để phân biệt các lỗi
OutOfMemoryError được đưa ra trong kịch bản này với các lỗi được đưa ra do sử
dụng hết vùng heap là thông báo.
Việc cố gắng để cấp phát một ByteBuffer trực tiếp khi thiếu bộ nhớ riêng

Lớp com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation cố gắng cấp phát
một đối tượng java.nio.ByteBuffer trực tiếp (nghĩa là được hậu thuẫn riêng) khi
vùng địa chỉ bị cạn kiệt. Khi chạy trong thời gian chạy Java của IBM, nó tạo kết
quả đầu ra sau đây:
Allocated 1019481472 bytes of native memory before running out
JVMDUMP006I Processing Dump Event "uncaught", detail
"java/lang/OutOfMemoryError" -
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters
|
Please Wait. JVMDUMP007I JVM Requesting Snap Dump using
'C:\Snap0001.20080324.100721.4232.trc' JVMDUMP010I Snap Dump
written to
C:\Snap0001.20080324.100721.4232.trc JVMDUMP007I JVM
Requesting Heap Dump using
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters
|
'C:\heapdump.20080324.100721.4232.phd' JVMDUMP010I Heap
Dump written to
C:\heapdump.20080324.100721.4232.phd JVMDUMP007I JVM
Requesting Java Dump using
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters
|
'C:\javacore.20080324.100721.4232.txt' JVMDUMP010I Java Dump
written to
C:\javacore.20080324.100721.4232.txt JVMDUMP013I Processed
Dump Event "uncaught",
| 10 20 30 40 50 60 70 80 9|

| XML error: The previous line is longer than the max of 90 characters
|
detail "java/lang/OutOfMemoryError". Exception in thread "main"
java.lang.OutOfMemoryError: Unable to allocate 1048576 bytes of
direct memory after
| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters
|
5 retries at
java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:167) at
java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:303) at
com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main(
DirectByteBufferUnderNativeStarvation.java:29) Caused by:
java.lang.OutOfMemoryError

| 10 20 30 40 50 60 70 80 9|
| XML error: The previous line is longer than the max of 90 characters
|
at sun.misc.Unsafe.allocateMemory(Native Method) at
java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:154) 2 more


Trong kịch bản này, một lỗi OutOfMemoryError được đưa ra, khởi động một tài
liệu lỗi mặc định. Lỗi OutOfMemoryError đạt tới đỉnh cao nhất của ngăn xếp của
luồng chính và được in ra thành lỗi tiêu chuẩn stderr.
Khi chạy trong thời gian chạy Java của Sun, bài thử nghiệm này đưa ra kết quả
đầu ra như sau:
Allocated 1953546760 bytes of native memory before running out
Exception in thread "main" java.lang.OutOfMemoryError at
sun.misc.Unsafe.allocateMemory(Native Method) at

java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:99) at
java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288) at
com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main(
DirectByteBufferUnderNativeStarvation.java:29)



Các cách tiếp cận và các kỹ thuật gỡ lỗi
Việc đầu tiên cần làm khi phải đối mặt với một java.lang.OutOfMemoryError
hoặc một thông báo lỗi về thiếu bộ nhớ là xác định loại bộ nhớ nào đã cạn kiệt.
Cách dễ nhất để làm điều này là trước tiên kiểm tra xem vùng heap java đã đầy
chưa. Nếu vùng heap Java đã không gây ra tình trạng OutOfMemory (thiếu bộ
nhớ), thì bạn nên phân tích cách sử dụng vùng heap riêng.
Đọc tài liệu hướng dẫn của nhà cung cấp của bạn
Các hướng dẫn trong bài viết này là các nguyên tắc gỡ lỗi chung, áp dụng được
với các kịch bản thiếu bộ nhớ riêng dễ hiểu. Nhà cung cấp thời gian chạy của bạn
có thể cung cấp các hướng dẫn gỡ lỗi riêng của mình, mà bạn cần làm theo các

×