Java Internals & Concurrency/Safepoint và Stop-The-World — tại sao GC pause dài hơn GC log báo
24/26
Bài 24 / 26~20 phútJVM InternalsMiễn phí lượt xem

Safepoint và Stop-The-World — tại sao GC pause dài hơn GC log báo

Safepoint là điểm JVM dừng thread để thực hiện global operation. Cơ chế polling, time-to-safepoint, STW pause, và cách diagnose TTSP spike bằng JFR và safepoint log.

TL;DR: Safepoint là vị trí trong execution nơi JVM biết hoàn toàn trạng thái mọi thread — mọi reference đều có thể được tìm thấy, không có operation nào đang dở. JVM yêu cầu mọi thread đến safepoint trước khi bắt đầu operation toàn cục (GC, deoptimization, JFR sample...). Time-to-safepoint (TTSP) là thời gian từ khi JVM request stop đến khi tất cả thread arrive. Thread đang chạy counted loop dài có thể không poll safepoint hàng chục ms, khiến TTSP spike dài hơn GC pause thực sự. GC log báo "20ms" nhưng app freeze 150ms vì TTSP 130ms trước đó.

Bài 05 giới thiệu GC pause như là thời gian STW (Stop-The-World). Thực tế đo được trên production đôi khi cho thấy app freeze dài hơn GC pause log nhiều. Khoảng chênh lệch đó là TTSP — thời gian "chờ mọi thread đến điểm an toàn". Bài này giải thích cơ chế safepoint, tại sao TTSP tồn tại, và cách diagnose.

1. Scenario — GC log báo 50ms nhưng app freeze 200ms

Team nhận alert SLA breach: latency p99 vượt 200ms. Kiểm tra GC log:

[info][gc] GC(42) Pause Young (Normal) 48.234ms
[info][gc] GC(43) Pause Young (Normal) 51.891ms

GC pause rõ ràng chỉ 50ms — không đủ gây breach. Nhưng trace từ user request cho thấy:

Request A: start 10:00:00.000, end 10:00:00.198  -> 198ms
GC(42):    start 10:00:00.020, end 10:00:00.068  -> 48ms (GC log)

Chênh lệch: request bị block từ 00:000 đến 00:068 = 68ms. Nhưng GC thực sự chỉ xảy ra từ 00:020 đến 00:068. Từ 00:000 đến 00:020 là gì?

Đó là khoảng thời gian JVM yêu cầu stop và chờ thread xử lý request A tự ngừng lại tại safepoint tiếp theo. Thread A chưa đến safepoint → GC chưa bắt đầu được. Thread A freeze nhưng GC chưa tính thời gian. GC log chỉ tính thời gian sau khi tất cả thread đã arrive.

Để hiểu toàn bộ latency, cần hiểu safepoint.

2. Safepoint — điểm execution biết toàn bộ

Safepoint là vị trí trong luồng thực thi (cụ thể là instruction) nơi JVM có thể xác định hoàn toàn trạng thái của thread — biết chính xác mọi object reference đang live ở đâu (stack frame, register), không có allocation đang dở, không có monitor acquisition đang dở.

Tại safepoint, GC có thể:

  • Scan toàn bộ stack của thread để tìm GC root.
  • Di chuyển (relocate) object mà không sợ đọc stale pointer.
  • Cập nhật reference table an toàn.

JIT compiler chèn safepoint poll instruction tại các vị trí xác định trong compiled code:

  • Method return (trước khi return về caller).
  • Loop back-edge (mỗi lần vòng lặp quay lại điểm đầu).
  • Allocation site (trước khi alloc object lớn).
  • Native call boundary (khi gọi JNI native method).
// JIT compile method nay va chen safepoint poll
public void processItems(List<String> items) {
    for (String item : items) {   // <-- safepoint poll tai loop back-edge
        process(item);            // <-- safepoint poll tai method return
    }
}
// Khi JVM can stop: poll page bi protect -> thread nao hit poll se trap -> suspend
Analogy — đèn giao thông trên xa lộ

