Giải thích chi tiết Java Memory Area (trọng tâm)
Nếu không có ghi chú đặc biệt, nội dung đều áp dụng cho HotSpot JVM.
Bài viết này được tổng hợp và bổ sung dựa trên cuốn Understanding the JVM: Advanced Features and Best Practices.
Các câu hỏi phỏng vấn thường gặp:
- Giới thiệu Java Memory Area (runtime data area)
- Quy trình tạo Java object (năm bước; nên tự viết lại được và biết JVM thực hiện gì ở mỗi bước)
- Hai cách định vị object khi truy cập (handle và direct pointer)
Lời nói đầu
Đối với lập trình viên Java, dưới cơ chế tự động quản lý bộ nhớ của JVM, bạn không còn phải viết thao tác delete/free tương ứng cho từng thao tác new như lập trình viên C/C++. Vì vậy, memory leak và memory overflow ít xảy ra hơn. Chính vì lập trình viên Java giao quyền kiểm soát bộ nhớ cho JVM, một khi xuất hiện vấn đề memory leak hoặc memory overflow, nếu không hiểu JVM sử dụng bộ nhớ như thế nào thì việc tìm lỗi sẽ rất khó khăn.
Runtime Data Area
Trong quá trình thực thi chương trình Java, JVM chia bộ nhớ do mình quản lý thành một số data area khác nhau.
JDK 1.8 và các phiên bản trước đó có một số khác biệt nhỏ. Ở đây lấy JDK 1.7 và JDK 1.8 làm ví dụ để giới thiệu.
JDK 1.7:

JDK 1.8:

Thread-private:
- Program counter
- Java Virtual Machine Stack
- Native method stack
Thread-shared:
- Heap
- Method area
Direct memory thường được thảo luận cùng các vùng này, nhưng nó không phải runtime data area do Java Virtual Machine Specification định nghĩa, cũng không nên được xem là “thread-shared runtime data area” trong specification.
Quy định của Java Virtual Machine Specification đối với runtime data area khá rộng. Lấy heap làm ví dụ: heap có thể là không gian liên tục hoặc không liên tục. Kích thước heap có thể cố định hoặc mở rộng theo nhu cầu trong runtime. Người triển khai JVM có thể dùng bất kỳ thuật toán garbage collection nào để quản lý heap, thậm chí hoàn toàn không thực hiện garbage collection cũng được.
Program Counter
Program counter là một vùng bộ nhớ nhỏ, có thể xem như chỉ báo số dòng của bytecode mà thread hiện tại đang thực thi. Khi bytecode interpreter hoạt động, nó thay đổi giá trị của program counter này để chọn bytecode instruction tiếp theo cần thực thi. Các chức năng như branch, loop, jump, exception handling và thread recovery đều cần dựa vào program counter này để hoàn thành.
Ngoài ra, để có thể khôi phục đúng vị trí thực thi sau khi chuyển thread, mỗi thread cần một program counter độc lập. Các program counter giữa các thread không ảnh hưởng lẫn nhau và được lưu trữ độc lập. Loại vùng bộ nhớ này được gọi là “thread-private memory”.
Từ phần giới thiệu trên, có thể thấy program counter chủ yếu có hai tác dụng:
- Bytecode interpreter đọc instruction lần lượt bằng cách thay đổi program counter, từ đó thực hiện code flow control như thực thi tuần tự, rẽ nhánh, lặp và exception handling.
- Khi có nhiều thread, program counter dùng để ghi lại vị trí thực thi của thread hiện tại, nhờ đó khi thread được chuyển lại thì có thể biết lần trước thread đã chạy đến đâu.
Vòng đời của program counter hoàn toàn đồng bộ với thread:
- Creation: được tạo cùng với thread.
- Destruction: bị hủy cùng với thread.
Khi thực thi Java method (không phải native), program counter ghi lại địa chỉ của JVM bytecode instruction đang được thực thi. Khi thread thực thi một native method, giá trị của program counter là Undefined, vì lúc này không thực thi JVM bytecode instruction.
⚠️ Lưu ý: program counter là vùng bộ nhớ duy nhất trong JVM specification không quy định bất kỳ trường hợp OutOfMemoryError nào. Specification chỉ quy định kết luận này, không yêu cầu mọi implementation sử dụng một biểu diễn có kích thước cố định nào.
Java Virtual Machine Stack
Giống program counter, Java Virtual Machine Stack (sau đây gọi tắt là stack) cũng là thread-private. Vòng đời của nó giống thread: được tạo cùng thread và bị hủy khi thread kết thúc.
Stack là một khu vực cốt lõi của JVM runtime data area. Ngoài một số lời gọi native method được thực hiện qua native method stack (sẽ nói sau), mọi lời gọi Java method khác đều được thực hiện qua stack (đồng thời cũng cần phối hợp với các runtime data area khác như program counter).
Dữ liệu của method call được truyền qua stack. Mỗi method call sẽ đẩy một stack frame tương ứng vào stack; sau khi mỗi method call kết thúc, một stack frame sẽ được lấy ra khỏi stack.
Stack gồm nhiều stack frame, mỗi stack frame có local variable table, operand stack, dynamic linking và method return address. Tương tự stack trong cấu trúc dữ liệu, cả hai đều là cấu trúc dữ liệu LIFO và chỉ hỗ trợ hai thao tác pop và push.

