Bài toán cấp phát bộ nhớ
Khi bạn viết new Object() trong Java, bạn có thể nghĩ rằng đối tượng đó luôn được tạo trên heap. Nhưng thực tế, trình biên dịch JIT của JVM có một kỹ thuật tối ưu hóa mạnh mẽ gọi là Escape Analysis, cho phép nó phân tích xem đối tượng có "thoát" ra khỏi phạm vi của phương thức hay không. Nếu đối tượng chỉ được sử dụng bên trong phương thức tạo ra nó và không bao giờ được truyền ra ngoài, JVM có thể cấp phát nó trên stack thay vì heap, giúp tiết kiệm đáng kể chi phí thu gom rác.
Ba cấp độ "thoát"
Escape Analysis phân loại đối tượng thành ba mức. Thứ nhất là NoEscape: đối tượng chỉ tồn tại trong phương thức hiện tại, không được gán cho biến nào bên ngoài. Đây là trường hợp lý tưởng nhất để tối ưu. Thứ hai là ArgEscape: đối tượng được truyền làm tham số cho phương thức khác nhưng không được lưu lại ở bất kỳ đâu. Trường hợp này vẫn cho phép một số tối ưu nhất định. Thứ ba là GlobalEscape: đối tượng được gán cho biến static, trả về từ phương thức, hoặc lưu vào cấu trúc dữ liệu có thời gian sống dài. Khi đối tượng thuộc nhóm này, JVM buộc phải cấp phát trên heap theo cách thông thường.
Những tối ưu JVM thực hiện
Khi Escape Analysis xác định một đối tượng là NoEscape, JVM có thể áp dụng ba kỹ thuật tối ưu chính. Stack Allocation chuyển đối tượng từ heap lên stack, giúp giải phóng bộ nhớ tự động khi phương thức kết thúc mà không cần garbage collector can thiệp. Scalar Replacement còn triệt để hơn: phân rã đối tượng thành các trường riêng lẻ, lưu thẳng vào thanh ghi CPU, loại bỏ hoàn toàn việc cấp phát bộ nhớ. Lock Elision tự động loại bỏ các khối synchronized trên đối tượng không thoát, vì nếu đối tượng chỉ tồn tại trong một luồng duy nhất thì việc đồng bộ hóa là không cần thiết.
Ví dụ thực tế
Lấy ví dụ một phương thức tính khoảng cách giữa hai điểm. Nếu bạn tạo đối tượng Point bên trong phương thức chỉ để truy xuất tọa độ x và y rồi tính toán, Escape Analysis sẽ nhận ra rằng đối tượng Point không bao giờ rời khỏi phương thức. Thay vì cấp phát trên heap, JVM sẽ áp dụng Scalar Replacement, biến đối tượng Point thành hai biến nguyên thủy x và y nằm trực tiếp trên stack hoặc trong thanh ghi. Kết quả là phương thức chạy nhanh hơn đáng kể và không tạo áp lực lên garbage collector.
Giới hạn cần biết
Escape Analysis không phải lúc nào cũng hoạt động hoàn hảo. Trình biên dịch C2 chỉ kích hoạt kỹ thuật này sau khi phương thức được gọi đủ nhiều lần để vượt ngưỡng biên dịch JIT, thường khoảng 10.000 lần. Các đối tượng quá lớn cũng không được cấp phát trên stack. Ngoài ra, nếu phương thức quá phức tạp hoặc có nhiều nhánh điều kiện, trình phân tích có thể không xác định chính xác được đường đi của đối tượng và sẽ chọn phương án an toàn là cấp phát trên heap. Bạn có thể theo dõi Escape Analysis bằng cờ -XX:+PrintEscapeAnalysis trong các bản build debug của OpenJDK để hiểu cách JVM đưa ra quyết định cho từng đối tượng cụ thể.





