Java Foundations
7/39
Bài 7 / 39~13 phútNhập môn & Tư duy lập trìnhLộ trình · chặng 02/21Miễn phí… lượt xem

Compile & Run — javac, bytecode, và vòng đời một chương trình Java

Hiểu hai lệnh javac và java, cơ chế bytecode stack-based, đọc bytecode bằng javap, single-file execution từ Java 11, và classpath cơ bản.

TL;DR: Khi bạn gõ javac Add.java, Java compiler dịch source code sang bytecode lưu trong Add.class. Khi gõ java Add, JVM nạp bytecode đó, xác minh an toàn qua Bytecode Verifier, rồi thực thi qua Interpreter hoặc JIT. Bytecode là tập lệnh stack-based — mọi tính toán đi qua một operand stack vô danh thay vì register có tên. Lệnh javap -c Add cho bạn nhìn thẳng vào các instruction như iload_0, iadd, ireturn. Từ Java 11, java Add.java compile in-memory và chạy luôn — tiện cho script nhỏ.

Bạn đã viết Hello World. Bạn đã gõ javac và java. Nhưng điều gì thực sự xảy ra giữa hai lệnh đó? Bytecode là gì? Tại sao nó lại là "superpower" của Java?

Bài này đi từ analogy đơn giản đến cấu trúc bytecode thực tế — để bạn hiểu chứ không chỉ thuộc lòng.

Vì sao công thức nấu ăn quốc tế là ẩn dụ chuẩn xác cho Bytecode?

Hình dung bạn viết công thức nấu phở bằng tiếng Việt rất ngon, nhưng đầu bếp Pháp, Nhật hay Mỹ không đọc được.

Giải pháp: dịch công thức sang tiếng Anh chuẩn quốc tế một lần duy nhất. Mọi đầu bếp toàn cầu đều hiểu và dùng chính gian bếp của họ (bếp gas, bếp từ) để nấu ra món ăn đồng nhất.

Nấu ănJava
Công thức tiếng ViệtSource code .java (con người viết)
Dịch sang tiếng Anh chuẩnjavac biên dịch sang bytecode .class
Bản tiếng Anh quốc tếBytecode trung lập không phụ thuộc hệ điều hành
Đầu bếp từng quốc giaJVM trên Windows, macOS, Linux
Gian bếp của mỗi đầu bếpCPU native của từng máy

Javac và java đảm nhận những vai trò gì trong vòng đời chương trình?

javac — biên dịch source code sang bytecode

Bash
javac Add.java
  • Input: Add.java (mã nguồn con người đọc được).
  • Output: Add.class (bytecode dành cho JVM).
  • Nếu có lỗi cú pháp, javac dừng lại ngay và không sinh file .class.

java — nạp và thực thi bytecode trên JVM

Bash
java Add
  • Input: Tên class (không kèm đuôi .class).
  • Thực thi: JVM tìm Add.class trong classpath, nạp vào bộ nhớ và kích hoạt hàm main.
LệnhMục đíchInputOutputThời điểm dùng
javacCompile source code.java.class (bytecode)Sau mỗi lần sửa mã nguồn
javaChạy chương trìnhTên classKết quả thực thiKhi đã có bytecode
javapDisassemble bytecode.classMã mnemonicKhi phân tích hiệu năng và debug

Vòng đời thực thi của một chương trình Java diễn ra như thế nào?

Hai pha tách rời: javac chạy một lần sinh ra file bytecode, sau đó mỗi lần chạy java nạp file đó qua ClassLoader, Verifier và Execution Engine; javap đọc chính file bytecode ấy

  1. Source code (Add.java): Lập trình viên soạn thảo logic nghiệp vụ.
  2. Biên dịch (javac): Tạo file bytecode Add.class.
  3. ClassLoader: Nạp các class cần thiết vào bộ nhớ RAM của JVM.
  4. Bytecode Verifier: Kiểm định tính hợp lệ của stack và kiểu dữ liệu trước khi chạy.
  5. Execution Engine: Thông dịch (Interpreter) hoặc JIT-compile thành mã máy CPU.

Single-file execution từ Java 11 hoạt động ra sao và dùng khi nào?

Từ Java 11 (JEP 330), bạn có thể chạy trực tiếp một file .java độc lập:

Bash
java Add.java

JVM sẽ biên dịch file mã nguồn trực tiếp trong RAM và thực thi ngay lập tức mà không ghi file .class ra ổ đĩa. Tính năng này lý tưởng cho script tự động hóa nhỏ hoặc thử nghiệm nhanh một file.

Classpath là gì và JVM tìm kiếm file class như thế nào?

Classpath là danh sách các đường dẫn mà JVM duyệt qua để tìm nạp các file .class. Mặc định classpath trỏ vào thư mục hiện tại (.):

