Một trong những thách thức đối với việc xây dựng các ứng dụng Java hiệu quả là nhiều người sử dụng công nghệ Java xem JVM giống như một chiếc hộp đen, do đó đã không hiểu được cách thức ứng dụng Java được vận hành bên trong JVM như thế nào, dẫn đến không khai thác được những đặc điểm của máy ảo để viết và tổ chức mã lệnh hiệu quả. Điều này làm cho việc cải tiến hiệu năng hay mở rộng ứng dụng Java trở thành nhiệm vụ hết sức khó khăn. Chính vì vậy, có sự hiểu biết cơ bản, nền tảng về JVM là điều cần thiết để có thể cải thiện hiệu năng của một ứng dụng Java.

Các bộ phần mềm máy ảo Java (JVM)

Rất nhiều lập trình viên nhầm tưởng rằng Java chỉ có một JVM đóng gói trong bản JRE/JDK của Oracle (Sun trước đây). Trong thực tế, có rất nhiều phần mềm máy ảo Java được các hãng phần mềm khác nhau xây dựng dựa trên đặc tả trong “Java Virtual Machine Specification”. Có thể kể ra đây một số JVM nổi bật về sự phổ biến hoặc phạm vi ứng dụng:

  • HotSpot VM: vừa là bản cài đặt tham chiếu cơ sở (primary reference implementation), vừa là máy ảo chính của Oracle JDK edition (miễn phí) cũng như OpenJDK edition (mã nguồn mở). Mặc dù có một số khác biệt nhỏ giữa HottSpot Oracle JDK edition và HotSpot OpenJDK edition, nhưng về tổng thể 2 bản này tương đồng với nhau.
  • JRockit: là bản cài đặt JVM được phát triển bởi Appeal Virtual Machines, lần lượt được sáp nhập vào BEA Systems và Oracle. Các tính năng của JRockit dần dần được tích hợp vào HotSpot VM.
  • Jamiga: là bản cài đặt JVM dành riêng cho nền tảng Amiga.
  • Jikes RVM: là máy ảo Java phát triển bởi IBM, nằm trong dự án nghiên cứu công nghệ mới. Jikes RVM chạy trên một số hệ điều hành họ Unix (chẳng hạn PowerPC và IAX-32).

Có thể tham khảo thêm về các máy ảo Java khác trong danh sách máy ảo Java trên wikipedia. Bài viết này, cũng như các bài viết tiếp theo trong chuỗi “Tổng quan về JVM” sẽ tập trung vào HotSpot VM, là máy ảo Java được sử dụng phổ biến nhất hiện nay.

Sơ lược các thành phần của HotSpot VM

Có ba thành phần chính của HotSpot VM: HotSpot VM Runtime, HotSpot JIT (Just-In-Time) Compiler, và HotSpot Garbage Collector. Phần này sẽ trình bày sơ lược về chức năng và nhiệm vụ của ba thành phần trên, giúp người đọc hình dung tổng quan về HotSpot VM.

VM Runtime

VM Runtime cung cấp các dịch vụ và API cho JIT compilers và Garbage Collector cũng như cung cấp các chức năng cơ bản cho JVM. Dưới đây là mô tả sơ lược các chức năng chính của VM Runtime:

  • Quản lý vòng đời máy ảo (VM Life Cycle): VM Runtime chịu trách nhiệm khởi động cũng như kết thúc hoạt động của máy ảo (và ứng dụng chạy trong đó). Thành phần khởi động máy ảo gọi là bộ kích hoạt (launcher). Có nhiều bộ kích hoạt khác nhau chẳng hạn: chương trình java, chương trình javaws, hoặc giao diện JNI có tên là JNI_CreateJavaVM.
  • Phân rã tham số dòng lệnh (Parsing of command line arguments): Máy ảo Java cho phép tùy chỉnh thông số bên trong (máy ảo) thông qua các tùy chọn cung cấp dưới dạng tham số dòng lệnh. Các tùy chọn dòng lệnh được phân chia thành 3 loại: các tùy chọn tiêu chuẩn (standard options) được định nghĩa trong tài liệu đặc tả và buộc tất cả các JVM đều phải hỗ trợ; các tùy chọn không chuẩn (non-standard options) không có trong tài liệu đặc tả, do JVM tự định nghĩa; tùy chọn dành cho phát triển (developer options) cũng do JVM tự định nghĩa và chỉ áp dụng trong quá trình phát triển ứng dụng Java.
  • Nạp lớp đối tượng (Class Loading): VM Runtime chịu trách nhiệm trong việc thực hiện và hỗ trợ thực hiện nạp các lớp đối tượng vào bộ nhớ để thực thi, theo nhiều cách khác nhau: Class.forName(), ClassLoader.loadClass(), JNI_FindClass, reflection APIs,…
  • Bytecode Verification: Java là ngôn ngữ an toàn kiểu (type-safe language). Quá trình kiểm tra kiểu được Java Compiler (javac) thực hiện khi biên dịch mã nguồn thành mã bytecode. Khi chạy chương trình, mã bytecode được nạp vào VM và được thực hiện kiểm tra kiểu trong mã bytecode bằng quá trình Bytecode Verification.
  • Trình thông dịch (Interpreter): VM Interpreter là trình thông dịch dựa trên mẫu (Template Based Interpreter). Interpreter thực hiện thông dịch dựa trên việc tra cứu cấu trúc dữ liệu có sẵn là TemplateTable. Bảng TemplateTable chứa mã máy (phụ thuộc vào hệ điều hành) tương ứng với từng bytecode.
  • Xử lý ngoại lệ (Exception Handling): VM Runtime sử dụng exception làm tín hiệu cảnh báo một chương trình đang gặp tình huống nằm ngoài dự tính. VM Runtime điều phối Interpreter, JIT compiler, và các thành phần khác trong VM cùng nhau cộng tác để thực hiện cơ chế xử lý exception.
  • Đồng bộ hóa (Synchronization): Đồng bộ hóa là quá trình kiểm soát phối hợp các thao tác thực thi đồng thời (concurrency operation). Thực thi đồng thời trong Java được thể hiện thông qua kết cấu Thread.
  • Quản lý Thread (Thread Management): là quá trình kiểm soát toàn bộ vòng đời của các thread trong ứng dụng.

JIT Compiler

Về cơ bản, quá trình thực thi một chương trình Java được thực hiện theo trình tự sau:

Sơ đồ chạy ứng dụng Java đơn giản

Tuy nhiên, trình thông dịch (interpreter) thường chạy với tốc độ khá chậm do phải chuyển đổi từng lệnh từ mã bytecode sang mã máy. Đối với những khối lệnh được thực thi lặp đi lặp lại nhiều lần, việc áp dụng thông dịch với chúng sẽ rất phí phạm thời gian. Chính vì vậy, những người thiết kế JVM đã kết hợp sử dụng Interpreter với một dạng trình biên dịch “tức thời” khi chạy (Just-In-Time Compiler).

Sơ đồ kết hợp Interpreter với JIT Compiler

JVM sẽ phân tích để xác định các khối lệnh thực thi nhiều lần (gọi các vùng mã lệnh này là các hotspot) để chuyển cho JIT Compiler biên dịch thành mã máy rồi mới thực thi. Những lệnh chỉ thực thi một lần sẽ được phân công cho interpreter. JIT Compiler có 2 phiên bản, một dành cho JVM chạy trên Client và một dành cho JVM trên Server.

Garbage Collector

Bộ nhớ hệ điều hành dành cho JVM bao gồm các phần sau:

Cấu trúc bộ nhớ của JVM

  • Bộ nhớ Heap: là vùng nhớ nằm trong “Runtime Data Areas”, dùng để cấp phát cho các đối tượng (thể hiện của lớp đối tượng) và mảng.
  • Bộ nhớ Non-Heap: là vùng nhớ dùng để nạp mã lệnh định nghĩa lớp đối tượng và các metadata khác.
  • Mã lệnh của bản thân JVM, các cấu trúc bên trong JVM, mã lệnh và dữ liệu của các bộ phân tích (profiler), v.v.

Trong 3 vùng nhớ này, vùng nhớ Heap có nhiều biến động nhất, do các đối tượng liên tục được tạo ra, hoạt động và kết thúc hoạt động trong suốt quá trình chạy chương trình. Vùng nhớ Non-heap ít biến động hơn, thường chỉ thay đổi khi JVM nạp thêm hoặc loại bỏ khai báo các lớp đối tượng. Vùng chứa mã lệnh được nạp lên khi JVM chạy và ít biến động nhất. Chính vì vậy, JVM đã được thiết kế để áp dụng mô hình quản lý tự động thu hồi bộ nhớ không còn dùng theo kiểu “thế hệ” (generation garbage collection), trong đó các đối tượng tồn tại ngắn hạn trong đó vùng nhớ Heap chứa sẽ được thu dọn với tần suất cao, còn các đối tượng tồn tại lâu dài trong vùng Heap cũng như dữ liệu trong vùng Non-Heap sẽ được thu dọn với tần suất thấp.

Comments