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

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

Andrew Hall, Kỹ sư phần mềm, IBM

Andrew Hall gia nhập Trung tâm Công nghệ Java của IBM trong năm 2004, bắt đầu từ nhóm kiểm thử hệ thống, nơi anh đã làm việc trong hai năm. Anh đã trải qua 18 tháng trong nhóm dịch vụ Java, nơi anh gỡ lỗi hàng chục vấn đề về bộ nhớ riêng trên nhiều nền tảng. Anh hiện là một nhà phát triển trong nhóm đảm bảo tính tin cậy, sẵn sàng và khả năng dịch vụ của Java. Trong thời gian rảnh rỗi anh thích đọc sách, chụp ảnh và làm trò tung hứng



16 08 2010

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® x863232
36 nếu có phần mở rộng địa chỉ vật lý (Pentium Pro và cao hơn)
x86 6464Hiện tại là 48-bit (có cơ hội để tăng lên sau)
PPC646450-bit với POWER 5
390 31-bit3231
390 64-bit6464

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ý
Ánh xạ bộ nhớ ảo

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
Vùng địa chỉ của 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
Vùng địa chỉ của 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-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 GetTypeArrayElementsGetTypeArrayRegion 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 ByteBuffertrự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
ByteBuffer memory arrangements

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
                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
                2.3 Windows XP x86-32 j9vmwi3223-2007100 7 (JIT enabled) J9VM -
                20071004_14218_lHdSMR JIT - 20070820_1846ifx1_r8 GC - 200708_10) JCL - 20071025

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" -
                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
                'C:\heapdump.20080323.182114.5172.phd' JVMDUMP010I Heap Dump written to
                C:\heapdump.20080323.182114.5172.phd JVMDUMP007I JVM Requesting Java Dump using
                'C:\javacore.20080323.182114.5172.txt' JVMDUMP010I Java Dump written to
                C:\javacore.20080323.182114.5172.txt JVMDUMP013I Processed Dump Event "systhrow",
                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
                com.ibm.oti.vm.AbstractClassLoader.getResourceAsStream(AbstractClassLoader.java:113)
                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)
                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
                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" -
                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
                'C:\heapdump.20080324.100721.4232.phd' JVMDUMP010I Heap Dump written to
                C:\heapdump.20080324.100721.4232.phd JVMDUMP007I JVM Requesting Java Dump using
                'C:\javacore.20080324.100721.4232.txt' JVMDUMP010I Java Dump written to
                C:\javacore.20080324.100721.4232.txt JVMDUMP013I Processed Dump Event "uncaught",
                detail "java/lang/OutOfMemoryError". Exception in thread "main"
                java.lang.OutOfMemoryError: Unable to allocate 1048576 bytes of direct memory after
                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
                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 hướng dẫn đó khi đang làm việc với nhóm hỗ trợ của họ. Nếu bạn đang nêu một vấn đề với nhà cung cấp thời gian chạy của bạn (bao gồm cả IBM), hãy luôn luôn đọc các tài liệu hướng dẫn gỡ lỗi và chẩn đoán của họ để biết bạn sẽ phải làm những bước nào tiếp theo khi gửi đi một bản báo cáo vấn đề.

Kiểm tra vùng heap Java

Phương thức để kiểm tra việc sử dụng vùng heap cũng khác nhau giữa các việc triển khai thực hiện Java. Trong việc triển khai thực hiện các bản Java 5 và 6 của IBM, tệp javacore được tạo ra khi có lỗi OutOfMemoryError sẽ báo cho bạn biết về sử dụng vùng heap. Tệp javacore thường được tạo ra trong thư mục làm việc của tiến trình Java và có tên theo dạng javacore.date.time.pid.txt. Nếu bạn mở tệp này trong một trình soạn thảo văn bản, bạn có thể tìm thấy một phần trông như sau:

 0SECTION MEMINFO subcomponent dump routine NULL
                ================================= 1STHEAPFREE Bytes of Heap Space Free: 416760
                1STHEAPALLOC Bytes of Heap Space Allocated: 1344800

Phần này cho thấy vùng heap Java còn chưa sử dụng là bao nhiêu khi javacore được tạo ra. Lưu ý rằng các giá trị số theo định dạng hệ đếm 16 (hexadecimal). Nếu OutOfMemoryError đã được đưa ra vì không đáp ứng được một việc cấp phát, thì sau đó phần dấu vết của GC sẽ hiển thị như sau:

 1STGCHTYPE GC History 3STHSTTYPE 09:59:01:632262775 GMT j9mm.80 -
                J9AllocateObject() returning NULL! 32 bytes requested for object of class 00147F80

J9AllocateObject() returning NULL! (J9AllocateObject () trả về NULL)! có nghĩa là thường trình cấp phát vùng heap Java không thành công và một lỗi OutOfMemoryError sẽ được đưa ra.

Cũng có khả năng một lỗi OutOfMemoryError được đưa ra vì các bộ dọn dữ liệu rác hoạt động quá thường xuyên (một dấu hiệu cho thấy vùng heap đã đầy và ứng dụng Java sẽ tiến triển rất ít hoặc không tiến triển). Trong trường hợp này, bạn sẽ thấy giá trị vùng heap chưa sử dụng (Heap Space Free) rất nhỏ và dấu vết GC sẽ hiển thị một trong các thông báo này:

 1STGCHTYPE GC History 3STHSTTYPE 09:59:01:632262775 GMT j9mm.83 -
                Forcing J9AllocateObject() to fail due to excessive GC
 1STGCHTYPE GC History 3STHSTTYPE 09:59:01:632262775 GMT j9mm.84 -
                Forcing J9AllocateIndexableObject() to fail due to excessive GC

