Escape Analysis và Scalar Replacement — khi JIT xoá object trước khi bạn tạo nó
Escape analysis phân tích object có thoát khỏi method không. Nếu không, JIT scalar-replace, elide lock, giảm GC pressure về 0 cho object đó. Cơ chế, pitfall, và cách verify.
TL;DR: Escape analysis là phân tích JIT-time xác định object có "thoát" ra khỏi method tạo ra nó không. Object không escape (NoEscape) mở ra 3 tối ưu: scalar replacement (decompose object thành các field primitive trên stack/register, không alloc heap), lock elision (bỏ synchronized vì không có contention), và stack allocation (lý thuyết — HotSpot thực tế dùng scalar replacement). Kết quả: object tạo ra trong vòng lặp hot không tốn heap, không tạo GC pressure. Nhưng escape analysis chỉ hoạt động sau khi method được JIT compiled, chỉ phân tích trong 1 method sau inline, và dễ bị đánh bại bởi virtual call hay exception path.
Bài 07 giải thích object header tốn 12-16 byte overhead. Câu hỏi tự nhiên: nếu object chỉ dùng cục bộ trong method rồi bị GC ngay, tại sao phải alloc heap tốn kém? Escape analysis là câu trả lời của JIT — phân tích tĩnh tại compile time, loại bỏ allocation hoàn toàn khi chứng minh được an toàn.
1. Scenario — benchmark Integer sum chạy nhanh đáng ngờ
Đoạn code sau sum 1 triệu Integer trong vòng lặp:
public long sumBoxed(int n) {
long total = 0;
for (int i = 0; i < n; i++) {
Integer boxed = Integer.valueOf(i); // alloc heap?
total += boxed; // unbox
}
return total;
}
Nếu hiểu theo mặt chữ: 1 triệu lần tạo Integer object trên heap (16 byte mỗi object), 1 triệu lần GC pressure, Eden đầy sau vài giây. Thực tế khi profile với JFR allocation profiling: không có allocation nào từ method này sau khi JIT warm up.
Vì sao? JIT thấy boxed không bao giờ thoát ra ngoài method sumBoxed. Không pass vào method khác, không return, không store vào field. JIT scalar-replace: biến Integer boxed thành int boxed_value = i trên stack, đọc boxed_value khi unbox. Object không bao giờ được tạo. GC pressure = 0.
Đây là escape analysis + scalar replacement.
2. Escape analysis — định nghĩa 3 mức escape
Escape analysis (bật mặc định từ Java 6u23, cờ -XX:+DoEscapeAnalysis) là phân tích JIT-time trả lời câu hỏi: "Object O được tạo trong method M có reference thoát ra ngoài M không?". Phân tích theo 3 mức:
NoEscape — reference đến object không bao giờ thoát khỏi thread hoặc method tạo ra nó:
- Không pass làm argument cho method khác (sau inline).
- Không return ra ngoài.
- Không assign vào static field hoặc heap field của object khác.
- Không lưu vào array.
ArgEscape — reference thoát qua argument khi gọi method khác, nhưng method đó không lưu reference:
- Pass vào method khác nhưng method chỉ đọc, không lưu lại.
- Ví dụ:
System.out.println(obj)—objthoát qua argument nhưngprintlnkhông giữ reference.
GlobalEscape — reference thoát hoàn toàn:
- Assign vào static field.
- Return ra ngoài caller.
- Store vào object khác đã escape.
Chỉ NoEscape mở ra tối ưu mạnh (scalar replacement, lock elision). ArgEscape mở ra một số tối ưu nhỏ hơn.
flowchart TD
A[New Object created in method M] --> B{Does reference escape M?}
B -- No --> C[NoEscape]
B -- Via argument only --> D[ArgEscape]
B -- Via return/static/field --> E[GlobalEscape]
C --> F[Scalar Replacement]
C --> G[Lock Elision]
C --> H[Stack Allocation - partial]
D --> I[Limited optimization]
E --> J[No optimization - normal heap alloc]3. Scalar replacement — decompose object thành primitives
Scalar replacement là tối ưu quan trọng nhất từ escape analysis. JIT decompose object NoEscape thành các scalar (giá trị đơn lẻ không thể decompose tiếp: int, long, reference tới object khác) và lưu chúng trên stack hoặc trong register thay vì alloc heap.
Ví dụ cụ thể:
class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
int distSquared() { return x * x + y * y; }
}
public int computeDistance(int px, int py) {
Point p = new Point(px, py); // NoEscape
return p.distSquared();
}
Sau scalar replacement (IR pseudo-code):
// JIT transform: Point biet mat, chi con 2 int tren stack
public int computeDistance(int px, int py) {
int p_x = px; // scalar: Point.x -> local int
int p_y = py; // scalar: Point.y -> local int
return p_x * p_x + p_y * p_y; // inline distSquared()
}
Không có new Point(...). Không có heap allocation. Không có GC. Object p chưa bao giờ tồn tại trên heap.
Tại sao scalar replacement mạnh hơn "stack allocation":
Nhiều tài liệu nói "object NoEscape được stack-allocate". Thực tế trong HotSpot, JIT không thực sự đẩy object header + fields lên stack. Thay vào đó, JIT decompose object thành từng field riêng lẻ và xử lý chúng như local variable bình thường (có thể lên register, có thể lên stack). Hiệu quả hơn stack allocation vì không cần header, không cần maintain object layout — fields được xử lý như primitives thuần.
4. Vòng lặp hot — scalar replacement loại GC pressure
Scalar replacement đặc biệt mạnh trong vòng lặp hot, nơi object tạo ra rồi chết trong cùng iteration:
// Pattern: object helper trong vong lap
public double[] normalize(double[] input) {
double[] output = new double[input.length];
for (int i = 0; i < input.length; i++) {
// Vec2 chi dung trong 1 iteration -> NoEscape
Vec2 v = new Vec2(input[i], 0.0);
double len = Math.sqrt(v.x * v.x + v.y * v.y);
output[i] = v.x / len;
}
return output;
}
Nếu JIT scalar-replace Vec2: vòng lặp 1 triệu iteration không tạo 1 triệu Vec2 object trên heap. GC pressure từ method này = 0. Kết quả: minor GC ít hơn, throughput tăng, latency p99 giảm.
flowchart LR
subgraph Before[Before Scalar Replacement]
B1[new Vec2 - heap alloc] --> B2[read v.x v.y] --> B3[compute] --> B4[GC eventually]
end
subgraph After[After Scalar Replacement]
A1[v_x = input-i] --> A2[v_y = 0.0] --> A3[compute with v_x v_y] --> A4[no alloc no GC]
end
Before -- JIT C2 optimize --> After5. Lock elision — xoá synchronized không cần thiết
Khi object là NoEscape, JVM biết chắc không có thread nào khác có reference đến object đó. Do đó, synchronized trên object đó không thể có contention — lock trở thành no-op.
Lock elision (còn gọi là lock elimination) là tối ưu JIT xoá synchronized block trên object NoEscape.
// Legacy code dung Vector (synchronized collection)
public int sumVector(int[] nums) {
Vector<Integer> v = new Vector<>(); // NoEscape -- chi dung trong method nay
for (int n : nums) v.add(n);
int total = 0;
for (Integer i : v) total += i;
return total;
}
Vector.add() và Vector.iterator() đều có synchronized. Tuy nhiên v không thoát ra ngoài sumVector — không thread nào khác có thể access. JIT elide tất cả synchronized blocks → overhead lock hoàn toàn biến mất. Code legacy dùng Vector có thể nhanh tương đương ArrayList sau lock elision.
Nhiều developer tránh dùng StringBuffer (synchronized) và dùng StringBuilder (không synchronized) vì lo overhead. Trong method scope (NoEscape), JIT elide lock của StringBuffer hoàn toàn — không có overhead. Nhưng viết StringBuilder vẫn tốt hơn về ý nghĩa code — thể hiện đúng intent thread-unsafe.
6. Pitfall 1 — escape analysis chỉ chạy sau JIT warm-up
Escape analysis là phân tích của JIT compiler, không phải interpreter. Khi method mới chạy lần đầu, JVM interpret bytecode — không có escape analysis, object được alloc heap bình thường.
Sau khi method được gọi đủ số lần (mặc định ~10,000 lần cho C2 tier), JIT compile method → thực hiện escape analysis → scalar replacement.
Lần 1-1000: interpreter -> new Object() alloc heap normally
Lần 1001-10000: C1 JIT (tier 3) -> partial opt, escape analysis limited
Lần 10001+: C2 JIT (tier 4) -> full escape analysis, scalar replacement
Hệ quả:
- Micro-benchmark không warm-up: đo allocation rate của method ngắn khi chưa JIT → kết quả sai lệch cao. Dùng JMH (tự warm-up trước khi đo).
- Method ít gọi: method gọi 1000 lần/giờ không bao giờ đạt C2 threshold → không có scalar replacement → allocation bình thường.
- Startup phase: app mới khởi động, mọi code chạy interpreter → GC pressure cao hơn sau warm-up.
// SAI: benchmark khong warm up
public static void main(String[] args) {
long start = System.nanoTime();
for (int i = 0; i < 100; i++) { // qua it lap de JIT warm up
hotMethod();
}
System.out.println(System.nanoTime() - start);
// Ket qua: do interpreter, khong phan anh JIT optimize
}
// DUNG: dung JMH voi @Warmup annotation
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.AverageTime)
public void benchmarkHotMethod() {
hotMethod();
}
7. Pitfall 2 — virtual call đánh bại escape analysis
Escape analysis trong HotSpot là intra-procedural sau inline: JIT phân tích object trong 1 method, nhưng chỉ sau khi inline các callee method. Nếu callee không thể inline (vì virtual call với nhiều target, vì quá lớn, vì native), JIT phải assume object có thể escape qua call.
Virtual call (interface method, abstract method) thường cản inline:
interface Processor {
void process(MyObject obj);
}
public void doWork(Processor p, MyObject obj) {
// obj co the escape qua p.process(obj)
// JIT khong biet p.process lam gi voi obj -> GlobalEscape assumed
p.process(obj);
// -> Khong scalar replacement cho obj
}
Nếu p.process có nhiều implementation (polymorphic call site), JIT không inline được → không thể chứng minh obj NoEscape.
Lambda và stream thường bị ảnh hưởng:
// Object tao trong lambda co the khong duoc scalar-replace
Optional<Point> result = points.stream()
.map(p -> new Point(p.x * 2, p.y * 2)) // Point co the escape qua stream pipeline
.filter(p -> p.x > 0)
.findFirst();
Stream pipeline dùng nhiều virtual call (lambda interface, stream intermediate operation) — JIT khó inline toàn bộ chain → object trong pipeline thường không được scalar-replace.
Fix: tránh virtual call trên hot path khi cần optimization:
// Thay vi stream, dung loop explicit cho hot path cuc ki critical
public Point[] scalePoints(Point[] input) {
Point[] output = new Point[input.length];
for (int i = 0; i < input.length; i++) {
// Neu Point la final class va constructor don gian:
// JIT co the inline + scalar-replace
Point scaled = new Point(input[i].x * 2, input[i].y * 2);
output[i] = scaled; // scaled ESCAPE vao array -> GlobalEscape -> khong optimize
}
return output;
}
JIT giữ inline cache per call site. Nếu 1 call site luôn gọi cùng 1 concrete type (monomorphic), JIT speculative-inline và escape analysis hoạt động. Nếu 2 type (bimorphic), JIT vẫn inline cả 2 với branch. Từ 3 type trở lên (megamorphic), JIT không thể inline → escape analysis fail. Profile bằng JFR hoặc -XX:+PrintInlining để phát hiện megamorphic hot sites.
8. Pitfall 3 — exception path phá vỡ scalar replacement
Exception handler phức tạp escape analysis. Khi JIT thấy try-catch bao quanh allocation, nó phải đảm bảo object có thể được inspect trong exception handler — điều này đôi khi buộc JIT giữ object trên heap thay vì scalar-replace.
// Try-catch xung quanh new co the can scalar replacement
public int risky(int x) {
try {
MyObject obj = new MyObject(x); // co the khong scalar-replace
return obj.compute();
} catch (Exception e) {
// Neu co exception, obj co the hien ra trong stack trace
// JIT phai giu obj on heap de debugger/JFR inspect duoc
return -1;
}
}
Không phải mọi try-catch đều cản — JIT phân tích exception type và path. Nhưng là yếu tố cần biết khi thấy method có exception path và allocation không được optimize.
Fix thực tế: không cần tránh try-catch trong code thông thường. Chỉ quan trọng nếu profile cho thấy method hot đang có unexpected allocation.
9. Verification — cách xác nhận scalar replacement đang hoạt động
Cách 1: JFR Allocation Profiling (production-safe)
java -XX:+FlightRecorder \
-XX:StartFlightRecording=duration=60s,filename=recording.jfr \
MyApp
# Mo trong JDK Mission Control:
# Events > Java Application > Object Allocation in New TLAB
# Neu method khong co allocation event = scalar replacement hoat dong
Cách 2: Diagnostic flags (debug/development only)
# Yeu cau JVM debug build hoac -ea builds
java -XX:+PrintEscapeAnalysis \
-XX:+PrintEliminateAllocations \
-XX:+UnlockDiagnosticVMOptions \
MyApp
# Output mau:
# [EA] Scalar replacement: Point [id=123] in method foo()
# [EA] Lock elision: synchronized [id=456] in method bar()
Cách 3: JITWatch (development)
JITWatch (github.com/AdoptOpenJDK/jitwatch) visualize JIT log. Tab "Eliminated Allocations" show object nào đã bị scalar-replace, method nào, iteration nào.
Cách 4: trước/sau benchmark JMH với allocation tracking
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
public void benchmarkWithAllocation() {
// JMH co the tich hop voi gc profiler:
// java -jar benchmarks.jar -prof gc
// Xem: gc.alloc.rate va gc.alloc.rate.norm
// Neu = 0 hoac rat thap -> scalar replacement hoat dong
computeDistance(3, 4);
}
Lưu ý quan trọng: -XX:+PrintEscapeAnalysis và -XX:+PrintEliminateAllocations chỉ hoạt động trên debug build của JVM hoặc khi dùng thêm -XX:+UnlockDiagnosticVMOptions. Production JVM thường không có debug flag này. Dùng JFR cho production diagnosis.
10. Lock elision trong practice — StringBuffer vs StringBuilder
Ví dụ thực tế về lock elision mang tính giáo dục:
// StringBuffer: synchronized method
public String buildStringBuffer(String[] parts) {
StringBuffer sb = new StringBuffer(); // NoEscape neu khong return sb
for (String part : parts) {
sb.append(part); // synchronized -- JIT elide
}
return sb.toString(); // toString() tra String, khong phai sb
// sb khong escape -> lock elision
}
// StringBuilder: khong synchronized
public String buildStringBuilder(String[] parts) {
StringBuilder sb = new StringBuilder();
for (String part : parts) {
sb.append(part); // khong lock
}
return sb.toString();
}
Sau JIT warm-up với C2, buildStringBuffer có performance tương đương buildStringBuilder. JIT elide toàn bộ synchronized trong StringBuffer.append() vì sb không escape.
Tuy nhiên: chọn StringBuilder vẫn là best practice vì:
- Code intent rõ ràng hơn — không thread-safe bằng design.
- Không phụ thuộc JIT warm-up để có performance tốt.
- Dễ review, tránh lầm tưởng thread-safety.
11. Giới hạn của escape analysis trong HotSpot
HotSpot escape analysis là intra-procedural: chỉ phân tích trong phạm vi 1 method sau inline. Không phân tích cross-method nếu method không inline được.
Cross-method escape là vấn đề mở trong research:
// Factory method -- JIT co the inline create() neu no du nho
Point p = PointFactory.create(x, y); // inline: p co the scalar-replace
// Neu create() qua lon hoac chua duoc JIT -> p GlobalEscape
GraalVM Native Image và GraalVM JIT thực hiện escape analysis mạnh hơn với partial escape analysis (bài toán: object escape trong 1 nhánh code nhưng không escape trong nhánh khác → alloc chỉ trên nhánh escape). HotSpot không có partial escape analysis.
Kinh nghiệm thực tế:
- Object nhỏ (1-5 field) constructor đơn giản trong loop → rất có thể được scalar-replace.
- Object có final field → JIT tin tưởng hơn.
- Object truyền qua nhiều layer method → ít khả năng scalar-replace.
- Hot code sau JMH warm-up: dùng JFR để confirm allocation rate trước khi optimize thủ công.
12. Deep Dive
- JEP 8141694 — Escape Analysis implementation note — bugs.openjdk.org/browse/JDK-8141694 — bug tracker entry cho escape analysis enhancements, kèm technical note về HotSpot C2 EA implementation.
- Aleksey Shipilёv — "Micro-benchmarking with JMH" — shipilev.net — section về elimination detection, JMH GC profiler, cách phân biệt allocation bị eliminated và không.
- Cliff Click và John Rose — HotSpot C2 Compiler design — các bài talk tại JavaOne giải thích IR (Ideal graph), JIT compilation pipeline, cách escape analysis tích hợp với C2 optimizer.
- GraalVM partial escape analysis — graalvm.org/latest/reference-manual/java/compiler/ — GraalVM JIT thực hiện partial escape analysis (PEA): object chỉ được alloc khi thực sự escape trong runtime path đó. Mạnh hơn HotSpot EA.
- JITWatch — github.com/AdoptOpenJDK/jitwatch — tool visualize JIT log, inline decision, eliminated allocation. Cột "Eliminated Allocations" trong main view. Free, open source.
- JFR Allocation Profiling — docs.oracle.com/.../jfr-api-guide/ — event
jdk.ObjectAllocationInNewTLABvàjdk.ObjectAllocationOutsideTLABtrack allocation mà không cần bytecode instrumentation. Production-safe.
13. Self-check
Q1Escape analysis là gì và 3 mức escape trong HotSpot được phân loại thế nào?▸
Escape analysis là phân tích JIT-time xác định xem reference đến object có "thoát" khỏi method tạo ra nó hay không. Phân tích xảy ra trong C2 compiler (tier 4 JIT), sau khi đã inline các callee method.
3 mức escape:
- NoEscape: reference không bao giờ thoát khỏi thread hoặc method. Không pass làm argument (sau inline), không return, không assign vào heap field hay static. Mở ra scalar replacement + lock elision.
- ArgEscape: thoát qua argument khi gọi method khác, nhưng callee không lưu reference. Ví dụ:
System.out.println(obj). Mở ra một số tối ưu nhỏ. - GlobalEscape: thoát hoàn toàn — assign vào static field, return ra ngoài, store vào object khác đã escape. Không có tối ưu đặc biệt — alloc heap bình thường.
Điểm cốt lõi: phân tích là conservative (an toàn). Nếu JIT không chắc chắn → assume GlobalEscape, không optimize. Đây là lý do virtual call hay cản escape analysis — JIT không inline được target → không biết object có escape trong callee không → assume GlobalEscape.
Q2Scalar replacement khác với stack allocation thế nào?▸
Stack allocation (lý thuyết): đẩy toàn bộ object (header + fields) lên thread stack thay vì heap. Object vẫn có layout đầy đủ, vẫn có header. Không cần GC — tự free khi method return (pop frame).
Scalar replacement (HotSpot thực tế): JIT decompose object thành từng field riêng lẻ (scalar). Mỗi field trở thành local variable độc lập — có thể lên register (không cần memory), hoặc lên stack như local int/long bình thường. Object header không tồn tại. Không có object layout.
Scalar replacement mạnh hơn stack allocation vì:
- Không cần object header (tiết kiệm 12-16 byte overhead).
- Field có thể live trong register (zero memory access).
- JIT có thể optimize field như primitive thông thường (constant fold, dead code eliminate).
Ví dụ: new Point(3, 4) trong method local → scalar replacement → 2 biến int p_x = 3; int p_y = 4; trên stack/register. Khi tính p.x * p.x + p.y * p.y → JIT constant-fold thành 25 nếu p_x, p_y là compile-time constant. Object không bao giờ tồn tại.
Q3Tại sao virtual call và interface method thường cản escape analysis?▸
Escape analysis trong HotSpot hoạt động sau khi inline callee method. Nếu method được inline thành công, JIT phân tích toàn bộ code mở rộng trong 1 context → có thể chứng minh object NoEscape.
Virtual call (qua interface hoặc abstract class) cản inline vì:
- JIT không biết concrete target tại compile time (polymorphic).
- Nếu call site đã thấy nhiều hơn 2 concrete type (megamorphic), JIT từ chối inline.
- Không inline được → JIT assume object pass vào callee có thể escape (callee có thể store reference vào field, static, v.v.).
- → GlobalEscape assumed → không scalar-replace.
Tuy nhiên, JIT dùng speculative inline: nếu call site thực tế monomorphic (chỉ thấy 1 concrete type), JIT inline optimistically với guard. Nếu sau đó xuất hiện type mới → deoptimize, recompile với type mới. Trong thực tế, nhiều "virtual call" trong app thực là monomorphic → JIT inline thành công → escape analysis hoạt động.
Stream/lambda: pipeline dùng nhiều virtual call (lambda interface), JIT khó inline toàn bộ chain → object trong pipeline thường không scalar-replace. Với loop explicit dùng concrete type → khả năng inline cao hơn → scalar replacement.
Q4Lock elision là gì? Tại sao Vector (synchronized) đôi khi nhanh như ArrayList sau JIT?▸
Vector (synchronized) đôi khi nhanh như ArrayList sau JIT?Lock elision (lock elimination) là tối ưu JIT xoá synchronized block trên object NoEscape. Lý do: nếu object không escape khỏi thread đang chạy, không thread nào khác có thể giữ lock đó → lock không có contention → lock là no-op.
Khi Vector v = new Vector() trong method local và v không thoát:
- JIT xác nhận
vNoEscape. - Mọi
synchronized(v)trongv.add(),v.get(),v.iterator()được elide. - Method thực thi như thể không có lock — throughput tương đương
ArrayList.
Điều kiện để lock elision hoạt động:
- Object (lock target) phải NoEscape.
- Method phải được C2 JIT compiled (sau warm-up).
- Callee method (
v.add()) phải được inline — để JIT thấy lock target.
Ý nghĩa rộng hơn: synchronized không phải luôn đắt. JIT optimize aggressively. Nhưng không nên dựa vào đây để viết code không rõ intent. Dùng StringBuilder thay StringBuffer vì ý nghĩa code rõ hơn, không phải vì performance.
Q5Làm sao verify rằng scalar replacement đang hoạt động cho 1 method cụ thể?▸
3 cách theo độ invasive tăng dần:
1. JFR Allocation Profiling (production-safe): thu thập JFR recording với event jdk.ObjectAllocationInNewTLAB. Nếu method hot không xuất hiện trong allocation profiling sau warm-up → scalar replacement đang hoạt động. Mở trong JDK Mission Control.
java -XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=r.jfr MyApp
# Sau do mo r.jfr trong JDK Mission Control2. JMH GC profiler: thêm -prof gc vào JMH benchmark. Output có gc.alloc.rate.norm — bytes allocated per operation. Nếu = 0 (hoặc rất nhỏ) → scalar replacement hoạt động.
java -jar benchmarks.jar -prof gc MyBenchmark.hotMethod3. Diagnostic flags (development JVM):
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintEscapeAnalysis -XX:+PrintEliminateAllocations MyAppChỉ hoạt động trên debug JVM hoặc JVM có diagnostic flag. Production OpenJDK thường không support flag này.
Lưu ý: không optimize prematurely. Đo trước bằng JFR/JMH, chỉ đào sâu khi profile cho thấy allocation là bottleneck thực sự.
Q6Đoạn code sau có được scalar replacement không? Giải thích tại sao.
public String format(int x, int y) { Point p = new Point(x, y); return "(" + p.x + "," + p.y + ")"; }▸
public String format(int x, int y) { Point p = new Point(x, y); return "(" + p.x + "," + p.y + ")"; }Câu trả lời: có thể không được scalar-replace vì String concatenation "(" + p.x + "," + p.y + ")" trong Java thường được compiler desugar thành StringBuilder operations.
Phân tích:
Point p: khởi tạo vớix,y. Nếu chỉ đọcp.xvàp.ymà không passpvào method khác →pcó thể NoEscape.- String concat được compiler expand thành
new StringBuilder().append("(").append(p.x).append(",").append(p.y).append(")"). Nếu JIT inlineStringBuilder.append→ thấyp.xvàp.yđược read nhưngpkhông store vào StringBuilder →pvẫn NoEscape → scalar-replace. - Tuy nhiên,
StringBuilderbên trong có thể được alloc rồi trả về qua.toString().StringBuilderinstance thoát quatoString()→ không scalar-replace (nhưng vẫn có thể được stack-alloc nếu JIT support).
Kết quả thực tế: p rất có khả năng scalar-replace (fields p_x = x, p_y = y dùng trực tiếp). StringBuilder không scalar-replace (escape qua toString()).
Cách verify: JMH với -prof gc, kiểm tra gc.alloc.rate.norm. Dùng JFR để trace allocation source. Đây là lý do dùng JOL + JFR thay vì đoán tay — JIT behavior phức tạp và thay đổi theo version.
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
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