「Pig in the Python」形容 GC 將大物件在分代間頻繁搬移,導致 JVM 消化不良、停頓劇增。本文透過一個模擬「巨蟒吞豬」的實驗,揭示較小堆 + 較大新生代 + Parallel GC 竟比大堆 + 大老年代 + CMS 吞吐量高 12% 的反直覺結果。
public class PigInThePython {
static volatile List pigs = new ArrayList<>();
static volatile int pigsEaten = 0;
static final int ENOUGH_PIGS = 5000;
// 生產者:每 100ms 吞入 32MB「豬」
static class PigEater extends Thread {
public void run() {
while (true) {
pigs.add(new byte[32 * 1024 * 1024]);
if (pigsEaten > ENOUGH_PIGS) return;
sleep(100);
}
}
}
// 消費者:每 2 秒消化一次(清空列表)
static class PigDigester extends Thread {
public void run() {
long start = System.currentTimeMillis();
while (true) {
sleep(2000);
pigsEaten += pigs.size();
pigs = new ArrayList<>(); // 丟棄舊列表,產生大量垃圾
if (pigsEaten > ENOUGH_PIGS) {
System.out.format("Digested %d pigs in %d ms.%n", pigsEaten, System.currentTimeMillis()-start);
return;
}
}
}
}
}
理論最大吞吐量:10 隻/秒(100ms/隻)。
| 配置 | Config 1(大堆 + CMS) | Config 2(小堆 + Parallel) |
|---|---|---|
| Heap | -Xms4g -Xmx4g | -Xms2g -Xmx2g |
| 新生代 | -Xmn512m (12.5%) | -Xmn1536m (75%) |
| GC 策略 | -XX:+UseConcMarkSweepGC -XX:+UseParNewGC | -XX:+UseParallelGC |
| 實測吞吐量 | 8.2 隻/秒 | 9.2 隻/秒 (+12%) |
jstat -gc -t -h20 PID 1s
# 統計:1129 次 GC,總暫停 63.7 秒
Timestamp S0C S1C ... YGC YGCT FGC FGCT GCT
594.0 174720 174720 ... 1006 63.182 116 0.236 63.419
595.0 174720 174720 ... 1008 63.310 117 0.236 63.546
...
# 統計:168 次 GC,總暫停 11.4 秒
Timestamp S0C S1C ... YGC YGCT FGC FGCT GCT
539.3 164352 164352 ... 27 2.969 141 8.441 11.409
...
jstat -gc / GCLogAnalysis 觀察 YGC/FGC 頻率與暫停-Xmn、用 Parallel/ParallelOld/G1Plumbr、Arthas、Async Profiler 做持續觀測這條「抽風的巨蟒」提醒我們:GC 調優是門需深入理解底層機制的手藝活。同樣的業務邏輯,參數微調可能帶來數倍效能差距。與其盲目複製網上參數,不如建立觀測 → 假設 → 驗證 → 固化的調優閉環,讓 JVM 真正為業務加速。