Ngày 19 tháng 7 năm 2024 - Máy tính
Java Virtual Machine (JVM) là môi trường chạy của chương trình Java (bytecode). JVM cung cấp các chức năng chính như thực thi bytecode Java (thông qua việc giải thích hoặc biên dịch ngay lập tức thành mã máy gốc), quản lý bộ nhớ (phân bổ bộ nhớ và thu gom rác), hỗ trợ đa luồng và kiểm soát an ninh, đóng vai trò nền tảng cho khẩu hiệu “Viết một lần, chạy mọi nơi” của ngôn ngữ Java.
Khu vực dữ liệu chạy thời gian của JVM đề cập đến các vùng bộ nhớ mà JVM quản lý trong quá trình chạy bytecode Java. Việc phân chia các vùng này nhằm mục đích phục vụ các mục đích khác nhau về phân bổ bộ nhớ, từ đó hỗ trợ tốt hơn cho hoạt động của chương trình Java.
Một số vùng dữ liệu được tạo ra và phá hủy khi JVM khởi động và dừng lại, trong khi những vùng khác thì thuộc riêng từng luồng và tồn tại cùng với vòng đời của chúng.
1. Phân Chia Khu Vực Dữ Liệu Chạy Thời Gian
Bộ Đếm Chương Trình (Program Counter Register)
Bộ đếm chương trình là một vùng bộ nhớ nhỏ, độc quyền đối với mỗi luồng (mỗi luồng có một bộ đếm chương trình riêng biệt). Nó lưu trữ địa chỉ B88bet Win của lệnh bytecode hiện đang được thực thi bởi luồng đó. Bytecode interpreter làm việc bằng cách thay đổi giá trị của bộ đếm này để chọn lệnh bytecode tiếp theo cần thực thi, do đó nó đóng vai trò chỉ báo dòng điều khiển chương trình. Các chức năng cơ bản như nhánh, vòng lặp, chuyển hướng, xử lý ngoại lệ và khôi phục luồng đều dựa vào bộ đếm này.
Khi một luồng thực thi phương thức Java, bộ đếm chương trình lưu trữ địa chỉ của lệnh hiện tại đang được thực thi. Nếu luồng thực thi phương thức Native, giá trị của bộ đếm sẽ là không xác định (Undefined).
Stack JVM (Java Virtual Machine Stacks)
Giống như bộ đếm chương trình, stack JVM cũng là độc quyền đối với mỗi luồng và tồn tại cùng với vòng đời của luồng đó. Mỗi luồng khi được tạo ra sẽ có một stack JVM tương ứng, dùng để lưu trữ biến cục bộ, tham số phương thức, giá trị trả về và thông tin gọi phương thức. Khi mỗi phương thức được thực thi, JVM sẽ đồng thời tạo ra một khung stack (Stack Frame) để lưu trữ bảng biến cục bộ, ngăn xếp toán hạng, liên kết động, và địa chỉ xuất phương thức. Quá trình gọi và hoàn thành một phương thức tương ứng với việc đẩy và pop khung stack khỏi stack JVM.
Cấu trúc của khung stack:
- Bảng Biến Cục Bộ (Local Variable Array): Dùng để lưu trữ biến cục bộ và tham số truyền vào phương thức.
- Ngăn Xếp Toán Hạng (Operand Stack): Dùng để lưu trữ toán hạng trong quá trình thực thi phương thức.
- Liên Kết Động (Dynamic Linking): Dùng để trỏ tới hằng số runtime pool của phương thức đó.
- Địa Chỉ Trả Về (Return Address): Dùng để lưu trữ địa chỉ trả về sau khi phương thức hoàn thành.
Chức năng:
- Gọi Phương Thức: Stack JVM ghi lại thứ tự và mối quan hệ giữa các lần gọi phương thức, thông qua các thao tác đẩy và pop khung stack để thực hiện gọi và trả về phương thức.
- Lưu Trữ Biến Cục Bộ: Bảng biến cục bộ của stack JVM dùng để lưu trữ biến cục bộ và tham số truyền vào phương thức.
- Truyền Tham Số: Khi gọi phương thức, các tham số sẽ được đẩy vào bảng biến cục bộ của khung stack để sử dụng.
- Xử Lý Ngoại Lệ: Khi phương thức ném ra ngoại lệ, stack JVM có thể dùng để định vị vị trí xử lý ngoại lệ.
Các ngoại lệ có thể xảy ra:
- Nếu độ sâu yêu cầu của stack vượt quá giới hạn cho phép của JVM, sẽ nảy sinh ngoại lệ
StackOverflowError
. - Nếu stack JVM có khả năng mở rộng động nhưng không thể cấp phát đủ bộ nhớ khi mở rộng, sẽ nảy sinh ngoại lệ
OutOfMemoryError
.
Đống (Heap)
Heap là vùng bộ nhớ lớn nhất được JVM quản lý, dùng để lưu trữ các thể hiện lớp và mảng, được tạo ra khi JVM khởi động và được tất cả các luồng chia sẻ.
Heap là vùng bộ nhớ được bộ thu gom rác (Garbage Collector) quản lý. Khi không đủ bộ nhớ trên heap để cấp phát cho đối tượng mới, bộ thu gom rác sẽ được kích hoạt để tiến hành thu gom rác.
Heap thường được chia thành các khu vực sau để tối ưu hóa việc thu gom và phân bổ bộ nhớ:
- Thế Hệ Mới (Young Generation): Được chia thành Eden Space và Survivor Space. Đối tượng mới được cấp phát đầu tiên vào Eden Space. Khi Eden đầy, các đối tượng sống sót sẽ được di chuyển sang Survivor Space. Survivor Space bao gồm hai vùng bằng nhau, thường gọi là From và To. Những đối tượng sống sót lâu hơn sẽ được sao chép vào vùng To, đồng thời tăng tuổi thọ. Sau nhiều lần thu gom rác, các đối tượng vẫn sống sót sẽ được di chuyển vào Thế Hệ Cũ.
- Thế Hệ Cũ (Old Generation): Lưu trữ các đối tượng sống sót lâu dài. Các đối tượng trong thế hệ cũ sẽ trải qua chu kỳ thu gom rác lâu hơn. Khi không đủ không gian trong thế hệ cũ, Full GC sẽ được kích hoạt để thu gom rác.
- Không Gian Meta (Metaspace): Sử dụng bộ nhớ gốc để lưu trữ siêu dữ liệu của lớp, không bị giới hạn bởi kích thước của heap.
Tổng quát, thế hệ mới dành để lưu trữ các đối tượng mới được tạo ra, sử dụng thuật toán copy để thu gom rác. Thế hệ cũ dành cho các đối tượng sống sót lâu dài, trải qua chu kỳ thu gom rác lâu hơn. Không gian meta dùng để lưu trữ siêu tải game 789 club tài xỉu dữ liệu của lớp Java và một số đối tượng khó bị thu gom.
Các ngoại lệ có thể xảy ra:
- Heap có thể được thực hiện với kích thước cố định hoặc có thể mở rộng, nhưng hầu hết các JVM hiện đại đều được thực hiện với khả năng mở rộng (thông qua các tham số
-Xmx
và-Xms
). Nếu không đủ bộ nhớ trên heap để cấp phát cho thể hiện mới và heap không thể mở rộng thêm, JVM sẽ nảy sinh ngoại lệOutOfMemoryError
.
Khu Vực Phương Thức (Method Area)
Khu vực phương thức giống như heap, là vùng bộ nhớ chia sẻ giữa các luồng, dùng để lưu trữ thông tin loại đã được tải bởi JVM, hằng số, biến tĩnh và mã code đã được biên dịch ngay lập tức.
Nội dung của khu vực phương thức:
- Thông Tin Lớp: Cấu trúc đầy đủ của lớp, bao gồm tên lớp, lớp cha, giao diện, trường, phương thức, v.v., được lưu trữ ở đây. Các biến tĩnh và hằng số của lớp cũng được lưu trữ ở đây.
- Hằng Số Runtime Pool: Lưu trữ các hằng số của lớp, chẳng hạn như hằng số chuỗi, hằng số kiểu nguyên thủy, v.v.
- Dữ Liệu Trường Và Phương Thức: Lưu trữ thông tin liên quan đến trường và phương thức của lớp, bao gồm tên trường, kiểu, trình sửa đổi truy cập, v.v., tên phương thức, danh sách tham số, kiểu trả về và trình sửa đổi truy cập.
- Mã Code Đã Được Biên Dịch Ngay Lập Tức: JVM JIT Compiler biên dịch mã hot (mã thường xuyên được thực thi) thành mã máy gốc để tăng hiệu suất. Mã đã biên dịch cũng được lưu trữ ở đây.
Các ngoại lệ có thể xảy ra:
- Nếu khu vực phương thức không thể đáp ứng nhu cầu cấp phát bộ nhớ mới, sẽ nảy sinh ngoại lệ
OutOfMemoryError
.
Hằng Số Runtime Pool (Run-Time Constant Pool)
Hằng số runtime pool là một phần của khu vực phương thức, là vùng bộ nhớ chia sẻ giữa các luồng. Bên cạnh các thông tin mô tả lớp như phiên bản, trường, phương thức, giao diện, v.v., file Class còn chứa bảng hằng số pool, dùng để lưu trữ các hằng số và tham chiếu ký hiệu được tạo ra trong giai đoạn biên dịch. Phần nội dung này sẽ được lưu trữ vào hằng số runtime pool sau khi lớp được tải.
Một đặc điểm quan trọng khác của hằng số runtime pool so với bảng hằng số trong file Class là tính động. Ngôn ngữ Java không yêu cầu rằng các hằng số phải được tạo ra chỉ trong giai đoạn biên dịch, nghĩa là không chỉ nội dung đã được đưa vào bảng hằng số trong file Class mới có thể vào hằng số runtime pool, mà ngay cả trong quá trình chạy cũng có thể thêm các hằng số mới vào pool. Đặc điểm này được các nhà phát triển sử dụng phổ biến trong phương thức intern()
của lớp String
.
Các ngoại lệ có thể xảy ra:
- Vì hằng số runtime pool là một phần của khu vực phương thức, nó chịu sự giới hạn về bộ nhớ của khu vực phương thức. Khi Khuyến Mãi 88king không thể cấp phát thêm bộ nhớ cho pool, sẽ nảy sinh ngoại lệ
OutOfMemoryError
.
Stack Phương Thức Bản Địa (Native Method Stacks)
Stack phương thức bản địa là vùng bộ nhớ riêng tư tương tự như stack JVM, dùng để hỗ trợ tương tác giữa chương trình Java và các phương thức bản địa (như các phương thức được viết bằng C, C++). Nó được cấu thành từ các khung stack tương tự như stack JVM, dùng để lưu trữ biến cục bộ, ngăn xếp toán hạng, liên kết động và địa chỉ trả về của các phương thức bản địa. Stack phương thức bản địa đóng vai trò quan trọng trong việc gọi phương thức bản địa, thực thi phương thức bản địa và quy ước gọi.
Các ngoại lệ có thể xảy ra:
- Giống như stack JVM, stack phương thức bản địa cũng sẽ nảy sinh ngoại lệ
StackOverflowError
khi độ sâu stack vượt quá giới hạn hoặc ngoại lệOutOfMemoryError
khi không thể mở rộng stack.
Bộ Nhớ Trực Tiếp (Direct Memory)
Bộ nhớ trực tiếp không phải là một phần của vùng dữ liệu chạy thời gian của JVM, nhưng nó liên quan mật thiết đến Java NIO. Bộ nhớ trực tiếp được cấp phát thông qua cách phân bổ bộ nhớ ngoài heap, không bị giới hạn bởi heap, và có thể cung cấp hiệu năng cao hơn trong một số trường hợp.
2. So Sánh Stack Và Heap
Theo phân chia trên, ta thấy stack và heap là hai phần quan trọng trong vùng dữ liệu chạy thời gian của JVM, có những đặc điểm và mục đích khác nhau trong quản lý bộ nhớ và lưu trữ dữ liệu.
Dưới đây là sự so sánh giữa chúng:
- Cách Phân Bố: Bộ nhớ stack được phân bổ và giải phóng tự động. Các biến cục bộ, tham số phương thức, giá trị trả về đều được lưu trữ trên stack, và bộ nhớ trên stack sẽ được giải phóng tự động khi phương thức hoàn thành. Ngược lại, bộ nhớ heap được phân bổ động và được quản lý bởi bộ thu gom rác. Các thể hiện lớp và mảng đều được cấp phát bộ nhớ trên heap, thông qua từ khóa
new
khi tạo đối tượng. - Khoảng Bộ Nhớ: Kích thước stack là cố định (có thể điều chỉnh thông qua tham số
-Xss
), và mỗi luồng có một không gian stack riêng biệt, dùng để lưu trữ biến cục bộ và thông tin gọi phương thức. Ngược lại, heap là vùng bộ nhớ lớn nhất trong JVM, dùng để lưu trữ các thể hiện lớp và mảng. Kích thước heap có thể điều chỉnh thông qua các tham số-Xmx
và-Xms
. - Tốc Độ Phân Bố Bộ Nhớ: Tốc độ phân bố bộ nhớ trên stack tương đối nhanh vì chỉ cần di chuyển con trỏ stack để hoàn thành việc phân bổ và giải phóng bộ nhớ. Ngược lại, tốc độ phân bố bộ nhớ trên heap chậm hơn vì cần phải thực hiện phân bổ động và thu gom rác.
- Vòng Đời Đối Tượng: Dữ liệu trên stack có vòng đời tương tự với phương thức đang thực thi, khi phương thức hoàn thành, dữ liệu trên stack sẽ được giải phóng tự động. Ngược lại, các đối tượng trên heap có vòng đời dài hơn, cho đến khi bộ thu gom rác xác định chúng không còn được sử dụng nữa.
- Phân Mảnh Bộ Nhớ: Bộ nhớ stack được phân bổ và giải phóng theo nguyên tắc先进-last-out, không tồn tại vấn đề phân mảnh bộ nhớ. Ngược lại, vì heap được phân bổ động, nên có thể xảy ra phân mảnh bộ nhớ, cần bộ thu gom rác để sắp xếp lại bộ nhớ.
- Sử Dụng Bộ Nhớ: Bộ nhớ stack được sử dụng theo cách tĩnh, kích thước được xác định trước khi biên dịch. Ngược lại, bộ nhớ heap được sử dụng theo cách động, có thể tăng hoặc giảm tùy theo nhu cầu.
Tóm lại, stack và heap có sự khác biệt rõ ràng về quản lý bộ nhớ, cách phân bố, khoảng bộ nhớ, tốc độ phân bố bộ nhớ, vòng đời đối tượng, phân mảnh bộ nhớ và sử dụng bộ nhớ. Hiểu rõ đặc điểm và mục đích của stack và heap giúp sử dụng bộ nhớ hợp lý hơn, từ đó cải thiện hiệu suất và độ ổn định của chương trình.
3. Kết Luận
Như vậy, bài viết đã giới thiệu cách phân chia khu vực dữ liệu chạy thời gian của JVM và tiến hành so sánh chi tiết giữa hai khu vực stack và heap từ nhiều góc độ khác nhau.
[1] The Java Virtual Machine Specification: Run-Time Data Areas - [2] Zhou Zhiming. (2019). Hiểu Sâu Java Virtual Machine (Ấn bản thứ 3). Nhà Xuất Bản Công Nghiệp Cơ Khí, Bắc Kinh. - [3] Blog CSDN: Giải Thích Chi Tiết Phân Chia Khu Vực Bộ Nhớ Chạy Thời Gian Của JVM -
#Java