HotSpot 虚拟机 — 运行时数据区 (堆分代 + 线程私有) / 垃圾回收 / JIT 分层编译: "一次编写, 到处运行"的引擎
JVM 把"内存怎么管、代码怎么执行快"这两件最难的事从程序员手里接管了: 堆分代 + GC 负责"对象生老病死", JIT 分层编译 负责"越跑越快"。理解运行时数据区是排查一切 Java 内存问题的地图 —— "对象在堆, 引用在栈, 类信息在 Metaspace"。
User u = new User(); // 关键: 引用在栈帧, 对象本体在堆 void f() { f(); } // 栈深度超限 → StackOverflowError // 堆/Metaspace 耗尽 → OutOfMemoryError, 方向完全不同
<init>。分配本身极快, 因为 TLAB 免锁。 // 类检查 → TLAB 分配 → 零值 → 对象头 → <init> Object o = new Object(); // TLAB 内指针碰撞, 免锁分配 // 关键: 线程私有 TLAB → Eden 分配无竞争, 这就是"极快"的根源
void handle(String req) { byte[] buf = new byte[8192]; // 朝生夕死: 下次 Minor GC 即回收 parse(buf, req); // 关键: 复制算法只搬存活者 → 又快又无碎片 }
jstat -gcutil <pid> 1000 # S0 S1 E O YGC FGC # 0.0 95.2 80.1 42.3 1024 3 ← Minor 频繁便宜, Full 稀少昂贵
PretenureSizeThreshold)。 // 每熬过一次 Minor GC 年龄+1, 默认 age ≥ 15 晋升 Old byte[] big = new byte[8 * 1024 * 1024]; // 大对象可越过 young 直接进 Old (PretenureSizeThreshold 控制)
for (int i = 0; i < N; i++) sum += i; // int counted loop 回边缺安全点 // 关键: 长循环拖长"全员到齐" → 意外 STW; 拆段/long 索引可缓解
static int add(int a, int b) { return a + b; } // 起步: 解释执行 for (int i = 0; i < 100_000; i++) add(i, 1); // 计数超阈值 → C1(Tier 3) → C2(Tier 4); 观察: -XX:+PrintCompilation
线上 java.lang.OutOfMemoryError: Java heap space, 标准处置链路:
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump.hprof -jar app.jar # 1. MAT 打开 dump → Dominator Tree(支配树) → 找最大对象与引用链 GC Roots # 2. 通常是: 无界缓存 / 一次查回百万行 SQL / ThreadLocal 未 remove / 大文件全量读入 # 3. 平时看趋势: jstat -gcutil <pid> 1000 → O 区持续上涨 = 泄漏或堆不够
JVM 总内存 = 堆 + Metaspace + 线程栈*N + 直接内存 + JIT 代码缓存。容器 memory limit=2G 却设 -Xmx2g, 会被内核 OOMKilled(堆外部分超限):
resources: { limits: { memory: "2Gi" } }
# 堆设为 limit 的 70~75%, 给堆外留余量:
java -Xmx1400m -Xms1400m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=256m -jar app.jar
# JDK 8u191+/10+ 已感知 cgroup, 但比例仍要自己控
G1 把堆切成 Region, 停顿由回收哪些 Region 决定。两个最常用的开关和一个最常见的坑:
java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms4g -Xmx4g -jar app.jar # 坑: 对象 ≥ Region/2(4G 堆默认 Region 2M → 1M 即 humongous)直接进老年代, # 批量读取大 List/大 JSON 反序列化会产生连串 humongous → 频繁 Full GC。 # 解法: 分页/流式处理, 或调大 Region: -XX:G1HeapRegionSize=8m
每秒一次 GC 概览, 告警口径定"FGC 频率"而非绝对值:
jstat -gcutil <pid> 1000 # S0 S1 E O M YGC YGCT FGC FGCT GCT # 0.0 95.2 80.1 42.3 95.0 1024 12.3 3 1.85 14.1 # 告警口径: FGC 增速 > 1次/小时 (P1); O 区持续 > 85% (P2); YGC 耗时环比翻倍 (P3)
RSS 涨但堆平稳 → 泄漏在堆外, Native Memory Tracking 定位:
java -XX:NativeMemoryTracking=summary -jar app.jar jcmd <pid> VM.native_memory baseline // 跑基线 # ……复现泄漏 30 分钟…… jcmd <pid> VM.native_memory summary.diff // 对比增长项 # 常见凶手: Thread(线程栈,线程泄漏) / Class(动态代理不释放) / Other(DirectBuffer)
顺序不能乱: 先堆大小 → 停顿目标 → 并发线程:
java -XX:+UseG1GC \
-Xms6g -Xmx6g \ // 1. 堆定宽(Xms=Xmx 防伸缩抖动)
-XX:MaxGCPauseMillis=100 \ // 2. 停顿目标(不是保证, 是方向)
-XX:ConcGCThreads=4 \ // 3. 标记线程=核数/8, CPU 富余可加
-Xlog:gc*:file=gc.log:time -jar app.jar
# 只改一个参数跑一轮压测, 三轮对比出结论 — 别一次改五个
"main 方法掐表"在 JVM 上毫无意义(死代码消除/预热/OSR), 微基准必须 JMH:
@Benchmark @BenchmarkMode(Mode.Throughput) public String measure() { return jsonSerializer.write(order); // 返回值防死代码消除 } # mvn test -Dtest=.*Benchmark -Djmh.fork=2 -Djmh.warmup=5 -Djmh.iter=5
APM(skywalking)/诊断(arthas)走字节码增强, 有真实开销:
// 预发环境 A/B: 同负载压测 有/无 javaagent 两组 java -javaagent:/skywalking-agent.jar -jar app.jar # 关注: 吞吐差 <10% 可接受; 超标则采样率降到 10% 或裁剪插件列表 # 生产 arthas 用完即卸: stop 后确认增强类已还原
G1 下大 List/大 JSON 反序列化会造成连串 humongous → 频繁 Full GC:
// gc.log 里搜 Humongous Reclaim 与 To-space exhausted: grep -c "to-space exhausted" gc.log // >0 即命中 # 解法按序: 分页/流式读 > 调大 Region(-XX:G1HeapRegionSize=8m) > 最后才是加堆
冷启动慢的大头是类加载, Class Data Sharing 直接共享已解析的类元数据:
// 1. 训练运行, 记录加载的类 java -XX:ArchiveClassesAtExit=app.jsa -jar app.jar --warmup # 2. 之后每次启动带归档 java -XX:SharedArchiveFile=app.jsa -jar app.jar # 常规效果: 启动 -20~40%, Serverless/FaaS 场景收益最大 (JDK17+ 还有 AOT/CRaC)
# 错: K8s limit=2Gi 却 -Xmx2g → 堆外被挤出 → OOMKilled(exit 137) java -Xmx1400m -Xms1400m -XX:MaxMetaspaceSize=256m -jar app.jar // 对: 堆 ≤ limit 75%
-XX:+DisableExplicitGC(注意 DirectBuffer 回收依赖它时改用 -XX:+ExplicitGCInvokesConcurrent)。 System.gc(); // 错: 显式 Full GC, 全线程 STW 打断所有请求 // 对: -XX:+DisableExplicitGC; DirectBuffer 依赖时改 +ExplicitGCInvokesConcurrent
List<Order> all = orderDao.findAll(); // 错: 百万行瞬间占满 Old try (Cursor<Order> c = dao.scan(500)) { handle(c); } // 对: 游标+fetchSize 流式
Map 当缓存只进不出 = 人为内存泄漏。正解: Caffeine(容量+TTL+W-TinyLFU)而非手写 HashMap。 static Map<String,Object> cache = new HashMap<>(); // 错: 只进不出 // 对: Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(...)
# 错: 服务刚起就全量压测 → JIT 未升温, 低估容量盲目扩容 # 对: 先打 10min 预热流量(或 JWarmup), 再开始采集压测数据
# 错: java -Xmx40g -XX:-UseCompressedOops # 随手关, 引用 4B→8B # 对: ≤32G 保持默认开启; 更大堆先算对象密度再决定
OutOfMemoryError: Metaspace。正解: 代理类缓存复用(生成一次存 Map); 监控 LoadedClassCount 趋势。 Object proxy = enhancer.create(); // 错: 每次生成新代理类 → Metaspace 只涨不跌 // 对: PROXIES.computeIfAbsent(clazz, k -> build(k)); 生成一次复用
String k = ("user-" + id).intern(); // 错: 海量动态串塞常量池 String k = "user-" + id; // 对: 正常分配, young GC 自然回收
Integer a = 127, b = 127; a == b; // → true (缓存命中, 碰巧对了) Integer x = 128, y = 128; x == y; // → false! 对: 一律用 equals
// 错: 池内线程只在其创建那刻继承, 后续任务拿到老上下文 // 对: TtlExecutors.getTtlExecutor(pool) 包装提交, 或任务显式传参
protected void finalize() { close(); } // 错: 等 Finalizer 线程, 回收延迟 try (Handle h = open()) { use(h); } // 对: AutoCloseable + Cleaner(9+)
static Map<Long, Order> RECENT = ...; // 错: 类级生命周期, 永不释放 // 对: Caffeine.maximumSize(1000); static 只放配置与不可变常量
-Dsun.rmi.dgc.server.gcInterval=36000000 拉长 + DisableExplicitGC 组合评估。 # 错: RMI DGC 默认每小时 System.gc() → "没人调用也 FGC" # 对: -Dsun.rmi.dgc.server.gcInterval=36000000 + DisableExplicitGC 评估
# 错: 12G dump.hprof 拉到笔记本直接 MAT 打开 → 分析机先 OOM # 对: 服务器命令行出 Leak Suspects 报告再拉小文件; MAT 本机只做索引
# 错: -XX:+UseG1GC -Xms6g -Xmx6g -Xmn2g # 手动固定年轻代 # 对: G1 不设 -Xmn/NewRatio, 交给 MaxGCPauseMillis 驱动自适应
# 错: S 区放不下存活对象 → 直接晋升 → Old 涨快 → Full GC 频繁 # 对: gc.log 看每轮晋升量, 调 -XX:NewSize / SurvivorRatio=8
ByteBuffer.allocateDirect(1 << 30); // 错: 用完不管, 堆外只涨不跌 // 对: Netty buf.release() 引用计数; 自用 try-finally 显式释放
# 错: 一见 FGC 就加堆 grep "Pause Full" gc.log # 对: 先看 cause — System.gc()/Metadata/promotion failed 药方不同
# 错: JDK 8→11 直接升, 默认 Parallel→G1, 吞吐型批任务掉速 java -XX:+UseParallelGC -jar app.jar // 对: 升级清单显式声明或按目标重选
// 错: 压测全用 10 行小订单 → 大客户万行订单的 humongous 才炸 // 对: 生产流量录制回放 + 专门构造大 key/大单用例