9. 方法区
JVM的方法区是Java虚拟机中用于存储类元数据的特殊内存区域。在Java 8之前,方法区通常通过永久代(PermGen)实现,但在Java 8及之后的版本中,它被元空间(Metaspace)所取代,这一改革是为了解决原有永久代大小固定的问题,并且将方法区的存储位置从Java堆移动到本地内存。本文主要介绍方法区的结构、方法区内存大小的设置、方法区的垃圾回收以及常量池等内容。
9.1 栈、堆、方法区的交互关系
9.1.2 运行区数据区结构图

从线程共享的角度来看

9.2.2 栈、堆、方法区的交互关系
Person person = new Person();


9.2 方法区的理解
9.2.1 官方文档
2.5.4. Method Area
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact(v.压缩) it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.
The following exceptional condition is associated with the method area:
- If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an
OutOfMemoryError.
官方文档地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4
9.2.2 方法区在哪里?
《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
所以,方法区看作是一块独立于Java堆的内存空间。

方法区并没有占用堆的大小。我们通过参数 -Xms 和 -Xmx 设置堆内存大小时,是不包含方法区的。如下示例
public class HeapDemo {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end...");
}
}
设置虚拟机内存大小:
-Xms600M -Xmx600M
然后通过 Visual VM 观察虚拟机内存

Eden + S0 + S1 + Old Gen = 600M,并没有包含 Metaspace
9.2.3 方法区的基本理解
- 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError: PermGen space (JDK 7) 或者java.lang.OutOfMemoryError: Metaspace (JDK 8及以上)
- 加载大量的第三方的jar包;
- Tomcat部署的工程过多(30~50个);
- 大量动态的生成反射类
- 关闭JVM就会释放这个区域的内存。
public class HeapDemo {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end...");
}
}
上面的代码很简单,但是加载的类远不止看到的这么多。类的加载数量如下:

9.2.4
- 在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代。
In Jdk 8, classes metadata is now stored in the native heap and this space is called Metaspace.
- 本质上,方法区和永久代并不等价。仅是对 HotSpot 而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEA JRockit / IBM J9 中不存在永久代的概念。
- 现在来看,当年使用永久代,不是好的idea。导致Java程序更容易OOM(超过
-XX:MaxPermsize上限)
- 现在来看,当年使用永久代,不是好的idea。导致Java程序更容易OOM(超过

- 而到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替

- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
- 永久代、元空间二者并不只是名字变了,内部结构也调整了
- 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常
9.3 设置方法区大小与OOM
设置方法区内存的大小
- 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整。
- JDK 7 及以前
- 通过
-XX:PermSize来设置永久代初始分配空间。默认值是 20.75M - 通过
-XX:MaxPermSize来设定永久代最大可分配空间。32 位机器默认是 64M,64 位机器模式是 82M - 当 JVM 加载的类信息容量超过了这个值,会报异常
OutOfMemoryError:PermGen space。
- 通过

- JDK8 以后
- 元数据区大小可以使用参数
-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定 - 默认值依赖于平台。windows 下,
-XX:MetaspaceSize是 21M,-XX:MaxMetaspaceSize的值是 -1,即没有限制。 - 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常
OutOfMemoryError:Metaspace -XX:MetaspaceSize:设置初始的元空间大小。对于一个 64 位的服务器端 JVM 来说,其默认的-XX:MetaspaceSize值为 21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于 GC 后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。- 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到 Full GC 多次调用。为了避免频繁地 GC,建议将
-XX:MetaspaceSize设置为一个相对较高的值。
- 元数据区大小可以使用参数
MetaspaceSize 默认大小
C:\Users\Administrator>jps
28096
57780 Launcher
62644 MethodAreaDemo
C:\Users\Administrator>jinfo -flag MetaspaceSize 62644
-XX:MetaspaceSize=21807104
C:\Users\Administrator>jinfo -flag MaxMetaspaceSize 62644
-XX:MaxMetaspaceSize=18446744073709486080
设置 MetaspaceSize 为 100M
-XX:MetaspaceSize=100M -XX:MaxMetaspaceSize=100M
如果再 JDK 8 及以后版本中设置了 PermSize ,程序可以正常运行,但是会有下面的提示
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=100M; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize; support was removed in 8.0
代码举例
《深入理解Java虚拟机》举的例子
/**
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
* @author zzm
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
举例2
/**
* jdk8中:
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*
* jdk6中:
* -XX:PermSize=10m -XX:MaxPermSize=10m
*/
public class OOMTest extends ClassLoader{
public static void main(String[] args){
int j = 0;
try{
OOMTest test = new OOMTest();
for (int i=0;i<10000;i++){
//创建Classwriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
//指明版本号,public,类名,包名,父类,接口
//classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, nu1l, "java/lang/Object", null);
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, nu1l, "java/lang/Object", null);
//返回byte[]
byte[] code = classWriter.toByteArray();
//类的加载
test.defineClass("Class" + i, code, 0, code.length); //CLass对象
j++;
}
} finally{
System.out.println(j);
}
}
}
如何解决这些OOM
- 要解决 OOM 异常或 heap space 的异常,一般的手段是首先通过内存映像分析工具(如 Eclipse Memory Analyzer)对 dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链。于是就能找到泄漏对象是 通过怎样的路径与 GCRoots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及 GCRoots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
- 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(
-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
9.4 方法区的内部结构

方法区(Method Area)存储什么?
《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

方法区的内部结构
类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于 interface 或是
java.lang.Object,都没有父类) - 这个类型的修饰符(
public,abstract,final的某个子集) - 这个类型直接接口的一个有序列表
域(Field)信息
- JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
- 域的相关信息包括:域名称、域类型、域修饰符(
public,private,protected,static,final,volatile,transient的某个子集)