James Gosling

Mọi lập trình viên Java đều biết rằng, bytecode sẽ dược execute trong JRE (Java Runtime Environment). Tuy nhiên rất ít người biết rằng JRE là một dạng implementation của JVM - Java Virtual Machine. JVM phân tích bytecode sau đó thông dịch và execute nó.

java

Nắm chắc kiến trúc của JVM giúp lập trình viên viết code tối ưu hơn và tự tin hơn trong các buổi phỏng vấn. Bài viết ngày hôm nay sẽ giúp bạn làm được điều đó.

#1 JVM

Virtual Machine - máy ảo, là một dạng implementation trên phần mềm của một máy tính vật lý nào đó.

Java được phát triển trên concept WORA - Write Once Run Anywhere - Viết 1 lần chạy mọi nơi. Và code Java được chạy trên máy ảo.

Bộ compiler biên dịch file *.java thành file *.class và *.class được đặt vào JVM, sau đó JVM load và execute class file đó. Biểu đồ dưới đây mô tả kiến trúc của JVM, nó sẽ theo bạn trong suốt bài viết này:

JVM diagrams

#2 Cơ chế làm việc của JVM:

Các bạn thấy đấy, JVM được chia thành 3 mô-đun chính:

  1. Class Loader
  2. Runtime Data Area
  3. Execution Engine

Và chúng ta sẽ tìm hiểu từng mô-đun một.

 

#2.1 Class Loader

Tính năng dynamic class loading của Java được xử lý bởi Class Loader. Bộ phận này load, link và khởi tạo (initialize) class file khi tồn tại một tham chiếu đầu tiên tới class đó trong quá trình runtime (quá trình chạy, chứ không phải quá trình biên dịch - compile time).

Tiếp tục theo dõi sơ đồ JVM, bạn sẽ thấy trong Class Loader Subsystem có 3 pha xử lý: Loading, Linking và Initialization.

Đầu tiên là pha loading, có 3 bộ loader tham gia vào việc loading: Boot Strap class Loader, Extension class Loader và Application class Loader.

  • Boot Strap Class Loader - Chịu trách nhiệm load các class từ bootstrap classpath. Bộ loader này có mức ưu tiên cao nhất.
  • Extension Class Loader - Load các class nằm trong folder jre/lib
  • Application Class Loader - load các class mức ứng dụng

Trong quá trình hoạt động, 3 bộ loader trên đều chạy dựa trên thuật toán tìm kiếm tài nguyên mang tên: Thuật toán phân cấp ủy quyền - Delegation Hierarchy Algorithm.

Thứ 2, pha linking, chia thành 3 bước như sau:

  • Verify: Bộ bytecode verifier sẽ kiểm tra đoạn bytecode được generate có hợp lệ hay không. Nếu không hợp lễ, verification error sẽ được bắn ra.
  • Prepare:  ở bước này, tất cả các biến static được cấp phát bộ nhớ và gán cho giá trị mặc định
  • Resolve: tất cả tham chiếu bộ nhớ dạng ký hiệu (symbolic memory reference) được thay thế bằng tham chiếu dạng nguyên thủy (original reference)

Thứ 3, pha initialization, là pha cuối cùng của bộ Class Loader. Tại pha này, tất cả biến static được gán giá trị (giá trị này đã được developer ghi trong file *.java) và các static block được thực thi.

write once run anywhere

#2.2 Runtime Data Area

Mô-đun này được chia nhỏ thành 5 mô-đun con:

  1. Method Area - nơi lưu trữ dữ liệu mức class - tức toàn bộ các dữ liệu có trong một class sẽ nằm ở đây. Mỗi JVM chỉ có một Method Area và nó có thể được sử dụng bởi nhiều tiến trình.
  2. Heap Area - lưu trữ object và các thứ liên quan như instance variable, arrays. Giống như Method Area, mỗi JVM chỉ có một Heap Area. Vì 2 vùng này được các tiến trình chia sẻ với nhau nên dữ liệu lưu ở đây không đảm bảo thread-safe.
  3. Stack Area - Stack Area đảm bảo thread-safe bởi mỗi tiến trình sẽ được cấp phát một runtime stack. Tất cả biến cục bộ được tạo trong bộ nhớ stack. Mỗi khi có method call - lệnh gọi hàm, một "lối vào" stack sẽ được "mở", lối vào này mang tên Stack Frame. Mỗi Stack Frame chứa 3 thực thể con:
    1. Local Variable Array - Mảng các biến cục bộ
    2. Operand Stack - ngăn xếp chứa các toán hạng
    3. Frame Data - các ký hiệu liên quan tới method được chứa ở đây, Trong trường hợp xảy ra exception, thông tin trong khối catch cũng nằm tại đây luôn.
  4. PC Registers - PC là viết tắt của Program Counter - một thanh ghi lưu địa chỉ của lệnh đang thực thi. Mỗi thread sẽ sở hữu riêng một PC.
  5. Native Method stacks - giữ các thông tin tự nhiên của method. Mỗi thread đều sở hữu một Native method stack.

 

#2.3 Bộ thực thi - Execution Engine

Phần bytecode được gán qua Runtime Data Area sẽ được thực thi bởi Execution Engine. Tiếp đó, mô-đun này đọc và thực thi từng đoạn byte code.

3 mô-đun con của Execution Engine là:

  1. Interpreter - Trình thông dịch. Trình thông dịch dẽ thông dịch bytecode nhanh nhưng nhược điểm là thực thi chậm. Bên cạnh đó, một nhược điểm nữa của trình thông dịch là method được gọi bao nhiêu lần thì cần bấy nhiêu lần thông dịch.
  2. JIT Compiler - Just In Time Compiler. JIT Compiler sẽ trung hòa các nhược điểm của interpreter. Execution Engine dùng Interpreter để thông dịch code, và khi nó phát hiện ra code bị lặp lại, nó sẽ dùng JIT Compiler. JIT Compiler biên dịch toàn bộ bytecode (thay vì thông dịch từng dòng như interpreter) sau đó chuyển đổi thành native code. Chỗ native code này sẽ được sử dụng trực tiếp cho các lời gọi hàm lặp đi lặp lại, nhờ đó, hiệu năng được cải thiện đáng kể. Các bước xử lý của JIT Compiler gồm:
    1. Intermediate Code Generator - Sinh mã trung gian
    2. Code Optimizer - Tối ưu mã
    3. Target Code Generator - Tạo mã máy hoặc native code
    4. Profiler - Một mô-đun đặc biệt, chịu trách nhiệm tìm các "điểm nóng", một ví dụ về "điểm nóng" là việc các lời gọi hàm bị lặp đi lặp lại.
  3. Garbage Collector - tìm kiếm và thu dọn các object đã tạo ra nhưng không được tham chiếu đến. Ta có thể kích hoạt thủ công bộ GC thông qua lệnh "System.gc()". Tuy nhiên GC có một vài điểm yếu có thể khiến code của bạn bị memory leaks. Hãy tham khảo bài viết này để nắm được các phương pháp phòng ngừa.

2 thành phần cuối cùng của JVM là JNI Native Method Libraries:

JNI - Java Native Interface - sẽ tương tác với Native Method Libraries và cung cấp Native Libraries cần thiết cho Execution Engine.