Command Palette

Search for a command to run...

[Java Basics] JDK, JRE, JVM là gì? Phân biệt và cách cài đặt môi trường Java

Ba từ viết tắt này đứng chắn giữa bạn và chương trình Java đầu tiên được compile thành công, và gần như mọi tutorial đều định nghĩa chúng bằng đúng một dòng rồi đi tiếp. Dòng đó thường cũng đã lỗi thời: nó mô tả một bản JRE mà bạn không còn tải được, và một cấu trúc thư mục đã biến mất từ năm 2017.

Bài này đưa ra định nghĩa chính xác, cho bạn thấy bên trong JDK thực sự có gì, rồi cài Java từ đầu trên macOS, Windows và Linux. Mọi command, listing và error message bên dưới đều được chạy thật trên OpenJDK 21.0.6 (bản Homebrew, macOS arm64) và copy nguyên văn từ terminal.

JDK, JRE và JVM

Tóm tắt trước: JVM chạy bytecode, JRE là JVM cộng class library, JDK là JRE cộng compiler — và JDK là thứ duy nhất trong ba cái bạn nên cài.

JDK, JRE và JVM: mỗi thứ thực sự là gì

JVM: một specification, và process chạy bytecode

JVM cùng lúc là hai thứ, và đó chính là nguồn gốc của phần lớn sự nhầm lẫn.

Nó là một specification: The Java Virtual Machine Specification, tài liệu định nghĩa format của class file, tập lệnh bytecode, type system và memory model. Ai cũng có quyền implement nó, và nhiều bên đã làm — HotSpot (bản implementation nằm trong OpenJDK), Eclipse OpenJ9, GraalVM, Azul Zing.

Nó cũng là một process. Khi bạn gõ java Hello, hệ điều hành khởi động một process; bên trong process đó, một bản implementation của specification kia load class của bạn, kiểm tra rồi chạy nó. java là launcher; JVM là thứ mà launcher khởi động. Không có file binary nào tên "jvm" nằm riêng trên ổ đĩa của bạn.

Hai hệ quả đáng nhớ:

  • JVM không chạy Java. Nó chạy class file. Kotlin, Scala, Groovy và Clojure compile ra cùng loại bytecode và chạy trên cùng JVM, nên chúng gọi thẳng được thư viện Java.
  • Bạn không bao giờ cài riêng một JVM. Nó nằm sẵn trong runtime, và runtime nằm sẵn trong JDK.

Các thành phần chính của một bản implementation JVM gồm class loader, execution engine (một interpreter cộng một JIT compiler), các vùng memory mà nó quản lý (heap, stack của từng thread, metaspace) và garbage collector. Từng thành phần làm gì trong lúc chương trình chạy là một chủ đề riêng; ở đây, JVM đơn giản là lớp thực thi bytecode.

JRE: JVM cộng standard class library

Java Runtime Environment là JVM cộng thêm mọi thứ một chương trình đã compile cần khi chạy:

  • standard class library — java.lang, java.util, java.io, java.net, java.time và phần còn lại
  • launcher java
  • các file phụ trợ: CA trust store, dữ liệu locale, config trong conf/

Bấy nhiêu là đủ để run một file .class hoặc một file .jar. Nhưng không đủ để compile bất cứ thứ gì, vì trong JRE không có javac.

JDK: JRE cộng bộ tool để compile

Java Development Kit là JRE cộng bộ tool phát triển: compiler, disassembler, trình sinh tài liệu, trình đóng gói archive, REPL, các tool tạo installer.

Quan hệ bao hàm là chặt chẽ và nên nhớ gọn trong một câu: JDK chứa JRE, và JRE chứa JVM. Mỗi lớp thêm khả năng cho lớp nằm bên trong nó.

Mỗi lớp thêm vào những gì: JDK chứa JRE, JRE chứa JVM

Quan hệ đó không chỉ là khái niệm. Hãy dựng một runtime image chứa toàn bộ standard library rồi nhìn vào thư mục bin của nó:

jlink --add-modules java.se --output jre-like
ls jre-like/bin
java
jrunscript
keytool
rmiregistry

Bốn file thực thi. Bản JDK mà nó được cắt ra có hai mươi chín. Hai mươi lăm cái chênh lệch chính là chữ "D" trong JDK.

Vì sao không còn tải được bản JRE riêng

Đến Java 8 thì mọi thứ đúng y như tutorial mô tả: Oracle phát hành một bộ cài JRE riêng cho người dùng cuối, và bản JDK chứa một thư mục con jre/ giữ đúng phần runtime của chính nó.