Safepoint giống đèn giao thông được đặt sẵn trên xa lộ. Bình thường xe chạy qua đèn xanh không dừng (poll không tốn gì). Khi cảnh sát (JVM) cần mọi xe dừng, họ bật đèn đỏ tại mọi điểm kiểm soát (protect polling page). Xe (thread) đến đèn đỏ tiếp theo thì dừng. Xe đang giữa 2 đèn (giữa 2 back-edge) phải chạy hết đến đèn giao thông kế tiếp mới dừng được.

3. Cơ chế safepoint polling — hardware page protection trick

JIT không thực sự check 1 biến boolean mỗi lần poll — điều đó tốn quá nhiều. HotSpot dùng một trick cực kỳ thông minh với memory page protection:

Bình thường (fast path):

; x86 assembly - safepoint poll instruction
; polling_page la dia chi 1 memory page JVM giu
TEST RAX, QWORD PTR [polling_page]   ; 1 instruction, doc memory
; Neu page readable -> no-op (result bo di)
; Cost: 1 memory read, ~1-5 ns khi in cache

Khi JVM muốn stop tất cả thread:

JVM gọi OS để mark polling page là unreadable (page không còn accessible). Bất kỳ thread nào thực thi instruction TEST RAX, [polling_page] tiếp theo sẽ gây segfault / access violation. JVM đã cài signal handler để catch segfault này — khi nhận được signal, handler biết thread đó đã arrive safepoint và suspend thread.

sequenceDiagram
    participant VM as JVM Main Thread
    participant T1 as Thread 1
    participant T2 as Thread 2
    participant OS as OS Memory
    participant GC as GC Thread

    VM->>OS: mprotect(polling_page, PROT_NONE)
    Note over VM: polling_page now unreadable

    T1->>OS: TEST RAX, [polling_page]  (safepoint poll)
    OS-->>T1: SIGSEGV / segfault
    T1->>VM: Signal handler: "I arrived safepoint"
    VM-->>T1: Suspend thread 1

    T2->>OS: Still running counted loop...
    Note over T2: T2 has no back-edge poll for next 10ms
    T2->>OS: TEST RAX, [polling_page]  (eventually)
    OS-->>T2: SIGSEGV
    T2->>VM: Signal handler: arrived
    VM-->>T2: Suspend thread 2

    VM->>GC: All threads at safepoint - start GC
    GC->>GC: Scan roots, collect garbage
    VM->>OS: mprotect(polling_page, PROT_READ)
    VM->>T1: Resume
    VM->>T2: Resume

Chi phí của poll trong fast path: 1 memory read instruction, khoảng 1-5 ns khi polling page được cache. Overhead gần như zero cho app bình thường.

4. Stop-The-World — các operation cần STW

STW (Stop-The-World) là cơ chế JVM yêu cầu tất cả Java thread phải arrive safepoint và suspend, sau đó JVM thực hiện 1 global operation, rồi resume tất cả thread.

Các operation cần STW:

OperationLý do cần STW
GC Young / Mixed / FullScan stack để tìm GC root, relocate object, update reference — cần heap không thay đổi trong quá trình
Biased lock revocationRevoke bias từ thread cũ khi thread khác muốn lock — deprecated Java 15+, removed Java 18
Class redefinition (JVMTI)Hot reload class qua Java agent — cần tất cả thread thoát khỏi method bị thay đổi
DeoptimizationJIT assumption sai (polymorphic call site có type mới) — cần patch compiled frame về interpreted
JFR thread sampleDump stack trace mỗi thread — cần stack ổn định để đọc
Thread-local handshakeJEP 312 — subset operation không cần full STW (xem mục 11)

STW latency = TTSP + actual operation time. GC log thường chỉ report operation time (sau khi tất cả thread đã suspend).

5. Time-to-safepoint — tại sao TTSP quan trọng