Bash
# Specify directory containing bytecode
java -cp build/classes Add

# Include external jar library
java -cp build/classes:libs/commons-lang3.jar com.example.Main

Nếu một class thuộc package com.example.Add, bạn bắt buộc phải gọi bằng tên đầy đủ (fully qualified class name): java -cp build/classes com.example.Add.

Làm thế nào để compile, chạy và giải mã bytecode bằng javap?

Lấy lại chương trình Hello World từ Bài 04 làm quen — lần này ta sẽ nhìn vào bên trong bytecode của nó. Tạo file Add.java:

Java
// Add.java
public class Add {
    public static int cong(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        int ketQua = cong(3, 5);
        System.out.println("3 + 5 = " + ketQua);
    }
}

Compile và chạy:

Bash
javac Add.java
java Add
# Output: 3 + 5 = 8

Đọc bytecode bằng javap:

Thu doan truoc

Trước khi chạy javap -c Add, hãy đoán: method cong(int a, int b) làm phép cộng a + b. JVM bytecode stack-based sẽ làm gì? Bạn nghĩ cần bao nhiêu instruction? Chúng trông như thế nào? Ghi đoán của bạn xuống rồi mới đọc tiếp.

Bash
javap -c Add

Output (rút gọn — chỉ phần method cong):

public static int cong(int, int);
  Code:
     0: iload_0       // day bien a len stack (int load tu slot 0)
     1: iload_1       // day bien b len stack (int load tu slot 1)
     2: iadd          // lay 2 gia tri tren stack, cong lai, day ket qua len stack
     3: ireturn       // lay gia tri tren stack, tra ve cho caller

6.1 Giải thích từng instruction

InstructionÝ nghĩa
iload_0integer load — đẩy giá trị của biến local thứ 0 (a) lên operand stack
iload_1Đẩy giá trị biến local thứ 1 (b) lên stack
iaddinteger add — lấy 2 giá trị trên stack, cộng, đẩy kết quả lên stack
ireturninteger return — lấy kết quả trên stack, trả về caller

6.2 Bytecode là stack-based, không phải register-based

CPU vật lý (x86, ARM) dùng kiến trúc register-based: tính toán thông qua các "ngăn" có tên cố định (EAX, EBX, R0...).

JVM bytecode dùng kiến trúc stack-based: tính toán thông qua một ngăn xếp vô danh. Thay vì "cộng giá trị trong register EAX với EBX", JVM làm:

  1. Đẩy giá trị a lên stack
  2. Đẩy giá trị b lên stack
  3. iadd lấy 2 giá trị trên stack, cộng, đẩy kết quả lên stack

Vì sao stack-based? Đơn giản hơn, dễ verify (Bytecode Verifier kiểm tra stack state tại compile time), và portable hơn — không cần biết CPU đích có bao nhiêu register.

Cach nho stack-based

Stack bytecode giống máy tính RPN (Reverse Polish Notation — kiểu Casio cũ): bạn nhập 3 ENTER 5 ENTER + thay vì 3 + 5 =. Mọi tính toán đều qua stack trung gian. Không có "hộp đựng" có tên — chỉ có "đỉnh stack".

Thử ngẫmbytecode stack-based để dễ verify và portable, nhưng CPU thật lại register-based. Phần "quy đổi" đó xảy ra ở đâu trong chuỗi javac → JVM → CPU, và vì sao Java chấp nhận trả giá thêm một tầng?

Tại sao kiến trúc bytecode lại đóng vai trò quyết định với Java?

7.1 Portable

Bytecode không phụ thuộc vào Windows, macOS hay Linux. JVM trên mỗi OS dịch bytecode thành machine code native của OS đó. Bạn compile một lần, file .class chạy được mọi nơi có JVM.

7.2 Verifiable — an toàn trước khi chạy

Trước khi execute, Bytecode Verifier của JVM kiểm tra:

  • Không có type violation (ví dụ: dùng integer như pointer)
  • Không có stack underflow / overflow
  • Không có illegal type cast

Đây là lý do Java an toàn hơn C/C++: ngay cả khi ai đó tạo bytecode giả mạo, JVM từ chối thực thi trước khi có cơ hội gây hại.

7.3 Optimizable — JIT compile hot code

JVM không chỉ "đọc" bytecode. Nó theo dõi "hot methods" — method được gọi nhiều lần — và JIT-compile thành native machine code của CPU hiện tại. Kết quả: code Java chạy lâu có thể đạt tốc độ gần bằng C++.

Hệ sinh thái các ngôn ngữ chạy trên JVM mang lại sức mạnh gì?

Bytecode không phải đặc quyền của Java. Nhiều ngôn ngữ khác cũng compile về JVM bytecode — và vì thế interop được với nhau:

