JavaRush /Blog Java /Random-VI /Biên dịch và thực thi các ứng dụng Java một cách chuyên s...
Павел Голов
Mức độ
Москва

Biên dịch và thực thi các ứng dụng Java một cách chuyên sâu

Xuất bản trong nhóm

Nội dung:

  1. Giới thiệu
  2. Biên dịch thành mã byte
  3. Ví dụ về biên dịch và thực thi chương trình
  4. Thực thi một chương trình trên máy ảo
  5. Biên dịch đúng lúc (JIT)
  6. Phần kết luận
Biên dịch và thực thi các ứng dụng Java một cách chuyên sâu - 1

1. Giới thiệu

Chào mọi người! Hôm nay tôi muốn chia sẻ kiến ​​thức về những gì xảy ra trong JVM (Máy ảo Java) sau khi chúng tôi chạy một ứng dụng viết bằng Java. Ngày nay, có những môi trường phát triển hợp thời giúp bạn tránh phải suy nghĩ về các phần bên trong của JVM, biên dịch và thực thi mã Java, điều này có thể khiến các nhà phát triển mới bỏ lỡ những khía cạnh quan trọng này. Đồng thời, các câu hỏi liên quan đến chủ đề này thường được hỏi trong các cuộc phỏng vấn, đó là lý do tại sao tôi quyết định viết một bài báo.

2. Biên dịch thành mã byte

Biên dịch và thực thi các ứng dụng Java cơ bản - 2
Hãy bắt đầu với lý thuyết. Khi chúng tôi viết bất kỳ ứng dụng nào, chúng tôi tạo một tệp có phần mở rộng .javavà đặt mã vào đó bằng ngôn ngữ lập trình Java. Tệp như vậy chứa mã mà con người có thể đọc được được gọi là tệp mã nguồn . Khi tệp mã nguồn đã sẵn sàng, bạn cần thực thi nó! Nhưng ở giai đoạn này, nó chứa thông tin chỉ có con người mới hiểu được. Java là ngôn ngữ lập trình đa nền tảng. Điều này có nghĩa là các chương trình viết bằng Java có thể được thực thi trên bất kỳ nền tảng nào có cài đặt hệ thống thời gian chạy Java chuyên dụng. Hệ thống này được gọi là Máy ảo Java (JVM). Để dịch một chương trình từ mã nguồn thành mã mà JVM có thể hiểu được, bạn cần phải biên dịch nó. Mã mà JVM hiểu được gọi là mã byte và chứa một tập hợp các hướng dẫn mà máy ảo sẽ thực thi sau đó. Để biên dịch mã nguồn thành mã byte, có một trình biên dịch javaccó trong JDK (Bộ công cụ phát triển Java). Là đầu vào, trình biên dịch chấp nhận một tệp có phần mở rộng .java, chứa mã nguồn của chương trình và là đầu ra, nó tạo ra một tệp có phần mở rộng .class, chứa mã byte cần thiết để chương trình được máy ảo thực thi. Khi một chương trình đã được biên dịch thành mã byte, nó có thể được thực thi bằng máy ảo.

3. Ví dụ về biên dịch và thực thi chương trình

Giả sử chúng ta có một chương trình đơn giản, chứa trong một tệp Calculator.java, nhận 2 đối số dòng lệnh bằng số và in kết quả phép cộng của chúng:
class Calculator {
    public static void main(String[] args){
        int a = Integer.valueOf(args[0]);
        int b = Integer.valueOf(args[1]);

        System.out.println(a + b);
    }
}
Để biên dịch chương trình này thành mã byte, chúng ta sẽ sử dụng trình biên dịch javactrên dòng lệnh:
javac Calculator.java
Sau khi biên dịch, chúng tôi nhận được một tệp có mã byte làm đầu ra Calculator.class, chúng tôi có thể thực thi tệp này bằng máy java được cài đặt trên máy tính của mình bằng lệnh java trên dòng lệnh:
java Calculator 1 2
Lưu ý rằng sau tên tệp, 2 đối số dòng lệnh đã được chỉ định - số 1 và 2. Sau khi thực hiện chương trình, số 3 sẽ được hiển thị trên dòng lệnh. Trong ví dụ trên, chúng ta có một lớp đơn giản tự tồn tại . Nhưng nếu lớp học nằm trong một gói nào đó thì sao? Hãy mô phỏng tình huống sau: tạo thư mục src/ru/javarushvà đặt lớp của chúng ta vào đó. Bây giờ nó trông như thế này (chúng tôi đã thêm tên gói vào đầu tệp):
package ru.javarush;