Khi việc triển khai thực hiện bản Java của Sun sử dụng hết bộ nhớ của vùng heap Java, nó sử dụng thông báo trường hợp ngoại lệ để chỉ báo rằng vùng heap Java đã cạn kiệt.

 Exception in thread "main" java.lang.OutOfMemoryError: Java heap
                space

Các việc triển khai thực hiện Java của IBM và Sun, cả hai đều có một tùy chọn hiển thị mọi thông báo GC (verbose GC), tùy chọn này sinh ra dữ liệu theo vết, cho biết vùng heap đầy đến mức nào trong mỗi chu kỳ GC. Thông tin này có thể được vẽ thành biểu đồ bằng một công cụ ví dụ như là Bộ công cụ giám sát và chẩn đoán cho Java của IBM (IBM Monitoring and Diagnostic Tools for Java)- Trình giám sát bộ nhớ và thu dọn dữ liệu rác (Garbage Collection and Memory Visualizer-GCMV) để hiển thị xem vùng heap Java có đang tăng lên không (xem Tài nguyên).

Đo việc sử dụng vùng heap riêng

Nếu bạn đã xác định rằng tình trạng thiếu bộ nhớ của bạn không phải do sự cạn kiệt vùng heap Java gây ra, thì giai đoạn tiếp theo là mô tả sơ lược việc sử dụng bộ nhớ riêng của bạn.

Công cụ PerfMon được cung cấp với Windows cho phép bạn giám sát và ghi lại nhiều số đo về hệ điều hành và tiến trình, bao gồm việc sử dụng bộ nhớ riêng (xem Tài nguyên). Nó cho phép các bộ đếm được theo dõi trong thời gian thực hoặc được lưu trữ trong một tệp ghi nhật ký (log) để xem lại khi ngoại tuyến (offline). Hãy sử dụng bộ đếm các Byte riêng (Private Bytes) để cho thấy cách sử dụng toàn bộ vùng địa chỉ. Nếu lượng sử dụng đạt đến gần giới hạn của vùng người dùng (từ 2 đến 3GB như thảo luận trước đây), bạn sẽ có nhiều khả năng gặp tình trạng thiếu bộ nhớ riêng.

Linux không có công cụ PerfMon tương đương, nhưng bạn có một vài lựa chọn. Các công cụ dòng lệnh như ps, toppmap có thể hiển thị dấu vết bộ nhớ riêng của ứng dụng. Mặc dù nhận được một ảnh chụp nhanh về cách sử dụng bộ nhớ của một tiến trình rất có ích, bạn sẽ hiểu biết nhiều hơn về bộ nhớ riêng đang được sử dụng như thế nào bằng cách vẽ biểu đồ việc sử dụng bộ nhớ theo thời gian. Một cách để làm điều này là sử dụng GCMV.

Trình giám sát bộ nhớ và thu dọn dữ liệu rác (GCMV) ban đầu được viết để vẽ biểu đồ dựa trên các bản ghi nhật ký GC đầy đủ thông báo, cho phép người dùng xem các thay đổi trong cách sử dụng vùng heap Java và hiệu năng của GC khi điều chỉnh bộ thu gom dữ liệu rác. GCMV sau đó được mở rộng để cho phép nó vẽ biểu đồ cả các nguồn dữ liệu khác, bao gồm dữ liệu bộ nhớ riêng của Linux và AIX. GCMV được phân phối kèm như là một trình cắm thêm (plug-in) cho Trợ lý Hỗ trợ IBM (IBM Support Assistant-ISA).

Để vẽ biểu đồ hiện trạng bộ nhớ riêng của Linux bằng GCMV, trước tiên bạn phải thu gom dữ liệu bộ nhớ riêng bằng cách sử dụng một kịch bản lệnh. Trình phân tích (parser) bộ nhớ riêng Linux của GCMV đọc kết quả đầu ra từ lệnh ps của Linux có xen lẫn với các dấu thời gian. Một kịch bản lệnh được cung cấp trong tài liệu trợ giúp GCMV để thu gom dữ liệu theo đúng khuôn dạng. Để tìm kịch bản lệnh:

  1. Hãy tải về và cài đặt ISA Phiên bản 4 (hoặc cao hơn) và cài đặt trình cắm thêm của công cụ GCMV.
  2. Khởi động ISA.
  3. Khởi động Help >> Help Contents từ thanh trình đơn để mở trình đơn trợ giúp ISA.
  4. Tìm các chỉ thị cho bộ nhớ riêng của Linux ở ô bên trái, trong mục Tool:IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer >> Using the Garbage Collection and Memory Visualizer >> Supported Data Types >> Native memory >> Linux native memory.

Hình 5 cho thấy vị trí của kịch bản lệnh trong tệp trợ giúp ISA. Nếu bạn không có mục GCMV Tool (Công cụ GCMV) trong tệp trợ giúp của bạn, nhiều khả năng là bạn chưa cài đặt trình cắm thêm GCMV.

Hình 5. Vị trí của kịch bản lệnh nắm bắt dữ liệu bộ nhớ riêng Linux trong hộp thoại trợ giúp ISA
IBM Support Assistant Help File

Kịch bản lệnh được cung cấp trong trợ giúp GCMV sử dụng một lệnh ps chỉ làm việc với phiên bản gần đây của ps. Trên một số bản phân phối Linux cũ hơn, lệnh này trong tệp trợ giúp sẽ không đưa ra thông tin chính xác. Để kiểm tra hành vi của bản phân phối Linux của bạn, hãy thử chạy ps -o pid,vsz=VSZ,rss=RSS. Nếu phiên bản ps của bạn hỗ trợ cú pháp đối số dòng lệnh mới, kết quả đầu ra sẽ như sau:

 PID VSZ RSS 5826 3772 1960 5675 2492 760

Nếu phiên bản ps của bạn không hỗ trợ cú pháp mới, kết quả đầu ra sẽ là:

 PID VSZ,rss=RSS 5826 3772 5674 2488

Nếu bạn đang chạy một phiên bản ps, hãy sửa đổi kịch bản lệnh bộ nhớ riêng bằng cách thay thế dòng

ps -p $PID -o
                    pid,vsz=VSZ,rss=RSS

bằng

ps -p $PID -o
                    pid,vsz,rss

Sao chép kịch bản lệnh từ bảng trợ giúp vào một tệp (trong ví dụ này, tệp có tên là memscript.sh), tìm mã nhận dạng (id) tiến trình (PID) của tiến trình Java mà bạn muốn giám sát (trong ví dụ này là 1234) và chạy:

 ./memscript.sh 1234 > ps.out

Thao tác này sẽ viết bản ghi nhật ký của bộ nhớ riêng vào tệp ps.out. Để vẽ biểu đồ cách sử dụng bộ nhớ:

  1. Trong ISA, chọn Analyze Problem (Phân tích vấn đề) từ trình đơn thả xuống Launch Activity (Hành động khởi chạy).
  2. Chọn tab Tools (Các công cụ) ở gần đỉnh của bảng Analyze Problem.
  3. Chọn IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer (Bộ công cụ giám sát và chẩn đoán cho Java của IBM-Trình giám sát bộ nhớ và dọn dữ liệu rác).
  4. Nhấn vào nút Launch gần đáy của bảng Tools.
  5. Nhấn vào nút Browse (Duyệt) và tìm đến tệp log. Nhấn OK để khởi chạy GCMV.

Một khi bạn có hiện trạng về việc sử dụng bộ nhớ riêng theo thời gian, bạn cần phải quyết định đây là lỗ rò bộ nhớ riêng hay chỉ cố gắng thực hiện quá nhiều việc trong bộ nhớ sẵn có. Dấu vết bộ nhớ riêng của một ứng dụng Java, ngay cả ứng dụng hoạt động tốt, cũng thay đổi từ lúc khởi động. Một số hệ thống thời gian chạy Java — đặc biệt là trình biên dịch JIT và các trình nạp lớp — khởi động lại theo thời gian, do đó có thể tiêu dùng bộ nhớ riêng. Biểu đồ phát triển bộ nhớ từ lúc khởi động chương trình sẽ dần dần nằm ngang, nhưng nếu kịch bản của bạn có một dấu vết bộ nhớ riêng ban đầu đạt gần tới giới hạn của vùng địa chỉ, thì giai đoạn khởi động này có thể cũng đủ để gây ra thiếu bộ nhớ riêng. Hình 6 cho thấy một ví dụ vẽ biểu đồ bộ nhớ riêng của GCMV từ một bài thử nghiệm chịu tải của Java và giai đoạn khởi động được làm nổi bật.

Hình 6. Ví dụ biểu đồ bộ nhớ riêng của Linux từ GCMV, cho thấy giai đoạn khởi động
GCMV native memory plot

Cũng có thể có một dấu vết riêng thay đổi theo khối lượng công việc. Nếu ứng dụng của bạn tạo ra nhiều luồng để xử lý khối lượng công việc đến hoặc cấp phát vùng lưu trữ được hậu thuẫn riêng như là các ByteBuffer trực tiếp một cách tỷ lệ với tải công việc đang được đặt lên hệ thống của bạn, có khả năng là bạn sẽ cạn bộ nhớ riêng khi tải cao.

Chạy hết bộ nhớ riêng do sự tăng trưởng bộ nhớ riêng trong giai đoạn khởi động JVM và sự tăng trưởng tỷ lệ theo tải công việc, là những ví dụ về việc cố gắng làm quá nhiều việc trong vùng nhớ có sẵn. Trong những kịch bản này, tùy chọn của bạn là:

  • Giảm sử dụng bộ nhớ riêng của bạn. Giảm kích thước vùng heap Java là điểm bắt đầu thích hợp.
  • Hạn chế sử dụng bộ nhớ riêng của bạn. Nếu sự tăng trưởng bộ nhớ riêng thay đổi theo tải, hãy tìm một cách để giảm tải hoặc tài nguyên được cấp phát vì lý do này.
  • Tăng số lượng vùng địa chỉ có sẵn cho bạn. Bạn có thể làm điều này bằng cách điều chỉnh hệ điều hành của bạn (tăng vùng người sử dụng của bạn bằng khóa chuyển đổi /3GB trên Windows hoặc dùng nhân hugemem trên Linux, ví dụ thế), việc thay đổi nền tảng (Linux thường có vùng người sử dụng nhiều hơn Windows), hoặc di chuyển đến hệ điều hành 64-bit.

