JVM là gì? JVM đã xây dựng gã khổng lồ Java như thế nào?
Ngôn Ngữ Lập Trình
Mục lục
Một trong những yếu tố tạo nên thành công đó bắt nguồn từ những chương trình của Java gần như có thể chạy trên bất kỳ thiết bị nào. Điều này có thể thực hiện được nhờ máy ảo Java (JVM) .
Trong bài đăng này, hãy cùng TechWorks khám phá các khía cạnh khác nhau của JVM một cách chi tiết nhất.
Máy tính chạy chương trình như thế nào?
Là một developer, chúng ta thường sử dụng các ngôn ngữ cấp cao như Java, PHP, JavaScript, v.v. để viết chương trình của mình.
Trong máy tính, CPU là thành phần chịu trách nhiệm thực hiện hoặc chạy các lệnh chúng ta viết. Thật không may, nó không hiểu bất kỳ ngôn ngữ cấp cao nào. Ngôn ngữ duy nhất mà CPU có thể hiểu được là ngôn ngữ máy. Nó trông có dạng như thế này:
000101110011
11001111011000
010110010110110
Như bạn có thể thấy, đây là một chuỗi các số 0 và số 1 được thực hiện một cách cơ học bởi máy tính.
Một điều khác bạn cần biết về ngôn ngữ máy là nó gắn liền với phần cứng. Vì vậy, mỗi máy tính có ngôn ngữ máy riêng.
JVM là gì?
Bây giờ, khi chúng ta đã hiểu rõ về cách máy tính thực thi chương trình, hãy cùng thảo luận về Máy ảo Java (Java Virtual Machine - JVM).
Máy ảo Java (Java Virtual Machine - JVM) là trái tim của toàn bộ quá trình thực thi chương trình Java. Về cơ bản, đây là một chương trình cung cấp môi trường chạy cần thiết để các chương trình Java có thể thực thi.
Nói cách khác, JVM là một máy tính trừu tượng, chịu trách nhiệm thực thi Java bytecode (một tập hợp các lệnh đã được tối ưu hóa cao) trên một nền tảng phần cứng cụ thể. JVM cũng được gọi là hệ thống chạy thời gian của Java (Java run-time system).
Bạn cần biết là JVM là một máy tính (hoặc một cỗ máy), nhưng không phải là loại máy thông thường. Máy tính này không tồn tại dưới dạng phần cứng thực tế và thậm chí không có hệ điều hành. Nó là một nền tảng máy tính giả định.
Tiếp theo, như đã thảo luận ở trên, một máy tính cần một ngôn ngữ máy để thực thi các chương trình, do đó JVM cũng có ngôn ngữ máy của riêng nó, gọi là Java bytecode. Vai trò của JVM là chuyển đổi bytecode thành ngôn ngữ máy cho máy tính thực tế (hoặc phần cứng).
JVM là một phần của Môi trường Chạy Java (Java Running Environment - JRE).
Lưu ý: Ban đầu, JVM được phát triển dành riêng cho ngôn ngữ Java, nhưng sau này đã được mở rộng để hỗ trợ nhiều ngôn ngữ khác như Scala, Groovy, và chủ yếu là Kotlin.
JVM hoạt động như thế nào?
Các ứng dụng Java được gọi là WORA (Write Once, Run Anywhere - Viết một lần, chạy ở mọi nơi). Các lập trình viên Java có thể viết mã Java trên một nền tảng và mong đợi nó hoạt động trên tất cả các hệ thống hỗ trợ Java. JVM làm cho điều này trở nên khả thi. Các nhà phát triển không cần lo lắng về việc cấu hình và cài đặt JVM vì nó được bao gồm trong gói JDK.
Mã nguồn trong Java được biên dịch thành bytecode. Nhiều quy trình được thực hiện trong JVM để chuyển đổi bytecode của Java thành mã máy.
Trình biên dịch tạo ra các tệp .class khi chúng ta biên dịch các tệp .java. Các tệp này chứa bytecode và có cùng tên lớp như các tệp .java. Khi chúng ta chạy các tệp .class, JVM thực hiện các thao tác sau:
- Tải bytecode
- Xác minh bytecode
- Thực thi bytecode
- Cung cấp môi trường chạy để hỗ trợ các ứng dụng khác nhau
Khi chúng ta tạo một chương trình trong Java, mã chương trình .java sẽ được trình biên dịch Java chuyển đổi thành một tệp .class bao gồm các lệnh bytecode. Trình biên dịch Java này nằm bên ngoài JVM.
Bây giờ, JVM thực hiện các bước sau:
Tệp .class này được chuyển đếnclass loader subsystem của JVM như mô tả trong hình.
Trong JVM, hệ thống nạp lớp là một mô-đun hoặc chương trình thực hiện các chức năng sau:
a) Trước hết, hệ thống nạp lớp sẽ tải tệp .class vào bộ nhớ.
b) Tiếp theo, trình xác minh bytecode kiểm tra xem tất cả các lệnh bytecode có đúng không. Nếu phát hiện bất kỳ lệnh nào đáng ngờ, quá trình thực thi sẽ bị từ chối ngay lập tức.
c) Nếu các lệnh bytecode là đúng, JVM sẽ phân bổ bộ nhớ cần thiết để thực thi chương trình.
Bộ nhớ này được chia thành 5 phần riêng biệt, gọi là các vùng dữ liệu thời gian chạy (run-time data areas). Chúng chứa dữ liệu và kết quả trong quá trình thực thi chương trình. Các vùng này bao gồm:
Vùng Lớp (Phương thức) (Class/Method Area)
Vùng Class (Phương thức) là một khối bộ nhớ lưu trữ mã lớp, mã của các biến và phương thức trong chương trình Java. Ở đây, phương thức có nghĩa là các hàm được khai báo trong lớp.
Heap
Đây là vùng dữ liệu thời gian chạy nơi các đối tượng được tạo ra. Khi JVM tải một lớp, một phương thức và một vùng heap được xây dựng ngay lập tức.
Ngăn Xếp (Stack)
Mã phương thức được lưu trữ trong vùng Phương thức. Nhưng trong quá trình thực thi phương thức, nó cần thêm bộ nhớ để lưu trữ dữ liệu và kết quả. Bộ nhớ này được phân bổ trên các ngăn xếp Java.
Java stacks là những vùng bộ nhớ - nơi để các phương thức Java được thực thi. Trong Java stacks, một khung riêng biệt được tạo ra để phương thức được thực thi.
Mỗi lần một phương thức được gọi, một khung mới được tạo vào ngăn xếp. Khi việc gọi phương thức hoàn tất, khung tương ứng đó sẽ bị hủy.
JVM luôn tạo một luồng (hoặc tiến trình) riêng biệt để thực thi từng phương thức.
Thanh ghi PC (PC Register)
Thanh ghi PC (Program Counter) là các thanh ghi (vùng nhớ) chứa địa chỉ bộ nhớ của lệnh JVM đang được thực thi.
Ngăn Xếp Phương Thức Gốc (Native Method Stack)
Các phương thức của chương trình Java được thực thi trên các ngăn xếp Java. Tương tự, các phương thức gốc sử dụng trong chương trình hoặc ứng dụng được thực thi trên các ngăn xếp phương thức gốc.
Nói chung, để thực thi các phương thức gốc, cần có các thư viện phương thức gốc Java. Các tệp tiêu đề này được định vị và kết nối với JVM thông qua một chương trình gọi là Giao diện phương thức gốc (Native Method Interface).
Ưu điểm của JVM
JVM ra mắt vào năm 1995 và mang đến hai khái niệm mang tính cách mạng:
- Viết một lần, chạy mọi nơi (Write Once, Run Anywhere - WORA)
- Quản lý bộ nhớ tự động
Ngay từ đầu, chúng ta đã nói rằng Java là một ngôn ngữ độc lập với nền tảng. Đây là một trong những điểm mạnh của Java: nó có thể chạy trên bất kỳ máy tính nào.
Điều này có thể thực hiện được vì chương trình Java sau khi biên dịch không được thiết kế để thực thi trực tiếp trên phần cứng thực tế, mà trên một máy ảo. Vì vậy, một khi máy tính có trình thông dịch bytecode Java, nó có thể chạy bất kỳ chương trình bytecode Java nào, và cùng một chương trình có thể chạy trên bất kỳ máy tính nào có trình thông dịch này.
Lưu ý: Mặc dù Java là ngôn ngữ độc lập với nền tảng, JVM thì không. Mỗi hệ điều hành có một phiên bản JVM riêng.
Ưu điểm thứ hai mà các lập trình viên Java có thể hài lòng là cách ngôn ngữ này quản lý bộ nhớ chương trình. Trong một JVM đang chạy, có một quá trình gọi là thu gom rác (garbage collection), chịu trách nhiệm theo dõi việc sử dụng bộ nhớ. Nó liên tục xác định và loại bỏ các vùng nhớ không còn sử dụng trong các chương trình Java.
Execution Engine của JVM
Execution Engine bao gồm hai thành phần: Trình thông dịch (Interpreter) và Trình biên dịch JIT (Just In Time). Chúng chuyển đổi các lệnh bytecode thành mã máy để bộ xử lý có thể thực thi.
Trong Java, JVM (Java Virtual Machine) sử dụng cả trình thông dịch và trình biên dịch JIT đồng thời để chuyển đổi bytecode thành mã máy. Kỹ thuật này được gọi là tối ưu hóa thích ứng (adaptive optimizer).
Thông thường, bất kỳ ngôn ngữ lập trình nào (như C/C++, Fortran, COBOL, v.v.) sẽ sử dụng trình thông dịch hoặc trình biên dịch để chuyển đổi dòng mã nguồn thành mã máy.
Kiến trúc JVM
Trong Java, có ba khái niệm về JVM: Implementation (Triển khai), Specification (Đặc tả), và Instance (Thể hiện).
Chúng ta hãy cùng xem xét từng cái một.
- Implementation: Còn được gọi là JRE (Java Runtime Environment).
- Specification: Một tài liệu mô tả các yêu cầu để triển khai JVM.
- Instance: Một Instance JVM được tạo ra mỗi khi bạn chạy một tệp lớp (.class) Java.
Kiến trúc JVM có thể được chia thành ba hệ thống con chính:
- ClassLoader
- Memory area / Runtime memory (Bộ nhớ thời gian chạy)
- Execution engine (Động cơ thực thi)
ClassLoader
JVM tải các tệp .class đã biên dịch để chạy ứng dụng Java, và để thực hiện điều này, JVM dựa vào ClassLoader của nó.
ClassLoader đọc tệp .class được tạo bởi lệnh javac và trích xuất thông tin cốt lõi. Sau đó, nó lưu trữ thông tin này trong khu vực Method Area (Vùng phương thức).
ClassLoader cho phép bạn truy xuất các thông tin như tên lớp, biến và phương thức.
Các ClassLoader sử dụng kỹ thuật lazy-loading (tải chậm) và bộ nhớ đệm để tối ưu hóa hiệu quả việc tải lớp.
Bootstrap ClassLoader
Bootstrap ClassLoader là mã máy khởi động khi Java Virtual Machine bắt đầu chạy.
Bootstrap ClassLoader không phải là một lớp Java; nhiệm vụ của nó là tải trình ClassLoader Java đầu tiên. Nó tải tệp rt.jar chứa mã cần thiết hỗ trợ cho Java Runtime Environment (JRE), bao gồm các tệp lớp trong các gói như java.util, java.lang, java.io.
Bootstrap ClassLoader còn được gọi là Primordial ClassLoader.
Extension ClassLoader
Extension ClassLoader (phần tử con của Bootstrap ClassLoader) tải các phần mở rộng lõi của Java từ thư viện mở rộng JDK.
Nó tải các tệp từ thư mục jre/lib/ext hoặc các thư mục khác được chỉ định bởi thuộc tính hệ thống java.ext.dirs.
System ClassLoader
System ClassLoader chịu trách nhiệm tải tất cả các tệp lớp liên quan đến ứng dụng đã được chỉ định trong đường dẫn lớp (classpath, -cp).
System ClassLoader còn được gọi là Application ClassLoader và là con của Extension ClassLoader.
Chức năng của ClassLoader
Ba chức năng chính của một ClassLoader bao gồm Loading (Tải), Linking (Liên kết), và Initialization (Khởi tạo).
Loading (Tải)
ClassLoader tải các tệp từ bộ nhớ thứ cấp vào bộ nhớ chính (RAM) để thực thi.
ClassLoader lấy tệp .class và tạo dữ liệu nhị phân, sau đó lưu nó vào khu vực Method Area.
- JVM lưu trữ một số thông tin cho mỗi .class trong khu vực Method Area, bao gồm:
- Tên đầy đủ của lớp cha trực tiếp và lớp được tải.
- Chi tiết về các giao diện hoặc lớp liên quan đến tệp .class.
- Thông tin về biến, bộ sửa đổi, phương thức, v.v.
JVM tải tệp .class và tạo một đối tượng có kiểu Class để biểu diễn tệp đó trong bộ nhớ heap.
Các lập trình viên có thể sử dụng đối tượng Class này để lấy thông tin cấp lớp như tên lớp, tên lớp cha, phương thức, và dữ liệu biến.
Linking (Liên kết)
Quá trình liên kết thực hiện theo ba bước chính sau:
- Verification (Xác minh): Bước này đảm bảo rằng tệp .class là chính xác. Sau đó, nó kiểm tra xem trình biên dịch hợp lệ có tạo và tạo tệp hay không. Nếu xác minh thất bại, chúng ta sẽ nhận được ngoại lệ java.lang.VerifyError và quá trình liên kết sẽ dừng lại.
- Preparation (Chuẩn bị): JVM phân bổ bộ nhớ cho các biến tĩnh của lớp và khởi tạo chúng với giá trị mặc định.
- Resolution (Giải quyết): Đây là quá trình thay thế các tham chiếu tượng trưng bằng các tham chiếu mới.
Initialization (Khởi tạo)
Bước cuối cùng trong quá trình tải lớp là gán giá trị cho các biến tĩnh và thực thi các khối tĩnh.
Memory area / Runtime memory (Khu vực bộ nhớ của JVM/Vùng dữ liệu thời gian chạy)
Khu vực bộ nhớ của JVM là thành phần thứ hai của Java Virtual Machine. Vùng dữ liệu thời gian chạy có năm khu vực con:
Method area
Mỗi JVM chỉ có một vùng Method area và khu vực này được chia sẻ cho tất cả các luồng. Nó lưu trữ cấu trúc của từng lớp, như tên lớp, dữ liệu phương thức, thông tin biến, v.v.
Heap
Vùng bộ nhớ Heap lưu trữ đối tượng và các biến thể hiện của đối tượng đó. Mỗi khi bạn tạo một đối tượng trong Java, nó sẽ được đưa vào Heap. Khu vực Heap biểu thị một vùng bộ nhớ được chia sẻ.
Stack
Stack (còn gọi là ngăn xếp luồng) là một khu vực dữ liệu trong bộ nhớ JVM được tạo ra cho một luồng thực thi.
Các ngăn xếp JVM lưu trữ kết quả tạm thời, biến cục bộ, và dữ liệu cho các phương thức gọi và trả về.
PC register
Thanh ghi PC lưu trữ địa chỉ của lệnh Java Virtual Machine hiện đang được thực thi. Mỗi luồng được chỉ định một thanh ghi PC riêng biệt.
Native method stack
Khu vực này chứa tất cả các phương thức gốc (native methods) cần thiết cho ứng dụng. Các phương thức gốc sử dụng các ngôn ngữ khác ngoài Java.
Execution engine
Đây là khu vực cốt lõi của Java Virtual Machine (JVM). Execution engine có khả năng giao tiếp với các khu vực bộ nhớ khác trong JVM.
Mỗi luồng trong một ứng dụng Java là một phiên bản của Execution engine máy ảo.
Execution engine thực thi mã bytecode đã được ClassLoader gán cho các khu vực dữ liệu thời gian chạy. Nó bao gồm ba thành phần chính:
- Interpreter (Trình thông dịch)
- JIT Compiler (Trình biên dịch Just-In-Time)
- Garbage Collector (Bộ thu gom rác)
Interpreter (Trình thông dịch)
Trình thông dịch đọc, giải mã và thực thi mã bytecode theo từng dòng.
Tuy nhiên, nếu bạn gọi một phương thức nhiều lần, thì mỗi lần đều cần phải giải mã lại. Và đây là nhược điểm của ngôn ngữ được thông dịch.
Just-In-Time Compiler (JIT)
JIT được sử dụng trong JVM để tăng hiệu quả cho trình thông dịch. JIT biên dịch toàn bộ mã bytecode và chuyển đổi nó thành mã máy.
Khi trình thông dịch gặp các phương thức được gọi nhiều lần, JIT sẽ cung cấp mã máy trực tiếp cho phần đó, giúp giảm nhu cầu giải mã lại, nhằm cải thiện hiệu quả.
Garbage Collector (Bộ thu gom rác)
Garbage Collector là một tính năng quan trọng của kiến trúc JVM. Mỗi đối tượng được tạo ra trong quá trình thực thi chương trình tiêu tốn một lượng bộ nhớ. Những đối tượng này sau đó không còn được tham chiếu bởi chương trình và không cần thiết nữa.
Bộ thu gom rác sẽ loại bỏ các đối tượng không còn được tham chiếu khỏi bộ nhớ để quản lý và tăng cường hiệu quả sử dụng bộ nhớ.
Kết luận
Trong bài viết này, bạn đã biết JVM trong Java là gì cũng như đã làm quen với kiến trúc JVM cùng với các thành phần của Java Virtual Machine. Đến giờ, bạn hẳn đã có cái nhìn thoáng qua về cách hoạt động của Java Virtual Machine. Hy vọng rằng bạn đã nắm bắt được những điều cơ bản của Java virtual machine (JVM).