Java 9 chấm dứt cả hai. JEP 220 tổ chức lại runtime image quanh module system, và thư mục jre/ lồng bên trong biến mất. Nhìn vào một bản JDK hiện đại:

ls "$JAVA_HOME"
bin
conf
demo
include
jmods
legal
lib
man
release

Không có jre. Runtime và tool giờ nằm chung trong một image, chia theo module chứ không chia theo thư mục. Từ Java 11 trở đi Oracle ngừng phát hành bản JRE riêng để tải.

Thứ thay thế nó là jlink, tool lắp ráp một runtime image tùy biến từ đúng những module mà ứng dụng cần:

jlink --add-modules java.base --strip-debug --no-header-files --no-man-pages --output myruntime
ls myruntime/bin
du -sh myruntime jre-like "$JAVA_HOME"
java
keytool
 41M	myruntime
 90M	jre-like
331M	/opt/homebrew/Cellar/openjdk@21/21.0.6/libexec/openjdk.jdk/Contents/Home

Ba image từ một bản cài: runtime tối thiểu 41 MB chỉ có java.base, image 90 MB có đủ standard library — đúng bằng cái mà JRE vẫn luôn là — và JDK 331 MB. Chỉ image cuối cùng có javac.

Có một điểm cần nói cho sòng phẳng, vì câu "JRE không còn tồn tại" là quá mạnh. Một số nhà phát hành vẫn ra bản runtime dưới tên đó: Eclipse Temurin vẫn có image type jre bên cạnh JDK cho Java 21, còn Debian và Ubuntu vẫn đóng gói openjdk-21-jre-headless. Đó là các runtime image dựng bằng jlink từ chính source của JDK, chỉ mang cái tên quen thuộc — không phải bản JRE upstream của Oracle. Chúng tồn tại cho server và container muốn image nhỏ.

Còn để học và để phát triển, quy tắc rất gọn: cài JDK. JRE không compile được, mà bạn thì sắp compile.

Bên trong thư mục bin của JDK có gì

ls "$JAVA_HOME/bin"
jar             jcmd            jhsdb           jpackage        jstatd
jarsigner       jconsole        jimage          jps             jwebserver
java            jdb             jinfo           jrunscript      keytool
javac           jdeprscan       jlink           jshell          rmiregistry
javadoc         jdeps           jmap            jstack          serialver
javap           jfr             jmod            jstat

Hai mươi chín chương trình. Bạn sẽ dùng thường xuyên khoảng sáu cái, số còn lại cứ bỏ qua cho tới khi thật sự cần.

ToolLàm gìKhi nào dùng
javacCompile source .java thành bytecode .classMọi lần build. Đây là thứ duy nhất JRE không làm được
javaKhởi động JVM và chạy một class hoặc một file .jarMọi lần chạy. Từ Java 11 nó chạy thẳng được một file .java
jshellREPL tương tác cho biểu thức JavaThử một API mà không cần viết class
jarTạo, liệt kê và giải nén archive .jarĐóng gói chương trình thành một file phân phối
javadocSinh tài liệu API dạng HTML từ comment /** ... */Xuất bản tài liệu cho library
javapDisassemble một class file: signature, constant pool, bytecodeKiểm tra xem compiler thực sự sinh ra cái gì
jlinkDựng runtime image rút gọn từ các module được chọnThu nhỏ container image; bản thay thế hiện đại cho việc ship JRE
jpackageGói ứng dụng cùng runtime thành installer native (.dmg, .msi, .deb)Ship app desktop cho người dùng không cài Java
jcmdGửi lệnh chẩn đoán tới một JVM đang chạy: thread dump, heap dump, flagDebug một process đang sống
jdepsBáo cáo các package và module mà một jar phụ thuộc vàoXác định cần truyền gì cho jlink

javap là cái bị người mới bỏ phí nhiều nhất. Nó là cách kết thúc mọi tranh cãi về việc compiler đã làm gì:

javap -v Hello.class | grep "major version"
  major version: 65

Con số đó sẽ quay lại ở phần xử lý lỗi.

Nên cài JDK distribution nào?

OpenJDK là dự án gốc — phần source code, phát triển công khai, cấp phép theo GPLv2 kèm Classpath Exception. Gần như không ai tải nó về dạng source. Thay vào đó, các nhà phát hành compile chính source đó, chạy bộ test tương thích, rồi phát hành binary.

