Vòng đời của một lớp trong JVM
Theo đặc tả máy ảo Java (JVM), một lớp trải qua 7 giai đoạn từ khi được nạp vào bộ nhớ cho đến khi bị loại bỏ: Tải (Loading), Liên kết (Linking - bao gồm Xác minh, Chuẩn bị, Phân giải), Khởi tạo (Initialization), Sử dụng (Using) và Gỡ bỏ (Unloading).
Trong Java, kiểu dữ liệu cơ bản được JVM định nghĩa sẵn, không cần tải lớp. Kiểu dữ liệu tham chiếu (đối tượng) mới cần trải qua quá trình tải lớp.
Các bước Tải, Liên kết và Khởi tạo thường được thực hiện liên tiếp, và đôi khi được gọi chung là "tải lớp" hoặc "khởi tạo lớp".
1. Giai đoạn Tải (Loading)
Tải là quá trình trình tải lớp (ClassLoader) đọc dữ liệu nhị phân của tệp .class vào vùng Method Area (Vùng phương thức) và tạo một đối tượng java.lang.Class trên Heap để đại diện cho lớp đó.
Các bước cụ thể:
- Lấy luồng dữ liệu nhị phân của lớp dựa trên tên đầy đủ (Fully Qualified Name).
- Phân tích luồng dữ liệu nhị phân thành cấu trúc dữ liệu trong Method Area (còn gọi là Java class model hay class template object).
- Tạo một thể hiện của lớp
java.lang.Classtrên Heap, đóng vai trò là cổng truy cập vào các thông tin của lớp trong Method Area.
Đối tượng khuôn mẫu lớp (class template) là một "ảnh chụp nhanh" của lớp Java trong bộ nhớ JVM. Nó lưu trữ các thông tin như hằng số, trường, phương thức,... giúp JVM có thể truy xuất và thao tác với lớp trong quá trình chạy.
Trình tải lớp (ClassLoader)
Vai trò: ClassLoader là thành phần cốt lõi, chịu trách nhiệm đọc luồng dữ liệu nhị phân của lớp và chuyển đổi thành đối tượng java.lang.Class. Nó chỉ ảnh hưởng đến giai đoạn tải, không can thiệp vào liên kết hay khởi tạo.
Phân loại ClassLoader
- Bootstrap ClassLoader (Trình tải khởi động): Được viết bằng C/C++, nằm trong JVM. Tải các thư viện lõi Java từ
JAVA_HOME/jre/lib(rt.jar, resources.jar, ...). Không có ClassLoader cha. Chỉ tải các lớp có tên bắt đầu bằngjava,javax,sun. - Platform ClassLoader (Extension ClassLoader cũ) (Trình tải nền tảng/mở rộng): Viết bằng Java, kế thừa từ
ClassLoader. Cha là Bootstrap ClassLoader. Tải các lớp từ thư mụcjre/lib/exthoặc biến hệ thốngjava.ext.dirs. (Từ Java 9, tên được đổi thành Platform ClassLoader). - Application ClassLoader (Trình tải ứng dụng): Viết bằng Java, kế thừa từ
ClassLoader. Cha là Platform ClassLoader. Tải các lớp từclasspathhoặc biến hệ thốngjava.class.path. Đây là ClassLoader mặc định cho các lớp do người dùng định nghĩa.
Ví dụ lấy thông tin ClassLoader:
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(appClassLoader); // AppClassLoader
ClassLoader platformClassLoader = appClassLoader.getParent();
System.out.println(platformClassLoader); // PlatformClassLoader
ClassLoader bootstrapClassLoader = platformClassLoader.getParent();
System.out.println(bootstrapClassLoader); // null (vì Bootstrap không phải là đối tượng Java)
ClassLoader stringLoader = String.class.getClassLoader();
System.out.println(stringLoader); // null (String được tải bởi Bootstrap)
ClassLoader peopleLoader = People.class.getClassLoader();
System.out.println(peopleLoader); // AppClassLoader
Mô hình ủy quyền cha (Parent Delegation Model)
Khi một ClassLoader nhận yêu cầu tải lớp, nó sẽ ủy quyền cho ClassLoader cha trước. Quá trình này lặp lại cho đến Bootstrap ClassLoader. Chỉ khi ClassLoader cha không tìm thấy lớp, ClassLoader con mới tự thực hiện tải.
Điều này đảm bảo tính an toàn (Sandbox Security): các lớp lõi Java không bị ghi đè bởi lớp tự định nghĩa.
Thay đổi trong Java 9
- Cơ chế mở rộng (Extension) bị loại bỏ. Extension ClassLoader được giữ lại với tên mới Platform ClassLoader.
- Cả Platform và Application ClassLoader không còn kế thừa từ
URLClassLoader. Chúng kế thừa từjdk.internal.loader.BuiltinClassLoader. - ClassLoader có tên (get trả về
getName()). Tên của Platform là "platform", của Application là "app". - Bootstrap ClassLoader được triển khai bằng Java (cộng tác với JVM), nhưng vẫn trả về
nullkhi gọigetClassLoader()để tương thích ngược. - Trước khi ủy quyền, ClassLoader sẽ kiểm tra xem lớp có thuộc về một module hệ thống không. Nếu có, nó sẽ ưu tiên ủy quyền cho ClassLoader của module đó.
2. Giai đoạn Liên kết (Linking)
a. Xác minh (Verification)
Đảm bảo tệp .class hợp lệ và tuân thủ các ràng buộc của JVM.
- Xác minh định dạng: Kiểm tra mã ma thuật (0xCAFEBABE), phiên bản, độ dài các mục.
- Xác minh ngữ nghĩa: Kiểm tra lớp có cha (trừ
Object), không kế thừa lớpfinal, lớp trừu tượng có triển khai đầy đủ các phương thức trừu tượng. - Xác minh bytecode: Phân tích luồng bytecode để đảm bảo tính hợp lệ (không nhảy đến lệnh không tồn tại, truyền đúng kiểu tham số).
- Xác minh tham chiếu ký hiệu: Kiểm tra các lớp, phương thức, trường được tham chiếu có tồn tại và có quyền truy cập. Giai đoạn này diễn ra trong quá trình phân giải.
b. Chuẩn bị (Preparation)
Cấp phát bộ nhớ cho các biến tĩnh (static) và gán giá trị mặc định (zero value).
- Ví dụ:
static int count = 10→ giá trị mặc định là 0, chưa phải 10. - Ngoại lệ: Các biến
static finalkiểu nguyên thủy hoặc String (được gán bằng hằng số) sẽ được gán giá trị ngay trong giai đoạn này.
private static final String MSG = "Hello"; // Chuẩn bị: gán "Hello"
private static final String MSG2 = new String("World"); // Sẽ được gán trong Initialization
c. Phân giải (Resolution)
Chuyển đổi các tham chiếu ký hiệu (Symbolic Reference) thành tham chiếu trực tiếp (Direct Reference).
- Tham chiếu ký hiệu: Là các chuỗi mô tả (tên lớp, tên phương thức, mô tả kiểu) trong constant pool. Không phụ thuộc vào bố trí bộ nhớ.
- Tham chiếu trực tiếp: Là con trỏ, offset, hoặc handle trực tiếp đến vùng nhớ. Phụ thuộc vào bố trí bộ nhớ cụ thể của JVM.
Trong HotSpot VM, giai đoạn phân giải có thể được thực hiện sau khi khởi tạo (lazy resolution).
3. Giai đoạn Khởi tạo (Initialization)
Thực thi phương thức khởi tạo lớp <clinit>() do trình biên dịch tự động tạo ra. Phương thức này kết hợp các phép gán cho biến tĩnh và các khối lệnh static.
Quy tắc static final
Biến static final được gán giá trị tại:
- Giai đoạn Chuẩn bị: Nếu là kiểu nguyên thủy hoặc String và giá trị là hằng số (literal, không gọi phương thức).
- Giai đoạn Khởi tạo: Các trường hợp còn lại (gọi phương thức, tạo đối tượng).
public static final int INT_CONST = 10; // Chuẩn bị
public static final int RANDOM_NUM = new Random().nextInt(10); // Khởi tạo
public static int a = 1; // Khởi tạo
public static final String S1 = "hello"; // Chuẩn bị
public static final String S2 = new String("hello"); // Khởi tạo
Tính an toàn luồng
JVM đảm bảo <clinit>() chỉ được thực thi một lần trong môi trường đa luồng bằng cơ chế khóa. Các luồng khác sẽ chờ nếu luồng đầu tiên đang thực thi. Nếu luồng đầu tiên hoàn thành, các luồng chờ sẽ bỏ qua. Điều này có thể gây ra deadlock nếu <clinit>() thực hiện tác vụ nặng.
Sử dụng chủ động (Active Use)
Lớp chỉ được khởi tạo khi có một trong các hành vi sau:
- Tạo thể hiện (
new, phản chiếu, clone, deserialization). - Truy cập hoặc gán biến tĩnh (ngoại trừ hằng số).
- Gọi phương thức tĩnh.
- Sử dụng phản chiếu (
Class.forName()). - Khởi tạo lớp con (cha chưa khởi tạo sẽ được khởi tạo trước).
- Lớp chính (có
main()) được chỉ định khi khởi động JVM. - Hỗ trợ ngôn ngữ động (xử lý các handle
REF_getStatic,REF_putStatic,REF_invokeStatic). - Interface có phương thức
defaultvà lớp triển khai được khởi tạo.
Sử dụng thụ động (Passive Use)
Các trường hợp sau không gây khởi tạo lớp:
- Sử dụng
ClassLoader.loadClass(). - Truy cập trường tĩnh của lớp cha thông qua lớp con.
- Tham chiếu hằng số (constant).
- Khai báo mảng tham chiếu đến lớp.
4. Giai đoạn Sử dụng (Using)
Sau khi hoàn thành ba giai đoạn trên, lớp sẵn sàng được sử dụng: tạo đối tượng, gọi phương thức tĩnh, truy cập trường tĩnh.
5. Giai đoạn Gỡ bỏ (Unloading)
Mối quan hệ giữa lớp, ClassLoader và thể hiện
- Đối tượng
Classtham chiếu đến ClassLoader đã tải nó (quan hệ hai chiều). - Thể hiện của lớp tham chiếu đến đối tượng
Classcủa nó (thông quagetClass()). - Tất cả các lớp đều có thuộc tính tĩnh
.classtham chiếu đến đối tượngClass.
Khi nào lớp bị gỡ bỏ?
Lớp bị gỡ bỏ khi đối tượng Class đại diện cho nó trở nên không thể truy cập (unreachable). Nếu sau đó có yêu cầu sử dụng lại lớp, JVM sẽ tải lại từ đầu và tạo đối tượng Class mới.
Điều kiện để một lớp có thể bị gỡ bỏ (dọn rác Method Area)
- Tất cả thể hiện của lớp đó (và các lớp con) đã bị thu gom.
- ClassLoader đã tải lớp đó cũng bị thu gom.
- Đối tượng
Classkhông được tham chiếu ở bất kỳ đâu (không thể dùng phản chiếu).
Trong thực tế, các lớp do Bootstrap, Platform, Application ClassLoader tải hầu như không bao giờ bị gỡ bỏ. Chỉ các lớp do ClassLoader tự định nghĩa mới có khả năng bị gỡ bỏ, nhưng điều này rất khó xảy ra trong các ứng dụng phức tạp.