Ngôn ngữMô tảĐiểm nổi bật
KotlinJetBrains phát triển, concise JavaAndroid first-class, null-safe, coroutines
ScalaFunctional + OOP hybridApache Spark, distributed systems
GroovyDynamic scripting trên JVMGradle build scripts, Spock testing
ClojureLisp dialect trên JVMImmutable data, concurrency

Vì sao đây là superpower?

Tất cả các ngôn ngữ trên đều dùng được mọi thư viện Java (500,000+ trên Maven Central) mà không cần wrapper hay binding. Một project có thể có code Java + Kotlin + Groovy trong cùng classpath — chúng gọi nhau trực tiếp vì cùng là bytecode.

KOTLIN
// Kotlin code calling Java library directly
import java.util.ArrayList  // Java standard library

fun main() {
    val list = ArrayList<String>()  // Java class, dung tu Kotlin
    list.add("Hello from Kotlin")
    println(list[0])
}

Khi project Kotlin được compile, ArrayList.class của Java và code Kotlin đều là bytecode — JVM không phân biệt.

Cach nho JVM ecosystem

JVM bytecode giống chuẩn USB-C: nhiều thiết bị (ngôn ngữ) khác nhau cùng dùng một cổng (bytecode). Thiết bị nào tương thích chuẩn đó đều cắm được vào nhau — không cần adapter.

Thử ngẫmteam bạn có 100.000 dòng Java và muốn thử Kotlin. Nhờ cùng bytecode, bạn viết được file Kotlin đầu tiên ngay cạnh code Java mà không sửa gì chứ? Mắt xích nào trong chuỗi build buộc phải đổi?

Tài liệu chuyên sâu nào giúp đào sâu kiến trúc JVM và bytecode?

Deep Dive Oracle (optional)

Spec chính thức:

Ghi chú đọc spec: JVM Spec §2.6 "Frames" giải thích cấu trúc của một stack frame: local variable array + operand stack + constant pool reference. Mỗi method call tạo một frame mới — đây là lý do StackOverflowError xảy ra khi recursion quá sâu.

Bài học này kết nối như thế nào với lộ trình lập trình Java?

  • Java là gì — Bài 01: Bài 01 giải thích tại sao Java cần tầng bytecode trung gian (write once, run anywhere) — bài này là phần "nhìn thấy bytecode thật" của lời hứa đó.
  • Hello World — Bài 04: Chương trình đầu tiên bạn compile + chạy. Bài này giải thích những gì thực sự xảy ra bên dưới hai lệnh javac/java mà bài 04 chỉ giới thiệu bề mặt.
  • Mini-challenge: Lịch tháng — Bài 07: Bài thực hành đầu tiên — bạn áp dụng toàn bộ vòng đời compile-run để xây chương trình thực.
  • Từ code bậc cao tới lệnh máy — khoá Máy tính chạy thế nào: bytecode chỉ là trạm trung gian — bài này đi tiếp xuống tầng dưới, xem trình thông dịch/JIT biến bytecode thành lệnh máy thật mà CPU chạy.

Những điểm cốt lõi cần nhớ về compile và bytecode là gì?

  • javac Foo.java → Foo.class (bytecode). java Foo → JVM load và chạy. Bytecode là tập lệnh stack-based, portable qua mọi OS, verifiable và optimizable bởi JIT.
  • javap -c Foo.class — đọc bytecode instruction (iload, iadd, invokevirtual...); công cụ thiết yếu để hiểu JVM thực thi gì.
  • Single-file execution (java Foo.java, Java 11+): compile in-memory, không tạo .class — tiện cho script, không dùng cho project nhiều file.
  • Kotlin, Scala, Groovy, Clojure đều compile về bytecode → interop trực tiếp với Java không cần wrapper — lý do JVM ecosystem rộng hơn chỉ Java.

12. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Khi javac Add.java chạy xong, file gì được tạo ra? Nội dung của nó là gì và ai đọc được nó?
  2. Q2
    Lệnh java Add.class báo lỗi, còn java Add thì không. Tại sao?
  3. Q3
    Bytecode JVM là "stack-based" nghĩa là gì? Khác gì với kiến trúc "register-based" của x86?
  4. Q4
    Bạn chạy java Foo.java và code hoạt động, nhưng không thấy file Foo.class nào được tạo ra. Tại sao?
  5. Q5
    Vì sao Kotlin có thể dùng trực tiếp thư viện Java (như ArrayList) mà không cần wrapper? Cơ chế nào cho phép điều này?

Bài tiếp theo: Mini-challenge: Viết chương trình in lịch tháng

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 2/21 · Lập trình từ số 0 với Java — bài 7/32 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Mini-challenge: In lịch tháng hiện tại