Time-to-safepoint (TTSP) là khoảng thời gian từ khi JVM request safepoint (protect polling page) đến khi thread cuối cùng arrive. Trong thời gian này:

  • Thread đã arrive: đã suspend, chờ.
  • Thread chưa arrive: đang chạy code giữa 2 safepoint poll.
  • JVM: chờ, không làm gì (kể cả GC chưa bắt đầu).
  • App: request mới không được xử lý vì thread pool có thể đã bị lock chờ.

TTSP tệ nhất xảy ra khi 1 thread đang chạy code dài giữa 2 back-edge poll. Thread không gặp back-edge poll vì:

  1. Counted loop được JIT optimize: for (int i = 0; i < N; i++) với N là int — JIT có thể xác định loop là counted loop (số lần chạy biết trước). Để tối ưu, JIT loại bỏ back-edge poll trong counted loop — chạy nhanh hơn nhưng TTSP tăng.

  2. Loop body rất nhanh, N rất lớn: 100 triệu iteration × 1ns/iteration = 100ms loop không có poll = TTSP 100ms.

  3. JNI native call dài: thread trong trạng thái in_native không thể bị stop tới khi return về Java.

6. Pitfall 1 — counted loop JIT remove back-edge poll

Đây là nguyên nhân phổ biến nhất gây TTSP spike trong production Java.

// Counted loop - int bound
// JIT CO THE remove safepoint poll tai back-edge
public long sumArray(int[] data) {
    long total = 0;
    for (int i = 0; i < data.length; i++) {   // counted loop
        total += data[i];
    }
    return total;
}

Với data.lengthint (ví dụ 100 triệu), JIT C2 nhận dạng counted loop và optimize aggressively — bao gồm việc loại bỏ safepoint poll tại back-edge. Vòng lặp chạy mà không poll. Nếu GC request safepoint trong lúc thread đang chạy loop này: TTSP = thời gian chạy hết phần còn lại của loop.

Workaround cổ điển (Java 8-10): dùng long bound thay int để JIT không nhận ra counted loop:

// long bound -> JIT khong consider "counted loop" theo nghia co the remove poll
public long sumArraySafe(long[] data) {
    long total = 0;
    for (long i = 0; i < data.length; i++) {   // not a classic counted loop
        total += data[i];
    }
    return total;
}

Java 10+ cải thiện: JIT thêm safepoint poll lại vào counted loop dài. Tuy nhiên, cách tốt nhất vẫn là đo và diagnose thay vì assume.

Java 16+: JVM thêm flag -XX:+UseCountedLoopSafepoints (enable by default từ Java 16) — buộc JIT giữ safepoint poll trong counted loop. Overhead nhỏ (~1-5%) nhưng TTSP giảm đáng kể.

# Java 16+: da mac dinh bat
# Java 8-15: phai bat thu cong
java -XX:+UseCountedLoopSafepoints MyApp

7. Pitfall 2 — JNI native call block safepoint

Thread gọi JNI native method chuyển sang trạng thái in_native. JVM không thể force-stop thread đang trong native code — native code không có safepoint poll.

JVM phải chờ thread return về Java, lúc đó safepoint poll tại native return boundary mới được check.

// Neu native method chay 500ms, TTSP co the tang len 500ms
public native void nativeLongRunning(byte[] data, int length);

public void process(byte[] largeFile) {
    nativeLongRunning(largeFile, largeFile.length);
    // Thread block trong JNI 500ms
    // GC phai cho 500ms truoc khi safepoint complete
}

Workaround trong native code: code JNI nên tự đặt safepoint check thủ công trong long-running loop:

// JNI C code - manual safepoint check moi N iteration
JNIEXPORT void JNICALL Java_Foo_nativeLongRunning(JNIEnv* env, ...) {
    for (long i = 0; i < count; i++) {
        // ... work ...
        if (i % 10000 == 0) {
            // Cho phep JVM check safepoint - attach/detach pattern
            // Hoac dung VM Thread: (*env)->DetachCurrentThread(vm) roi AttachCurrentThread
        }
    }
}

