内存区域
JVM 把内存划分为:堆(对象实例)、虚拟机栈(方法帧/局部变量)、元空间(类元数据)、程序计数器等。OOM 往往来自堆或元空间。
| 区域 | 存什么 | 溢出错误 |
|---|---|---|
| 堆 Heap | 对象实例 | OutOfMemoryError: Java heap space |
| 虚拟机栈 | 方法调用帧 | StackOverflowError |
| 元空间 Metaspace | 类元数据 | Metaspace OOM |
堆与栈
基本类型与对象引用在栈上;对象实例在堆上。方法结束栈帧出栈,对象等 GC 回收。理解这点能解释"参数为何是值传递"。
void f(User u) { u = new User(); } // 仅改变局部引用副本,调用方不变
void g(User u) { u.setName("x"); } // 修改同一堆对象,调用方可见传递对象时复制的是"引用的值",所以能改对象状态,但无法让调用方的变量指向新对象。
垃圾回收
GC 通过"可达性分析"从 GC Roots 出发标记存活对象,回收不可达者。堆分代:新生代(Eden+Survivor)与老年代,采用不同策略。
新对象 → Eden
频繁 Minor GC(复制存活对象到 Survivor)
熬过多次 → 老年代
老年代满 → Major/Full GC(更慢)大对象直接进入老年代,易触发 Full GC 停顿。缓存、大集合要控制生命周期,避免无谓晋升。
常见收集器
G1 是默认平衡选择;ZGC/Shenandoah 追求亚毫秒级停顿,适合大堆低延迟。选收集器要权衡吞吐与停顿。
| 收集器 | 特点 | 适用 |
|---|---|---|
| G1 | 区域化、可预测停顿 | 通用默认 |
| ZGC | 并发、<10ms 停顿 | 大堆低延迟 |
| Serial/Parallel | 吞吐优先 | 小应用/批处理 |
类加载
类加载分加载→链接(验证/准备/解析)→初始化。双亲委派:先让父加载器尝试,保证核心类不被篡改、全局唯一。
Bootstrap → Platform(Ext) → Application → 自定义
(子加载器优先委托父加载器)JMM 内存模型
JMM 定义多线程下变量的可见性与有序性。普通变量修改对其他线程未必立即可见;volatile 保证可见性与禁止重排(不保证原子性)。
private volatile boolean running = true;
// 一个线程改 running=false,另一线程循环读能立即看到i++ 这种"读-改-写"即使 volatile 也非原子,需用 AtomicInteger 或锁。
基础调优
通过 JVM 参数设定堆大小与 GC 日志,结合监控定位瓶颈。盲目调大堆可能反而延长 GC 停顿。
java -Xms2g -Xmx2g -XX:+UseG1GC \
-Xlog:gc*:file=gc.log:time \
-jar app.jar用 GC 日志与 APM 找出真实瓶颈(是停顿、内存还是 CPU?)再动手,别凭感觉改参数。
常见陷阱与 FAQ
存在——通常是"本该释放的引用被长期持有"(如静态 Map、未注销的监听器、线程池里的 ThreadLocal 没 remove)。GC 只回收不可达对象。
看 GC 日志判断是老年代增长快还是元空间满;用堆转储(heap dump)+ MAT 找大对象与引用链。
局部变量在栈上,线程私有,不存在跨线程可见性问题,所以无需 volatile。