Thời lượng 6:51. Tập này được làm từ video "How the JVM Actually Works" của ByteByteGo. Nghe tập này trên Apple Podcasts rồi đọc kèm bản chữ dưới đây.
Linh: Chào anh, mừng anh quay lại với Học cùng Huan Muse. Tập hôm nay em làm từ video How the JVM Actually Works của kênh ByteByteGo, một video rất đáng xem cho lập trình viên Java. Câu hỏi của tập này nghe thì đơn giản. Mỗi ngày anh đều biên dịch, chạy và gỡ lỗi code Java. Nhưng chính xác thì JVM làm gì ở khoảng giữa, từ lúc biên dịch xong cho tới lúc chương trình thật sự chạy?
Minh: Ừ, đó là đoạn mà hầu hết mọi người đều bỏ qua. Mình bấm nút Run, chương trình chạy, hết. Bên trong cái hộp đen đó có gì hả Linh?
Linh: Mình sẽ đi theo đúng bảy chặng mà video trình bày. Chặng một là build, chặng hai là load, chặng ba là link, chặng bốn là initialize, chặng năm là memory, chặng sáu là execute, và chặng cuối là trạng thái chạy thật, nơi code thông dịch và code đã biên dịch chạy trộn với nhau.
Minh: Bắt đầu từ chặng build đi.
Linh: Ở chặng build, trình biên dịch javac dịch mã nguồn Java của anh thành bytecode. Bytecode này không phụ thuộc vào hệ điều hành hay phần cứng, và nó được lưu trong các file class, hoặc đóng gói trong file JAR và module. Đây chính là nền tảng của câu khẩu hiệu nổi tiếng của Java, viết một lần, chạy ở mọi nơi. Anh biên dịch một lần ra bytecode, rồi mang bytecode đó tới bất kỳ máy nào có JVM phù hợp là chạy được.
Minh: Vậy JVM là người phiên dịch giữa bytecode trung lập đó với từng cái máy cụ thể đúng không?
Linh: Chính xác. Sang chặng hai, là load, tức là nạp class. Hệ thống class loader sẽ mang các class vào bộ nhớ khi chương trình cần tới chúng, chứ không nạp hết mọi thứ ngay từ đầu. Việc nạp này tuân theo một cơ chế gọi là parent delegation, nghĩa là class loader con sẽ nhờ class loader cha nạp trước, và chỉ khi cha không tìm thấy thì con mới tự nạp. Có ba tầng chính. Bootstrap nạp các class lõi của JDK. Platform lo phần mở rộng. Còn System thì nạp code ứng dụng của chính anh.
Minh: Nghe cơ chế nhờ cha trước này có vẻ là để tránh mỗi nơi nạp một bản class khác nhau, gây loạn đúng không Linh?
Linh: Đúng vậy, nó vừa tránh trùng lặp, vừa bảo vệ các class lõi khỏi bị giả mạo. Nạp xong thì tới chặng ba, là link, tức là liên kết. Chặng này có ba bước nhỏ. Đầu tiên là verify, kiểm tra bytecode có an toàn và hợp lệ không. Tiếp theo là prepare, cấp phát bộ nhớ cho các trường static và gán cho chúng giá trị mặc định. Và cuối cùng là resolve, biến các tham chiếu tượng trưng trong bytecode thành địa chỉ bộ nhớ trực tiếp, để lúc chạy thì JVM không phải đi tìm lại nữa.
Minh: Khoan, vậy ở bước prepare, trường static mới chỉ có giá trị mặc định thôi à? Thế khi nào nó mới nhận giá trị thật mà mình viết trong code?
Linh: Câu hỏi rất hay, và đó chính là chặng bốn, initialize, tức là khởi tạo. Chỉ tới chặng này, các biến static mới được gán giá trị thật, và các khối static initializer mới được chạy. Và điểm cực kỳ quan trọng mà video nhấn mạnh là việc này chỉ xảy ra ở lần đầu tiên class được sử dụng thật sự. Đây cũng là lý do đôi khi anh chạm vào một class lần đầu thì chương trình khựng nhẹ một nhịp, vì lúc đó class mới thật sự được khởi tạo.
Minh: Rồi, class đã sẵn sàng. Thế bộ nhớ lúc chạy được chia ra sao? Đây là phần em thấy nối thẳng sang tập một về heap dump nè.
Linh: Chuẩn luôn. Chặng năm là memory, bộ nhớ runtime. Có hai vùng được chia sẻ chung cho mọi thread, là heap, nơi chứa các object, và method area, nơi chứa thông tin về các class. Ngược lại, có ba thứ được tạo riêng cho từng thread, là JVM stack, thanh ghi PC, và native method stack. Và cũng trong vùng bộ nhớ này, bộ thu gom rác, garbage collector, làm nhiệm vụ thu hồi phần heap không còn được dùng nữa. Anh thấy không, heap mà ở tập một mình chụp lại thành heap dump để săn memory leak, chính là vùng heap dùng chung này đây.
Minh: Hèn gì hiểu JVM thì đọc heap dump cũng sáng hơn hẳn. Còn chặng execute, thực thi, thì sao?
Linh: Ở chặng sáu, ban đầu trình thông dịch, interpreter, sẽ chạy bytecode trực tiếp, từng lệnh một. Cách này khởi động rất nhanh, nhưng chạy lâu thì không phải nhanh nhất. Cho nên khi một method được gọi đi gọi lại nhiều lần, trình biên dịch JIT sẽ vào cuộc. JIT là viết tắt của Just In Time, biên dịch đúng lúc cần. Nó dịch method nóng đó thành mã máy gốc của CPU, rồi lưu vào một vùng nhớ gọi là code cache, để những lần sau chạy thẳng bằng mã máy luôn. Còn khi chương trình cần gọi xuống thư viện gốc viết bằng C hay C cộng cộng, nó sẽ đi qua cầu nối JNI.
Minh: Vậy là chương trình Java không chạy hoàn toàn bằng thông dịch, cũng không biên dịch hết ngay từ đầu.
Linh: Đúng, và đó là chặng cuối, trạng thái chạy thật. Một chương trình Java đang chạy là một hỗn hợp, một phần chạy bằng thông dịch, một phần đã được JIT biên dịch thành mã máy. Nhờ vậy nó vừa khởi động nhanh, vừa đạt hiệu năng cao dần theo thời gian. Đây là lời giải thích cho hiện tượng dân backend gặp hoài, là ứng dụng vừa deploy xong thì trả lời hơi chậm, nhưng chạy một lúc thì nhanh dần lên. Người ta hay gọi giai đoạn đó là warm up, vì JIT cần thời gian để phát hiện và biên dịch các đoạn code nóng.
Minh: Hay quá. Vậy nếu phải gói tập này thành vài câu để nhớ, anh nên nhớ gì?
Linh: Anh nhớ ba ý này là đủ. Ý một, javac biến mã nguồn thành bytecode không phụ thuộc nền tảng, và class loader nạp bytecode đó theo cơ chế parent delegation, qua ba tầng Bootstrap, Platform và System. Ý hai, một class phải đi qua link rồi mới tới initialize, và nó chỉ được khởi tạo ở lần sử dụng đầu tiên. Ý ba, lúc chạy, heap và method area là của chung mọi thread, còn stack là của riêng từng thread, và hiệu năng đỉnh đến từ JIT, thứ biến các method nóng thành mã máy và lưu trong code cache.
Minh: Trước khi kết thúc, cho anh một câu tự kiểm tra nhanh đi Linh.
Linh: Đây ạ. Nếu một biến static chưa có giá trị thật mà mới chỉ có giá trị mặc định, thì class đó đang dừng ở chặng nào? Đáp án là chặng link, ở bước prepare, vì phải tới chặng initialize thì giá trị thật mới được gán. Anh nào trả lời được câu này là đã nắm được ranh giới giữa link và initialize rồi đó.
Minh: Rõ ràng lắm. Cảm ơn Linh.
Linh: Cảm ơn anh đã nghe. Tập này mình đã đi hết hành trình của một chương trình Java, từ mã nguồn, qua bytecode, class loader, linking, khởi tạo, bộ nhớ, cho tới JIT và bộ thu gom rác. Ở tập sau, anh cứ gửi em video hoặc chủ đề anh muốn học, em lại làm thành podcast cho anh nghe trên điện thoại. Hẹn anh ở tập tiếp theo.