Local variable table chủ yếu lưu các kiểu dữ liệu đã biết ở compile time (boolean, byte, char, short, int, float, long, double) và object reference (reference type, khác với chính object; nó có thể là một reference trỏ đến địa chỉ bắt đầu của object, hoặc trỏ đến handle đại diện cho object hay một vị trí khác liên quan đến object này).

Operand stack chủ yếu được dùng làm trạm trung chuyển cho method call, dùng để lưu các kết quả tính toán trung gian sinh ra trong quá trình thực thi method. Ngoài ra, temporary variable sinh ra trong quá trình tính toán cũng được đặt trên operand stack.
Dynamic linking là một chức năng của stack frame. Mỗi stack frame giữ reference đến runtime constant pool của type mà method hiện tại thuộc về, dùng để chuyển symbol reference của method trong code thành reference cụ thể đến method, đồng thời chuyển việc truy cập variable thành offset trong runtime storage structure tương ứng. Khi resolve symbol chưa được xác định, nó còn có thể trigger class loading. Việc chọn virtual method implementation dựa trên actual type của receiver là dynamic dispatch của method invocation instruction, không thể đồng nhất đơn giản với dynamic linking ở đây.

Mặc dù stack space không vô hạn, thông thường sẽ không có vấn đề trong các lời gọi bình thường. Tuy nhiên, nếu một lời gọi hàm rơi vào infinite recursion, quá nhiều stack frame sẽ được push vào stack và chiếm quá nhiều bộ nhớ, khiến stack quá sâu. Khi độ sâu stack mà thread yêu cầu vượt quá độ sâu tối đa của Java Virtual Machine Stack hiện tại, lỗi StackOverflowError sẽ được ném.
Java method có hai cách return:
- Normal return: thực thi câu lệnh
return, truyền return value cho caller. - Exceptional return: exception được throw trong quá trình thực thi method nhưng không được catch.
Bất kể cách return nào, stack frame đều bị pop. Nói cách khác, stack frame được tạo khi method call và bị hủy khi method kết thúc. Method kết thúc dù hoàn thành bình thường hay do exception đều được tính là method kết thúc.
Ngoài lỗi StackOverflowError, stack còn có thể xuất hiện lỗi OutOfMemoryError. Nguyên nhân là nếu stack có thể mở rộng động, khi JVM mở rộng stack nhưng không thể xin đủ bộ nhớ thì sẽ ném exception OutOfMemoryError.
Tóm tắt đơn giản, trong khi chương trình chạy, stack có thể xuất hiện hai lỗi:
StackOverflowError: khi stack space cần cho thread execution vượt quá kích thước JVM cho phép, ném lỗiStackOverflowError.OutOfMemoryError: nếu stack có thể mở rộng động, khi JVM mở rộng stack nhưng không thể xin đủ bộ nhớ thì ném exceptionOutOfMemoryError.

