Part 01 · Java Core · Chuyên đề 06

Class loading, bytecode và JPMS

Hiểu vòng đời class giúp giải thích lỗi linkage, static initialization, plugin architecture, memory leak do class loader và ranh giới module. Đây là phần nối giữa mã Java bạn viết và cách JVM thực sự nạp, liên kết, khởi tạo và tổ chức code khi chạy.

1. Class file và bytecode

File .class chứa magic number, version, constant pool, metadata của field/method, attributes và bytecode của method. Constant pool lưu literal cùng symbolic reference; JVM resolve symbolic reference thành runtime reference trong quá trình linking hoặc khi reference được dùng lần đầu. Bytecode là tập instruction theo mô hình operand stack, không phải mã máy của CPU.

javac PaymentService.java
javap -c -p -v PaymentService.class

javap cho thấy descriptor sau type erasure, synthetic/bridge method, constant pool và instruction. Đây là công cụ tốt để kiểm chứng thay vì đoán compiler đã tạo gì.

Mental model: source Java được compiler dịch thành class file có cấu trúc chuẩn. Khi chạy, JVM đọc metadata và bytecode trong class file, sau đó interpreter/JIT mới thực thi hoặc biên dịch những instruction cần thiết thành mã máy phù hợp runtime.

2. Loading, linking và initialization

LoadingĐọc binary representation và tạo runtime Class.
VerificationKiểm tra class file và bytecode hợp lệ.
PreparationCấp storage cho static fields, gán default value.
ResolutionĐổi symbolic reference thành runtime/direct reference.
InitializationThực thi <clinit>.
Giai đoạnViệc xảy ra
LoadingTìm binary representation và tạo đối tượng Class.
VerificationKiểm tra class file hợp lệ và bytecode an toàn theo constraint của JVM.
PreparationCấp storage cho static field và gán default value.
ResolutionChuyển symbolic reference thành direct/runtime reference; có thể lazy.
InitializationChạy class initialization method <clinit>, gồm static initializer và static field initializer.

Initialization được kích hoạt bởi “active use” như tạo instance, gọi static method, ghi hoặc đọc static field không phải compile-time constant. Chỉ load class chưa nhất thiết chạy static block. JVM đồng bộ initialization để một class không được initialize đồng thời nhiều lần.

Lỗi khác nhau: ClassNotFoundException thường đến từ API load theo tên; NoClassDefFoundError là JVM đã cần class nhưng definition không còn khả dụng hoặc initialization trước đó thất bại; NoSuchMethodError thường báo binary incompatibility giữa compile-time và runtime dependency.
Debug theo lifecycle: nếu class “không tìm thấy”, kiểm classpath/module path và loader trước; nếu class có nhưng member không tồn tại, kiểm version dependency runtime; nếu class lỗi sau một static initializer failure, đừng chỉ nhìn vào lần truy cập sau mà hãy tìm exception gốc từ lần initialization đầu tiên.

3. Class loader và delegation

Class identity là cặp binary name + defining class loader. Hai class cùng tên do hai loader khác nhau định nghĩa không phải cùng runtime type. Built-in loaders gồm bootstrap, platform và application loader. Mô hình parent delegation thường hỏi parent trước, giúp core classes không bị tùy tiện thay thế.

Plugin/container có thể dùng child-first hoặc loader hierarchy riêng để cô lập dependency. Khi unload plugin, mọi reference đến class, instance và defining loader phải biến mất. Thread context class loader, static cache, driver registry hoặc ThreadLocal có thể giữ loader và gây metaspace leak.

ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> pluginType = Class.forName("demo.Plugin", true, loader);
Điểm dễ nhầm: tên class giống nhau chưa đủ để cast được cho nhau. Nếu chúng được define bởi hai class loader khác nhau, JVM xem chúng là hai runtime type khác nhau.

4. Java Platform Module System (JPMS)

JPMS tạo explicit module graph. Module khai báo dependency và package được export; strong encapsulation giúp phân biệt public cho nội bộ module với public API cho module khác.

module com.example.orders {
    requires java.net.http;
    exports com.example.orders.api;
    uses com.example.orders.spi.PricingProvider;
}
DirectiveÝ nghĩa
requiresĐọc module khác.
exportsCho module khác compile/access public types của package.
opensCho deep reflection tại runtime; không giống exports.
uses / provides ... withKhai báo consumer/provider cho ServiceLoader.
requires transitiveConsumer của module hiện tại cũng đọc dependency đó.

Unnamed module giữ compatibility cho classpath. Automatic module giúp migration nhưng module name có thể thiếu ổn định. Split package bị hạn chế trong module graph. Với framework reflection, ưu tiên opens package to framework.module thay vì mở toàn module.

Phân biệt ranh giới: exports là API access ở mức public type; opens phục vụ deep reflection runtime. Một package có thể cần một trong hai hoặc cả hai tùy cách framework truy cập.

5. ServiceLoader và runtime image

ServiceLoader<PricingProvider> providers = ServiceLoader.load(PricingProvider.class);
PricingProvider provider = providers.findFirst().orElseThrow();

ServiceLoader hỗ trợ plugin theo contract mà consumer không phụ thuộc implementation cụ thể.

jdeps --print-module-deps app.jar
jlink --add-modules com.example.app --output runtime

jdeps phân tích module dependency; jlink tạo runtime image chỉ chứa module cần thiết. Lợi ích là image nhỏ và kiểm soát runtime tốt hơn, đổi lại build/deployment phức tạp hơn.

Chuỗi thiết kế: contract nằm ở API/SPI module → consumer khai báo uses → provider khai báo provides ... withServiceLoader tìm implementation → jdeps/jlink giúp kiểm soát dependency và runtime image.

6. Checklist hiểu sâu

Tự kiểm tra tốt nhất bằng cách giải thích từng mục bằng ví dụ thực tế, sau đó dùng javap, dependency tree/runtime image hoặc class-loader logging để chứng minh thay vì chỉ nhớ định nghĩa.

Nguồn chính thức: JVMS 4 · Class File · JVMS 5 · Loading, Linking, Initializing · JLS 12 · Execution · JLS 7.7 · Module Declarations · JEP 261 · Module System