一、方法区是什么
方法区(Method Area)是 JVM 运行时数据区的一部分,用于存储:
-
类的元信息(类名、方法、字段)
-
运行时常量池
-
静态变量
-
JIT 编译后的代码缓存(部分实现)
JDK 版本区别
-
JDK7 及之前:方法区 = 永久代(PermGen)
-
JDK8 之后:方法区 = 元空间(Metaspace)
👉 关键变化:
-
永久代使用 JVM 内存
-
元空间使用 本地内存(Native Memory)
二、方法区是否需要垃圾回收?
👉 结论:需要,但回收效率很低
原因:
-
方法区中的数据生命周期长
-
类一旦加载,很少卸载
-
回收条件苛刻
三、方法区回收什么?
方法区 GC 主要回收两类内容:
1. 废弃常量
运行时常量池中的无用常量
👉 示例:
java
String s1 = "abc";
String s2 = "abc";
如果 "abc" 不再被任何引用使用,就可能被回收。
2. 无用的类(重点)
类回收才是方法区 GC 的核心难点。
四、类什么时候可以被回收?
一个类要被卸载,必须满足 3个条件(面试高频):
-
该类所有实例都被回收
-
加载该类的 ClassLoader 被回收
-
该类的 Class 对象没有被引用
👉 必须同时满足!
举个例子
java
ClassLoader loader = new MyClassLoader();
Class<?> clazz = loader.loadClass("Test");
如果:
-
loader 还在
-
clazz 被引用
👉 类不会被回收
五、为什么类很难被回收?
主要原因:
-
类加载器通常是长生命周期(如应用类加载器)
-
静态变量会持有引用
-
框架(Spring、MyBatis)大量使用类缓存
-
线程上下文 ClassLoader 持有引用
👉 所以:
方法区 GC ≠ 堆 GC,触发频率极低
六、方法区 GC 触发时机
主要在以下情况发生:
-
Full GC 时
-
元空间不足时
-
显式调用
System.gc()
七、元空间内存溢出(重点)
异常:
java
java.lang.OutOfMemoryError: Metaspace
常见原因:
-
动态生成大量类(如代理类)
-
类加载器泄漏
-
热部署频繁(Tomcat)
-
使用 CGLIB / 动态代理过多
经典场景:类加载器泄漏
java
while(true){
new CustomClassLoader().loadClass("Test");
}
👉 不断创建新类加载器,类无法卸载 → Metaspace 爆炸
八、方法区垃圾回收算法
方法区没有单独的 GC 算法,通常依赖:
-
标记-清除
-
可达性分析
👉 重点:
-
类卸载依赖 GC Roots 可达性分析
-
条件极其严格
九、如何判断类是否"无用"?
JVM 会进行如下判断:
-
是否存在实例对象
-
ClassLoader 是否仍然存活
-
是否存在反射引用(Class对象)
十、如何触发类卸载?
需要满足 + 配置支持:
-XX:+ClassUnloading
-XX:+ClassUnloadingWithConcurrentMark
👉 G1 默认支持类卸载
十一、调优与排查(实战重点)
1. 查看元空间使用
jstat -gcutil <pid>
2. 查看类加载情况
jcmd <pid> GC.class_histogram
3. 分析工具
-
jvisualvm
-
MAT(Memory Analyzer Tool)
十二、如何避免 Metaspace OOM?
1. 设置大小限制
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
2. 避免类加载器泄漏
-
不要频繁创建 ClassLoader
-
注意线程池 + ThreadLocal
3. 控制动态代理数量
-
减少 CGLIB 生成类
-
使用单例代理
十三、面试高频问题总结
1. 方法区会不会 GC?
👉 会,但主要回收:
-
常量
-
无用类
2. 类卸载条件?
👉 三个必须同时满足:
-
无实例
-
ClassLoader 被回收
-
Class 对象无引用
3. 为什么难回收?
👉 因为:
-
类加载器长生命周期
-
静态变量引用
-
框架缓存
4. Metaspace OOM 原因?
👉 重点答:
-
类加载器泄漏(最常见)
-
动态生成类过多
十四、一句话总结
👉 方法区 GC 本质:
回收无用常量 + 卸载无用类(极难触发)