DistributionNhà phát hànhLicenseGhi chú
OpenJDK reference buildOracle, tại jdk.java.netGPLv2 + Classpath ExceptionBinary gốc. Mỗi release chỉ được update tới release kế tiếp, tức sáu tháng
Eclipse TemurinEclipse AdoptiumGPLv2 + Classpath ExceptionMiễn phí cho mọi mục đích, có TCK, bản LTS được bảo trì nhiều năm. Lựa chọn mặc định
Amazon CorrettoAmazonGPLv2 + Classpath ExceptionMiễn phí, hỗ trợ dài hạn, là bản chạy trên AWS Lambda
Azul ZuluAzul SystemsGPLv2 + Classpath ExceptionBản build miễn phí; Azul bán riêng dịch vụ hỗ trợ và một JVM low-latency
Microsoft Build of OpenJDKMicrosoftGPLv2 + Classpath ExceptionMiễn phí; tích hợp với Azure và extension Java của VS Code
Oracle JDKOracleNFTCCùng source code, khác license. Đọc đoạn ngay dưới trước khi chọn

Điểm license này quan trọng và thường bị nói lấp lửng. Mọi dòng trừ dòng cuối đều là cùng một source OpenJDK dưới GPLv2 kèm Classpath Exception: tải miễn phí, chạy production miễn phí, miễn phí vĩnh viễn, không cần đăng ký và không có rủi ro bị audit. Classpath Exception chính là điều khoản ngăn GPL lan sang code của bạn.

Oracle JDK là đúng source đó nhưng phát hành theo No-Fee Terms and Conditions của Oracle. NFTC cho dùng miễn phí kể cả production, nhưng chỉ trong một khoảng thời gian: với một bản LTS, khoảng đó kéo dài tới một năm sau khi bản LTS kế tiếp ra mắt. Hết khoảng đó, muốn tiếp tục nhận security update thì phải mua Java SE Universal Subscription, mà từ tháng 1 năm 2023 Oracle tính giá theo đầu nhân viên — đếm toàn bộ nhân viên công ty, không phải chỉ developer. Không có cái bẫy nào bạn sập vào một cách vô tình ở đây, nhưng cũng chẳng có lý do gì để đi lại gần nó.

Hãy cài Eclipse Temurin. Nó là cùng một JDK tương thích bytecode, do một tổ chức trung lập bảo trì, và mọi ví dụ trong series này đều được kiểm chứng trên bản build OpenJDK.

Nên học Java phiên bản nào?

Từ Java 10, cứ sáu tháng lại có một release. Phần lớn trong số đó chỉ được hỗ trợ đúng sáu tháng. Một số ít được đánh dấu LTS và nhận security update trong nhiều năm, và đó là những bản mà cả hệ sinh thái quy chuẩn theo.

ReleaseRa mắtTình trạng
Java 803/2014LTS. Vẫn chạy rất nhiều hệ thống cũ. Đừng học trên bản này
Java 1109/2018LTS. Mốc chuẩn đầu tiên của thời kỳ module
Java 1709/2021LTS. Mốc chuẩn của Spring Boot 3 và hầu hết framework hiện đại
Java 2109/2023LTS. Virtual thread, record, pattern matching, sealed type
Java 2509/2025LTS. Bản dài hạn hiện hành

Hãy học trên Java 21. Nó có vùng giao lớn nhất giữa những gì tutorial giả định, những gì framework yêu cầu và những gì tin tuyển dụng ghi, đồng thời được hỗ trợ ở khắp nơi. Java 25 mới hơn và mọi ví dụ trong series này chạy nguyên xi trên đó; chọn nó nếu bạn thích dùng bản mới nhất. Đừng bắt đầu từ Java 8 — bạn sẽ học một ngôn ngữ thiếu mười lăm năm cú pháp, gồm cả var, record, text block và switch expression.

Nên cài JDK nào: cây quyết định từ hoàn cảnh của bạn tới một lựa chọn cụ thể

Ba câu hỏi là chốt xong. Mọi nhánh phía sau vẫn là cùng một bytecode.

Cài đặt JDK

macOS

Đường ngắn nhất là Homebrew:

brew install --cask temurin@21

Cask cài một file .pkg vào /Library/Java/JavaVirtualMachines/, đúng nơi macOS tìm JDK. Ngoài ra còn một formula, brew install openjdk@21, nhưng nó là keg-only và cài dưới /opt/homebrew; muốn macOS thấy được thì cần một symlink vào thư mục kia:

ls -l /Library/Java/JavaVirtualMachines/
lrwxr-xr-x  1 root  wheel  48 Feb  3  2025 openjdk-17.jdk -> /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk
lrwxr-xr-x  1 root  wheel  48 Jan 29  2025 openjdk-21.jdk -> /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk
lrwxr-xr-x  1 root  wheel  45 Jan 29  2025 openjdk.jdk -> /opt/homebrew/opt/openjdk/libexec/openjdk.jdk

Không dùng Homebrew thì tải file .pkg đúng kiến trúc từ adoptium.net — aarch64 cho máy Apple Silicon, x64 cho Mac Intel — rồi chạy. Cách nào cũng không cần làm thêm gì: /usr/bin/java trên macOS là một stub nhỏ, nó tự tìm JDK thật qua thư mục trên, nên mở terminal mới là java chạy được ngay.

Windows

Dùng package manager có sẵn:

winget install EclipseAdoptium.Temurin.21.JDK

Nếu dùng installer, tải file .msi từ adoptium.net và để ý đúng một màn hình. Bước Custom Setup liệt kê các tính năng tùy chọn, và hai trong số đó mặc định bị tắt:

  • Set JAVA_HOME variable
  • Add to PATH

Cả hai hiển thị "Entire feature will be unavailable" cho tới khi bạn bấm vào và chọn "Will be installed on local hard drive". Hãy bật cả hai. Bỏ qua cái thứ nhất là nguyên nhân phổ biến nhất khiến một máy Windows chạy được java trong terminal nhưng Maven, Gradle và một nửa số plugin của IDE vẫn khăng khăng là chưa cài Java.

Sau đó đóng terminal và mở cái mới. Windows đọc biến môi trường ngay lúc process khởi động, nên cửa sổ PowerShell đang mở sẽ không thấy thay đổi.

Linux

Debian và Ubuntu:

sudo apt update
sudo apt install openjdk-21-jdk

Lệnh này cài vào /usr/lib/jvm/java-21-openjdk-amd64. Chú ý tên package: còn có openjdk-21-jreopenjdk-21-jre-headless, cài một runtime không có compiler — đúng cái bẫy JRE-hay-JDK mà cả bài này đang nói tới.

Fedora và RHEL:

sudo dnf install java-21-openjdk-devel

Hậu tố -devel mới là thứ kéo javac về. Thiếu nó, java-21-openjdk chỉ cho bạn runtime.

Arch:

sudo pacman -S jdk21-openjdk

Package của distro thường chậm hơn upstream, và một số distro không đóng gói bản LTS mới nhất. Nếu apt chỉ có Java 17, hãy dùng SDKMAN thay vì vật lộn với package manager.

SDKMAN: cách gọn nhất để giữ nhiều version

SDKMAN cài JDK vào thư mục home của bạn và tự viết lại JAVA_HOME cùng PATH mỗi lần bạn chuyển version. Nó chạy trên macOS, Linux và WSL, và là tool nên dùng ngay khi bạn cần hai version cùng lúc.

curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk list java

Danh sách hiện ra mọi nhà phát hành kèm identifier cho từng bản build. Trên macOS/arm64, các dòng Temurin hiện tại là:

 Temurin        |     | 26.0.2             | 26.0.2-tem
                |     | 25.0.4             | 25.0.4-tem
                |     | 21.0.12+1.1        | 21.0.12+1.1-tem
                |     | 17.0.20            | 17.0.20-tem
                |     | 11.0.32            | 11.0.32-tem

Cài theo identifier, rồi chọn phạm vi áp dụng:

sdk install java 21.0.12+1.1-tem
sdk use java 21.0.12+1.1-tem       # chỉ shell hiện tại
sdk default java 21.0.12+1.1-tem   # mọi shell mở sau này
sdk current java

sdk install java không kèm identifier sẽ cài bản mặc định của nhà phát hành, hiện là bản Temurin LTS mới nhất. Chạy sdk env init trong một project sẽ tạo file .sdkmanrc, và từ đó sdk env chuyển sang đúng JDK của project mỗi khi bạn vào thư mục đó.

JAVA_HOME và PATH dùng để làm gì

Hai biến này không thay thế cho nhau, và nhầm lẫn giữa chúng sinh ra đúng một loại bug: java -version chạy ngon nhưng build tool khăng khăng là không tìm thấy Java.

BiếnTrỏ tớiTrả lời câu hỏiAi đọc
PATHDanh sách thư mục chứa file thực thi"Gõ java thì chương trình nào chạy?"Shell
JAVA_HOMEThư mục gốc của một bản cài JDK — thư mục chứa bin, lib, conf"Tool này nên dùng JDK nào?"Maven, Gradle, Ant, Tomcat, run configuration của IDE, hầu hết script .sh

PATH quyết định java nào chạy; JAVA_HOME quyết định build tool dùng JDK nào