class Calculator {
    public static void main(String[] args){
        int a = Integer.valueOf(args[0]);
        int b = Integer.valueOf(args[1]);

        System.out.println(a + b);
    }
}
Hãy biên dịch một lớp như vậy bằng lệnh sau:
javac -d bin src/ru/javarush/Calculator.java
Trong ví dụ này, chúng tôi đã sử dụng một tùy chọn trình biên dịch bổ sung -d binđể đặt các tệp đã biên dịch vào một thư mục bincó cấu trúc tương tự như thư mục src, nhưng thư mục đó binphải được tạo trước. Kỹ thuật này được sử dụng để tránh nhầm lẫn giữa tệp mã nguồn với tệp mã byte. Trước khi chạy chương trình đã biên dịch, cần giải thích khái niệm này classpath. Classpathlà đường dẫn mà máy ảo sẽ tìm kiếm các gói và lớp đã biên dịch. Nghĩa là, bằng cách này, chúng tôi cho máy ảo biết thư mục nào trong hệ thống tệp là gốc của hệ thống phân cấp gói Java. Classpathcó thể được chỉ định khi bắt đầu chương trình bằng cờ -classpath. Chúng tôi khởi chạy chương trình bằng lệnh:
java -classpath ./bin ru.javarush.Calculator 1 2
Trong ví dụ này, chúng tôi yêu cầu tên đầy đủ của lớp, bao gồm tên của gói chứa nó. Cây tập tin cuối cùng trông như thế này:
├── src
│     └── ru
│          └── javarush
│                  └── Calculator.java
└── bin
      └── ru
           └── javarush
                   └── Calculator.class

4. Thực thi chương trình bằng máy ảo

Vì vậy, chúng tôi đã khởi động chương trình bằng văn bản. Nhưng điều gì sẽ xảy ra khi một chương trình đã biên dịch được khởi chạy bởi một máy ảo? Trước tiên, hãy tìm hiểu ý nghĩa của các khái niệm biên dịch và giải thích mã. Biên dịch là việc dịch một chương trình được viết bằng ngôn ngữ nguồn cấp cao sang một chương trình tương đương bằng ngôn ngữ cấp thấp tương tự như mã máy. Phiên dịch là phân tích, xử lý và thực thi ngay lập tức chương trình nguồn hoặc yêu cầu theo từng câu lệnh (từng dòng lệnh, từng dòng) (ngược lại với việc biên dịch, trong đó chương trình được dịch mà không thực thi nó). Ngôn ngữ Java có cả trình biên dịch ( javac) và trình thông dịch, là một máy ảo chuyển đổi mã byte thành từng dòng mã máy và thực thi nó ngay lập tức. Do đó, khi chúng tôi chạy một chương trình đã biên dịch, máy ảo bắt đầu diễn giải nó, nghĩa là chuyển đổi từng dòng mã byte thành mã máy, cũng như việc thực thi nó. Thật không may, việc giải thích mã byte thuần túy là một quá trình khá dài và làm cho java chậm hơn so với các đối thủ cạnh tranh. Để tránh điều này, một cơ chế đã được đưa ra để tăng tốc độ giải thích mã byte của máy ảo. Cơ chế này được gọi là biên dịch đúng lúc (JITC).

5. Biên dịch đúng lúc (JIT)