Trong practice: document rõ native method chạy dài, đặt timeout, hoặc chia nhỏ batch để native call không block quá lâu.

8. Pitfall 3 — String.indexOf trên String rất dài gây TTSP spike

Trong Java 8 và một số Java 11 patch cũ, String.indexOf(String) trên String hàng trăm MB (ví dụ parse log file lớn) có thể chạy hàng chục ms mà không có safepoint poll. JVM dùng native intrinsic (vectorized SIMD instruction) cho indexOf — intrinsic không có safepoint poll.

// TTSP spike: indexOf tren string 100MB
String logContent = readEntireLogFile();   // 100 MB
int pos = logContent.indexOf("ERROR");    // co the 50ms khong safepoint
// GC phai cho 50ms TTSP

Modern JDK (Java 17+) fix bằng cách thêm bounded chunks vào intrinsic — mỗi chunk kiểm tra safepoint. Nếu dùng Java 11 cũ mà gặp TTSP spike, xét dùng BufferedReader để xử lý từng dòng thay vì indexOf trên toàn file.

9. Diagnose TTSP — công cụ và cách đọc

Cách 1: -Xlog:safepoint*=info (Java 9+)

java -Xlog:safepoint*=info:file=safepoint.log MyApp

Output mẫu:

[10.234s][info][safepoint] Safepoint "GC_Minor", Time since last: 4234567900 ns, Reaching safepoint: 128000000 ns, Cleanup: 1234000 ns, At safepoint: 48234000 ns, Total: 177468000 ns

Trường quan trọng:

  • Reaching safepoint: TTSP = 128 ms. Đây là thời gian chờ thread arrive.
  • At safepoint: thời gian GC thực sự = 48 ms.
  • Total: tổng = 177 ms (user thấy).

Nếu "Reaching safepoint" >> "At safepoint" → TTSP là vấn đề, không phải GC algorithm.

Cách 2: JFR event (production-safe)

java -XX:+FlightRecorder \
     -XX:StartFlightRecording=duration=60s,filename=recording.jfr \
     MyApp

Trong JDK Mission Control: Events → VM Operations → filter "Safepoint Begin" + "Safepoint End". Column "Time To Safepoint" chính là TTSP. Sort descending để tìm spike lớn nhất.

Cách 3: -XX:+PrintSafepointStatistics (legacy, pre-Java 9)

java -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 MyApp

Print bảng safepoint statistics sau mỗi N safepoint. Java 9+ dùng unified logging thay.

Pattern chẩn đoán:

TTSP spike lớn (> 50ms):
  -> Tim thread dang chay gi trong khoang thoi gian do (JFR thread sample)
  -> Neu counted loop: check -XX:+UseCountedLoopSafepoints (Java 16+)
  -> Neu JNI: chia nho native call
  -> Neu String.indexOf: upgrade JDK hoac process stream

10. Sơ đồ timeline STW pause

gantt
    title Timeline STW Pause (thuc te user thay)
    dateFormat  X
    axisFormat  %Lms

    section JVM
    GC request safepoint    : milestone, 0, 0
    Wait all threads arrive  : crit, 0, 50
    GC work (STW)           : active, 50, 120
    Resume all threads      : milestone, 120, 120

    section Thread 1
    Running                 : 0, 10
    Suspended at safepoint  : crit, 10, 120
    Resumed                 : 120, 130

    section Thread 2
    Running (counted loop)  : 0, 50
    Suspended               : crit, 50, 120
    Resumed                 : 120, 130

    section GC Log
    Reports pause           : active, 50, 120

GC log chỉ báo 70ms (thời gian GC work). User thực sự mất 120ms (TTSP 50ms + GC 70ms). Thread 2 là "slow thread" — đang chạy counted loop 50ms mà không gặp safepoint poll.

11. Thread-local handshake — JEP 312, tối ưu STW