Một lỗ rò bộ nhớ riêng chính cống sẽ hiển hiện dưới dạng sự tăng trưởng liên tục trong vùng heap riêng mà không hề giảm xuống khi đã gỡ bỏ tải hoặc khi bộ thu dọn dữ liệu rác chạy. Tỷ lệ lỗ rò bộ nhớ có thể khác nhau tùy theo tải, nhưng tổng số thất thoát bộ nhớ sẽ không giảm. Bộ nhớ rò rỉ không có khả năng được tham chiếu đến, do đó, nó có thể được trao đổi ra và duy trì trao đổi ra.

Khi đối mặt với một lỗ rò, các tùy chọn của bạn bị hạn chế. Bạn có thể tăng số lượng vùng người sử dụng (do đó có nhiều chỗ để rò rỉ) nhưng điều đó sẽ chỉ kéo dài thêm thời gian trước khi rốt cuộc bạn dùng hết bộ nhớ. Nếu bạn có đủ bộ nhớ vật lý và vùng địa chỉ, bạn có thể cho phép lỗ rò vẫn còn trên cơ sở là bạn sẽ khởi động lại ứng dụng của mình trước khi vùng địa chỉ tiến trình bị cạn kiệt.

Cái gì đang sử dụng bộ nhớ riêng của tôi?

Một khi bạn đã xác định bạn đang cạn kiệt bộ nhớ riêng, câu hỏi hợp lý tiếp theo là: cái gì đang sử dụng bộ nhớ đó? Việc trả lời câu hỏi này rất khó bởi vì, theo mặc định, Windows và Linux không lưu trữ thông tin về tuyến mã nào được cấp phát một đoạn bộ nhớ cụ thể.

Bước đầu tiên của bạn khi cố gắng hiểu bộ nhớ riêng của bạn đã chạy đi đâu là tính toán đại khái có bao nhiêu bộ nhớ riêng sẽ được sử dụng dựa trên các giá trị thiết lập Java của bạn. Một giá trị chính xác là rất khó tính được mà không có kiến thức chuyên sâu về các hoạt động của JVM, nhưng bạn có thể thực hiện một sự ước lượng gần đúng dựa trên các chỉ dẫn sau đây:

  • Vùng heap Java chiếm giá trị -Xmx ít nhất.
  • Mỗi luồng Java yêu cầu vùng ngăn xếp. Kích thước ngăn xếp khác nhau tùy theo các việc triển khai thực hiện, nhưng với giá trị thiết lập mặc định, mỗi luồng có thể chiếm đến 756KB của bộ nhớ riêng.
  • Các ByteBuffer trực tiếp chiếm các giá trị ít nhất cũng bằng các giá trị được cung cấp cho thường trình allocate().

Nếu tổng số của bạn ít hơn nhiều so với vùng người dùng tối đa của bạn, cũng không nhất thiết là bạn đã an toàn. Nhiều thành phần khác trong một thời gian chạy Java có thể cấp phát lượng bộ nhớ đủ để gây ra các vấn đề; tuy nhiên, nếu các tính toán ban đầu của bạn cho thấy bạn đã đạt gần tới vùng người dùng tối đa của bạn, nhiều khả năng là bạn sẽ gặp vấn đề về bộ nhớ riêng. Nếu bạn nghi ngờ bạn có một lỗ rò bộ nhớ riêng hoặc bạn muốn hiểu chính xác bộ nhớ của bạn chạy đi đâu, có một số công cụ có thể giúp bạn.

Microsoft cung cấp công cụ UMDH (user-mode dump heap – heap kết xuất của chế độ người sử dụng) và công cụ LeakDiag để gỡ lỗi tăng bộ nhớ riêng trên Windows (xem Tài nguyên). Cả hai đều làm việc theo cùng cách tương tự như nhau: ghi lại tuyến mã nào được cấp phát cho một vùng bộ nhớ cụ thể và cung cấp một cách để xác định vị trí đoạn mã cấp phát bộ nhớ mà sau này không được giải phóng. Tôi giới thiệu cho bạn bài viết "Umdhtools.exe: Sử dụng Umdh.exe như thế nào để tìm các lỗ rò bộ nhớ trên Windows" để tìm chỉ dẫn về cách sử dụng UMDH (xem Tài nguyên). Trong bài này, tôi sẽ tập trung vào kết quả đầu ra của UMDH trông thế nào khi chạy nó với một ứng dụng JNI có lỗ rò.

Gói các ví dụ mẫu cho bài viết này có chứa một ứng dụng Java được gọi là LeakyJNIApp; nó chạy trong một vòng lặp gọi một phương thức JNI có lỗ rò bộ nhớ riêng. Lệnh UMDH chụp ảnh nhanh về vùng heap riêng hiện tại cùng với dấu vết ngăn xếp riêng của các tuyến mã được cấp phát cho từng vùng của bộ nhớ. Bằng cách chụp hai ảnh nhanh và sử dụng công cụ UMDH để phân tích sự khác biệt, bạn nhận được một báo cáo về sự tăng trưởng của vùng heap giữa hai ảnh chụp nhanh.

Đối với LeakyJNIApp, tệp khác có chứa thông tin này:

 // _NT_SYMBOL_PATH set by default to C:\WINDOWS\symbols // // Each
                log entry has the following syntax: // // + BYTES_DELTA (NEW_BYTES - OLD_BYTES)
                NEW_COUNT allocs BackTrace TRACEID // + COUNT_DELTA (NEW_COUNT - OLD_COUNT)
                BackTrace TRACEID allocations // ... stack trace ... // // where: // // BYTES_DELTA
                - increase in bytes between before and after log // NEW_BYTES - bytes in after log
                // OLD_BYTES - bytes in before log // COUNT_DELTA - increase in allocations between
                before and after log // NEW_COUNT - number of allocations in after log // OLD_COUNT
                - number of allocations in before log // TRACEID - decimal index of the stack trace
                in the trace database // (can be used to search for allocation instances in the
                original // UMDH logs). // + 412192 ( 1031943 - 619751) 963 allocs BackTrace00468
                Total increase == 412192

