Java — Từ Zero đến Senior/Mini-challenge: Debug memory leak với heap dump và JFR
~35 phútJVM InternalsMiễn phí

Mini-challenge: Debug memory leak với heap dump và JFR

Bài thực hành khép lại Module 12 — đọc app có leak, dùng jstat phát hiện trend, jcmd dump heap, MAT phân tích retained, fix root cause với data structure phù hợp. Workflow production-grade từ symptom đến fix.

Mini-challenge khép lại Module 12. Bạn nhận được codebase một service đang leak memory trong production. Symptom: heap leo từ 200MB lúc start lên 3GB sau 1 giờ, 80% time GC, p99 latency tăng từ 50ms lên 2s.

Bạn sẽ:

  1. Reproduce leak local với load tester.
  2. Capture diagnostic (jstat, JFR, heap dump).
  3. Phân tích heap dump với MAT-style approach.
  4. Identify root cause.
  5. Fix với data structure phù hợp.
  6. Verify fix với heap dump diff.

Đây là task điển hình senior Java engineer trong on-call: từ alert đến fix trong 1-2 giờ.

🎯 Đề bài

Setup

Service EventTracker ghi nhận event user clicks vào button trên website. Mỗi event có ID, timestamp, userId, button name. Service expose 2 endpoint:

  • POST /track — nhận event, lưu vào memory + DB.
  • GET /stats/{userId} — trả về số click 24h gần nhất của user.

Code dùng "in-memory cache" để tránh hit DB cho mọi /stats call. Cache sử dụng HashMap<String, List<Event>> mapping userId → danh sách event.

Sau 1 giờ load test (1000 user × 100 click/user/giờ = 100k event/giờ), heap đầy.

Yêu cầu

  1. Reproduce: chạy EventTrackerApp.main với JVM args set heap nhỏ (-Xmx512m -XX:+HeapDumpOnOutOfMemoryError -Xlog:gc*=info:file=gc.log). Chạy LoadGenerator.main 5 phút. Quan sát heap.

  2. Diagnose: dùng tools:

    • jstat -gcutil <pid> 1000 — confirm Old growing.
    • jcmd <pid> JFR.start name=leak duration=120s filename=/tmp/leak.jfr — capture allocation profile.
    • jcmd <pid> GC.heap_dump /tmp/leak.hprof — heap dump.
    • jcmd <pid> GC.class_histogram | head -20 — top class theo size.
  3. Analyze: từ histogram + heap dump, identify:

    • Class nào chiếm nhiều heap nhất.
    • Tại sao instance không bị GC (giữ bởi reference nào).
    • Pattern leak (cache không bound? listener leak? thread local leak?).
  4. Fix: refactor EventCache với:

    • Bound size (max N user trong cache).
    • TTL (event > 24h tự evict — đúng business semantic "click 24h gần nhất").
    • Thread-safe (concurrent access).
  5. Verify: rerun load test, capture heap dump sau 5 phút và sau 30 phút, diff. Heap stable, no growth.

📦 Concept dùng trong bài

ConceptBàiDùng ở đây
Heap layout (young/old)12.4Đọc jstat thấy Old growing
OOM heap space12.4Symptom chính
GC log12.5Phân tích pause time + reclaim ratio
jstat realtime12.6Confirm leak trend
JFR allocation profile12.6Tìm hot allocation site
Heap dump + MAT12.6Identify class retain memory
Path to GC roots12.6Trace chain reference giữ object
Concurrent collection10.4Fix với ConcurrentHashMap
Caffeine cache (3rd-party)bonusFix với bounded cache

▶️ Starter code

EventTrackerApp.java:

import java.time.Instant;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

public class EventTrackerApp {

    public record Event(String id, String userId, String button, Instant timestamp) { }

    // BUG: leak here
    public static class EventCache {
        private final Map<String, List<Event>> cache = new HashMap<>();

        public void add(Event e) {
            cache.computeIfAbsent(e.userId(), k -> new ArrayList<>()).add(e);
        }

        public List<Event> getEventsLast24h(String userId) {
            List<Event> all = cache.getOrDefault(userId, List.of());
            Instant cutoff = Instant.now().minusSeconds(86400);
            return all.stream()
                .filter(e -> e.timestamp().isAfter(cutoff))
                .toList();
        }

        public int size() {
            return cache.values().stream().mapToInt(List::size).sum();
        }
    }

    public static class EventTracker {
        private final EventCache cache = new EventCache();
        private final AtomicLong idGen = new AtomicLong();

        public void track(String userId, String button) {
            Event e = new Event(
                String.valueOf(idGen.incrementAndGet()),
                userId, button, Instant.now());
            cache.add(e);
            // Imagine DB persist here
        }

        public int statsLast24h(String userId) {
            return cache.getEventsLast24h(userId).size();
        }

        public int totalEvents() {
            return cache.size();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        EventTracker tracker = new EventTracker();

        // Print cache size every 5s
        Thread monitor = new Thread(() -> {
            while (true) {
                try { Thread.sleep(5000); } catch (InterruptedException e) { return; }
                Runtime r = Runtime.getRuntime();
                long used = (r.totalMemory() - r.freeMemory()) / 1024 / 1024;
                long max = r.maxMemory() / 1024 / 1024;
                System.out.printf("Heap %d/%d MB - cache total events: %d%n",
                    used, max, tracker.totalEvents());
            }
        });
        monitor.setDaemon(true);
        monitor.start();

        // Server thread - simulate accept track requests
        // Read userId from stdin or expose via simple HTTP server
        // For challenge, run via LoadGenerator below
        System.out.println("EventTracker started. PID: " + ProcessHandle.current().pid());
        System.out.println("Run LoadGenerator in another terminal to load this.");

        // Block - keep JVM alive
        Thread.currentThread().join();
    }
}

LoadGenerator.java:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

public class LoadGenerator {
    public static void main(String[] args) throws Exception {
        // Connect to EventTracker via shared static field for simplicity
        // (In real life, would HTTP POST. Here in-process for clarity.)

        EventTrackerApp.EventTracker tracker = new EventTrackerApp.EventTracker();

        ExecutorService exec = Executors.newFixedThreadPool(50);
        AtomicLong totalSent = new AtomicLong();

        // 1000 distinct user, each fires 1 event every 100ms
        for (int u = 0; u < 1000; u++) {
            final int userId = u;
            exec.submit(() -> {
                while (true) {
                    tracker.track("user-" + userId,
                        "button-" + (userId % 5));
                    totalSent.incrementAndGet();
                    try { Thread.sleep(100); } catch (InterruptedException e) { return; }
                }
            });
        }

        // Print stats every 10s
        while (true) {
            Thread.sleep(10000);
            System.out.println("Total events sent: " + totalSent.get());
        }
    }
}

Compile và chạy:

javac EventTrackerApp.java LoadGenerator.java

# Terminal 1: chạy app với heap nhỏ + JFR setup
java -Xms256m -Xmx512m \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/tmp/heap.hprof \
     -Xlog:gc*=info:file=/tmp/gc.log \
     EventTrackerApp

# Terminal 2: chạy load (note: PoC - same process, run main directly)
java LoadGenerator

# Terminal 3: monitor
jps -l                                  # tim PID EventTrackerApp
jstat -gcutil <PID> 1000                # realtime GC
jcmd <PID> GC.heap_info                 # heap stats
jcmd <PID> GC.class_histogram | head -20

Note: starter code dùng cùng JVM cho App và LoadGenerator để đơn giản. Production thực tế tách 2 process qua HTTP.

Dành 30-40 phút làm.

💡 Gợi ý — workflow diagnose

💡 Gợi ý — đọc khi bị kẹt

Bước 1: Confirm leak qua jstat

Chạy jstat -gcutil <PID> 1000 trong 2-3 phút:

S0     S1     E      O      M      YGC    YGCT    FGC    FGCT
0.00  88.31  35.12  20.20  93.92      5    0.123     0    0.000
0.00  90.50  60.85  35.20  93.92     12    0.234     0    0.000
0.00  92.50  72.12  55.85  93.92     20    0.456     0    0.000
0.00  94.50  85.12  75.20  93.92     30    0.678     1    0.234
0.00  96.50  90.85  90.85  93.92     40    1.123     2    0.789

Pattern leak rõ ràng:

  • O% (Old) leo từ 20% → 90% trong 2 phút.
  • FGC xuất hiện và tăng → full GC bắt đầu trigger.
  • YGC tăng nhanh (mỗi 1-2s) → high allocation rate.

Bước 2: Class histogram

jcmd <PID> GC.class_histogram | head -20
 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:        500000      40000000  EventTrackerApp$Event
   2:        500000      32000000  java.lang.String
   3:          1000       8000000  java.util.ArrayList
   4:          1000        128000  java.util.HashMap$Node
   ...

Top: Event 500k instance, String 500k. ArrayList chỉ 1000 (bằng số user) → mỗi user 1 ArrayList giữ ngày càng nhiều Event.

Bước 3: Heap dump

jcmd <PID> GC.heap_dump /tmp/leak.hprof

Mở MAT (Eclipse Memory Analyzer):

  1. Leak Suspects Report auto-run khi mở:

    Problem Suspect 1
    The class "EventTrackerApp$EventCache",
    loaded by "jdk.internal.loader.ClassLoaders$AppClassLoader",
    occupies 380,234,567 bytes (76.05%).
    The memory is accumulated in one instance of "java.util.HashMap"
    loaded by "<system class loader>".
    
  2. Histogram sort by Retained Heap:

    • EventCache$1 (HashMap) — 380 MB retained.
    • Event total retained — 280 MB.
    • ArrayList retained — 100 MB (chứa Event refs).
  3. Path to GC Roots cho instance EventCache:

    EventCache@0x... (380 MB retained)
      cache field of EventTracker@0x...
        tracker field of LoadGenerator (or main thread local)
          GC root: thread "main"
    

    EventCache giữ qua chain: main thread localEventTrackerEventCache.cache (HashMap) → ArrayList per userEvent x N.

Bước 4: Identify root cause

// EventCache
private final Map<String, List<Event>> cache = new HashMap<>();

public void add(Event e) {
    cache.computeIfAbsent(e.userId(), k -> new ArrayList<>()).add(e);
    // BUG: never remove old event
}

public List<Event> getEventsLast24h(String userId) {
    List<Event> all = cache.getOrDefault(userId, List.of());
    Instant cutoff = Instant.now().minusSeconds(86400);
    return all.stream()
        .filter(e -> e.timestamp().isAfter(cutoff))
        .toList();
    // BUG: filter chi tao List moi return, KHONG xoa event cu khoi cache
}

Tóm tắt:

  • Cache thêm mọi event vào ArrayList per user — không bao giờ remove.
  • getEventsLast24h filter chỉ tạo list mới, không touch cache.
  • Cache size grow tuyến tính theo total event sent — leak.

Bước 5: Fix — strategy

Có 3 approach, mỗi cái fix khác nhau:

Approach A — Eager cleanup: trong add, evict event > 24h trước.

public void add(Event e) {
    Instant cutoff = Instant.now().minusSeconds(86400);
    List<Event> userEvents = cache.computeIfAbsent(e.userId(), k -> new ArrayList<>());
    userEvents.removeIf(ev -> ev.timestamp().isBefore(cutoff));
    userEvents.add(e);
}

Đơn giản, đúng semantic. Nhược: removeIf O(n) mỗi add — performance issue nếu user có 10000 event/24h.

Approach B — Background cleanup: thread riêng quét cache mỗi 1-5 phút, evict event cũ.

public EventCache() {
    ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor(r -> {
        Thread t = new Thread(r, "EventCache-Cleaner");
        t.setDaemon(true);
        return t;
    });
    cleaner.scheduleAtFixedRate(this::cleanup, 1, 1, TimeUnit.MINUTES);
}

private void cleanup() {
    Instant cutoff = Instant.now().minusSeconds(86400);
    cache.values().forEach(list -> list.removeIf(e -> e.timestamp().isBefore(cutoff)));
    cache.entrySet().removeIf(entry -> entry.getValue().isEmpty());
}

Cleanup amortized — không impact add. Nhược: tạm thời cache lớn hơn 24h between cleanup. Cần thread-safe (synchronization hoặc ConcurrentHashMap).

Approach C — Caffeine bounded cache với TTL: dùng thư viện chuyên cache.

import com.github.benmanes.caffeine.cache.*;

private final Cache<String, List<Event>> cache = Caffeine.newBuilder()
    .maximumSize(10_000)                           // Max 10k user
    .expireAfterWrite(Duration.ofHours(24))        // Auto evict 24h
    .build();

Ngắn nhất, robust nhất production. Nhược: dependency thêm.

Quyết định: cho challenge, dùng Approach A (eager cleanup) hoặc Approach B (background cleanup) để giữ pure JDK. Approach C là answer production thật.

Bước 6: Refactor cho thread-safe

EventTracker chạy concurrent (50 thread load). HashMap không thread-safe → cần ConcurrentHashMap. ArrayList per user cũng cần synchronize hoặc dùng thread-safe variant.

private final Map<String, List<Event>> cache = new ConcurrentHashMap<>();

public void add(Event e) {
    Instant cutoff = Instant.now().minusSeconds(86400);
    cache.compute(e.userId(), (k, list) -> {
        if (list == null) list = new ArrayList<>();
        synchronized (list) {
            list.removeIf(ev -> ev.timestamp().isBefore(cutoff));
            list.add(e);
        }
        return list;
    });
}

compute của ConcurrentHashMap là atomic per key. Inner ArrayList vẫn cần sync nếu shared.

Bước 7: Verify

Sau fix, rerun load test:

# Heap dump 1: 5 phút sau start
jcmd <PID> GC.heap_dump /tmp/dump1.hprof
jcmd <PID> GC.class_histogram | head -10

# Heap dump 2: 30 phút sau start
jcmd <PID> GC.heap_dump /tmp/dump2.hprof
jcmd <PID> GC.class_histogram | head -10

# So sanh
diff <(head -10 /tmp/dump1-stat.txt) <(head -10 /tmp/dump2-stat.txt)

Kết quả mong đợi: Event count stable (~24h × 1000 user × 36/giờ = 864k max nếu user fire 36/giờ liên tục — bound by 24h TTL). Heap stable, không growing.

Bonus: JFR allocation hot

jcmd <PID> JFR.start name=alloc duration=60s filename=/tmp/alloc.jfr
# ... wait 60s ...
# Open JMC, Memory Allocation tab

JMC show top allocation method:

  • EventTracker.track — alloc Event mỗi call (acceptable, ~100k/giờ).
  • EventCache.getEventsLast24h — stream allocation noise.

Optimize stream nếu cần (avoid .stream().filter().toList() cho hot path, dùng manual loop).

✅ Lời giải

✅ Lời giải — xem sau khi đã thử

Code fix với Approach B (background cleanup) + thread-safe

import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

public class EventTrackerApp {

    public record Event(String id, String userId, String button, Instant timestamp) { }

    public static class EventCache implements AutoCloseable {
        private final Map<String, List<Event>> cache = new ConcurrentHashMap<>();
        private final ScheduledExecutorService cleaner;
        private final long ttlSeconds;

        public EventCache(long ttlSeconds) {
            this.ttlSeconds = ttlSeconds;
            this.cleaner = Executors.newSingleThreadScheduledExecutor(r -> {
                Thread t = new Thread(r, "EventCache-Cleaner");
                t.setDaemon(true);
                return t;
            });
            cleaner.scheduleAtFixedRate(this::cleanup, 1, 1, TimeUnit.MINUTES);
        }

        public void add(Event e) {
            cache.compute(e.userId(), (k, existing) -> {
                List<Event> list = existing != null ? existing : new ArrayList<>();
                synchronized (list) {
                    list.add(e);
                }
                return list;
            });
        }

        public List<Event> getEventsLast24h(String userId) {
            List<Event> userEvents = cache.get(userId);
            if (userEvents == null) return List.of();
            Instant cutoff = Instant.now().minusSeconds(ttlSeconds);
            synchronized (userEvents) {
                return userEvents.stream()
                    .filter(e -> e.timestamp().isAfter(cutoff))
                    .toList();
            }
        }

        private void cleanup() {
            Instant cutoff = Instant.now().minusSeconds(ttlSeconds);
            int removed = 0;
            for (Iterator<Map.Entry<String, List<Event>>> it = cache.entrySet().iterator(); it.hasNext(); ) {
                Map.Entry<String, List<Event>> entry = it.next();
                List<Event> list = entry.getValue();
                synchronized (list) {
                    int before = list.size();
                    list.removeIf(e -> e.timestamp().isBefore(cutoff));
                    removed += before - list.size();
                    if (list.isEmpty()) {
                        it.remove();
                    }
                }
            }
            System.err.println("[Cleanup] Removed " + removed + " stale events. Cache size: " + size());
        }

        public int size() {
            int total = 0;
            for (List<Event> list : cache.values()) {
                synchronized (list) {
                    total += list.size();
                }
            }
            return total;
        }

        public int userCount() {
            return cache.size();
        }

        @Override
        public void close() {
            cleaner.shutdown();
        }
    }

    public static class EventTracker {
        private final EventCache cache = new EventCache(86400);
        private final AtomicLong idGen = new AtomicLong();

        public void track(String userId, String button) {
            Event e = new Event(
                String.valueOf(idGen.incrementAndGet()),
                userId, button, Instant.now());
            cache.add(e);
        }

        public int statsLast24h(String userId) {
            return cache.getEventsLast24h(userId).size();
        }

        public int totalEvents() { return cache.size(); }
        public int activeUsers() { return cache.userCount(); }
    }

    public static void main(String[] args) throws InterruptedException {
        EventTracker tracker = new EventTracker();

        Thread monitor = new Thread(() -> {
            while (true) {
                try { Thread.sleep(5000); } catch (InterruptedException e) { return; }
                Runtime r = Runtime.getRuntime();
                long used = (r.totalMemory() - r.freeMemory()) / 1024 / 1024;
                long max = r.maxMemory() / 1024 / 1024;
                System.out.printf("Heap %d/%d MB | events: %d | users: %d%n",
                    used, max, tracker.totalEvents(), tracker.activeUsers());
            }
        });
        monitor.setDaemon(true);
        monitor.start();

        System.out.println("EventTracker started. PID: " + ProcessHandle.current().pid());
        Thread.currentThread().join();
    }
}

Điểm chính

  • ConcurrentHashMap thay HashMap: thread-safe cho 50 worker thread track concurrent. compute atomic per key.
  • synchronized (list) quanh ArrayList op: ConcurrentHashMap chỉ guard map structure (put/get key), không guard inner ArrayList. Nhiều thread có cùng userId truy cập cùng list → race nếu không sync.
  • Background cleaner thread: chạy mỗi 1 phút, evict event > 24h, remove user entry rỗng. daemon = true để không block JVM exit.
  • Iterator.remove() an toàn cho ConcurrentHashMap: ConcurrentHashMap iterator weakly consistent — không throw ConcurrentModificationException, support it.remove().
  • AutoCloseable: shutdown cleaner khi cache dispose. Production cần để tránh leak thread.

Verify

Chạy lại 5 phút:

Heap 234/512 MB | events: 50000 | users: 1000
Heap 256/512 MB | events: 100000 | users: 1000
Heap 278/512 MB | events: 150000 | users: 1000
[Cleanup] Removed 0 stale events. Cache size: 150000
Heap 280/512 MB | events: 200000 | users: 1000
[Cleanup] Removed 0 stale events. Cache size: 200000
...

Sau cleanup đầu tiên (1 phút), khi đã chạy đủ 24h sẽ thấy event removed. Trong 5 phút test ngắn, tất cả event vẫn trong window 24h → cache grow đến cap natural (1000 user × 5 phút × 600 event/phút = 300k event).

Để test cleanup hiệu quả, set TTL ngắn hơn cho test:

private final EventCache cache = new EventCache(60);   // TTL 60s for test

Chạy 5 phút:

Heap 200/512 MB | events: 60000 | users: 1000
[Cleanup] Removed 0 stale events. Cache size: 60000
Heap 220/512 MB | events: 120000 | users: 1000
[Cleanup] Removed 60000 stale events. Cache size: 60000
Heap 220/512 MB | events: 120000 | users: 1000
[Cleanup] Removed 60000 stale events. Cache size: 60000
...

Heap stable. Cache grow đến steady state (60s × 1000 user × 10 event/s = 60k), không leak.

So sánh heap dump trước / sau fix

# Truoc fix - 5 phut
jcmd <PID> GC.class_histogram | head -5
# 1: 300000 events EventTrackerApp$Event 24 MB
# 2: 300000 events String 19 MB

# Truoc fix - 30 phut (neu chua OOM)
jcmd <PID> GC.class_histogram | head -5
# 1: 1800000 events EventTrackerApp$Event 144 MB
# 2: 1800000 events String 115 MB
# Growing 6x tuyen tinh -> leak

# Sau fix - 5 phut
jcmd <PID> GC.class_histogram | head -5
# 1: 60000 events EventTrackerApp$Event 4.8 MB
# 2: 60000 events String 3.8 MB

# Sau fix - 30 phut
jcmd <PID> GC.class_histogram | head -5
# 1: 60000 events EventTrackerApp$Event 4.8 MB
# 2: 60000 events String 3.8 MB
# Stable -> no leak

🎓 Mở rộng

Mức 1 — Caffeine library:

<!-- pom.xml -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>
import com.github.benmanes.caffeine.cache.*;

private final Cache<String, List<Event>> cache = Caffeine.newBuilder()
    .maximumSize(10_000)                           // Bound user count
    .expireAfterWrite(Duration.ofHours(24))        // Auto evict
    .recordStats()                                  // Metric for monitoring
    .build();

Ngắn hơn 50 dòng code, thread-safe built-in, eviction policy tunable (LFU, LRU, custom). Production standard.

Mức 2 — Off-heap cache với Chronicle Map:

Nếu cache lớn (10GB+), in-heap GC pressure cao. Chronicle Map alloc off-heap:

ChronicleMap<String, List<Event>> cache = ChronicleMap
    .of(String.class, List.class)
    .averageKey("user-12345")
    .averageValueSize(1024)
    .entries(1_000_000)
    .createPersistedTo(new File("/tmp/event-cache.dat"));

Survive JVM restart, không impact GC. Trade-off: serialization overhead mỗi access.

Mức 3 — Time-series database thay cache:

Cache Event trong app process là anti-pattern cho production scale. Production dùng:

  • InfluxDB / Prometheus cho metric / event count.
  • Redis cho session-like data với TTL.
  • App service stateless, query DB cho /stats.

Trade-off: latency từ in-memory ~1μs lên DB ~1ms. Acceptable cho stats endpoint không hit mỗi request.

Mức 4 — Continuous JFR monitoring trong production:

// Embedded in app startup
String jfrConfig = "settings=profile,maxsize=200m,maxage=1h,disk=true,filename=/var/log/myapp.jfr";
ManagementFactory.getPlatformMBeanServer().invoke(
    new ObjectName("jdk.management.jfr:type=FlightRecorder"),
    "newRecording", null, null);
// ... configure with jfrConfig

Hoặc set -XX:StartFlightRecording=... trong JVM args. JFR ghi liên tục 1 giờ. Khi alert fire, dump snapshot:

jcmd <PID> JFR.dump filename=/tmp/incident-$(date +%s).jfr

Capture state lúc incident, phân tích offline với JMC.

✨ Điều bạn vừa làm được

Hoàn thành mini-challenge này, bạn đã:

  • Reproduce production issue trong môi trường local — skill đầu tiên cho debug.
  • Đọc jstat realtime để confirm pattern leak (Old growing).
  • Capture heap dump với jcmd, không restart JVM.
  • Phân tích heap dump với class histogram + Path to GC Roots.
  • Identify root cause: cache không bound, không TTL.
  • Fix với data structure phù hợp: ConcurrentHashMap + background cleanup hoặc Caffeine bounded cache.
  • Verify fix với heap dump diff trước/sau.
  • Áp dụng production debug workflow end-to-end — kỹ năng phân biệt junior/senior.

Chúc mừng — bạn đã hoàn thành Module 12: JVM Internals!

Bạn giờ hiểu JVM ở mọi tầng:

  • Class loader — class load qua loader nào, pattern isolation Tomcat / Spring Boot.
  • Bytecode — đọc javap, hiểu invokedynamic, lambda compile thế nào.
  • JIT — interpreter → C1 → C2, inlining, escape analysis, deoptimization.
  • Memory layout — heap young/old, metaspace, native memory, object header overhead.
  • GC — chọn collector phù hợp (Parallel / G1 / ZGC) theo SLA.
  • Tools — workflow debug từ symptom đến root cause.

Khoá Java đã đi qua 12 module — cú pháp, OOP, exception, generics, stream, concurrency, I/O, JVM internals. Bạn có nền tảng vững để chuyển sang framework (Spring), memory & performance chuyên sâu (Module 13), hoặc testing & build (Module 15).

Java production-grade engineering không phải về việc viết được nhiều code — mà về việc debug code khi nó đi sai trên production. Bạn vừa thực hành đúng kỹ năng đó.

Bài này có giúp bạn hiểu bản chất không?

Bình luận (0)

Đang tải...