JEP 312 (Java 10) giới thiệu thread-local handshake: khả năng pause một thread cụ thể mà không cần full global STW.

Trước Java 10: mọi operation cần JVM control đều phải stop tất cả thread → STW.

Sau Java 10: JVM có thể gửi signal đến 1 thread cụ thể, pause thread đó, thực hiện operation, rồi resume — trong khi tất cả thread khác vẫn chạy bình thường.

Operation sử dụng thread-local handshake:

  • Sampling thread stack (JFR profiling, JVM TI).
  • Biased lock revocation (pause chỉ thread đang hold biased lock).
  • Deoptimization (pause chỉ thread đang chạy method được deopt).
  • Class unloading (một phần).

ZGC tận dụng thread-local handshake để thực hiện nhiều phase GC mà không cần full STW. ZGC chỉ dùng full STW cho Mark Start và Mark End (~1ms mỗi pha).

# Thread-local handshake bat mac dinh tu Java 10+
# Khong can flag rieng - JVM tu dung khi phu hop
ZGC và thread-local handshake

Bài 10 sẽ đi sâu vào ZGC mechanism. ZGC dùng thread-local handshake + load barrier để relocate object concurrent với app. Mỗi thread tự fix pointer của mình khi đọc reference — không cần global STW cho relocation. Đây là nguồn gốc của pause dưới 10ms trong ZGC.

12. Deep Dive

Deep Dive — spec và reference
  • JEP 312 — Thread-Local Handshakes (Java 10)openjdk.org/jeps/312 — mô tả mechanism handshake, các operation chuyển từ STW sang per-thread. Giải thích implementation: JVM set "handshake closure" trên thread descriptor, thread check khi qua safepoint poll.
  • HotSpot wiki — Safepointswiki.openjdk.org/display/HotSpot/Safepoints — chi tiết kỹ thuật polling mechanism, page protection trick, thread state machine (in_Java, in_native, in_vm, blocked).
  • Aleksey Shipilёv — "Safepoints: Meaning, Side Effects and Overheads"shipilev.net/blog/2015/safepoints/ — bài viết deep dive về safepoint overhead, counted loop problem, benchmark đo TTSP thực tế. Bắt buộc đọc nếu cần diagnose TTSP production.
  • JVM Unified Logging (Java 9+)openjdk.org/jeps/158-Xlog:safepoint*=info syntax. Giải thích tag system, level, output format.
  • JFR Safepoint eventsdocs.oracle.com/en/java/javase/21/jfr-api-guide/ — events jdk.SafepointBegin, jdk.SafepointEnd, jdk.SafepointStateSynchronization — field "timeToSafepoint" chính xác.
  • JEP 374 — Biased Locking Deprecation (Java 15)openjdk.org/jeps/374 — biased lock revocation là nguồn STW thường bị bỏ qua, đặc biệt trong app dùng nhiều synchronized object scope nhỏ. JEP 374 deprecated, Java 18 disabled — giảm STW unexplained.

13. Self-check

Tự kiểm tra
Q1
Safepoint là gì và tại sao JVM cần tất cả thread đến safepoint trước khi thực hiện GC?

Safepoint là vị trí trong execution nơi JVM có thể xác định hoàn toàn trạng thái của thread: biết chính xác mọi object reference đang live ở đâu (stack, register), không có operation đang dở (không giữa allocation, không giữa monitor acquire).

GC cần tất cả thread ở safepoint vì:

  • Scan GC root: GC cần đọc stack của mọi thread để tìm live reference. Nếu thread đang chạy, stack thay đổi liên tục — không thể đọc consistent. Thread phải suspend tại safepoint để stack ổn định.
  • Relocate object: khi GC copy object sang địa chỉ mới (young copy, compaction), mọi reference cũ phải được update. Nếu thread đang chạy đọc reference cũ giữa lúc GC move object → stale pointer → crash.
  • Update reference table: Remembered Set, Card Table, handle table — tất cả cần nhất quán tại thời điểm GC.

Tại safepoint, JVM biết chính xác nội dung từng register và stack slot của mọi thread (qua oop map — bảng do JIT generate mô tả register nào là reference). Đây là thông tin không có được khi thread đang giữa 2 instruction.

Ngoài GC, safepoint còn cần cho: deoptimization, biased lock revocation, class redefinition (JVMTI), JFR snapshot. Tất cả là "global operation" cần heap/thread state nhất quán.

Q2
Cơ chế safepoint polling hoạt động thế nào? Tại sao chi phí polling trong fast path gần như zero?

JIT chèn 1 instruction TEST RAX, [polling_page] tại mỗi safepoint location (method return, loop back-edge, allocation site, native call boundary). polling_page là địa chỉ của 1 memory page JVM giữ.

Fast path (bình thường): polling page ở trạng thái readable. Instruction TEST đọc memory và discard kết quả — là no-op về mặt logic. Chi phí: 1 memory read instruction, ~1-5 ns khi page đã cached. Không có branch, không có function call, không có syscall.

Slow path (JVM muốn stop): JVM gọi mprotect(polling_page, PROT_NONE) — mark page unreadable. Thread nào thực thi TEST RAX, [polling_page] tiếp theo sẽ nhận SIGSEGV (segfault). JVM đã install signal handler để catch — handler biết thread đó đã arrive safepoint, suspend thread.

Lý do dùng page protection trick thay vì check biến boolean:

  • Boolean check cần 1 branch instruction → branch predictor phải xử lý → micro-architecture overhead.
  • Page protection: fast path là TEST (no branch, always predict "go through") → zero branch misprediction overhead.
  • OS page protection atomic và hardware-enforced — không cần memory barrier.

JEP 312 (thread-local handshake) thêm per-thread polling page — cho phép stop 1 thread mà không affect thread khác.

Q3
Time-to-safepoint (TTSP) là gì và tại sao nó khác với GC pause time?

TTSP (Time-to-safepoint): khoảng thời gian từ khi JVM request safepoint (protect polling page) đến khi thread cuối cùng arrive và suspend.

GC pause time: khoảng thời gian từ khi tất cả thread đã suspend đến khi GC work xong và thread được resume. Đây là số GC log báo.

Tổng latency user thấy = TTSP + GC pause time. GC log chỉ báo GC pause time (bắt đầu sau khi TTSP xong).

Ví dụ: GC log báo "50ms pause". Thread đang counted loop không gặp safepoint poll 100ms. User thực sự bị block 150ms (100ms TTSP + 50ms GC).

Trong thời gian TTSP:

  • Thread đã arrive: suspend, chờ.
  • Thread chưa arrive: vẫn chạy (đang giữa 2 safepoint poll).
  • JVM: chờ tất cả arrive — chưa thể bắt đầu GC.
  • HTTP thread pool: 1 số thread suspend → server có thể queue request mới không xử lý được.

Diagnose: -Xlog:safepoint*=info báo "Reaching safepoint: XXX ns" = TTSP. Hoặc JFR event jdk.SafepointStateSynchronization field timeToSafepoint.

Red flag: TTSP vượt 50ms thường xuyên → tìm counted loop dài hoặc JNI call dài.

Q4
Counted loop tại sao gây TTSP spike và cách fix trong Java 16+?

Counted loop: vòng lặp có bound xác định tại compile time hoặc JIT compile time, thường là for (int i = 0; i < N; i++) với Nint. JIT C2 nhận ra và optimize aggressively.

Một trong các tối ưu: loại bỏ safepoint poll tại back-edge. JIT lý luận: "counted loop sẽ kết thúc sau N iteration — không cần check safepoint mỗi iteration vì loop sẽ tự kết thúc". Kết quả: thread chạy N iteration liên tục không gặp safepoint poll.

Vấn đề khi N = 100 triệu, mỗi iteration 1 ns: loop chạy 100 ms không poll. GC request safepoint → TTSP = thời gian còn lại của loop = có thể 100ms.

Fix:

  • Java 16+: -XX:+UseCountedLoopSafepoints (bật mặc định). JIT giữ back-edge poll trong counted loop. Overhead nhỏ (~1-5%) nhưng TTSP giảm đáng kể.
  • Java 8-15: dùng long bound thay int để cản JIT nhận ra counted loop (workaround không chính thống). Hoặc thêm -XX:+UseCountedLoopSafepoints thủ công (available từ Java 8 nhưng không mặc định).
  • Architectural: chia loop lớn thành batch nhỏ với Thread.yield() hoặc explicit safepoint check (không thực tế, JIT có thể optimize đi).

Verify: trước và sau enable flag, đo Reaching safepoint từ -Xlog:safepoint*=info. Nếu TTSP giảm rõ rệt → đây là nguyên nhân.

Q5
Thread-local handshake (JEP 312) cải thiện gì so với full STW?

Trước JEP 312 (Java 10): bất kỳ operation nào cần JVM control đều phải dùng full global STW — stop tất cả thread, thực hiện, resume tất cả. Ngay cả operation chỉ cần affect 1 thread (lấy stack trace, revoke biased lock của 1 thread) cũng phải stop 100 thread.

Thread-local handshake: JVM có thể gửi "closure" (operation) đến 1 thread cụ thể. Thread đó nhận và thực thi closure tại safepoint tiếp theo, rồi tiếp tục — trong khi tất cả thread khác chạy bình thường.

Operation hưởng lợi:

  • JFR profiling: sample stack của từng thread tuần tự thay vì stop tất cả. User-visible pause = 0.
  • Biased lock revocation: chỉ pause thread đang giữ biased lock, không phải tất cả.
  • Deoptimization: chỉ pause thread đang chạy method cần deopt.
  • ZGC relocation barrier fixup: từng thread tự fix pointer của mình khi load reference — không cần global stop cho phase này.

Hạn chế: không thể thay thế hoàn toàn full STW. GC scan heap vẫn cần tất cả thread ổn định — không thể để 1 thread alloc object trong khi GC đang scan heap của thread khác. Full STW vẫn cần cho Young GC, Mark Start, Mark End.

Tác động thực tế: giảm frequency và duration của STW pause cho non-GC operation. G1 và ZGC đều tận dụng handshake để giảm observed latency.

Q6
App bạn gặp GC log báo 30ms nhưng SLA breach 200ms xảy ra đồng thời. Bạn diagnose thế nào?

Gap 170ms giữa GC log và SLA breach gợi ý nguyên nhân ngoài GC thực sự. Quy trình diagnose:

Bước 1: Xác nhận TTSP là vấn đề

java -Xlog:safepoint*=info:file=safepoint.log:time MyApp

Tìm dòng: Reaching safepoint: XXX ns. Nếu XXX tương ứng với gap 170ms → TTSP confirmed.

Bước 2: Tìm thread gây TTSP

Dùng JFR: thu recording trong khi reproduce SLA breach. Trong JDK Mission Control: Events → Threads → thread activity trong khoảng thời gian TTSP. Thread nào vẫn chạy khi thread khác đã suspend → đó là slow thread.

Bước 3: Xác định code trong slow thread

JFR CPU sampling: trong khoảng TTSP, slow thread đang ở method nào? Hot method trong sample = nơi gây TTSP.

Bước 4: Fix theo nguyên nhân

  • Counted loop: enable -XX:+UseCountedLoopSafepoints (Java 16+ default on).
  • JNI native call dài: chia nhỏ hoặc thêm safepoint check trong native code.
  • String.indexOf trên string lớn: upgrade JDK hoặc process streaming.
  • Allocate bất thường: method alloc nhiều → Eden đầy nhanh → GC thường hơn → TTSP xảy ra nhiều hơn.

Lưu ý: JFR là non-invasive và production-safe. Ưu tiên JFR trước các flag diagnostic khác.

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

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên