Monitoring & Tối ưu Memory
Bài trước: Thread Memory & Virtual Threads — Bạn đã hiểu cách threads tương tác với memory. Bài này hướng dẫn cách quan sát, chẩn đoán, và tối ưu memory trong thực tế.
JVM Command-Line Tools
JDK cung cấp sẵn các tools để monitor JVM — không cần cài thêm:
jps — List Java Processes
$ jps
12345 MyApplication
12346 Jps
12347 GradleDaemon
$ jps -v # Hiện JVM arguments
12345 MyApplication -Xmx2g -XX:+UseG1GC
jstat — GC Statistics Real-time
# GC statistics, cập nhật mỗi 1 giây
$ jstat -gcutil 12345 1000
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 45.23 67.89 34.56 95.12 92.34 42 0.234 3 0.567 0.801
| Cột | Ý nghĩa |
|---|---|
| S0, S1 | Survivor 0, 1 usage (%) |
| E | Eden usage (%) |
| O | Old Gen usage (%) |
| M | Metaspace usage (%) |
| YGC | Young GC count |
| YGCT | Young GC total time (seconds) |
| FGC | Full GC count |
| FGCT | Full GC total time (seconds) |
- E tăng nhanh, YGC thường xuyên → bình thường (objects chết trẻ)
- O tăng dần không giảm → có thể memory leak
- FGC nhiều → Old Gen đầy thường xuyên → cần điều tra
- FGCT cao → Full GC quá chậm → xem xét GC collector khác
jmap — Heap Dump
# Tạo heap dump (file chứa toàn bộ Heap snapshot)
$ jmap -dump:format=b,file=heapdump.hprof 12345
# Histogram: top classes chiếm memory
$ jmap -histo 12345 | head -20
num #instances #bytes class name
1: 1234567 98765432 [B (byte arrays)
2: 567890 45631200 java.lang.String
3: 345678 27654240 java.util.HashMap$Node
jstack — Thread Dump
# In stack trace của tất cả threads
$ jstack 12345
"main" #1 prio=5 os_prio=0 tid=0x... nid=0x... runnable
java.lang.Thread.State: RUNNABLE
at com.myapp.Service.process(Service.java:42)
at com.myapp.Main.main(Main.java:15)
"Thread-1" #12 prio=5 os_prio=0 tid=0x... nid=0x... waiting on condition
java.lang.Thread.State: WAITING
at java.lang.Object.wait(Native Method)
at com.myapp.Queue.take(Queue.java:30)
jstack tự động phát hiện deadlocks:
Found one Java-level deadlock:
=============================
"Thread-1": waiting to lock monitor 0x... (object 0x..., a java.lang.Object)
which is held by "Thread-2"
"Thread-2": waiting to lock monitor 0x... (object 0x..., a java.lang.Object)
which is held by "Thread-1"
jcmd — Diagnostic Commands (Đa năng)
# GC heap info
$ jcmd 12345 GC.heap_info
# Thread dump dạng JSON (Java 21+)
$ jcmd 12345 Thread.dump_to_file -format=json threads.json
# Force GC (chỉ dùng khi debug)
$ jcmd 12345 GC.run
# List tất cả commands available
$ jcmd 12345 help
Common Memory Issues
1. OutOfMemoryError: Java heap space
Nguyên nhân: Heap đầy, GC không thể giải phóng đủ memory.
Triệu chứng:
- Full GC xảy ra liên tục
- Application chậm dần rồi crash
- jstat: O (Old Gen) usage = 99-100%
Chẩn đoán:
# 1. Kiểm tra heap usage
jstat -gcutil <pid> 1000
# 2. Tạo heap dump
jmap -dump:format=b,file=heap.hprof <pid>
# 3. Mở heap dump bằng Eclipse MAT hoặc VisualVM
# → Tìm Leak Suspects → thấy class/object chiếm nhiều memory nhất
Fix:
- Tăng heap:
-Xmx4g(tạm thời) - Tìm và fix memory leak (dài hạn) — xem phần Memory Leak Patterns
2. OutOfMemoryError: Metaspace
Nguyên nhân: Quá nhiều classes loaded — thường do dynamic class generation (Reflection, CGLib proxies):
# Kiểm tra Metaspace
jstat -gcutil <pid> 1000 # Cột M = Metaspace usage
# Fix: Giới hạn Metaspace
java -XX:MaxMetaspaceSize=256m -jar app.jar
3. StackOverflowError
Nguyên nhân: Stack frame quá nhiều (recursion sâu) hoặc stack size quá nhỏ:
# Tăng stack size per thread (mặc định ~512KB-1MB)
java -Xss2m -jar app.jar
# Hoặc fix recursion: thêm base case, chuyển sang iteration
Memory Leak Patterns
Memory leak trong Java = objects vẫn reachable nhưng không còn cần thiết:
Pattern 1: Static Collections giữ reference
// ❌ Memory leak — static collection không bao giờ bị GC
public class EventLog {
private static final List<Event> events = new ArrayList<>();
public static void log(Event event) {
events.add(event); // Chỉ add, không bao giờ remove!
}
}
// events → ArrayList → tất cả Event objects → KHÔNG bao giờ GC
// ✅ Fix: Giới hạn size hoặc dùng bounded collection
private static final List<Event> events = new ArrayList<>();
public static void log(Event event) {
if (events.size() > 10000) {
events.subList(0, 5000).clear(); // Remove oldest half
}
events.add(event);
}
// Hoặc dùng CircularBuffer, Caffeine cache, etc.
Pattern 2: Listeners không unregister
// ❌ Listener giữ reference → object không bị GC
public class UserProfile {
public UserProfile(EventBus bus) {
bus.register(this); // this → bị EventBus giữ reference
}
// Nếu UserProfile không unregister khi không cần → leak
}
// ✅ Fix: Luôn unregister
public class UserProfile implements AutoCloseable {
private final EventBus bus;
public UserProfile(EventBus bus) {
this.bus = bus;
bus.register(this);
}
@Override
public void close() {
bus.unregister(this); // Giải phóng reference
}
}
Pattern 3: Inner Class giữ Outer Reference
// ❌ Non-static inner class giữ implicit reference đến outer class
public class DataProcessor {
private byte[] hugeData = new byte[100_000_000]; // 100MB
public Runnable createTask() {
// Anonymous inner class giữ reference đến DataProcessor.this
return new Runnable() {
@Override
public void run() {
System.out.println("Processing...");
// Không dùng hugeData, nhưng vẫn giữ reference!
}
};
}
}
// ✅ Fix: Dùng static inner class hoặc lambda (nếu không capture outer)
public Runnable createTask() {
return () -> System.out.println("Processing...");
// Lambda không capture this → không giữ reference đến outer
}
Pattern 4: Unclosed Resources
// ❌ Connection không close → pool exhaustion → memory leak
public void query(String sql) {
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// Nếu exception xảy ra → conn không bao giờ close!
}
// ✅ Fix: try-with-resources
public void query(String sql) {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
// Process results
} // Auto-close tất cả, kể cả khi exception
}
Optimization Techniques
1. Specify Initial Capacity
// ❌ Nhiều lần resize
List<String> list = new ArrayList<>(); // 7+ resizes cho 1000 items
Map<String, Integer> map = new HashMap<>(); // 7+ rehashes cho 1000 entries
// ✅ Set capacity
List<String> list = new ArrayList<>(1000);
Map<String, Integer> map = new HashMap<>(1334); // 1000 / 0.75 + 1
2. Primitive Streams thay Boxed Streams
// ❌ Boxing overhead — mỗi int → Integer object
int sum = numbers.stream()
.map(n -> n * 2) // Stream<Integer>
.reduce(0, Integer::sum);
// ✅ Primitive stream — không boxing
int sum = numbers.stream()
.mapToInt(Integer::intValue) // IntStream
.map(n -> n * 2)
.sum();
// ✅ Tốt nhất: IntStream từ đầu
int sum = IntStream.rangeClosed(1, 1_000_000)
.map(n -> n * 2)
.sum();
3. StringBuilder trong Loops
// ❌ Mỗi += tạo StringBuilder mới → nhiều objects
String result = "";
for (String item : items) {
result += item + ", "; // Tạo StringBuilder + String mới mỗi iteration
}
// ✅ Một StringBuilder cho toàn bộ loop
StringBuilder sb = new StringBuilder(items.size() * 20); // Estimate capacity
for (String item : items) {
sb.append(item).append(", ");
}
String result = sb.toString();
4. Weak/Soft References cho Caches
import java.lang.ref.WeakReference;
import java.lang.ref.SoftReference;
// WeakReference: GC collect ngay khi không có strong reference
WeakReference<BigObject> weak = new WeakReference<>(new BigObject());
// weak.get() có thể null bất cứ lúc nào sau GC
// SoftReference: GC collect khi sắp OOM (giữ lâu hơn weak)
SoftReference<BigObject> soft = new SoftReference<>(new BigObject());
// soft.get() null khi JVM cần memory
// WeakHashMap: keys là weak references → auto-cleanup
Map<Key, Value> cache = new WeakHashMap<>();
5. Avoid Premature Optimization
- Viết code đúng trước — correctness > performance
- Đo hiệu năng — dùng jstat, jmap, profiler
- Tìm bottleneck thực sự — không đoán
- Optimize vị trí cụ thể — chỉ optimize nơi đo được vấn đề
- Đo lại — confirm improvement
"Premature optimization is the root of all evil" — Donald Knuth
GC Tuning Cơ bản
Chọn Heap Size
# Rule of thumb: Xmx = 2-4x live data size
# Live data = heap usage sau Full GC
# Ví dụ: live data = 500MB → Xmx = 1-2GB
java -Xms1g -Xmx2g -jar app.jar
# Best practice: Xms = Xmx (tránh resize overhead)
java -Xms2g -Xmx2g -jar app.jar
Chọn GC Collector
# G1 — default, phù hợp hầu hết apps
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar app.jar
# ZGC — latency-critical
java -XX:+UseZGC -jar app.jar
# Parallel — batch/throughput
java -XX:+UseParallelGC -jar app.jar
Enable GC Logging
# Java 9+ unified logging
java -Xlog:gc*:file=gc.log:time,level -jar app.jar
# Verbose GC logging
java -Xlog:gc*=debug:file=gc-debug.log:time -jar app.jar
Ví dụ thực tế: Chẩn đoán Memory Issue
// Bước 1: Monitor bằng jstat
// $ jstat -gcutil <pid> 5000
// Thấy: O (Old Gen) tăng dần từ 30% → 50% → 70% → 90%
// → Old Gen leak!
// Bước 2: Heap dump
// $ jmap -dump:format=b,file=heap.hprof <pid>
// Bước 3: Phân tích bằng Eclipse MAT hoặc VisualVM
// → Tìm thấy: HashMap chứa 500,000 Session objects
// → Root: static SessionManager.sessions không bao giờ remove expired
// Bước 4: Fix
public class SessionManager {
// ❌ Trước: không remove expired sessions
private static final Map<String, Session> sessions = new HashMap<>();
// ✅ Fix: scheduled cleanup
private static final Map<String, Session> sessions = new ConcurrentHashMap<>();
static {
// Remove expired sessions mỗi 5 phút
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
sessions.entrySet().removeIf(e -> e.getValue().isExpired());
}, 5, 5, TimeUnit.MINUTES);
}
}
Lỗi thường gặp
Lỗi 1: Tăng Xmx khi có memory leak
# ❌ Chỉ delay vấn đề, không fix
java -Xmx8g -jar app.jar # OOM sau 8 giờ thay vì 2 giờ
# ✅ Tìm và fix root cause
# Dùng heap dump + MAT → tìm leak → fix code
Lỗi 2: Không set Xms = Xmx
# ❌ JVM phải resize heap nhiều lần khi startup
java -Xms256m -Xmx2g -jar app.jar
# ✅ Set bằng nhau — tránh resize overhead
java -Xms2g -Xmx2g -jar app.jar
Lỗi 3: Optimize quá sớm
// ❌ "Tối ưu" không cần thiết — GC hiện đại xử lý tốt
Object pool = objectPool.borrow(); // Object pooling thủ công
try { use(pool); } finally { objectPool.return(pool); }
// ✅ GC hiện đại rất hiệu quả — object allocation rẻ (~10ns)
Object obj = new Object(); // Đơn giản hơn, JVM optimize
Bài tập
Bài 1: JVM Monitoring [Cơ bản]
Viết một chương trình Java tạo dần objects (mỗi giây thêm 10,000 strings vào List). Chạy chương trình và dùng jstat -gcutil <pid> 1000 để quan sát GC hoạt động. Ghi lại: YGC count, FGC count, Old Gen usage trend.
Xem lời giải
import java.util.ArrayList;
import java.util.List;
public class GCObserver {
public static void main(String[] args) throws Exception {
System.out.println("PID: " + ProcessHandle.current().pid());
System.out.println("Run: jstat -gcutil " + ProcessHandle.current().pid() + " 1000");
List<String> data = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < 10_000; j++) {
data.add("Item-" + i + "-" + j);
}
System.out.println("Added batch " + i + ", total: " + data.size());
Thread.sleep(1000);
}
}
}
Kỳ vọng: E (Eden) dao động lên xuống (Minor GC), O (Old Gen) tăng dần, YGC tăng đều, FGC xảy ra khi Old Gen gần đầy.
Bài 2: Memory Leak Detection [Trung bình]
Cho code sau, xác định memory leak và fix:
public class EventSystem {
private static final List<EventListener> listeners = new ArrayList<>();
public static void addListener(EventListener listener) {
listeners.add(listener);
}
public static void fireEvent(Event event) {
for (EventListener listener : listeners) {
listener.onEvent(event);
}
}
}
// Trong một servlet/controller:
public void handleRequest(Request req) {
EventSystem.addListener(event -> {
log(event); // Lambda capture 'this' nếu log() là instance method
});
// Mỗi request thêm 1 listener, không bao giờ remove!
}
Xem lời giải
Leak: Mỗi request thêm listener vào static list → không bao giờ remove → memory tăng mãi.
Fix options:
// Fix 1: removeListener sau khi dùng xong
public void handleRequest(Request req) {
EventListener listener = event -> log(event);
EventSystem.addListener(listener);
try {
processRequest(req);
} finally {
EventSystem.removeListener(listener);
}
}
// Fix 2: Dùng WeakReference cho listeners
private static final List<WeakReference<EventListener>> listeners = new ArrayList<>();
// Fix 3: Scope listener — không dùng static global system
Bài 3: Optimization Challenge [Thách thức]
Đoạn code sau xử lý 1 triệu records. Optimize nó để giảm memory usage và tăng speed. Đo trước và sau.
public List<String> processRecords(List<Map<String, String>> records) {
List<String> results = new ArrayList<>();
for (Map<String, String> record : records) {
String name = record.get("name");
String city = record.get("city");
String result = "Name: " + name + ", City: " + city;
results.add(result);
}
return results;
}
Gợi ý
Optimization points:
new ArrayList<>(records.size())— pre-allocateStringBuildercho string concatenationString.intern()chocitynếu ít giá trị unique- Stream API với parallel nếu list lớn
Tóm tắt
| Tool | Mục đích |
|---|---|
| jps | List Java processes |
| jstat | GC statistics real-time |
| jmap | Heap dump, histogram |
| jstack | Thread dump, deadlock detection |
| jcmd | Diagnostic commands tổng hợp |
| Memory Issue | Nguyên nhân | Fix |
|---|---|---|
| OOM: Heap | Heap đầy / memory leak | Fix leak hoặc tăng -Xmx |
| OOM: Metaspace | Quá nhiều classes | Giới hạn -XX:MaxMetaspaceSize |
| StackOverflow | Recursion sâu | Base case / tăng -Xss |
| Memory Leak | Objects reachable nhưng không cần | Close resources, remove listeners, limit caches |