Thứ 2 - Chủ Nhật, 8h - 21h.
Hãy gọi: 0906 12 56 56

Bộ nhớ riêng trong JVM (JAVA)

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:

 

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:

 

 

 

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.

 

Phần tiếp theo: Thời gian chạy của Java sử dụng bộ nhớ riêng như thế nào