Hai đường tra cứu này độc lập với nhau, và đó chính là lý do chúng lệch nhau.

Một tool cần JAVA_HOME thay vì PATH khi nó cần nhiều hơn cái launcher. Gradle phải tìm được javac và các module descriptor của JDK; một application server phải khóa cứng vào đúng một JDK bất kể thứ gì đang đứng đầu PATH. Đưa thẳng một thư mục là cách xác định rõ ràng, còn dò theo PATH thì không.

Vậy nên hãy set JAVA_HOME trước rồi suy ra PATH từ nó. Như thế hai biến không thể mâu thuẫn.

zsh và bash

Trên macOS, zsh là shell mặc định từ Catalina, và /usr/libexec/java_home chuyển một số version thành đường dẫn:

# ~/.zshrc
export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
export PATH="$JAVA_HOME/bin:$PATH"

Trên Linux không có java_home, nên ghi thẳng thư mục:

# ~/.bashrc
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH="$JAVA_HOME/bin:$PATH"

Sau đó chạy source ~/.zshrc — hoặc mở terminal mới — vì sửa file khởi động không tác động gì tới shell đang chạy dở.

Đặt $JAVA_HOME/binđầu PATH. Nối vào cuối nghĩa là một JDK hệ thống nằm trước trong danh sách vẫn tiếp tục thắng, và đó là loại bug rất lâu mới tìm ra.

Có một chi tiết riêng của macOS đáng biết ở đây. /usr/bin/java không phải một JDK thật: nó là stub đi tra bản JDK đã cài, và nó tôn trọng JAVA_HOME khi biến này trỏ tới một JDK hợp lệ.

JAVA_HOME=$(/usr/libexec/java_home -v 17) /usr/bin/java -version
openjdk version "17.0.14" 2025-01-21

Biến môi trường trên Windows

Cách chắc chắn nhất là dùng GUI. Bấm Win+R, chạy sysdm.cpl, vào Advanced → Environment Variables, rồi trong phần User variables:

  1. New… — tên JAVA_HOME, giá trị là thư mục cài, ví dụ C:\Program Files\Eclipse Adoptium\jdk-21.0.12.1-hotspot. Phải là thư mục chứa bin, không phải chính bin.
  2. Chọn Path → Edit… → New, rồi thêm %JAVA_HOME%\bin.

GUI lưu %JAVA_HOME%\bin dưới dạng giá trị có thể mở rộng, nên sau này đổi JAVA_HOMEPATH đi theo luôn.

Từ terminal, setx ghi vào cùng chỗ trong registry:

setx JAVA_HOME "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.1-hotspot"

⚠️ Đừng dùng setx PATH "%JAVA_HOME%\bin;%PATH%". setx mở rộng giá trị trước khi lưu, làm PATH của bạn bị bẹp thành chuỗi cố định, và nó cắt âm thầm mọi ký tự vượt quá 1024. Hãy sửa Path bằng GUI.

Cách nào thì sau đó cũng phải mở terminal mới.

Kiểm tra lại bản cài đặt

Bốn lệnh, theo đúng thứ tự này.

java -version
openjdk version "21.0.6" 2025-01-21
OpenJDK Runtime Environment Homebrew (build 21.0.6)
OpenJDK 64-Bit Server VM Homebrew (build 21.0.6, mixed mode, sharing)
javac -version
javac 21.0.6

Nếu lệnh thứ hai lỗi, bạn đang có runtime chứ không phải JDK. Hai số version cũng phải khớp nhau, vì lý do mà phần xử lý lỗi sẽ minh họa.

echo "$JAVA_HOME"
/opt/homebrew/Cellar/openjdk@21/21.0.6/libexec/openjdk.jdk/Contents/Home

Ra dòng trống thì bản thân java vẫn chạy, nhưng Maven và Gradle sẽ hỏng về sau.

Có một điểm lắt léo trong java -version hay làm khổ người viết script cài đặt: nó in ra stderr, không phải stdout. Pipe sang grep sẽ ra rỗng mà không báo gì. Từ Java 9 có dạng hai gạch ngang in ra stdout:

java -version 2>/dev/null      # không in ra gì cả
java --version                 # in ra stdout
openjdk 21.0.6 2025-01-21
OpenJDK Runtime Environment Homebrew (build 21.0.6)
OpenJDK 64-Bit Server VM Homebrew (build 21.0.6, mixed mode, sharing)

