Quản lý bộ nhớ trong JVM - Heap, Stack và Runtime Data Areas
Khám phá cách JVM tổ chức và quản lý bộ nhớ thông qua Runtime Data Areas như Heap, Stack, Method Area, Metaspace và PC Register. Hiểu vòng đời của object, cơ chế Garbage Collection và cách từng vùng nhớ ảnh hưởng đến hiệu năng cũng như khả năng mở rộng của ứng dụng Java.

Giới thiệu
Trong bài viết trước, chúng ta đã tìm hiểu cách JVM nạp và quản lý các class thông qua Class Loader Subsystem. Chúng ta cũng đã tìm hiểu về Bootstrap, Platform và Application ClassLoader, cũng như cách Lazy Loading mang lại sự linh hoạt cho Java.
Nhưng sau khi các class được nạp, chúng sẽ được lưu ở đâu trong bộ nhớ? Và khi bạn tạo object, gọi method hoặc quản lý thread — toàn bộ dữ liệu đó sẽ được lưu ở đâu?
Đây chính là lúc Runtime Data Areas của JVM phát huy vai trò của mình. Những vùng nhớ này được thiết kế cẩn thận nhằm:
- Tách biệt vùng nhớ dùng chung (shared memory) có thể được truy cập bởi mọi thread với vùng nhớ riêng của từng thread (thread-local memory)
- Hỗ trợ việc thực thi bytecode một cách hiệu quả
- Cho phép Garbage Collection hoạt động tự động
- Đảm bảo an toàn bộ nhớ trong môi trường thực thi đồng thời (concurrent)
Hiểu rõ các vùng nhớ này là nền tảng để:
- Tối ưu hiệu năng: Biết object được cấp phát ở đâu và Garbage Collector ảnh hưởng đến ứng dụng như thế nào
- Debug các vấn đề về bộ nhớ: Phân tích OutOfMemoryError, memory leak và các lần GC pause
- Đảm bảo Thread Safety: Hiểu vùng nhớ nào cần được đồng bộ hóa (synchronization)
- Thiết kế ứng dụng: Đưa ra các quyết định hợp lý về vòng đời object và chiến lược caching
Trong bài viết này, bạn sẽ hiểu rõ kiến trúc bộ nhớ của JVM, từ mô hình phân chia thế hệ của Heap (Generational Heap), Stack Frame, Metaspace cho đến các vùng nhớ riêng của từng thread giúp JVM thực thi đồng thời.
Runtime Data Area
Runtime Data Area là gì?
Runtime Data Areas là tập hợp các vùng nhớ được JVM tạo ra và quản lý trong suốt vòng đời của một ứng dụng.
Java Virtual Machine (JVM) chia bộ nhớ thành nhiều vùng riêng biệt nhằm phân loại không gian lưu trữ theo mục đích sử dụng. Ý tưởng cốt lõi của cách thiết kế này là giúp JVM nhanh chóng xác định cách một object được sử dụng và chỉ tập trung xử lý những object thực sự cần thiết.
Các vùng nhớ này được thiết kế để đảm bảo:
- Sự tách biệt giữa các thread
- Hiệu năng thực thi bytecode
- An toàn bộ nhớ
- Tối ưu Garbage Collection (GC)
- Khả năng hoạt động đa nền tảng (thông qua mô hình trừu tượng của JVM)
Mỗi vùng nhớ đảm nhận một vai trò riêng trong:
- nạp class
- thực thi bytecode
- lưu trữ object
- lưu trữ local variable
- quản lý việc gọi method
- hỗ trợ native code
Một số vùng nhớ được chia sẻ giữa tất cả các thread, trong khi những vùng khác được tạo riêng cho từng thread nhằm đảm bảo tính an toàn và cô lập dữ liệu.
Kiến trúc tổng quan của Runtime Data Areas — Java Memory Model:

Shared Areas
JVM có một số vùng nhớ dùng chung (shared data areas) có thể được truy cập bởi tất cả các thread đang chạy trong JVM. Vì vậy, nhiều thread có thể đồng thời truy cập vào các vùng nhớ này.
HEAP
HEAP là gì?
Heap là vùng nhớ dùng chung của JVM, được sử dụng để lưu trữ toàn bộ object trong JVM. Mỗi JVM chỉ có một Heap duy nhất, vì vậy Heap được chia sẻ giữa tất cả các thread. Thiết kế này giúp tiết kiệm bộ nhớ và cho phép nhiều thread cùng thao tác trên một object (khi cần sẽ sử dụng cơ chế đồng bộ hóa phù hợp).
Khi một object được tạo bằng từ khóa new, JVM sẽ cấp phát một vùng nhớ trên Heap để lưu trữ object đó. Heap lưu trữ instance data và reference field, nhưng không lưu trữ code của method — phần triển khai của method được lưu trong Method Area.
Heap được khởi tạo khi JVM bắt đầu chạy.
Bộ nhớ Heap được quản lý bởi Garbage Collector (GC) — hệ thống quản lý bộ nhớ tự động có nhiệm vụ thu hồi các object không còn được sử dụng trong JVM.
Heap được chia như thế nào?
Trong JVM cổ điển (HotSpot trước Java 8), Heap được chia thành:
- Young Generation
- Old Generation
- Permanent Generation (PermGen) → lưu trữ metadata của class

Trong JVM hiện đại (Java 8+):
- Young Generation
- Old Generation
- Metaspace (thay thế PermGen và nằm trong native memory)

Trong phần này, chúng ta sẽ tập trung vào Young Generation và Old Generation — hai vùng nhớ quan trọng nhất, quyết định trực tiếp đến hiệu năng của ứng dụng.
Young Generation
Young Generation là vùng nhớ nơi tất cả các object mới được tạo sẽ được cấp phát. Khi Young Generation đầy, JVM sẽ kích hoạt Garbage Collection. Quá trình Garbage Collection diễn ra tại vùng nhớ này được gọi là Minor Garbage Collection.
Young Generation được chia thành ba vùng: Eden và hai Survivor Space (S0 và S1).
Eden (vùng cấp phát object)
- Khoảng 90% object mới được tạo sẽ được cấp phát tại đây.
- Khi Eden đầy → Minor GC sẽ được kích hoạt.
Survivor 0 & Survivor 1
- Sau mỗi chu kỳ GC, các object còn sống sẽ được sao chép từ Eden → S0 → S1 → và cuối cùng được chuyển sang Old Generation.
- JVM sử dụng moving algorithm, đảm bảo luôn có một Survivor Space trống.
Đặc điểm chính của Young Generation:
- Hầu hết các object mới được tạo sẽ nằm trong Eden Space.
- Khi Eden đầy, Minor GC sẽ được kích hoạt và các object còn sống sẽ được chuyển sang một trong hai Survivor Space.
- Minor GC tiếp tục đánh giá các object còn sống và chuyển chúng sang Survivor Space còn lại, đảm bảo rằng luôn có một Survivor Space trống.
- Những object sống sót qua nhiều chu kỳ GC cuối cùng sẽ được chuyển lên Old Generation.
Old Generation (Tenured Space)
Đây là nơi lưu trữ các object có vòng đời dài — những object đã "tốt nghiệp" từ Young Generation.
Đặc điểm:
- Lưu trữ các object có tuổi thọ dài: session, cache, singleton
- GC diễn ra chậm hơn (Major GC / Full GC)
- Sử dụng thuật toán Mark–Sweep–Compact
Khi nào một object được chuyển sang Old Generation?
- Object sống sót qua nhiều chu kỳ GC trong Young Generation
- Hoặc Eden chịu áp lực cấp phát quá lớn → object được chuyển lên sớm (early promotion)
- Hoặc object có kích thước quá lớn → bỏ qua Eden và được cấp phát trực tiếp trong Old Generation
Các rủi ro cần lưu ý:
- Full GC có thể tạm dừng ứng dụng từ vài mili giây đến vài giây
- Nếu Old Generation đầy → JVM sẽ ném ra lỗi
java.lang.OutOfMemoryError: Java heap space
Vòng đời của Object trong HEAP

Vòng đời này cho phép Garbage Collector thu hồi hiệu quả các object có vòng đời ngắn, đồng thời giữ lại các object có vòng đời dài trong Old Generation, giúp cải thiện hiệu năng và giảm nguy cơ xảy ra memory leak.
Permanent Generation (PERM)
Trước Java 8, JVM có một vùng nhớ đặc biệt gọi là Permanent Generation (PermGen). Vùng nhớ này được sử dụng để lưu trữ metadata của class.
Việc dự đoán lượng bộ nhớ cần thiết cho vùng nhớ này khá khó khăn. Khi ước lượng không chính xác, JVM thường sẽ ném ra lỗi:
java.lang.OutOfMemoryError: PermGen space
Nếu nguyên nhân không phải do memory leak thực sự, cách xử lý phổ biến là tăng kích thước của PermGen, ví dụ giới hạn tối đa là 256 MB:
java -XX:MaxPermSize=256mMột số điểm cần lưu ý về Permanent Generation:
- Chỉ tồn tại trong các phiên bản Java trước Java 8.
- Được sử dụng để lưu trữ metadata của class.
- Rất khó dự đoán lượng bộ nhớ cần thiết.
Metaspace
Do việc dự đoán dung lượng bộ nhớ cho metadata khá phức tạp và bất tiện, Permanent Generation đã bị loại bỏ từ Java 8 và được thay thế bằng Metaspace. Kể từ phiên bản này, hầu hết các thành phần liên quan được chuyển sang sử dụng cơ chế mới.
Các Class Definition được nạp vào Metaspace. Metaspace nằm trong native memory (off-heap), vì vậy nó không ảnh hưởng trực tiếp đến vùng nhớ Java Heap nơi lưu trữ object. Theo mặc định, kích thước của Metaspace chỉ bị giới hạn bởi lượng native memory mà tiến trình Java có thể sử dụng. Thiết kế này giúp tránh trường hợp chỉ cần nạp thêm một class cũng khiến ứng dụng gặp lỗi:
java.lang.OutOfMemoryError: PermGen space
Một số điểm cần lưu ý về Metaspace:
- Nếu để Metaspace tăng không giới hạn, hệ thống có thể xảy ra hiện tượng swapping mạnh và thất bại khi cấp phát native memory.
- Nếu muốn giới hạn để đảm bảo an toàn, bạn có thể cấu hình kích thước tối đa của Metaspace, ví dụ 256 MB:
java -XX:MaxMetaspaceSize=256mMethod Area
Tương tự Heap, mỗi JVM chỉ có một Method Area duy nhất, vì vậy vùng nhớ này được chia sẻ giữa tất cả các thread. Thiết kế này giúp tiết kiệm bộ nhớ và tránh việc lưu trữ trùng lặp metadata của cùng một class được nạp bởi Class Loader hierarchy.
| Kiểu dữ liệu | Mô tả |
|---|---|
| Class Metadata | Tên class, cấu trúc field, thông tin method |
| Runtime Constant Pool | Hằng số, method reference, symbolic reference, literal |
| Class References | Tham chiếu đến superclass, interface, annotation, ... |
Nói ngắn gọn, Method Area là nơi JVM lưu trữ bản thiết kế (blueprint) của các class cần thiết để thực thi chương trình.
Runtime Constant Pool (RCP)
Là một phần quan trọng của Method Area, Runtime Constant Pool chứa:
- Các hằng số đã được biên dịch (int, long, string, ...)
- Các tham chiếu đến class, field và method
Khi một class được nạp, Runtime Constant Pool (RCP) của class đó cũng được tạo và điền đầy đủ các hằng số liên quan. Điều này cho phép JVM và các method truy cập nhanh vào các tài nguyên này mà không cần phải phân tích lại bytecode nhiều lần.
Lưu ý:
Mặc dù Runtime Constant Pool có thể lưu trữ các tham chiếu đến String literal, bản thân Runtime Constant Pool vẫn nằm trong Method Area và được tạo riêng cho từng class hoặc interface tại runtime. Trong khi đó, String Pool nằm trên Heap và là một pool toàn cục được chia sẻ giữa tất cả các class.
Per-thread Data Areas
Bên cạnh các vùng nhớ dùng chung (shared memory areas) mà tất cả các thread đều có thể truy cập, JVM còn tạo ra các vùng nhớ riêng cho từng thread (thread-local memory areas) để lưu trữ dữ liệu đặc thù của từng thread. Những vùng nhớ này giúp nhiều thread có thể thực thi đồng thời bằng cách cô lập trạng thái thực thi và dữ liệu cục bộ của từng thread.
PC Register
PC (Program Counter) Register là một thanh ghi đặc biệt được gắn với từng thread trong JVM. Nhiệm vụ của nó là lưu địa chỉ của chỉ thị bytecode tiếp theo sẽ được CPU (thông qua JVM) thực thi. Nó đóng vai trò như một "con trỏ điều hướng", kiểm soát luồng thực thi và đảm bảo các chỉ thị được thực hiện đúng thứ tự hoặc chuyển đến vị trí khác theo đúng logic của chương trình.
Mỗi thread đang chạy trong JVM đều có một vùng nhớ riêng gọi là JVM Stack, và mỗi Stack Frame trong stack đó đều có PC Register tương ứng. Điều này cho phép các thread thực thi độc lập bằng cách:
- Theo dõi tiến trình thực thi của Stack Frame hiện tại
- Ghi nhớ chỉ thị bytecode hiện tại và chỉ thị bytecode tiếp theo cần được thực thi
Vì vậy, PC Register là một phần của thread context, được sử dụng để lưu vị trí bytecode của từng thread và hoàn toàn tách biệt, không bao giờ được chia sẻ giữa các thread.
Mối quan hệ giữa PC Register và OS Thread Scheduler
Trên mọi hệ điều hành (Linux, Windows, macOS):
- Một CPU không thể thực sự thực thi nhiều thread đồng thời trên cùng một core.
- Hệ điều hành sẽ quyết định:
- Thread nào được phép chạy
- Chạy trên core nào
- Chạy trong bao lâu
- Khi nào chuyển sang thread khác (context switch)
→ JVM không điều khiển việc lập lịch thread, mà hoàn toàn dựa vào hệ điều hành.
Trách nhiệm của JVM là chuẩn bị một execution context riêng cho mỗi thread, bao gồm:
- Stack: chứa các Stack Frame của các method đang được gọi
- Local Variables: các biến cục bộ của từng method, chỉ thuộc về thread đó
- PC Register: lưu chỉ thị bytecode tiếp theo mà thread sẽ thực thi
Điều này cho thấy PC Register là một phần của thread context, được hệ điều hành lưu lại và khôi phục trong mỗi lần context switch.
Điều gì xảy ra trong một Context Switch?
Hãy xem một ví dụ thực tế: Thread A đang chạy và hệ điều hành (OS) quyết định chuyển CPU sang Thread B. Lúc này, hệ điều hành sẽ thực hiện một context switch — và đây chính là lúc vai trò của PC Register trở nên đặc biệt quan trọng.
-
Bước 1: OS tạm dừng Thread A — lưu trạng thái CPU
Để có thể tiếp tục thực thi Thread A sau này, hệ điều hành phải lưu toàn bộ execution context của Thread A, bao gồm:
- Các CPU Register (eax, ebx, r1, r2... tùy theo kiến trúc CPU)
- Stack Pointer (SP) → trỏ đến Stack Frame hiện tại trong Stack của Thread A
- CPU Flags (carry, zero, sign, ...)
- PC Register của Thread A → thành phần quan trọng nhất, lưu địa chỉ của chỉ thị bytecode hoặc instruction tiếp theo sẽ được thực thi
-
Bước 2: OS chuyển sang Thread B — khôi phục trạng thái của Thread B
- Nạp PC Register của Thread B → cho CPU biết Thread B đang thực thi đến đâu
- Khôi phục các CPU Register của Thread B
- Khôi phục Stack Pointer của Thread B
- CPU tiếp tục thực thi từ đúng chỉ thị được lưu trong PC Register của Thread B
Lúc này, PC Register đóng vai trò như một "tọa độ", giúp hệ điều hành có thể tạm dừng và tiếp tục một thread chính xác tại vị trí mà nó đã dừng trước đó.
Tại sao PC Register lại quan trọng trong Multithreading?
Khi nhiều thread chạy đồng thời, JVM cần biết chỉ thị hiện tại của từng thread.
Việc mỗi thread có một PC Register riêng đảm bảo không có sự nhầm lẫn giữa các thread, giúp chương trình thực thi chính xác và tránh xung đột trong luồng điều khiển của bytecode.
Tại sao việc mỗi thread có một PC Register riêng lại giúp tránh race condition trong luồng thực thi bytecode?
Mỗi thread đều có PC Register riêng, lưu địa chỉ của chỉ thị bytecode tiếp theo cần thực thi.
Điều này đảm bảo rằng khi nhiều thread chạy song song, Thread A và Thread B luôn biết chính xác mình đang ở đâu trong luồng bytecode của chính mình.
Mỗi thread tiếp tục thực thi từ đúng vị trí của nó mà không bị ảnh hưởng bởi thread khác.
Nếu tất cả các thread cùng chia sẻ một PC Register:
- Thread A đang thực thi đến instruction 120
- Thread B thay đổi PC thành instruction 45
→ Khi Thread A tiếp tục chạy, nó sẽ quay lại instruction 45 thay vì instruction 121, khiến toàn bộ logic chương trình bị sai.
Khi mỗi thread có một PC Register riêng:
- Thread A tự quản lý luồng bytecode của mình
- Thread B tự quản lý luồng bytecode của mình
→ Không thread nào có thể làm thay đổi luồng điều khiển của thread khác, từ đó loại bỏ xung đột ở cấp độ bytecode.
Nói một cách tổng quát, PC Register giúp ngăn ngừa race condition trong luồng điều khiển (control flow).
Chính xác hơn, PC Register lưu vị trí bytecode hiện tại của từng thread, vì vậy nó là một phần quan trọng của thread context.
Tuy nhiên, chỉ riêng PC Register vẫn chưa đủ để đảm bảo tính nhất quán, bởi nó chỉ hoạt động khi thread đang thực sự chạy trên CPU. Khi thread bị tạm dừng, PC Register sẽ ngừng cập nhật và giá trị của nó được lưu lại.
Lúc này, OS Scheduler đóng vai trò rất quan trọng bằng cách đảm bảo rằng mỗi CPU core chỉ thực thi một thread tại một thời điểm. Không có hai execution context cùng chạy đồng thời trên cùng một CPU, từ đó tránh được xung đột ở cấp độ CPU.
Nói cách khác:
- OS Scheduler ngăn ngừa race condition ở cấp độ CPU/Core
- PC Register ngăn ngừa race condition trong luồng thực thi bytecode của từng thread
Java Stack
Java Stack là vùng nhớ nơi JVM quản lý việc gọi method (method invocation) và các biến cục bộ (giá trị primitive và object reference) của từng thread.
Mỗi khi một method được gọi, một Stack Frame mới sẽ được tạo và đưa lên Stack để lưu trữ các biến cục bộ cũng như dữ liệu phục vụ cho việc thực thi method đó. Khi method kết thúc, Stack Frame sẽ được lấy ra khỏi Stack và vùng nhớ tương ứng cũng được giải phóng.
Stack hoạt động theo nguyên tắc LIFO (Last-In, First-Out).
Các nhiệm vụ chính của Java Stack:
- Quản lý vòng đời của các lời gọi Java Method
- Lưu trữ Local Variable và Method Parameter
- Lưu trữ metadata phục vụ cho quá trình thực thi
- Quản lý Stack Frame theo nguyên tắc LIFO
Cấu trúc của Java Stack
Java Stack được chia thành nhiều Stack Frame, trong đó mỗi Stack Frame tương ứng với một lần gọi method.
Một Stack Frame bao gồm:
1. Local Variable Array
Được sử dụng để lưu trữ:
- Local Variable (các kiểu primitive như int, long, boolean, ...)
- Method Parameter
- Object Reference trỏ đến các object trên Heap
Note: Bản thân object luôn nằm trên Heap, Stack chỉ lưu Reference của object.
2. Operand Stack
Đây là nơi JVM thực hiện các phép tính.
Khác với CPU sử dụng Register, JVM sử dụng Operand Stack để:
- Push giá trị vào Stack
- Thực hiện các phép tính
- Pop kết quả ra khỏi Stack
3. Frame Data (Additional Info)
Chứa các metadata cần thiết cho quá trình thực thi:
- Reference đến Constant Pool của class
- Độ sâu tối đa của Operand Stack
- Exception Handler
- Return Address
Những thông tin này cho phép JVM:
- Biết bytecode nào cần được thực thi
- Điều hướng exception đến đúng nơi xử lý
- Trả quyền điều khiển về method gọi sau khi method hiện tại kết thúc
Lưu ý: Đây là vùng nhớ được JVM quản lý và không phải là cấu trúc dữ liệu Stack trong Java Collections Framework.
Native Method Stack
Native Method Stack là một thành phần ít được nhắc đến nhưng lại đóng vai trò rất quan trọng khi Java cần tương tác với hệ điều hành hoặc các thư viện được viết bằng C/C++. Đây là một phần của Runtime Data Areas và hoạt động song song với Java Stack, Heap, Method Area và PC Register.
Native Method Stack là nơi JVM chuyển sang thực thi native code, xử lý những tác vụ mà bytecode Java không thể hoặc không nên thực hiện. Các method này được gọi thông qua JNI (Java Native Interface).
Nó hoàn toàn tách biệt với Java Stack, nơi chỉ chứa các Stack Frame của Java Method.
Điểm khác biệt so với Java Stack:
| Native Method Stack | Java Stack |
|---|---|
| Quản lý và thực thi các method được viết bằng ngôn ngữ native (C/C++) | Quản lý và thực thi Java Method |
Mục đích của Native Method Stack
Native Method Stack được thiết kế cho những trường hợp Java không thể xử lý trực tiếp, chẳng hạn như:
- Tương tác với hệ điều hành: Truy cập tài nguyên hệ thống hoặc các API của hệ điều hành (network, thiết bị, driver, ...)
- Tối ưu hiệu năng: Một số tác vụ tính toán hoặc thao tác ở mức thấp sẽ chạy nhanh hơn khi được triển khai bằng C/C++
- Gọi các thư viện hệ thống hoặc thư viện của bên thứ ba: Nhiều thư viện chỉ tồn tại dưới dạng C/C++ (ví dụ: OpenSSL, graphics library, codec, ...)
- Native code bên trong chính JVM: Ngay cả JVM cũng sử dụng native method cho một số API nội bộ như
Object.wait()hoặcSystem.arraycopy()
Stack này cho phép các Java Thread thực thi native code một cách an toàn và hiệu quả song song với việc thực thi bytecode Java thông thường.
Cấu trúc và cách hoạt động của Native Method Stack
Native Method Stack hoạt động tương tự Java Stack:
- Mỗi thread có một Native Method Stack riêng (thread-local)
- Hoạt động theo nguyên tắc LIFO (Last-In, First-Out)
- Mỗi khi một native method được gọi, JVM sẽ tạo một Native Frame để lưu trữ:
- Các tham số truyền vào native method
- Con trỏ đến native function trong thư viện C/C++
- Native Local Variable
- Trạng thái thực thi của JNI
- Thông tin để quay trở lại Java Stack sau khi native method hoàn thành
Điểm khác biệt so với Java Stack Frame:
- Native Frame không lưu bytecode
- Thay vào đó, nó lưu các cấu trúc dữ liệu tương thích với C/C++ và hệ điều hành, phục vụ cho việc thực thi native code
Java Stack
Java Stack là vùng nhớ nơi JVM quản lý việc gọi method (method invocation) và các biến cục bộ (giá trị primitive và object reference) của từng thread.
Mỗi khi một method được gọi, một Stack Frame mới sẽ được tạo và đưa lên Stack để lưu trữ các biến cục bộ cũng như dữ liệu phục vụ cho việc thực thi method đó. Khi method kết thúc, Stack Frame sẽ được lấy ra khỏi Stack và vùng nhớ tương ứng cũng được giải phóng.
Stack hoạt động theo nguyên tắc LIFO (Last-In, First-Out).
Các nhiệm vụ chính của Java Stack:
- Quản lý vòng đời của các lời gọi Java Method
- Lưu trữ Local Variable và Method Parameter
- Lưu trữ metadata phục vụ cho quá trình thực thi
- Quản lý Stack Frame theo nguyên tắc LIFO
Cấu trúc của Java Stack
Java Stack được chia thành nhiều Stack Frame, trong đó mỗi Stack Frame tương ứng với một lần gọi method.
Một Stack Frame bao gồm:
1. Local Variable Array
Được sử dụng để lưu trữ:
- Local Variable (các kiểu primitive như int, long, boolean, ...)
- Method Parameter
- Object Reference trỏ đến các object trên Heap
Note: Bản thân object luôn nằm trên Heap, Stack chỉ lưu Reference của object.
2. Operand Stack
Đây là nơi JVM thực hiện các phép tính.
Khác với CPU sử dụng Register, JVM sử dụng Operand Stack để:
- Push giá trị vào Stack
- Thực hiện các phép tính
- Pop kết quả ra khỏi Stack
3. Frame Data (Additional Info)
Chứa các metadata cần thiết cho quá trình thực thi:
- Reference đến Constant Pool của class
- Độ sâu tối đa của Operand Stack
- Exception Handler
- Return Address
Những thông tin này cho phép JVM:
- Biết bytecode nào cần được thực thi
- Điều hướng exception đến đúng nơi xử lý
- Trả quyền điều khiển về method gọi sau khi method hiện tại kết thúc
Lưu ý: Đây là vùng nhớ được JVM quản lý và không phải là cấu trúc dữ liệu Stack trong Java Collections Framework.
Native Method Stack
Native Method Stack là một thành phần ít được nhắc đến nhưng lại đóng vai trò rất quan trọng khi Java cần tương tác với hệ điều hành hoặc các thư viện được viết bằng C/C++. Đây là một phần của Runtime Data Areas và hoạt động song song với Java Stack, Heap, Method Area và PC Register.
Native Method Stack là nơi JVM chuyển sang thực thi native code, xử lý những tác vụ mà bytecode Java không thể hoặc không nên thực hiện. Các method này được gọi thông qua JNI (Java Native Interface).
Nó hoàn toàn tách biệt với Java Stack, nơi chỉ chứa các Stack Frame của Java Method.
Điểm khác biệt so với Java Stack:
| Native Method Stack | Java Stack |
|---|---|
| Quản lý và thực thi các method được viết bằng ngôn ngữ native (C/C++) | Quản lý và thực thi Java Method |
Mục đích của Native Method Stack
Native Method Stack được thiết kế cho những trường hợp Java không thể xử lý trực tiếp, chẳng hạn như:
- Tương tác với hệ điều hành: Truy cập tài nguyên hệ thống hoặc các API của hệ điều hành (network, thiết bị, driver, ...)
- Tối ưu hiệu năng: Một số tác vụ tính toán hoặc thao tác ở mức thấp sẽ chạy nhanh hơn khi được triển khai bằng C/C++
- Gọi các thư viện hệ thống hoặc thư viện của bên thứ ba: Nhiều thư viện chỉ tồn tại dưới dạng C/C++ (ví dụ: OpenSSL, graphics library, codec, ...)
- Native code bên trong chính JVM: Ngay cả JVM cũng sử dụng native method cho một số API nội bộ như
Object.wait()hoặcSystem.arraycopy()
Stack này cho phép các Java Thread thực thi native code một cách an toàn và hiệu quả song song với việc thực thi bytecode Java thông thường.
Cấu trúc và cách hoạt động của Native Method Stack
Native Method Stack hoạt động tương tự Java Stack:
- Mỗi thread có một Native Method Stack riêng (thread-local)
- Hoạt động theo nguyên tắc LIFO (Last-In, First-Out)
- Mỗi khi một native method được gọi, JVM sẽ tạo một Native Frame để lưu trữ:
- Các tham số truyền vào native method
- Con trỏ đến native function trong thư viện C/C++
- Native Local Variable
- Trạng thái thực thi của JNI
- Thông tin để quay trở lại Java Stack sau khi native method hoàn thành
Điểm khác biệt so với Java Stack Frame:
- Native Frame không lưu bytecode
- Thay vào đó, nó lưu các cấu trúc dữ liệu tương thích với C/C++ và hệ điều hành, phục vụ cho việc thực thi native code
Kết luận
Hiểu rõ cách JVM quản lý bộ nhớ là nền tảng để xây dựng các ứng dụng Java có hiệu năng cao và khả năng mở rộng tốt. Trong bài viết này, chúng ta đã cùng tìm hiểu:
Shared Memory Areas:
- Heap: Vùng nhớ chính của JVM dùng để lưu trữ object, được chia thành Young Generation (Eden, S0, S1) và Old Generation, với vòng đời object được quản lý bởi Garbage Collection.
- Metaspace: Thay thế PermGen từ Java 8+, lưu trữ metadata của class trong native memory.
- Method Area & Runtime Constant Pool: Lưu trữ "bản thiết kế" của class và các hằng số được chia sẻ giữa tất cả các thread.
Per-thread Memory Areas:
- PC Register: Theo dõi vị trí thực thi bytecode của từng thread, giúp tránh xung đột trong luồng điều khiển.
- Java Stack: Quản lý việc gọi method thông qua các Stack Frame chứa Local Variable, Operand Stack và Frame Data.
- Native Method Stack: Quản lý việc thực thi native code C/C++ thông qua JNI.
Những điểm quan trọng cần ghi nhớ:
- Thread Safety: Các vùng nhớ dùng chung (Heap, Method Area) cần được đồng bộ hóa khi truy cập đồng thời; các vùng nhớ riêng của từng thread (Stack, PC Register) vốn đã an toàn vì không được chia sẻ.
- Memory Optimization: Hiểu cách Generational Garbage Collection hoạt động sẽ giúp tối ưu vòng đời của object và giảm thời gian GC pause.
- Performance: Biết dữ liệu được lưu ở đâu sẽ giúp bạn đưa ra các quyết định hợp lý về caching, object pooling và tinh chỉnh bộ nhớ.
Sau khi đã hiểu class được lưu ở đâu và bộ nhớ của JVM được tổ chức như thế nào, bạn đã sẵn sàng khám phá cách JVM thực sự thực thi code. Trong bài viết tiếp theo, chúng ta sẽ tìm hiểu về Execution Engine, bao gồm Interpreter, JIT Compiler với cơ chế tối ưu hotspot và cách Garbage Collector thu hồi bộ nhớ.
📚 Tiếp tục tìm hiểu
Bài trước trong Series: JVM Architecture & Class Loading — Tìm hiểu nền tảng của Java
Bài tiếp theo trong Series: JVM Execution Engine — Từ Bytecode đến Native Code
Khám phá cách JVM chuyển đổi bytecode thành native machine code đã được tối ưu, phát hiện hotspot và điều phối Garbage Collection để đạt hiệu năng tối đa.
Bài viết này là một phần của JVM Fundamentals Series. Mỗi bài viết sẽ kế thừa kiến thức từ bài trước, giúp bạn từng bước xây dựng một cái nhìn toàn diện về cách các ứng dụng Java thực sự vận hành phía sau hậu trường.