Tối ưu hóa bộ nhớ JVM
Tomcat hoạt động dựa trên máy ảo Java (JVM), và cấu hình mặc định của JVM thường không tối ưu cho môi trường sản xuất. Việc cấu hình lại tham số khởi động là cần thiết để tận dụng tối đa tài nguyên phần cứng và nâng cao hiệu suất hệ thống. Bài viết này tập trung vào hai khía cạnh chính: tinh chỉnh bộ nhớ JVM (Memory) và tối ưu hóa chiến lược thu gom rác (Garbage Collection - GC) trên nền tảng Oracle JDK 8.
Phân tích bộ nhớ Tomcat
Tổng dung lượng bộ nhớ bộ nhớ Tomcat sử dụng được tính theo công thức:
Bộ nhớ Tomcat = Xmx (Heap tối đa) + Metaspace (JDK 8) + (Số lượng luồng * Kích thước Stack luồng)
Cần lưu ý rằng bộ nhớ Stack của luồng (Thread Stack) được cấp phát trong bộ nhớ hệ điều hành (Native Memory), nằm ngoài vùng Heap do JVM quản lý. Từ JDK 5 trở đi, kích thước mặc định của mỗi Stack luồng là 1MB. Do đó, nếu cấp phát quá nhiều bộ nhớ cho Heap (Xmx), số lượng luồng tối đa mà ứng dụng có thể tạo sẽ bị hạn chế, dẫn đến lỗi OutOfMemoryError: unable to create new native thread.
Các tham số cấu hình bộ nhớ
Dưới đây là danh sách các tham số quan trọng cần lưu ý khi cấu hình JVM:
- -server: Bật chế độ máy chủ, giúp JVM tối ưu hóa hiệu suất cho các ứng dụng chạy lâu dài trên các hệ thống đa nhân. Đây nên là tham số đầu tiên.
- -Xmx: Xác định kích thước tối đa của Heap Memory. Mặc định là 1/4 bộ nhớ vật lý. Với máy chủ chuyên dụng chạy Tomcat, nên thiết lập khoảng 60-80% tổng RAM.
- -Xms: Xác định kích thước khởi tạo của Heap Memory. Nên đặt giá trị này bằng với
-Xmxđể tránh việc JVM phải tự động điều chỉnh lại kích thước Heap trong quá trình chạy, gây ra tốn kém chi phí xử lý. - -Xss: Thiết lập kích thước Stack cho từng luồng. Giảm giá trị này (ví dụ 256k hoặc 128k) cho phép tạo nhiều luồng hơn, nhưng quá thấp có thể gây tràn Stack (StackOverflowError).
- -Xmn: Thiết lập kích thước cho vùng Young Generation (thế hệ trẻ). Giá trị recommended thường là 1/3 hoặc 3/8 của tổng Heap.
- -XX:MetaspaceSize & -XX:MaxMetaspaceSize: (JDK 8) Quản lý kích thước vùng Metaspace (lưu trữ metadata của class), thay thế cho PermGen của JDK 7. Nên thiết lập giới hạn tối đa để tránh rò rỉ bộ nhớ.
Thực hành cấu hình trong catalina.sh
Để áp dụng các cấu hình trên, ta cần chỉnh sửa biến môi trường JAVA_OPTS trong file ${TOMCAT_HOME}/bin/catalina.sh.
Ví dụ dưới đây cấu hình cho một máy chủ có 64GB RAM, dành 32GB cho Heap và tối ưu hóa vùng Metaspace:
export JAVA_OPTS="${JAVA_OPTS} -server \
-Xms32g \
-Xmx32g \
-Xmn12g \
-Xss256k \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:SurvivorRatio=8"
Giải thích:
-Xms32g -Xmx32g: Khóa kích thước Heap ở mức 32GB để triệt tiêu việc tự động resize.-Xmn12g: Cấp 12GB cho vùng Young Generation.-XX:MetaspaceSize=256m: Đặt ngưỡng kích hoạt GC cho Metaspace cao hơn mặc định để giảm tần suất Full GC khi khởi động.
Kiểm tra cấu hình bằng JMap
Sau khi khởi động lại Tomcat, sử dụng công cụ jmap đi kèm JDK để xác nhận cấu hình đã được áp dụng.
Đầu tiên, lấy PID của tiến trình Tomcat:
jps -l
# Kết quả ví dụ:
# 10234 org.apache.catalina.startup.Bootstrap
Tiếp theo, xem chi tiết bộ nhớ:
jmap -heap 10234
Kết quả trả về sẽ hiển thị thông tin chi tiết về cấu hình Heap, Young Generation, Old Generation và Metaspace. Hãy kiểm tra xem các giá trị MaxHeapSize và NewSize có trùng khớp với tham số -Xmx và -Xmn đã thiết lập hay không.
Tối ưu hóa chiến lược Garbage Collection
Việc chọn thuật toán GC phù hợp ảnh hưởng trực tiếp đến độ trễ (latency) và throughput của hệ thống. Với JDK 8 và các ứng dụng có yêu cầu phản hồi nhanh, bộ thu gom Concurrent Mark Sweep (CMS) hoặc G1 GC thường được ưu tiên hơn Parallel GC mặc định.
Dưới đây là ví dụ về cấu hình sử dụng bộ thu gom CMS để giảm thời gian dừng (pause time) của ứng dụng, phù hợp với các hệ thống xử lý chỉ mục (như Solr/Elasticsearch) hoặc dịch vụ web tương tác cao.
Thêm các tham số sau vào biến JAVA_OPTS trong catalina.sh:
export JAVA_OPTS="${JAVA_OPTS} \
-XX:+UseConcMarkSweepGC \
-XX:CMSInitiatingOccupancyFraction=70 \
-XX:+UseCMSInitiatingOccupancyOnly \
-XX:+CMSParallelRemarkEnabled \
-XX:MaxGCPauseMillis=200 \
-XX:+ExplicitGCInvokesConcurrent"
Phân tích các tham số GC:
- -XX:+UseConcMarkSweepGC: Kích hoạt bộ thu gom CMS, cho phép luồng GC chạy song song với luồng ứng dụng để giảm thời gian "Stop The World".
- -XX:CMSInitiatingOccupancyFraction=70: Yêu cầu CMS bắt đầu chu trình thu gom khi vùng Old Generation đầy 70% (mặc định là 92%). Giá trị thấp giúp tránh việc Full GC xảy ra quá đột ngột khi bộ nhớ gần đầy.
- -XX:+ExplicitGCInvokesConcurrent: Khi gọi
System.gc(), thực hiện thu gom theo chế độ đồng thời thay vì dừng toàn bộ ứng dụng (Full GC).
Lưu ý rằng JDK 9 trở đi đã đánh dấu CMS là deprecated, nhưng đối với Tomcat 8 chạy trên JDK 8, đây vẫn là lựa chọn ổn định cho các hệ thống yêu cầu low-latency.