Cuối cùng, compile và chạy thử một chương trình. Lưu đoạn này thành Hello.java:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Java " + System.getProperty("java.version"));
        System.out.println("Vendor: " + System.getProperty("java.vendor"));
        System.out.println("Home: " + System.getProperty("java.home"));
    }
}
javac Hello.java
java Hello
Java 21.0.6
Vendor: Homebrew
Home: /opt/homebrew/Cellar/openjdk@21/21.0.6/libexec/openjdk.jdk/Contents/Home

javac không in gì ra tức là thành công — nó đã ghi file Hello.class ngay cạnh source. Lưu ý java Hello nhận tên class, không kèm đuôi .class.

Từ Java 11 bạn có thể bỏ hẳn bước compile với file đơn lẻ, rất tiện để kiểm tra nhanh:

java Hello.java
Java 21.0.6
Vendor: Homebrew
Home: /opt/homebrew/Cellar/openjdk@21/21.0.6/libexec/openjdk.jdk/Contents/Home

Còn jshell cho bạn một chỗ thử biểu thức mà không cần file nào:

$ jshell
|  Welcome to JShell -- Version 21.0.6
|  For an introduction type: /help intro

jshell> int x = 2 + 3
x ==> 5

jshell> System.out.println("Hello from jshell")
Hello from jshell

jshell> /exit
|  Goodbye

Chạy song song nhiều version JDK

Sớm muộn bạn sẽ có nhiều hơn một JDK — project cũ cần 17, tutorial cần 21. Cài thêm cái thứ hai không làm hỏng cái thứ nhất; chỉ JAVA_HOMEPATH quyết định cái nào đang hoạt động.

Trên macOS, liệt kê những gì đã cài:

/usr/libexec/java_home -V
Matching Java Virtual Machines (2):
    21.0.6 (arm64) "Homebrew" - "OpenJDK 21.0.6" /opt/homebrew/Cellar/openjdk@21/21.0.6/libexec/openjdk.jdk/Contents/Home
    17.0.14 (arm64) "Homebrew" - "OpenJDK 17.0.14" /opt/homebrew/Cellar/openjdk@17/17.0.14/libexec/openjdk.jdk/Contents/Home

Danh sách này đi ra stderr; đường dẫn đang được chọn được in riêng ra stdout, và đó là lý do $(/usr/libexec/java_home -v 21) dùng được trong một phép gán biến.

Chuyển version cho riêng một shell, không đụng vào file config nào:

export JAVA_HOME=$(/usr/libexec/java_home -v 17)
export PATH="$JAVA_HOME/bin:$PATH"
java -version
javac -version
openjdk version "17.0.14" 2025-01-21
OpenJDK Runtime Environment Homebrew (build 17.0.14+0)
OpenJDK 64-Bit Server VM Homebrew (build 17.0.14+0, mixed mode, sharing)
javac 17.0.14

Đóng terminal là quay lại 21.

Trên Debian và Ubuntu, việc này do hệ thống alternatives lo, và bạn chọn mặc định theo kiểu tương tác:

sudo update-alternatives --config java
sudo update-alternatives --config javac

Phải set cả hai mục, nếu không bạn rơi đúng vào tình huống toolchain lệch nhau mô tả bên dưới.

Trên mọi hệ Unix, SDKMAN là lựa chọn ít sai sót nhất: sdk use java 17.0.20-tem đổi cho shell hiện tại, sdk default java 21.0.12+1.1-tem đổi cho các shell mở sau. Trên Windows, hoặc giữ một JDK và sửa JAVA_HOME khi project cần bản khác, hoặc chạy toàn bộ toolchain trong WSL rồi dùng SDKMAN ở đó.

Xử lý lỗi thường gặp

command not found: javac

zsh: command not found: javac
bash: javac: command not found

Ba nguyên nhân, xếp theo mức độ phổ biến:

  1. Bạn cài runtime chứ không phải JDK. java chạy được, javac thì không. Trên Ubuntu nghĩa là bạn đã cài openjdk-21-jre thay vì openjdk-21-jdk.
  2. JDK đã cài nhưng thư mục bin của nó không nằm trong PATH.
  3. Bạn sửa file khởi động shell mà chưa mở shell mới.

Lần lượt kiểm tra bằng:

which java javac
echo "$JAVA_HOME"
ls "$JAVA_HOME/bin" | grep javac

Nếu lệnh thứ ba in ra javac mà lệnh đầu không tìm thấy, vấn đề nằm ở PATH chứ không phải ở bản cài.

UnsupportedClassVersionError

Đây là lỗi đi kèm mọi trường hợp lệch version, và chính câu chữ của nó đã chứa lời chẩn đoán. Compile bằng JDK mới rồi chạy trên JVM cũ:

javac --release 21 -d out21 Hello.java
"$(/usr/libexec/java_home -v 17)/bin/java" -cp out21 Hello
Error: LinkageError occurred while loading main class Hello
	java.lang.UnsupportedClassVersionError: Hello has been compiled by a more recent version of the Java Runtime (class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 61.0

Đọc hai con số trong đó. Class file ở version 65, tức Java 21. JVM chỉ đọc tới 61, tức Java 17. Class này được build cho một runtime mới hơn cái đang được yêu cầu load nó.

Ngoài đời lỗi này hiếm khi đến từ hai lệnh cố ý như trên. Nó đến từ một cái máy mà javajavac là hai version khác nhau:

java -version
javac -version
openjdk version "17.0.14" 2025-01-21
OpenJDK Runtime Environment Homebrew (build 17.0.14+0)
OpenJDK 64-Bit Server VM Homebrew (build 17.0.14+0, mixed mode, sharing)
javac 21.0.6

Compile thì trơn tru mà chạy thì không cái nào chạy được. Đó là lý do phần kiểm tra ở trên yêu cầu xem cả hai.

Có hai cách sửa. Nâng JVM lên cho khớp compiler — đây là cách đúng trên máy của chính bạn — hoặc compile nhắm vào bản cũ hơn:

javac --release 17 -d out17 Hello.java
javap -v out17/Hello.class | grep "major version"
  major version: 61

Class file đó giờ load được trên Java 17 và mọi bản mới hơn.

Hãy dùng --release, đừng dùng cặp -source/-target kiểu cũ. --release 17 compile dựa trên cả class library lẫn cú pháp của Java 17, nên một lời gọi tới method chỉ có từ Java 21 sẽ bị chặn ngay lúc compile. -source 17 -target 17 chỉ giới hạn cú pháp; lời gọi kia vẫn compile được với library mới rồi đổ vỡ lúc runtime bằng NoSuchMethodError, một chỗ tệ hơn nhiều để phát hiện ra vấn đề.

Class file version và Java release

Ánh xạ này thuần số học: từ Java 5 trở đi, class file major version bằng số release cộng 44.

Class file versionJava releaseClass file versionJava release
49Java 559Java 15
50Java 660Java 16
51Java 761Java 17
52Java 862Java 18
53Java 963Java 19
54Java 1064Java 20
55Java 1165Java 21
56Java 1266Java 22
57Java 1367Java 23
58Java 1468Java 24

Java 25 là 69. Cái gì ở mức 52 thì được build cho Java 8 và chạy được trên mọi JVM từ đó tới nay.

Kiểm tra bất kỳ class file nào bằng javap, và đây cũng là cách soi một file jar không phải do bạn build:

javap -v Hello.class | grep "major version"
  major version: 65

Nhân tiện, một JDK cũng chỉ compile lùi được tới một mức nhất định. Compiler của Java 21 đã bỏ hỗ trợ các target cũ hơn 8:

javac --release 7 -d r7 Hello.java
error: release version 7 not supported
Usage: javac <options> <source files>
use --help for a list of possible options

Could not find or load main class

Error: Could not find or load main class Hello
Caused by: java.lang.ClassNotFoundException: Hello

JVM đã khởi động, rồi không tìm thấy class trên classpath của nó. Các nguyên nhân quen thuộc:

  • Bạn đang đứng sai thư mục, hoặc đã compile với -d out mà quên -cp out.
  • Bạn truyền tên file thay vì tên class. java Hello.class cho ra đúng lỗi đó kèm một chi tiết tố cáo:
Error: Could not find or load main class Hello.class
Caused by: java.lang.ClassNotFoundException: Hello.class
  • Class nằm trong một package, nên tên thật của nó là com.example.Hello và phải chạy từ thư mục cha của com/.

Một lỗi họ hàng đến từ class tồn tại nhưng không có điểm vào:

Error: Main method not found in class NoMain, please define the main method as:
   public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

Signature phải đúng y public static void main(String[] args). Bỏ static đi là ra đúng thông báo này.

class Hello2 is public, should be declared in a file named Hello2.java

Wrong.java:1: error: class Hello2 is public, should be declared in a file named Hello2.java
public class Hello2 {
       ^
1 error

Một public class phải nằm trong file có tên trùng khít với nó, phân biệt cả hoa thường. Đây là compile error chứ không phải warning, và là quy tắc mà người mới phá đầu tiên — thường là do đổi tên class trong editor mà quên đổi tên file.

Dấu mũ chỉ vào token gây lỗi là phần rất đáng giá trong output của javac. Hãy nhìn cột nó chỉ trước khi đọc câu thông báo.

FAQ

Có phải cài JVM riêng không? Không. Không có bản tải nào như vậy. JVM nằm sẵn trong mọi runtime, và runtime nằm sẵn trong JDK.

Chỉ có JRE thì chạy được chương trình Java không? Được — chạy chính là việc mà runtime sinh ra để làm. Nhưng bạn không compile được, vì trong đó không có javac. Trên máy của developer, hãy cài JDK.

Có nên cài cả JRE lẫn JDK không? Không. JDK đã chứa sẵn một runtime đầy đủ. Cài cả hai chỉ để lại hai file java tranh nhau vị trí trong PATH.

Java có miễn phí không? Mọi distribution trong bảng trên trừ Oracle JDK đều là GPLv2 kèm Classpath Exception: miễn phí cho mọi mục đích, kể cả production thương mại. Oracle JDK là cùng source code nhưng theo điều khoản khác, với thời gian miễn phí có giới hạn. Cài Temurin thì câu hỏi này không bao giờ phát sinh.

Vì sao java -versionjavac -version báo khác nhau? Vì có hai JDK cùng được cài và PATH lấy launcher từ bản này, compiler từ bản kia. Hãy set JAVA_HOME về đúng một JDK và đặt $JAVA_HOME/bin lên đầu PATH.

Ở công ty tôi đang dùng Java 8. Vậy có nên học Java 8 không? Không. Học trên 21, rồi những khác biệt cần cho một codebase Java 8 chỉ là một danh sách ngắn các tính năng cần tránh. Học trên 8 nghĩa là không bao giờ gặp var, record, text block hay switch expression, những thứ xuất hiện trong mọi tutorial hiện nay.

Có phải gỡ JDK cũ trước khi cài bản mới không? Không. Nhiều JDK cùng tồn tại không xung đột; chỉ JAVA_HOMEPATH quyết định bản nào đang hoạt động.

IDE cần JDK hay JRE? Cần JDK. IDE compile code của bạn ngay trong lúc gõ, mà compile thì phải có compiler.

Kết luận

JVM là một specification và là process thực thi bytecode. JRE là JVM cộng standard class library, và đủ để chạy một chương trình. JDK là JRE cộng javac cùng phần còn lại của bộ tool, và đó là thứ bạn cài. Từ Java 9 không còn thư mục jre/ lồng bên trong, cũng không còn bản JRE riêng từ Oracle — jlink dựng runtime rút gọn thay thế, và đó chính là bản chất của những package mang tên "JRE" ngày nay.

Về mặt thực hành: cài Eclipse Temurin 21, set JAVA_HOME trỏ tới thư mục cài, đặt $JAVA_HOME/bin lên đầu PATH, và xác nhận java -version với javac -version báo cùng một số. Khi chúng không khớp, bạn sẽ gặp UnsupportedClassVersionError, và giờ thì bạn đọc được hai con số version trong đó.

Bài tiếp theo chọn một Java IDE — IntelliJ IDEA, Eclipse hay VS Code — cấu hình nó với đúng bản JDK bạn vừa cài, rồi tạo project đầu tiên từ con số không.

Bài viết liên quan

[Java Basics] Vòng lặp lồng nhau, break và continue trong Java

Vòng lặp lồng nhau trong Java và hai keyword cắt ngang vòng lặp: số lần thân vòng lặp trong chạy, break chỉ thoát vòng lặp gần nhất, continue bỏ qua update trong while, labeled break và continue, cùng bẫy switch nằm trong loop.

[Java Basics] Java là gì? Lịch sử, đặc điểm và tại sao nên học Java

Java là gì? Bài viết giới thiệu Java ở cả hai vai trò ngôn ngữ và platform: lịch sử từ Oak năm 1991 đến Java 21 LTS, các đặc điểm cốt lõi, nơi Java thực sự chạy trong production, so sánh thẳng thắn với Python, JavaScript, C# và những lầm tưởng cần dẹp bỏ.

[Java Basics] Field, method và constructor trong Java

Field, instance method và constructor trong Java: giá trị default của field, field initialiser, overloading constructor và chaining bằng this(...), thứ tự khởi tạo chứng minh bằng lệnh in, cùng mọi error message thật của javac từ việc viết void trước constructor tới việc đặt this(...) không phải đầu tiên.

[Java Basics] Tham số, giá trị trả về và overloading trong Java

Parameter, giá trị trả về và overloading trong Java: vì sao Java luôn là pass-by-value, vì sao swap không chạy được, bốn phase của overload resolution, quy tắc varargs, cùng mọi compile error chạy thật trên JDK 21.