新对象默认且唯一首选分配在Eden区,仅大对象超-XX:PretenureSizeThreshold时直接入老年代;Survivor双区为复制算法服务;System.gc()仅为可忽略建议;Metaspace溢出多因类加载器泄漏。

对象分配在哪?Eden区不是“默认起点”而是“唯一首选”
新对象几乎总在 Eden 区分配,这是 JVM 分代设计的硬性约定,不是可配置的“偏好”。哪怕你把 -Xmn 设得再小,只要堆还有空闲,JVM 就不会跳过 Eden 直接往 Survivor 或老年代扔对象(大对象例外,见下条)。
- 触发
Minor GC的条件是 Eden 区满,不是“用了 80% 就回收”——它不看比例,只看是否还能成功分配下一个对象 - 若对象大小超过
-XX:PretenureSizeThreshold(默认为 0,即禁用),JVM 会直接将其分配到老年代,避免在新生代反复复制造成浪费 - 注意:字符串字面量(如
"hello")不进 Eden,它们存于堆中的运行时常量池(JDK 7+ 已移出方法区),而new String("hello")才会在 Eden 分配实例
为什么 Survivor 区要设两个(S0 和 S1)?不是为了“备份”,而是为复制算法腾空间
Survivor 区的存在,本质是为 复制算法 服务:每次 Minor GC 后,Eden + 当前使用的 Survivor(比如 S0)中存活的对象,被一次性复制到另一个空的 Survivor(S1),然后清空 Eden 和 S0。没有“双区”,复制就无法原子完成。
- 比例
-XX:SurvivorRatio=8表示 Eden : 单个 Survivor = 8:1,不是 Eden : 总 Survivor;两个 Survivor 大小相等、角色互换 - 对象在 Survivor 中“熬过”多少次 GC 才晋升老年代,由
-XX:MaxTenuringThreshold控制(默认 15),但实际晋升可能更早——当某次 GC 后,To Survivor 空间不足以容纳所有存活对象时,JVM 会提前把部分对象送入老年代(动态年龄判定) - 误配
-XX:SurvivorRatio过大会导致 Survivor 过小,频繁触发提前晋升,加剧老年代压力;过小则浪费空间,且易因复制失败触发 Full GC
System.gc() 不是“手动触发回收”,而是向 JVM 发送一个“可忽略的建议”
调用 System.gc() 仅等价于 Runtime.getRuntime().gc(),它不保证任何行为:G1 可能完全无视,CMS 可能转为并发模式,Serial/Parallel 则大概率触发一次 Full GC——但前提是 JVM 当前没在 GC 中、没被 -XX:+DisableExplicitGC 拦截。
- 线上服务务必加
-XX:+DisableExplicitGC,否则第三方 SDK 或日志框架里一句System.gc()就可能引发数秒停顿 - 显式 GC 常见于内存敏感场景(如 Android 应用退到后台),但在服务器端,它和手动调用
Thread.stop()一样危险:破坏 JVM 自主调度节奏 - 真正需要“及时释放”的资源(如 DirectByteBuffer),应依赖
Cleaner或try-with-resources,而非寄望于 GC 回收
元空间(Metaspace)溢出 ≠ 类太多,很可能是类加载器泄漏
JDK 8+ 用本地内存实现的 Metaspace 替代了永久代,java.lang.OutOfMemoryError: Metaspace 错误出现时,第一反应不该是加 -XX:MaxMetaspaceSize,而应检查是否有自定义类加载器未被回收——每个加载器加载的类元数据都独占一块 Metaspace,且只有加载器本身被 GC 时,这些元数据才释放。
- 典型泄漏场景:OSGi 容器、热部署框架(如 Spring Boot DevTools)、或反复
new URLClassLoader加载同一 JAR - 排查命令:
jstat -gc看MU(Metaspace used)持续上涨;jcmd查 native 内存分布VM.native_memory summary - 修复关键:确保类加载器引用链可被 GC(如清除静态 Map 缓存、关闭线程上下文类加载器绑定)










