Máy ảo Java - Trình biên dịch JIT

Trong chương này, chúng ta sẽ tìm hiểu về trình biên dịch JIT và sự khác biệt giữa các ngôn ngữ biên dịch và thông dịch.

Ngôn ngữ được biên dịch so với Ngôn ngữ được thông dịch

Các ngôn ngữ như C, C ++ và FORTRAN là các ngôn ngữ biên dịch. Mã của chúng được phân phối dưới dạng mã nhị phân được nhắm mục tiêu vào máy bên dưới. Điều này có nghĩa là mã cấp cao được biên dịch thành mã nhị phân ngay lập tức bởi trình biên dịch tĩnh được viết riêng cho kiến ​​trúc bên dưới. Hệ nhị phân được tạo ra sẽ không chạy trên bất kỳ kiến ​​trúc nào khác.

Mặt khác, các ngôn ngữ thông dịch như Python và Perl có thể chạy trên bất kỳ máy nào, miễn là chúng có trình thông dịch hợp lệ. Nó đi từng dòng qua mã cấp cao, chuyển đổi mã đó thành mã nhị phân.

Mã được thông dịch thường chậm hơn mã đã biên dịch. Ví dụ, hãy xem xét một vòng lặp. Một thông dịch sẽ chuyển đổi mã tương ứng cho mỗi lần lặp lại của vòng lặp. Mặt khác, một mã được biên dịch sẽ làm cho bản dịch chỉ có một. Hơn nữa, vì trình thông dịch chỉ nhìn thấy một dòng tại một thời điểm, họ không thể thực hiện bất kỳ mã quan trọng nào, chẳng hạn như thay đổi thứ tự thực hiện các câu lệnh như trình biên dịch.

Chúng ta sẽ xem xét một ví dụ về tối ưu hóa như vậy bên dưới:

Adding two numbers stored in memory. Vì việc truy cập bộ nhớ có thể tiêu tốn nhiều chu kỳ CPU, một trình biên dịch tốt sẽ đưa ra hướng dẫn để tìm nạp dữ liệu từ bộ nhớ và chỉ thực hiện việc bổ sung khi dữ liệu có sẵn. Nó sẽ không chờ đợi và trong khi chờ đợi, hãy thực hiện các hướng dẫn khác. Mặt khác, không thể tối ưu hóa như vậy trong quá trình diễn giải vì trình thông dịch không biết về toàn bộ mã tại bất kỳ thời điểm nào.

Nhưng sau đó, các ngôn ngữ được thông dịch có thể chạy trên bất kỳ máy nào có trình thông dịch hợp lệ của ngôn ngữ đó.

Java được biên dịch hay thông dịch?

Java đã cố gắng tìm ra điểm trung gian. Vì JVM nằm giữa trình biên dịch javac và phần cứng bên dưới, trình biên dịch javac (hoặc bất kỳ trình biên dịch nào khác) biên dịch mã Java trong Bytecode, được hiểu bởi JVM nền tảng cụ thể. JVM sau đó biên dịch Bytecode ở dạng nhị phân bằng cách sử dụng biên dịch JIT (Just-in-time), khi mã thực thi.

HotSpots

Trong một chương trình điển hình, chỉ có một đoạn mã nhỏ được thực thi thường xuyên và thường thì đoạn mã này ảnh hưởng đáng kể đến hiệu suất của toàn bộ ứng dụng. Các phần mã như vậy được gọi làHotSpots.

Nếu một số đoạn mã chỉ được thực thi một lần, thì việc biên dịch nó sẽ rất lãng phí công sức và thay vào đó, việc diễn giải Bytecode sẽ nhanh hơn. Nhưng nếu phần này là một phần nóng và được thực thi nhiều lần, thì JVM sẽ biên dịch nó thay thế. Ví dụ: nếu một phương thức được gọi nhiều lần, thì các chu kỳ bổ sung mà nó cần để biên dịch mã sẽ được bù đắp bởi nhị phân nhanh hơn được tạo ra.

Hơn nữa, JVM càng chạy một phương thức hoặc một vòng lặp cụ thể, thì nó càng thu thập được nhiều thông tin để thực hiện các tối ưu hóa lặt vặt để tạo ra một tệp nhị phân nhanh hơn.

Chúng ta hãy xem xét đoạn mã sau:

for(int i = 0 ; I <= 100; i++) {
   System.out.println(obj1.equals(obj2)); //two objects
}

Nếu mã này được thông dịch, trình thông dịch sẽ suy ra cho mỗi lần lặp lại các lớp của obj1. Điều này là do mỗi lớp trong Java có một phương thức .equals (), được mở rộng từ lớp Đối tượng và có thể bị ghi đè. Vì vậy, ngay cả khi obj1 là một chuỗi cho mỗi lần lặp, việc khấu trừ vẫn sẽ được thực hiện.

Mặt khác, điều thực sự sẽ xảy ra là JVM sẽ nhận thấy rằng đối với mỗi lần lặp, obj1 thuộc lớp String và do đó, nó sẽ trực tiếp tạo ra mã tương ứng với phương thức .equals () của lớp String. Do đó, không cần tra cứu và mã đã biên dịch sẽ thực thi nhanh hơn.

Loại hành vi này chỉ có thể thực hiện được khi JVM biết mã hoạt động như thế nào. Do đó, nó đợi trước khi biên dịch các phần nhất định của mã.

Dưới đây là một ví dụ khác -

int sum = 7;
for(int i = 0 ; i <= 100; i++) {
   sum += i;
}

Trình thông dịch, đối với mỗi vòng lặp, lấy giá trị của 'sum' từ bộ nhớ, thêm 'I' vào nó và lưu trữ lại vào bộ nhớ. Truy cập bộ nhớ là một hoạt động tốn kém và thường mất nhiều chu kỳ CPU. Vì mã này chạy nhiều lần, nó là một HotSpot. JIT sẽ biên dịch mã này và thực hiện tối ưu hóa sau.

Bản sao cục bộ của 'sum' sẽ được lưu trữ trong một thanh ghi, cụ thể cho một chuỗi cụ thể. Tất cả các hoạt động sẽ được thực hiện với giá trị trong thanh ghi và khi vòng lặp hoàn thành, giá trị sẽ được ghi trở lại bộ nhớ.

Điều gì sẽ xảy ra nếu các luồng khác cũng đang truy cập biến? Vì các cập nhật đang được thực hiện cho bản sao cục bộ của biến bởi một số luồng khác, chúng sẽ thấy một giá trị cũ. Đồng bộ hóa luồng là cần thiết trong những trường hợp như vậy. Một nguyên thủy đồng bộ rất cơ bản sẽ là khai báo 'sum' là dễ bay hơi. Bây giờ, trước khi truy cập một biến, một luồng sẽ xóa các thanh ghi cục bộ của nó và lấy giá trị từ bộ nhớ. Sau khi truy cập nó, giá trị ngay lập tức được ghi vào bộ nhớ.

Dưới đây là một số tối ưu hóa chung được thực hiện bởi trình biên dịch JIT -

  • Nội tuyến phương pháp
  • Loại bỏ mã chết
  • Heuristics để tối ưu hóa các trang web cuộc gọi
  • Gấp liên tục