Nói một cách đơn giản, cơ chế biên dịch Just-In-Time là thế này: nếu có các phần mã trong chương trình được thực thi nhiều lần thì chúng có thể được biên dịch một lần thành mã máy để tăng tốc độ thực thi trong tương lai. Sau khi biên dịch phần chương trình như vậy thành mã máy, với mỗi lệnh gọi tiếp theo đến phần này của chương trình, máy ảo sẽ ngay lập tức thực thi mã máy đã biên dịch thay vì diễn giải nó, điều này sẽ tăng tốc độ thực thi chương trình một cách tự nhiên. Việc tăng tốc chương trình đạt được bằng cách tăng mức tiêu thụ bộ nhớ (chúng ta cần lưu trữ mã máy đã biên dịch ở đâu đó!) và bằng cách tăng thời gian dành cho việc biên dịch trong khi thực hiện chương trình. Biên dịch JIT là một cơ chế khá phức tạp, vì vậy chúng ta hãy tìm hiểu kỹ hơn. Có 4 cấp độ biên dịch JIT mã byte thành mã máy. Mức độ biên dịch càng cao thì càng phức tạp nhưng đồng thời việc thực hiện phần đó sẽ nhanh hơn phần có mức độ thấp hơn. JIT - Trình biên dịch quyết định mức độ biên dịch nào sẽ được đặt cho từng đoạn chương trình dựa trên tần suất thực thi đoạn đó. Bên trong, JVM sử dụng 2 trình biên dịch JIT - C1 và C2. Trình biên dịch C1 còn được gọi là trình biên dịch máy khách và chỉ có khả năng biên dịch mã ở cấp độ thứ 3. Trình biên dịch C2 chịu trách nhiệm về mức độ biên dịch thứ 4, phức tạp nhất và nhanh nhất.
Biên dịch và thực thi các ứng dụng Java một cách chuyên sâu - 3
Từ những điều trên, chúng ta có thể kết luận rằng đối với các ứng dụng khách đơn giản, việc sử dụng trình biên dịch C1 sẽ có lợi hơn, vì trong trường hợp này, điều quan trọng đối với chúng ta là ứng dụng khởi động nhanh như thế nào. Các ứng dụng tồn tại lâu dài, phía máy chủ có thể mất nhiều thời gian hơn để khởi động, nhưng trong tương lai chúng phải hoạt động và thực hiện chức năng của mình một cách nhanh chóng - ở đây trình biên dịch C2 phù hợp với chúng ta. Khi chạy chương trình Java trên phiên bản x32 của JVM, chúng ta có thể chỉ định thủ công chế độ nào chúng ta muốn sử dụng bằng cách sử dụng cờ -client-server. Khi cờ này được chỉ định, -clientJVM sẽ không thực hiện tối ưu hóa mã byte phức tạp, điều này sẽ tăng tốc thời gian khởi động ứng dụng và giảm lượng bộ nhớ tiêu thụ. Khi chỉ định cờ, -serverứng dụng sẽ khởi động lâu hơn do tối ưu hóa mã byte phức tạp và sẽ sử dụng nhiều bộ nhớ hơn để lưu trữ mã máy, nhưng chương trình sẽ chạy nhanh hơn trong tương lai. Trong phiên bản x64 của JVM, cờ này -clientbị bỏ qua và cấu hình máy chủ ứng dụng được sử dụng theo mặc định.

6. Kết luận

Điều này kết thúc phần tổng quan ngắn gọn của tôi về cách thức hoạt động của việc biên dịch và thực thi một ứng dụng Java. Ý chính:
  1. Trình biên dịch javac chuyển đổi mã nguồn của chương trình thành mã byte có thể được thực thi trên bất kỳ nền tảng nào có cài đặt máy ảo Java;
  2. Sau khi biên dịch, JVM diễn giải mã byte kết quả;
  3. Để tăng tốc các ứng dụng Java, JVM sử dụng cơ chế biên dịch Just-In-Time để chuyển đổi các phần được thực thi thường xuyên nhất của chương trình thành mã máy và lưu trữ chúng trong bộ nhớ.
Tôi hy vọng bài viết này đã giúp bạn hiểu sâu hơn về cách hoạt động của ngôn ngữ lập trình yêu thích của chúng tôi. Cảm ơn đã đọc, những lời chỉ trích được chào đón!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION