Java — Từ Zero đến Senior/Nhập môn & Tư duy lập trình/Compile & Run — javac, bytecode, và vòng đời một chương trình Java
6/7
~16 phútNhập môn & Tư duy lập trình

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.

Bạn đã viết Hello World. Bạn đã gõ javacjava. 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.

1. Analogy — "Công thức nấu ăn quốc tế"

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

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. Giờ đây mọi đầu bếp trên thế giới đều đọc được. Mỗi đầu bếp sẽ dùng bếp của họ (bếp than, bếp gas, bếp điện từ) để nấu — nhưng món ăn ra là giống nhau.

Ánh xạ sang Java:

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 compile → bytecode .class
Tiếng Anh chuẩn quốc tếBytecode (không phụ thuộc OS)
Đầu bếp ở từng quốc giaJVM trên Windows, macOS, Linux
Bếp của từng đầu bếpCPU native của mỗi máy

💡 💡 Cách nhớ

Bytecode giống như công thức tiếng Anh quốc tế: bạn dịch một lần, mọi đầu bếp (JVM) ở mọi nơi đều thực hiện được trên bếp của họ (CPU). Bạn không cần viết lại công thức cho từng quốc gia.

2. Hai lệnh phải biết

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

javac Add.java
  • Input: Add.java — source code bạn viết, con người đọc được
  • Output: Add.class — bytecode, JVM đọc được
  • Nếu có lỗi syntax: javac báo lỗi ngay, không tạo .class

Ví dụ:

# Truoc khi compile
ls
# Add.java

javac Add.java

# Sau khi compile
ls
# Add.java   Add.class

2.2 java — chạy bytecode trên JVM

java Add
  • Input: tên class (không có đuôi .class)
  • Hành động: JVM load Add.class, tìm method main, bắt đầu thực thi
  • Lưu ý: Bạn truyền tên class, không phải tên file. java Add.class là sai.
# Sai
java Add.class   # Error: Could not find or load main class Add.class

# Dung
java Add         # JVM tu tim Add.class trong classpath hien tai

2.3 Bảng tóm tắt: javac vs java vs javap

ToolMục đíchInputOutputDùng khi
javacCompile source code.java.class (bytecode)Trước khi chạy lần đầu, sau mỗi lần sửa code
javaChạy chương trìnhTên class (.class phải có sẵn)Kết quả thực thiSau khi compile xong
javapDisassemble bytecode.classBytecode dạng textDebug, học bytecode, kiểm tra compiler tạo gì

3. Sơ đồ vòng đời một chương trình Java

flowchart LR
  src["Add.java<br/>(source code)"]
  javac_tool["javac<br/>(compiler)"]
  cls["Add.class<br/>(bytecode)"]

  subgraph jvm_box["JVM"]
    cl["ClassLoader<br/>Load class vao memory"]
    bv["Bytecode Verifier<br/>Kiem tra an toan"]
    subgraph ee["Execution Engine"]
      interp["Interpreter<br/>(lan dau chay)"]
      jit["JIT Compiler<br/>(hot code -> native)"]
    end
  end

  result["Ket qua<br/>(console output)"]

  src --> javac_tool --> cls --> cl --> bv --> ee --> result
  1. Add.java — bạn viết source code
  2. javac — compile thành Add.class (bytecode)
  3. ClassLoader — load .class vào JVM memory
  4. Bytecode Verifier — kiểm tra bytecode an toàn trước khi chạy
  5. Execution Engine — interpret hoặc JIT-compile thành native code, thực thi

4. Single-file execution từ Java 11+

Từ Java 11 (JEP 330), bạn có thể chạy thẳng file .java mà không cần javac thủ công:

# Truoc Java 11 -- 2 buoc bat buoc
javac Add.java
java Add

# Tu Java 11 -- 1 buoc
java Add.java

Cơ chế bên trong: JVM tự gọi compiler trong bộ nhớ, tạo bytecode tạm thời (không lưu file .class ra disk), rồi thực thi ngay.

✅/❌ Khi nào dùng single-file execution?

Dùng khi:

  • ✅ Script nhỏ, automation đơn giản — chạy nhanh mà không cần build step
  • ✅ Demo, học tập, proof-of-concept 1 file
  • ✅ Shebang script trên Unix: #!/usr/bin/java --source 21 ở đầu file

Không dùng khi:

  • ❌ Project có nhiều file Java phụ thuộc nhau — single-file mode chỉ compile 1 file duy nhất
  • ❌ Cần dependency từ file .jar bên ngoài — phải dùng classpath thủ công
  • ❌ CI/CD build — cần javac + jar để tạo artifact
  • ❌ App production — cần build system (Maven, Gradle) để quản lý dependency

💡 💡 Quy tắc thực tế

Dùng java Foo.java khi bạn muốn chạy nhanh một script. Khi project có hơn 1 file Java hoặc cần thư viện bên ngoài, dùng javac + build tool.

5. Classpath — JVM tìm class ở đâu?

Khi bạn chạy java Add, JVM phải tìm file Add.class. Nó tìm ở đâu? Trong classpath.

Mặc định: classpath là . (thư mục hiện tại). Đó là lý do java Add hoạt động khi bạn đứng trong cùng thư mục với Add.class.

5.1 Chỉ định classpath thủ công

# -cp hoac -classpath
java -cp build/classes Add

# Nhieu directory: ngan cach nhau bang : (Unix) hoac ; (Windows)
java -cp build/classes:src/main Add

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

5.2 Fully qualified class name với package

Nếu class có package com.example.Add:

# Compile
javac -d build/classes src/com/example/Add.java

# Chay (fully qualified name)
java -cp build/classes com.example.Add

💡 💡 Cách nhớ

Classpath là địa chỉ nhà của JVM: "tôi sẽ tìm class trong những thư mục này". Thiếu classpath đúng → JVM không tìm thấy class → ClassNotFoundException. Trong project thực, Maven/Gradle quản lý classpath tự động — bạn không cần gõ tay.

6. Ví dụ thực tế: compile, chạy, và đọc bytecode

Tạo file Add.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:

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

Đọc bytecode bằng javap:

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.

💡 💡 Cách nhớ 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".

7. Tại sao bytecode quan trọng?

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++.

8. Superpower: các JVM language khác

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
KotlinGoogle-backed, 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
KotlinCompiles to JVM bytecodeDùng mọi Java library trực tiếp

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 code -- dung truc tiep Java library
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.

💡 💡 Cách nhớ 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.

9. 📚 Deep Dive Oracle

ℹ️ 📚 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.

10. Tóm tắt

  • javac Foo.java → tạo Foo.class (bytecode). java Foo → JVM load .class và chạy.
  • Bytecode là tập lệnh stack-based, không phụ thuộc OS — JVM của mỗi nền tảng dịch tiếp thành native code.
  • javap -c Foo.class — đọc bytecode: iload, iadd, ireturn... là các instruction của JVM virtual CPU.
  • Single-file execution (java Foo.java từ Java 11): compile in-memory, không tạo .class. Dùng cho script nhỏ, không dùng cho project nhiều file.
  • Classpath (-cp) — nơi JVM tìm file .class. Mặc định là . (thư mục hiện tại).
  • Bytecode quan trọng vì: portable, verifiable (Bytecode Verifier), optimizable (JIT), và là nền tảng của toàn bộ JVM ecosystem.
  • Kotlin, Scala, Groovy, Clojure đều compile về bytecode → interop trực tiếp với Java không cần wrapper.

11. Tự kiểm tra

  1. 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. Lệnh java Add.class báo lỗi, còn java Add thì không. Tại sao?
  3. Bytecode JVM là "stack-based" nghĩa là gì? Khác gì với kiến trúc "register-based" của x86?
  4. 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. 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