Native Method Stack
Chức năng của native method stack rất giống Java Virtual Machine Stack. Điểm khác biệt là: Java Virtual Machine Stack phục vụ JVM thực thi Java method (tức bytecode), còn native method stack phục vụ Native method mà JVM sử dụng. Trong HotSpot JVM, hai stack này được hợp nhất.
Khi thực thi native method, implementation của JVM có thể dùng native stack truyền thống (thường gọi là C stack). Java Virtual Machine Specification không quy định native method stack phải sử dụng local variable table, operand stack và cấu trúc dynamic linking giống stack frame của JVM.
Sau khi native method thực thi xong, dữ liệu stack liên quan đến implementation tương ứng sẽ được giải phóng. Native method stack cũng có thể xuất hiện hai lỗi StackOverflowError và OutOfMemoryError.
Heap
Java Heap là vùng lớn nhất trong bộ nhớ do JVM quản lý, là một memory area được mọi thread dùng chung và được tạo khi JVM khởi động. Mục đích duy nhất của memory area này là lưu object instance; gần như mọi object instance và array đều được cấp phát bộ nhớ tại đây.
Trong Java, “gần như” mọi object đều được cấp phát trên heap. Tuy nhiên, JIT compiler có thể thực hiện scalar replacement dựa trên escape analysis, từ đó loại bỏ việc cấp phát thực tế một số object. Không thể mô tả đơn giản việc tối ưu hóa này của HotSpot là “object được cấp phát trực tiếp trên Java Virtual Machine Stack”.
Java Heap là vùng chính do garbage collector quản lý, vì vậy còn được gọi là GC Heap (Garbage Collected Heap). Từ góc độ garbage collection, vì các collector hiện nay cơ bản đều dùng thuật toán garbage collection phân thế hệ, Java Heap còn có thể chia nhỏ thành Young Generation và Old Generation; chi tiết hơn nữa có Eden, Survivor, Old và các space khác. Mục đích của việc chia nhỏ là thu hồi bộ nhớ tốt hơn hoặc cấp phát bộ nhớ nhanh hơn.
Trong HotSpot của JDK 7 và các phiên bản cũ hơn, GC thường được giới thiệu qua ba phần sau. Permanent Generation là implementation của method area, không thuộc Java Heap:
- Young Generation
- Old Generation
- Permanent Generation
Eden area và hai Survivor area S0, S1 trong hình dưới đều thuộc Young Generation; tầng ở giữa thuộc Old Generation, tầng dưới cùng thuộc Permanent Generation.

Từ JDK 8, PermGen (Permanent Generation) đã được thay thế bằng Metaspace. Metaspace sử dụng native memory. (Phần method area sẽ giới thiệu chi tiết nội dung này.)
Trong đa số trường hợp, object trước hết được cấp phát trong Eden area. Sau một lần garbage collection ở Young Generation, nếu object vẫn còn sống thì nó sẽ vào S0 hoặc S1, đồng thời age của object tăng 1 (sau khi đi từ Eden area -> Survivor area, age ban đầu của object thành 1). Khi object đạt promotion age threshold, nó sẽ vào Old Generation. Threshold này chịu ảnh hưởng đồng thời của collector, adaptive policy và parameter -XX:MaxTenuringThreshold; giá trị mặc định không phải 15 với mọi collector. Với generational collector dùng age field 4 bit, giới hạn trên của parameter này là 15.
MaxTenuringThreshold of 20 is invalid; must be between 0 and 15Tại sao age chỉ có thể là 0-15?
Vì vùng ghi age nằm trong object header, vùng này thường có kích thước 4 bit. 4 bit này biểu diễn được binary number lớn nhất là 1111, tức decimal 15. Vì vậy age của object bị giới hạn từ 0 đến 15.
Ở đây kết hợp object layout để giới thiệu chi tiết hơn.
Trong HotSpot JVM, object layout trong bộ nhớ có thể chia thành 3 vùng: object header (Header), instance data (Instance Data) và alignment padding (Padding). Object header gồm hai phần: mark field (Mark Word) và type pointer (Klass Word). Phần giới thiệu chi tiết về object memory layout sẽ được trình bày sau, nên không lặp lại ở đây.
Thông tin age được lưu trong mark field (mark field còn lưu các thông tin khác của object như hash code, lock state...). Đoạn HotSpot source code cũ markOop.hpp dưới đây thể hiện cấu trúc của mark word (Mark Word) khi đó; implementation tương ứng và object header layout của JDK hiện đại đã thay đổi:

Có thể thấy age của object thực sự chiếm 4 bit.
🐛 Đính chính (tham khảo: issue552): “Khi HotSpot duyệt qua mọi object, nó cộng dồn kích thước mà chúng chiếm theo thứ tự age tăng dần. Khi tổng kích thước đến một age nào đó vượt quá một nửa Survivor area, nó lấy giá trị nhỏ hơn giữa age đó và
MaxTenuringThresholdlàm promotion age threshold mới”.Code tính age động như sau
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { // survivor_capacity là kích thước của survivor space size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);// TargetSurvivorRatio là 50 size_t total = 0; uint age = 1; while (age < table_size) { total += sizes[age];// mảng sizes là kích thước object của mỗi age if (total > desired_survivor_size) break; age++; } uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; ... }
Lỗi thường gặp nhất ở heap là OutOfMemoryError. Lỗi này có thể biểu hiện dưới một số dạng, ví dụ:
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded: lỗi này xảy ra khi JVM dành quá nhiều thời gian thực hiện garbage collection nhưng chỉ thu hồi được rất ít heap space.java.lang.OutOfMemoryError: Java heap space: khi tạo object mới, nếu heap memory không đủ chỗ chứa object thì lỗi này sẽ xảy ra (liên quan đến dung lượng heap tối đa được cấu hình và chịu giới hạn bởi bộ nhớ vật lý. Dung lượng heap tối đa có thể cấu hình bằng parameter-Xmx; nếu không cấu hình đặc biệt thì sẽ dùng giá trị mặc định, xem Default Java 8 max heap size).- ……
Method Area
Method area là một vùng logic trong JVM runtime data area, là vùng bộ nhớ được mọi thread dùng chung.
Java Virtual Machine Specification chỉ quy định khái niệm method area và chức năng của nó; cách triển khai cụ thể là việc JVM tự quyết định. Nói cách khác, các JVM implementation khác nhau có cách triển khai method area khác nhau.
Khi JVM load một class, nó parse thông tin tương ứng từ Class file rồi lưu metadata đó vào method area. Cụ thể, method area chủ yếu lưu các dữ liệu cốt lõi sau:
- Class metadata: gồm cấu trúc đầy đủ của class như class name, parent class, interface đã implement, access modifier và thông tin chi tiết của field, method (name, type, modifier...).
- Method bytecode: chuỗi instruction gốc của mỗi method.
- Runtime constant pool: mỗi class có một pool riêng, được chuyển đổi từ constant pool trong Class file, dùng để lưu các literal do compiler tạo ra và symbol reference đến type, field, method.
Cần đặc biệt lưu ý: các loại dữ liệu sau đây tuy về logic liên quan đến class, nhưng trong HotSpot JVM không được lưu trong method area:
- Static variable (Static Variables): từ JDK 7, static variable của class được lưu trong Java Heap cùng với object
java.lang.Classtương ứng. - String constant pool (String Pool): cũng từ JDK 7, string constant pool được chuyển vào Java Heap.
- Code cache sau khi JIT compiler biên dịch (JIT Code Cache): JIT compiler biên dịch bytecode của hot method thành native machine code và lưu trong một memory area độc lập có tên “Code Cache”, không phải bản thân method area. Cách làm này nhằm thực hiện execution và memory management hiệu quả hơn.

Quan hệ giữa method area, Permanent Generation và Metaspace là gì? Quan hệ giữa method area với Permanent Generation và Metaspace khá giống quan hệ giữa interface và class trong Java: class triển khai interface; ở đây có thể xem class là Permanent Generation và Metaspace, interface là method area. Nói cách khác, Permanent Generation và Metaspace là hai implementation của method area trong JVM specification của HotSpot JVM. Permanent Generation là implementation method area trước JDK 1.8; từ JDK 1.8 trở đi, implementation method area chuyển thành Metaspace.

Tại sao thay Permanent Generation (PermGen) bằng Metaspace (MetaSpace)?
Hình dưới đây lấy từ Understanding the JVM, bản 3, mục 2.2.5.

- Dung lượng Permanent Generation chịu giới hạn trên
-XX:MaxPermSize; giới hạn này có thể cấu hình. Metaspace chuyển sang dùng native memory và có thể giới hạn bằng-XX:MaxMetaspaceSize. Cả hai đều có thể bị tràn; rủi ro thực tế phụ thuộc configuration và tốc độ tăng của class metadata.
Khi Metaspace bị tràn sẽ nhận được lỗi sau:
java.lang.OutOfMemoryError: Metaspace
Bạn có thể dùng -XX:MaxMetaspaceSize để đặt maximum Metaspace size. Nếu không đặt, class metadata space có thể tiếp tục tăng cho đến khi bị giới hạn bởi available native memory và các điều kiện khác. -XX:MetaspaceSize không phải initial capacity của Metaspace mà là initial high-water threshold để trigger metadata GC; sau đó JVM sẽ điều chỉnh động threshold này.
Metaspace lưu class metadata. Số class metadata được load không còn do
MaxPermSizekiểm soát mà do available space thực tế của system kiểm soát, vì vậy có thể load nhiều class hơn.Khi merge code của HotSpot và JRockit trong JDK 8, JRockit chưa từng có thành phần gọi là Permanent Generation. Sau khi merge, không cần thiết phải thiết lập thêm một khu vực Permanent Generation như vậy.
Permanent Generation tạo ra độ phức tạp không cần thiết cho GC và hiệu quả thu hồi tương đối thấp.
Các parameter thường dùng của method area là gì?
Trước JDK 1.8, khi Permanent Generation chưa bị loại bỏ hoàn toàn, kích thước method area thường được điều chỉnh bằng các tham số sau.
-XX:PermSize=N // kích thước ban đầu của method area (Permanent Generation)
-XX:MaxPermSize=N // kích thước tối đa của method area (Permanent Generation), vượt quá giá trị này sẽ throw exception OutOfMemoryError: java.lang.OutOfMemoryError: PermGenTương đối mà nói, garbage collection trong vùng này khá ít xảy ra, nhưng không phải dữ liệu đã vào method area sẽ “tồn tại vĩnh viễn”.
Trong JDK 1.8, method area (Permanent Generation của HotSpot) đã bị loại bỏ hoàn toàn (quá trình này đã bắt đầu từ JDK 1.7), thay vào đó là Metaspace; Metaspace sử dụng native memory. Dưới đây là một số parameter thường dùng:
-XX:MetaspaceSize=N // đặt initial high-water threshold trigger metadata GC
-XX:MaxMetaspaceSize=N // đặt maximum Metaspace sizeKhác biệt lớn với Permanent Generation là nếu không chỉ định MaxMetaspaceSize, Metaspace không có giới hạn trên cố định do parameter này đặt. Việc class metadata tiếp tục tăng có thể tiêu thụ lượng lớn native memory.
Runtime Constant Pool
Ngoài các thông tin mô tả như phiên bản, field, method và interface của class, Class file còn có constant pool table (Constant Pool Table) dùng để lưu các literal (Literal) và symbol reference (Symbolic Reference) do compiler tạo ra.
Literal là cách biểu diễn giá trị cố định trong source code; chỉ cần nhìn literal là có thể biết ý nghĩa giá trị của nó. Literal gồm integer, floating-point number và string literal. Các symbol reference thường gặp gồm class symbol reference, field symbol reference, method symbol reference và interface method symbol.
Giải thích về symbol reference và direct reference trong mục 7.34, bản 3 của Understanding the JVM như sau:

Constant pool table được lưu vào runtime constant pool của method area sau khi class loading.
Chức năng của runtime constant pool tương tự symbol table trong ngôn ngữ lập trình truyền thống, dù nó chứa dữ liệu phong phú hơn symbol table điển hình.
Vì runtime constant pool là một phần của method area, đương nhiên nó chịu giới hạn bộ nhớ của method area. Khi constant pool không thể xin thêm bộ nhớ, lỗi OutOfMemoryError sẽ được throw.
String Constant Pool
String constant pool là một vùng được JVM dành riêng cho string (class String) nhằm nâng cao performance và giảm mức tiêu thụ bộ nhớ, với mục đích chính là tránh tạo string trùng lặp.
// 1. Tìm string object "ab" trong string constant pool; nếu chưa có thì tạo "ab" và đặt vào string constant pool
// 2. Gán reference của string object "ab" cho aa
String aa = "ab";
// Trả về trực tiếp string object "ab" trong string constant pool và gán cho reference bb
String bb = "ab";
System.out.println(aa==bb); // trueImplementation của string constant pool trong HotSpot JVM có thể xem tại src/hotspot/share/classfile/stringTable.cpp. StringTable native của HotSpot hiện đại dùng concurrent hash table và dùng weak handle để lưu string object trên heap, hỗ trợ expansion, rehash và cleanup các entry không còn hiệu lực; -XX:StringTableSize dùng để đặt số bucket ban đầu. Vì vậy, cách nói “fixed-length array + linked list” chỉ áp dụng cho một số implementation cũ, không thể dùng để khái quát HotSpot hiện tại.
Trước JDK 1.7, string constant pool được lưu trong Permanent Generation. JDK 1.7 chuyển string constant pool và class static variable vào Java Heap.


Tại sao JDK 1.7 chuyển string constant pool vào heap?
Chủ yếu vì hiệu quả thu hồi của GC ở Permanent Generation (implementation của method area) quá thấp, chỉ thực hiện GC khi full heap collection (Full GC). Chương trình Java thường có rất nhiều string được tạo và chờ thu hồi; đặt string constant pool vào heap giúp thu hồi string memory hiệu quả và kịp thời hơn.
Câu hỏi liên quan: JVM constant pool lưu object hay reference? - Câu trả lời của RednaxelaFX - Zhihu
Cuối cùng, dưới đây là một đoạn lời của thầy Zhou Zhiming trong sample code và errata của Understanding the JVM (bản 3), tại issue#112 của GitHub repository:
Runtime constant pool, method area và string constant pool đều là khái niệm logic không thay đổi theo JVM implementation, có tính public và abstract; Metaspace và Heap là khái niệm vật lý liên quan đến một JVM implementation cụ thể, có tính private và concrete.
Direct Memory
Direct memory là một loại buffer bộ nhớ đặc biệt, không được cấp phát trong Java Heap hay method area mà được cấp phát trong native memory. Cơ chế cấp phát cụ thể là implementation detail của JVM, không đồng nghĩa với việc bắt buộc phải cấp phát qua JNI.
Direct memory không thuộc JVM runtime data area, cũng không phải memory area được JVM specification định nghĩa, nhưng vùng bộ nhớ này được sử dụng thường xuyên. Nó cũng có thể gây ra lỗi OutOfMemoryError.
NIO (New I/O) được thêm vào trong JDK 1.4, giới thiệu cách I/O dựa trên channel (Channel) và buffer (Buffer). Direct byte buffer có thể được cấp phát bên ngoài Java Heap và thao tác thông qua object DirectByteBuffer trong Java Heap. JVM sẽ cố gắng thực hiện native I/O trực tiếp trên buffer này, từ đó giảm việc sao chép dữ liệu giữa các intermediate buffer trong một số trường hợp. Trong NIO, chỉ một số phần như selectable channel và Selector cung cấp khả năng non-blocking I/O; không thể mở rộng toàn bộ NIO thành Non-Blocking I/O.
Direct memory không tính vào Java Heap nhưng vẫn chịu giới hạn của tổng native memory, processor addressing space và các điều kiện khác. Với NIO direct buffer, có thể dùng -XX:MaxDirectMemorySize để giới hạn tổng cấp phát.
Khái niệm tương tự còn có off-heap memory. Một số bài viết đồng nhất direct memory với off-heap memory, nhưng theo tôi cách này không hoàn toàn chính xác.
Off-heap memory là tên gọi chung cho bộ nhớ được cấp phát bên ngoài Java Heap, nền tảng bên dưới thường sử dụng native memory do operating system cung cấp. Việc nó có chịu sự quản lý của JVM hoặc JDK hay không và được quản lý thế nào phụ thuộc vào cách cấp phát cụ thể. Ví dụ, trong OpenJDK/HotSpot, native memory tương ứng với DirectByteBuffer liên kết với buffer object trong Java Heap, và sau khi object trở nên unreachable sẽ tham gia cleanup qua cơ chế như Cleaner. Vì thời điểm cleanup không do application trực tiếp kiểm soát, dùng off-heap memory có thể giảm pressure lên Java Heap nhưng vẫn cần chú ý native memory leak và OutOfMemoryError.
Khám phá object trong HotSpot JVM
Phần trên đã khái quát về bộ nhớ của JVM. Bây giờ hãy tìm hiểu chi tiết toàn bộ quá trình HotSpot JVM cấp phát, bố cục và truy cập object trong Java Heap.
Object Creation
Bạn nên tự viết lại quy trình tạo Java object và nắm được mỗi bước thực hiện gì.
Step1: Class Loading Check
Khi JVM gặp một lệnh new, trước tiên nó kiểm tra xem trong constant pool có thể định vị class symbol reference tương ứng với parameter của instruction này hay không, đồng thời kiểm tra class do symbol reference này đại diện đã được load, resolve và initialize chưa. Nếu chưa, trước hết phải thực hiện class loading process tương ứng.
Step2: Memory Allocation
Sau khi class loading check hoàn tất, JVM sẽ allocate memory cho object mới. Sau khi class loading hoàn tất, kích thước memory cần cho object đã được xác định; nhiệm vụ allocate space cho object tương đương với việc chia một memory space có kích thước xác định từ Java Heap. Có hai allocation method là “bump-the-pointer” và “free list”. Chọn allocation method nào phụ thuộc vào heap có contiguous hay không; heap có contiguous hay không lại phụ thuộc vào garbage collector được sử dụng có chức năng compact hay không.
Hai allocation method (nội dung bổ sung, cần nắm vững):
- Bump-the-pointer:
- Trường hợp áp dụng: heap contiguous (không có memory fragmentation).
- Nguyên lý: toàn bộ used memory được gom về một phía, unused memory ở phía còn lại; giữa hai phía có một boundary pointer, chỉ cần di chuyển pointer theo hướng unused memory một đoạn bằng object memory size.
- GC collector sử dụng allocation method này: Serial, ParNew
- Free list:
- Trường hợp áp dụng: heap không contiguous.
- Nguyên lý: JVM duy trì một list ghi lại các memory block còn usable. Khi allocate, nó tìm một memory block đủ lớn để chia cho object instance rồi update record trong list.
- GC collector sử dụng allocation method này: CMS
Chọn một trong hai method trên phụ thuộc vào Java Heap có contiguous hay không. Java Heap có contiguous hay không phụ thuộc vào algorithm của GC collector là “mark-sweep” hay “mark-compact” (còn gọi là “mark-compress”). Cần lưu ý rằng memory của copying algorithm cũng contiguous.
Vấn đề concurrency khi memory allocation (nội dung bổ sung, cần nắm vững)
Khi tạo object, một vấn đề rất quan trọng là thread safety, vì trong thực tế object được tạo rất thường xuyên. JVM phải đảm bảo thread safety; thông thường JVM dùng hai cách sau để đảm bảo điều đó:
- CAS + retry on failure: CAS là một implementation của optimistic lock. Optimistic lock nghĩa là mỗi lần không lock mà giả định không có conflict để hoàn thành operation; nếu thất bại do conflict thì retry cho đến khi thành công. JVM dùng CAS kết hợp retry on failure để đảm bảo atomicity của update operation.
- TLAB: cấp phát trước một memory block trong Eden area cho mỗi thread. Khi JVM cấp phát memory cho object trong thread, trước hết cấp phát trong TLAB; nếu object lớn hơn remaining memory của TLAB hoặc TLAB đã hết memory, JVM mới dùng CAS như trên để cấp phát memory.
Step3: Zero-Value Initialization
Sau khi memory allocation hoàn tất, JVM cần initialize toàn bộ allocated memory space thành zero (không bao gồm object header). Operation này đảm bảo instance field của object có thể được dùng trực tiếp trong Java code mà không cần gán initial value; chương trình có thể đọc zero value tương ứng với data type của field.
Step4: Set Object Header
Sau khi zero-value initialization hoàn tất, JVM phải thực hiện các thiết lập cần thiết cho object, chẳng hạn object là instance của class nào, làm thế nào để tìm class metadata, object hash code và object GC generational age. Các thông tin này được lưu trong object header. Ngoài ra, object header layout cụ thể liên quan đến JDK version và JVM configuration; ví dụ biased lock mặc định bị disable từ JDK 15, các implementation liên quan sau đó cũng bị remove.
Step5: Execute init Method
Sau khi các công việc trên hoàn tất, xét từ góc nhìn JVM thì một object mới đã được tạo; nhưng từ góc nhìn chương trình Java, object creation mới bắt đầu, method <init> vẫn chưa execute và mọi field vẫn bằng zero. Vì vậy, thông thường sau khi execute new instruction sẽ tiếp tục execute method <init>, initialize object theo ý muốn của programmer; khi đó object thực sự usable mới được xem là tạo hoàn chỉnh.
Object Memory Layout
Trong HotSpot JVM, object layout trong bộ nhớ có thể chia thành 3 vùng: object header (Header), instance data (Instance Data) và alignment padding (Padding).
Object header gồm hai phần thông tin:
- Mark field (Mark Word): dùng để lưu runtime data của object như hash code (HashCode), GC generational age, lock state... Thread ID và timestamp của biased lock chỉ áp dụng cho HotSpot version cũ còn implement và enable biased lock.
- Type pointer (Klass pointer): pointer từ object đến class metadata của nó; JVM dùng pointer này để xác định object là instance của class nào.
Instance data là thông tin có giá trị mà object thực sự lưu trữ, cũng là nội dung của các field thuộc những type được định nghĩa trong program.
Alignment padding không nhất thiết tồn tại và không có ý nghĩa đặc biệt, chỉ dùng để chiếm chỗ. HotSpot allocate object theo object alignment boundary, default thường là 8 byte và cũng có thể chịu ảnh hưởng của configuration như -XX:ObjectAlignmentInBytes. Vì vậy tổng object size cần được pad đến alignment boundary; bản thân object header không đảm bảo lúc nào cũng là bội số nguyên của 8 byte, ví dụ object header thường có size 12 byte khi enable compressed class pointer.
Object Access Location
Object được tạo ra để sử dụng. Chương trình Java thao tác object cụ thể trên heap thông qua reference data trên stack. Cách truy cập object phụ thuộc vào JVM implementation. Hiện nay có hai cách truy cập phổ biến: handle và direct pointer.
Handle
Nếu dùng handle, Java Heap sẽ dành một memory area làm handle pool. reference lưu handle address của object, còn handle chứa address cụ thể của instance data và object type data.

Direct Pointer
Nếu dùng direct pointer access, reference lưu trực tiếp address của object.

Hai cách truy cập object này đều có ưu điểm riêng. Ưu điểm lớn nhất của handle access là reference lưu một handle address ổn định; khi object được move, chỉ cần thay đổi instance data pointer trong handle, không cần sửa bản thân reference. Ưu điểm lớn nhất của direct pointer access là tốc độ nhanh vì tiết kiệm chi phí định vị pointer một lần.
HotSpot JVM chủ yếu dùng direct pointer access để truy cập object.
Tham khảo
- Understanding the JVM: Advanced Features and Best Practices (Second Edition)
- Write a Java Virtual Machine from Scratch
- Chapter 2. The Structure of the Java Virtual Machine: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html
- JVM stack frame internal structure - dynamic linking: https://chenxitag.com/archives/368
- Khi
new String("literal") thì “literal” vào string constant pool lúc nào? - Câu trả lời của Cô gái gỗ - Zhihu: https://www.zhihu.com/question/55994121/answer/147296098 - JVM constant pool lưu object hay reference? - Câu trả lời của RednaxelaFX - Zhihu: https://www.zhihu.com/question/57109429/answer/151717241
- http://www.pointsoftware.ch/en/under-the-hood-runtime-data-areas-javas-memory-model/
- https://dzone.com/articles/jvm-permgen-–-where-art-thou
- https://stackoverflow.com/questions/9095748/method-area-and-permgen
Lời cuối
Nếu nội dung hữu ích với bạn, hãy tiện tay tặng JavaGuide một Star miễn phí để ủng hộ: GitHub | Gitee.
JavaGuide đã được duy trì gần bảy năm, tích lũy 6100+ commit, với sự chung tay hoàn thiện của 620+ contributor. Star, phản hồi và PR của bạn đều là động lực để dự án tiếp tục cập nhật.
Nếu bạn đang chuẩn bị phỏng vấn backend / phát triển ứng dụng AI, có thể tham khảo Knowledge Planet của tôi, bao gồm các project thực tế về backend và AI, tối ưu CV, hỏi đáp 1-1 và tài liệu về các trọng điểm thường gặp, đã được duy trì liên tục sáu năm.
