java 8起永久代被元空间取代,因其易导致outofmemoryerror且管理僵硬;元空间使用本地内存,需设-xx:maxmetaspacesize防内存耗尽;类卸载仍依赖类加载器可回收等条件。

Java 8 之后为什么没有永久代了
因为永久代(PermGen)被元空间(Metaspace)取代了,根本原因是类元数据内存管理太僵硬——容易 java.lang.OutOfMemoryError: PermGen space,尤其在热部署、反射大量生成类的场景下。JVM 把这部分挪到本地内存(Native Memory),由操作系统直接管理,不再受 -XX:MaxPermSize 限制。
实操注意点:
-
-XX:MaxPermSize在 Java 8+ 已废弃,设了也不生效,还会报警告 - 元空间默认无上限(只受本地内存限制),但生产环境必须设
-XX:MaxMetaspaceSize,否则可能把机器内存耗尽 - 类卸载机制没变:只有满足“类加载器可被回收 + 类无实例 + 无引用”时,其元数据才可能被清理
- 用
jstat -gc <pid></pid>看MU(Metaspace Used)和MC(Metaspace Capacity),别再找PU/PGC
新生代里 Eden 和两个 Survivor 区怎么配合工作
新生代不是简单三分,而是“一次分配 + 两次拷贝”的协作结构:对象优先进 Eden;GC 触发时,存活对象从 Eden 和一个 Survivor(比如 S0)复制到另一个空的 Survivor(S1),同时年龄 +1;复制完成后清空 Eden 和原 Survivor。
关键细节:
立即学习“Java免费学习笔记(深入)”;
-
SurvivorRatio控制Eden : Survivor比例(如-XX:SurvivorRatio=8表示 Eden 占新生代 8/10,两个 Survivor 各占 1/10) - 对象不会在两个 Survivor 之间反复横跳——每次 GC 后,目标 Survivor 是固定的,源区清空,角色轮换
- 年龄阈值(
-XX:MaxTenuringThreshold)决定何时晋升老年代,默认是 15,但实际常被动态调整(如某次 GC 后 S1 空间不够,部分年龄小的对象也会提前晋升) - 大对象(超过
-XX:PretenureSizeThreshold)会直接进老年代,绕过新生代,避免在 Eden/Survivor 之间多次复制
老年代触发 Full GC 的常见条件有哪些
老年代本身不主动 GC,但它的状态会直接引发 Full GC:要么是分配担保失败(Minor GC 后发现老年代没空间容纳晋升对象),要么是老年代使用率超过阈值(-XX:InitiatingOccupancyFraction,对 CMS)或 G1 的混合回收决策点。
典型诱因:
- Minor GC 后,JVM 检查老年代剩余空间是否 ≥ 历史平均晋升大小 × 保守系数(
-XX:TargetSurvivorRatio影响该估算),不满足就直接 Full GC - 显式调用
System.gc()(除非加了-XX:+DisableExplicitGC,否则大概率触发 Full GC) - G1 中老年代占用达到
-XX:InitiatingOccupancyFraction(默认 45%),启动并发标记周期,后续进入混合回收阶段 - CMS 在老年代使用率到达
-XX:CMSInitiatingOccupancyFraction(默认未设,依赖内部启发式算法)时尝试并发收集,失败则退化为 Full GC
为什么 Metaspace 占用持续上涨却没触发 GC
元空间的 GC 不是由“空间满”驱动的,而是依赖类卸载——只有发生 Full GC(或 G1 的混合回收)且满足类卸载条件时,元空间中对应已卸载类的内存才被回收。如果应用一直持有类加载器(比如 Spring Boot 的 DevTools、OSGi、自定义 ClassLoader 未释放),元空间就会只增不减。
排查建议:
- 用
jcmd <pid> VM.native_memory summary scale=MB</pid>查看Metaspace实际占用,比jstat更准 - 加
-XX:+TraceClassUnloading看日志里有没有unloading class xxx,没有说明类根本没卸载 - 用
jmap -clstats <pid></pid>统计类加载器数量,异常增长往往意味着泄漏 - 不要迷信
-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio——它们只影响是否触发 GC,而 GC 是否真回收元数据,取决于类卸载是否成功
真正难处理的从来不是参数调优,而是类加载器生命周期和类卸载路径是否干净。这点比堆内存划分更隐蔽,也更容易被忽略。