Dòng quan trọng là + 412192 ( 1031943 - 619751) 963 allocs BackTrace00468. Nó cho thấy rằng một lệnh dò vết ngăn xếp (backtrace) đã thực hiện 963 lần cấp phát chưa được giải phóng — dùng hết 412.192 byte của bộ nhớ. Bằng cách nhìn vào một trong những tệp ảnh chụp nhanh, bạn có thể kết hợp BackTrace00468 với đường tuyến mã có ý nghĩa. Tìm kiếm BackTrace00468 trong ảnh chụp đầu tiên cho thấy:

 000000AD bytes in 0x1 allocations (@ 0x00000031 + 0x0000001F) by:
                BackTrace00468 ntdll!RtlpNtMakeTemporaryKey+000074D0
                ntdll!RtlInitializeSListHead+00010D08 ntdll!wcsncat+00000224
                leakyjniapp!Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod+000000D6

Điều này cho thấy lỗ rò đến từ mô đun leakyjniapp.dll trong hàm Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod.

Vào thời điểm viết bài này, Linux chưa có một công cụ UMDH hoặc LeakDiag tương đương. Nhưng vẫn còn có các cách để gỡ lỗi rò rỉ bộ nhớ riêng trên Linux. Nhiều trình gỡ lỗi bộ nhớ có sẵn trên Linux, thường rơi vào một trong các loại sau:

  • Mức bộ xử lý trước. Những trình gỡ lỗi này yêu cầu một tiêu đề (header) được biên dịch cùng với mã nguồn thử nghiệm. Có thể biên dịch lại các thư viện JNI riêng của bạn với một trong những công cụ này để theo dõi một lỗ rò bộ nhớ riêng trong mã của bạn. Trừ khi bạn có mã nguồn Java của chính thời gian chạy Java, việc này không thể tìm thấy một lỗ rò trong JVM (và rồi ngay cả khi việc kết hợp các công cụ loại này thành một dự án lớn như JVM chắc chắn sẽ rất khó khăn và tốn thời gian). Dmalloc là một ví dụ về loại công cụ này (xem Tài nguyên).
  • Mức trình liên kết. Những trình gỡ lỗi này yêu cầu các mã nhị phân trong lúc thử nghiệm để được liên kết lại với một thư viện gỡ lỗi. Một lần nữa, điều này có thể khả thi cho các thư viện JNI riêng lẻ, nhưng không nên dùng cho toàn bộ các thời gian chạy Java bởi vì ít có khả năng là nhà cung cấp thời gian chạy sẽ hỗ trợ bạn chạy các mã nhị phân đã sửa đổi. Ccmalloc là một ví dụ của loại công cụ này (xem Tài nguyên).
  • Mức trình liên kết-thời gian chạy. Những trình gỡ lỗi này sử dụng biến môi trường LD_PRELOAD để nạp trước một thư viện thay thế các thường trình bộ nhớ tiêu chuẩn bằng các phiên bản có cài công cụ. Chúng không yêu cầu biên dịch lại hoặc liên kết lại mã nguồn, nhưng nhiều trình gỡ lỗi trong số chúng không làm việc tốt với các thời gian chạy Java. Một thời gian chạy Java là một hệ thống phức tạp có thể sử dụng bộ nhớ và các luồng theo nhiều cách khác thường có thể gây lúng túng hoặc phá hỏng loại công cụ này. Nên thử nghiệm một số ít để xem chúng có làm việc trong kịch bản của bạn không. NJAMD là một ví dụ về loại công cụ này (xem Tài nguyên).
  • Dựa vào-trình mô hình hóa (Emulator). Công cụ memcheck của Valgrind là ví dụ duy nhất của kiểu trình gỡ lỗi bộ nhớ này (xem Tài nguyên). Nó mô phỏng bộ xử lý ở bên dưới theo một cách tương tự như cách một thời gian chạy Java mô phỏng JVM. Có thể chạy Java trong Valgrind, nhưng ảnh hưởng hiệu năng rất nặng nề (10-30 lần chậm hơn), có nghĩa là sẽ rất khó để chạy các ứng dụng Java lớn, phức tạp theo cách này. Valgrind hiện tại đang có trên Linux x86, AMD64, PPC 32 và PPC 64. Nếu bạn sử dụng Valgrind, hãy cố gắng thu hẹp vấn đề đến mức bài thử nghiệm nhỏ nhất mà bạn có thể (tốt hơn là cắt bớt toàn bộ thời gian chạy Java nếu có thể) trước khi sử dụng nó.

Với các kịch bản đơn giản có thể bỏ qua sự quá tải hiệu năng, memcheck của Valgrind là công cụ đơn giản nhất và thân thiện với người dùng trong số các công cụ miễn phí có sẵn. Nó có thể cung cấp việc theo vết ngăn xếp đầy với các tuyến mã có lỗ rò bộ nhớ theo cùng cách mà UMDH có thể làm trên Windows.

LeakyJNIApp đủ đơn giản để chạy trong Valgrind. Công cụ memcheck của Valgrind có thể in ra một bản tóm tắt về bộ nhớ bị rò rỉ khi kết thúc chương trình mô phỏng. Theo mặc định, chương trình LeakyJNIApp chạy vô hạn; để tắt nó sau một khoảng thời gian ấn định, chuyển thời gian chạy tính bằng giây làm đối số dòng lệnh duy nhất.

Một số các thời gian chạy Java sử dụng các ngăn xếp luồng và các thanh ghi của bộ xử lý theo những cách khác thường; điều này có thể làm một số công cụ gỡ lỗi lúng túng, vì các công cụ này giả định các chương trình riêng tuân theo các quy ước chuẩn về sử dụng thanh ghi và cấu trúc ngăn xếp. Khi sử dụng Valgrind để gỡ lỗi các ứng dụng JNI có lỗ rò, bạn có thể thấy rằng nhiều cảnh báo về việc sử dụng bộ nhớ được đưa ra và một số ngăn xếp luồng sẽ trông khác thường; điều này là do cách thời gian chạy Java cấu trúc dữ liệu của nó bên trong và không có gì phải lo lắng về nó.

Để theo dõi LeakyJNIApp bằng công cụ memcheck của Valgrind, hãy sử dụng lệnh này (trên chỉ có một dòng):

 valgrind --trace-children=yes --leak-check=full java
                -Djava.library.path=. com.ibm.jtc.demos.LeakyJNIApp 10

Tùy chọn --trace-children=yes cho Valgrind làm cho nó theo vết tiến trình bất kỳ được bắt đầu bởi trình khởi chạy (launcher) Java. Một số phiên bản của trình khởi chạy Java tự chạy lại chính mình (chúng tự khởi động lại từ đầu, một lần nữa thiết lập các biến môi trường để thay đổi hành vi). Nếu bạn không chỉ rõ --trace-children, bạn không thể theo vết thời gian chạy Java thực tế.

Tùy chọn --leak-check=full yêu cầu các dấu vết ngăn xếp đầy của các vùng mã có lỗ rò được in ra khi kết thúc, thay vì chỉ là tóm tắt trạng thái của bộ nhớ.

Valgrind in nhiều cảnh báo và lỗi trong khi lệnh chạy thi hành (hầu hết trong số chúng là không đáng chú ý trong ngữ cảnh này) và cuối cùng in một danh sách các ngăn xếp cuộc gọi có lỗ rò theo thứ tự tăng dần của lượng bộ nhớ bị rò rỉ. Đoạn kết của phần tóm tắt kết quả đầu ra của Valgrind cho LeakyJNIApp trên Linux x86 là:

 ==20494== 8,192 bytes in 8 blocks are possibly lost in loss record
                36 of 45 ==20494== at 0x4024AB8: malloc (vg_replace_malloc.c:207) ==20494== by
                0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod (in
                /home/andhall/LeakyJNIApp/libleakyjniapp.so) ==20494== by 0x535CF56: ??? ==20494==
                by 0x46423CB: gpProtectedRunCallInMethod (in
                /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so) ==20494== by 0x46441CF:
                signalProtectAndRunGlue (in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
                ==20494== by 0x467E0D1: j9sig_protect (in
                /usr/local/ibm-java2-i386-50/jre/bin/libj9prt23.so) ==20494== by 0x46425FD:
                gpProtectAndRun (in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so) ==20494== by
                0x4642A33: gpCheckCallin (in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
                ==20494== by 0x464184C: callStaticVoidMethod (in
                /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so) ==20494== by 0x80499D3: main (in
                /usr/local/ibm-java2-i386-50/jre/bin/java) ==20494== ==20494== ==20494== 65,536
                (63,488 direct, 2,048 indirect) bytes in 62 blocks are definitely lost in loss
                record 42 of 45 ==20494== at 0x4024AB8: malloc (vg_replace_malloc.c:207) ==20494==
                by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod (in
                /home/andhall/LeakyJNIApp/libleakyjniapp.so) ==20494== by 0x535CF56: ??? ==20494==
                by 0x46423CB: gpProtectedRunCallInMethod (in
                /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so) ==20494== by 0x46441CF:
                signalProtectAndRunGlue (in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
                ==20494== by 0x467E0D1: j9sig_protect (in
                /usr/local/ibm-java2-i386-50/jre/bin/libj9prt23.so) ==20494== by 0x46425FD:
                gpProtectAndRun (in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so) ==20494== by
                0x4642A33: gpCheckCallin (in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
                ==20494== by 0x464184C: callStaticVoidMethod (in
                /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so) ==20494== by 0x80499D3: main (in
                /usr/local/ibm-java2-i386-50/jre/bin/java) ==20494== ==20494== LEAK SUMMARY:
                ==20494== definitely lost: 63,957 bytes in 69 blocks. ==20494== indirectly lost:
                2,168 bytes in 12 blocks. ==20494== possibly lost: 8,600 bytes in 11 blocks.
                ==20494== still reachable: 5,156,340 bytes in 980 blocks. ==20494== suppressed: 0
                bytes in 0 blocks. ==20494== Reachable blocks (those to which a pointer was found)
                are not shown. ==20494== To see them, rerun with: --leak-check=full
                --show-reachable=yes

Dòng thứ hai của các ngăn xếp cho thấy rằng bộ nhớ bị rò rỉ do phương thức com.ibm.jtc.demos.LeakyJNIApp.nativeMethod().

Một số ứng dụng gỡ lỗi độc quyền có thể gỡ lỗi các lỗ rò bộ nhớ riêng cũng có sẵn. Nhiều công cụ nữa (cả nguồn mở lẫn độc quyền) đang được phát triển không ngừng và nên nghiên cứu để nắm được trình độ hiện nay.

Hiện nay việc gỡ lỗi các lỗ rò bộ nhớ riêng trên Linux bằng các công cụ có sẵn miễn phí có nhiều thách thức hơn so với việc làm điều này trên Windows. Trong khi UMDH cho phép các lỗ rò riêng trên Windows được gỡ lỗi tại chỗ của nó, trên Linux có thể bạn sẽ cần làm một số việc gỡ lỗi truyền thống hơn là dựa vào một công cụ để giải quyết vấn đề cho bạn. Dưới đây là một số bước gỡ lỗi được đề xuất:

  • Lấy ra một trường hợp thử nghiệm. Tạo ra một môi trường độc lập mà bạn có thể tái tạo lại các lỗ rò riêng với nó. Nó sẽ làm cho việc gỡ lỗi đơn giản hơn nhiều.
  • Thu hẹp trường hợp thử nghiệm trong chừng mực có thể. Hãy thử ngắt các hàm để nhận biết các tuyến mã nào đang gây ra lỗ rò riêng. Nếu bạn có các thư viện JNI riêng của mình, hãy thử ngắt chúng hoàn toàn từng cái một để xác định xem có phải chúng đang gây ra lỗ rò hay không.
  • Giảm kích thước vùng heap Java. Vùng heap Java nhiều khả năng là kẻ tiêu dùng lớn nhất vùng địa chỉ ảo trong tiến trình. Bằng cách làm giảm vùng heap Java, bạn có thể tạo thêm nhiều vùng sẵn dùng dành cho những người dùng bộ nhớ riêng khác.
  • Tương quan với kích thước tiến trình riêng. Một khi bạn có một biểu đồ về việc sử dụng bộ nhớ riêng theo thời gian, bạn có thể so sánh nó với khối lượng công việc của ứng dụng và dữ liệu GC. Nếu tỷ lệ lỗ rò là tỷ lệ thuận với mức tải, điều này gợi ý rằng một cái gì đó gây ra lỗ rò trên đường đi của mỗi giao dịch hoặc hoạt động. Nếu kích thước tiến trình riêng giảm đáng kể khi việc thu gom rác (GC) xảy ra, điều này gợi ý rằng bạn không chứng kiến một lỗ rò — bạn đang chứng kiến sự tăng thêm các đối tượng được hậu thuẫn riêng (ví dụ như là các ByteBuffer trực tiếp). Bạn có thể làm giảm số lượng bộ nhớ bị chiếm giữ bởi đối tượng được hậu thuẫn riêng bằng cách giảm kích thước vùng heap Java (do đó buộc việc thu dọn rác xảy ra thường xuyên hơn), hoặc bằng cách tự mình quản lý chúng trong một bộ nhớ sẵn (cache) đối tượng hơn là dựa vào các bộ thu dọn dữ liệu rác để làm sạch cho bạn.

Nếu bạn nhận ra một lỗ rò hoặc sự tăng thêm bộ nhớ mà bạn nghĩ là bắt nguồn từ chính thời gian chạy Java, bạn có thể muốn kéo nhà cung cấp thời gian chạy của bạn vào việc tiếp tục gỡ lỗi.


Loại bỏ hạn chế: Đổi sang 64-bit

Rất dễ gặp phải tình trạng cạn bộ nhớ riêng với các thời gian chạy 32-bit vì vùng địa chỉ tương đối nhỏ. Vùng người sử dụng từ 2 đến 4GB mà các hệ điều hành 32-bit cung cấp thường ít hơn dung lượng bộ nhớ vật lý được gán cho hệ thống và các ứng dụng hiện đại xử lý nhiều dữ liệu có thể dễ dàng mở rộng để lấp đầy vùng nhớ có sẵn.

Nếu ứng dụng của bạn không thể được làm cho vừa khớp với một vùng địa chỉ 32-bit, thì bạn có thể làm tăng nhiều vùng người dùng hơn bằng cách di chuyển đến thời gian chạy Java 64-bit. Nếu bạn đang chạy một hệ điều hành 64-bit, thì một thời gian chạy Java 64-bit sẽ mở cửa cho các vùng heap Java rất lớn và sẽ ít gặp vấn đề đau đầu liên quan đến-vùng-địa chỉ. Bảng 2 liệt kê các vùng người dùng hiện có sẵn với các hệ điều hành 64-bit:

Bảng 2. Các kích thước vùng người dùng trên các hệ điều hành 64-bit
Hệ điều hànhKích thước vùng người dùng mặc định
Windows x86-648192GB
Windows Itanium7152GB
Linux x86-64500GB
Linux PPC641648GB
Linux 390 644EB

Tuy nhiên, di chuyển đến 64-bit không phải là một giải pháp chung cho tất cả các vấn đề bộ nhớ riêng; bạn vẫn cần đủ bộ nhớ vật lý để duy trì tất cả dữ liệu của bạn. Nếu thời gian chạy Java của bạn không khớp với bộ nhớ vật lý thì hiệu năng sẽ kém đến mức không thể chấp nhận được vì hệ điều hành bị buộc phải quăng quật dữ liệu của thời gian chạy Java vào rồi lại ra khỏi vùng trao đổi. Vì cùng lý do tương tự, việc di chuyển đến 64-bit không phải là giải pháp lâu dài cho lỗi rò rỉ bộ nhớ — bạn đang chỉ tạo thêm nhiều vùng cho các lỗ rò, chỉ kéo dài thêm thời gian giữa các lần bắt buộc khởi động lại.

Không thể sử dụng mã riêng 32-bit với một thời gian chạy 64-bit; bất kỳ mã riêng nào: các thư viện JNI; các giao diện công cụ JVM (JVM Tool Interface-JVMTI); giao diện lược tả JVM (JVM Profiling Interface-JVMPI) và giao diện gỡ lỗi JVM (JVM Debug Interface-JVMDI) phải được biên dịch lại theo 64-bit. Hiệu năng của thời gian chạy 64-bit cũng có thể chậm hơn so với thời gian chạy 32-bit tương ứng trên phần cứng giống nhau. Thời gian chạy 64-bit sử dụng các con trỏ 64-bit (các tham khảo địa chỉ riêng), do đó, cùng đối tượng Java trên 64-bit chiếm nhiều vùng nhớ hơn so với một đối tượng có chứa cùng dữ liệu trên 32-bit. Các đối tượng lớn hơn có nghĩa là vùng heap lớn hơn để chứa cùng một lượng dữ liệu khi duy trì một hiệu năng GC tương tự, làm cho hệ điều hành và bộ nhớ đệm (caches) kém hiệu quả hơn. Đáng ngạc nhiên là, một vùng heap Java lớn hơn không nhất thiết có nghĩa là các thời gian tạm nghỉ của GC dài hơn, vì số lượng dữ liệu hoạt động trên vùng heap có thể không tăng lên và một số thuật toán GC có hiệu quả hơn với các vùng heap lớn hơn.

Một số thời gian chạy Java hiện đại có công nghệ để giảm bớt "sự phình to đối tượng" 64-bit và cải thiện hiệu năng. Những tính năng này hoạt động bằng cách sử dụng các tham chiếu ngắn hơn trên các thời gian chạy 64-bit. Điều này được gọi là các tham chiếu nén trong các bản triển khai thực hiện của IBM và các đối tượng nén trong bản triển khai thực hiện của Sun.

Một nghiên cứu so sánh về hiệu năng thời gian chạy Java vượt quá phạm vi của bài viết này, nhưng nếu bạn đang xem xét việc di chuyển tới 64-bit thì rất nên thử nghiệm ứng dụng của bạn trước để hiểu cách nó thực hiện. Vì việc thay đổi kích thước địa chỉ ảnh hưởng đến vùng heap Java, bạn sẽ cần phải điều chỉnh lại các giá trị cài đặt GC của mình trên kiến trúc mới thay vì chỉ mang sang các giá trị cài đặt hiện có của bạn.


Kết luận

Một sự hiểu biết về bộ nhớ riêng là cốt yếu khi bạn thiết kế và chạy nhiều ứng dụng Java lớn, nhưng nó thường bị xem nhẹ bởi vì nó gắn kết với phần cứng bụi bặm và các chi tiết hệ điều hành mà thời gian chạy Java đã được thiết kế để tránh cho chúng ta điều đó. JRE là một tiến trình riêng phải làm việc trong môi trường xác định bởi các chi tiết bụi bặm ấy. Để thu được hiệu năng tốt nhất từ ứng dụng Java của bạn, bạn phải hiểu ứng dụng ảnh hưởng đến việc sử dụng bộ nhớ riêng của thời gian chạy Java như thế nào.

Việc dùng hết bộ nhớ riêng có thể trông giống như việc dùng hết vùng heap Java, nhưng nó đòi hỏi một bộ công cụ khác để gỡ lỗi và giải quyết. Chìa khóa để sửa chữa các vấn đề bộ nhớ riêng là phải hiểu các hạn chế áp đặt bởi phần cứng và hệ điều hành mà các ứng dụng Java của bạn đang chạy trên đó và kết hợp điều này với kiến thức về các công cụ của hệ điều hành để giám sát việc sử dụng bộ nhớ riêng. Bằng cách làm theo cách tiếp cận này, bạn sẽ được trang bị để giải quyết một số vấn đề hiểm hóc nhất mà ứng dụng Java của bạn có thể đặt ra.


Tải về

Mô tảTênKích thước
Native memory example codej-nativememory-linux.zip115KB

Tài nguyên

Học tập

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

  • Valgrind: Valgrind: Tải về khung công tác có dùng dung cụ Valgrind, bao gồm cả máy dò lỗi-bộ nhớ.
  • Dmalloc: Tải về thư viện Debug Malloc.
  • ccmalloc: Tải về thư viện trình gỡ lỗi-bộ nhớ ccmalloc.
  • NJAMD: Tải về thư viện trình gỡ lỗi-bộ nhớ NJAMD (Not Just Another Debugger malloc-Không chỉ là trình gỡ lỗi malloc khác).
  • Công cụ Giám sát và chẩn đoán của IBM cho Java: Truy cập vào trang công cụ Java của IBM.
  • Trợ lý Hỗ trợ của IBM (ISA): Đây khung công tác hỗ trợ miễn phí có chứa các công cụ như Trình theo dõi bộ nhớ và thu dọn dữ liệu rác (Garbage Collection and Memory Visualizer) và Trợ lý hoạt động có hướng dẫn của IBM (IBM Guided Activity Assistant), có thể hướng dẫn bạn qua việc gỡ lỗi một tình trạng cạn kiệt bộ nhớ riêng.

Thảo luậ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=508340
ArticleTitle=Cảm ơn bộ nhớ
publish-date=08162010