1、方法区概述
方法区(Method Area)和Java堆内存一样是线程共享的一块内存区域,它被主要用来存储已经被虚拟机加载的类信息(字段、方法、构造器的字节码)、常量、静态变量、JIT编译后的代码等等(说的再简单直白点就是用来存储每个已经记载的类的结构信息)。 然而方法区只是JVM规范中定义的一个概念,在具体的虚拟机产品中对于方法区有不同的实现,以Java目前商用最广泛的HotSpot虚拟机来说,在JDK1.7及以前方法区的实现叫做永久代(Permenent Generation),在JDK1.8以后又变成了元空间(Metaspace)。他们其实是一个东西,只不过是不同版本下的不同实现而已。

图1 方法区
2、方法区中存储内容概述
2.1 类信息
存储 JVM 中类的类型信息,对每一个加载的类型, JVM 必须在方法区中存储以下的类型信息
● 这个类型的全限定名; ● 这个类型直接父类的全限定名(除非这个类型是 interface 或者是java.lang.Object,这两种情况下都没有父类); ● 这个类型的修饰符(public,abstract,final的某个子集);类型名称在java类文件和jvm中都以完整有效名出现。在java源代码中,完整有效名由类的所属包名称加一个".",再加上类名 组成。例如,类Object的所属包为java.lang,那它的完整名称为java.lang.Object,但在类文件里,所有的"."都被 斜杠“/”代替,就成为java/lang/Object。完整有效名在方法区中的表示根据不同的实现而不同。
2.2 域(属性)信息(程序中的一个范围)
JVM必须在方法区中保存类的所有属性的相关信息以及属性的声明顺序 ,具体需要保持的相关信息包括:
● 属性名; ● 属性类型;
● 属性修饰符(public, private, protected,static,final volatile,transient的某个子集)
2.3 方法信息
JVM必须保存所有方法的如下信息,同样和属性信息一样也要保存方法声明顺序,方法的相关信息
● 方法名;
● 方法的返回类型(或 void);
● 方法参数的数量和类型(有序的); ● 方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)除了abstract和native方法外,其他方法还有保存方法的字节码(bytecodes)操作数栈和方法栈帧的局部变量区的大小。
2.4 类变量
● 类变量被类的所有实例共享并且在内存中只有一份,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在JVM使用一个类之前,它必须在方法区中为每个non-final类变量分配空间(在类加载的准阶段)。 ●常量(被final修饰的类变量)的处理方法则不同,每个常量都会在常量池中有一个拷贝。non-final类变量被存储在声明它的类信息内。
注意 !
● java类中的成员变量有静态和非静态,静态成员变量是共享数据,在共享区,也叫方法区中; ● 非静态成员变量在堆内存中,作用于每个实例(在堆上创建对象时即给其分配区域) ● 局部变量在栈内存内,Java虚拟机栈为每一个被调用方法都分配一个栈帧,用于存放方法中的局部变量,对象的引用类型都会在此分配内存,引用指向的对象是在堆上。
2.5 运行时常量池 (Runtime Constant Pool)
运行时常量池(Runtime Constant Pool)是方法区的一部分。.class字节码文件中除了有类的版本、字段、方法、接口等描述类的信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量(文本字符串、声明为final的常量值等)和 符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符),这部分内容将在类加载后进入方法区的运行时常量池中存放。

图2 使用javap命令查看字节码中的常量池
运行时常量池相对于常量池的一个重要特征是具有动态性。这是什么意思呢,就是当你的java源文件文件一旦编译形成class文件后,你的常量池就确定了,而运行时常量池在运行期间也可能有新的常量放入池中(如String类的intern()方法)。
3、方法区的OOM
3.1 永久代的OOM和元空间的OOM
由于方法区只是JVM规范中的一个概念,具体的实现在不同的虚拟机上有很多不同,即使是同一个虚拟机的不同版本对于方法区的实现也会有许多差异,接下来我们就以HotSpot为例来演示一下方法区的OOM(OutOfMemoryError)。 JDK 7及以前在HotSpot虚拟机中方法区的具体实现是永久代,此时的永久代真实划分的内存还是在运行时数据内的,当空间不够时将抛出错误java.lang.OutOfMemoryError:PermGen space
;JDK8及以后方法区的具体实现变成了元空间,元空间的真实内存划分是在物理内存(Native Memory)上的,解决了JDK 7以前方法区容易发生OOM的问题,但是这并不代表元空间就不会发生OOM了,极端情况下当物理内存满了还是会导致抛出错误java.lang.OutOfMemoryError:Metaspace
。
3.2 对方法区内存大小的调节JVM参数
依然是先看管方文档,调节永久代或元空间大小的JVM参数如下:

图3.1 永久代内存大小调节参数
● -XX:MaxPermSize=size是用来设置永久代最大内存的,当超出这个内存限制将会抛出java.lang.OutOfMemoryError:PermGen space
。 ● -XX:PermSize=size是用来设置永久代触发垃圾回收的最小内存的。 注意!这些参数只有在JDK8以前的Java版本中有用。

图3.2 元空间内存大小调节参数
● -XX:MaxMetaspaceSize=size是用来设置元空间最大内存大小的,从官网的说明可以看到,这片区域默认是没有大小限制的,当设置了大小并且使用超过了限制或者没有设置大小使用导致物理内存被占满都将会抛出java.lang.OutOfMemoryError:Metaspace
。 ● -XX:MetaspaceSize=size是用来设置元空间触发垃圾回收的最小内存的。 注意!这些参数只有在JDK8及以后的Java版本中有用。
3.3 方法区OOM再现
分别在JDK6和JDK8环境下运行下面使用CGLib实现的一个代理操作:
package top.easyblog;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @author :huangxin
* @modified By:
* @Description:TODO
* @since :2020/02/16 00:20
*/
public class MethodAreaOOMTest {
public static void main(String[] args) {
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
导入代码后还需要导入CGLib的依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
在JDK8环境下运行上面的代码,发生如下图所示报错:

在JDK6环境下运行上面代码,发生如下图所示报错:

对于这个例子值得特别注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,这样的应用经常会出现在实际应用中:当前的很多主流框架,如 Spring 和 Hibernate 对类进行增强时,都会使用到 CGLib 这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的 Class 可以加载入内存,此时就需要我们根据实际情况通过JVM参数调节方法区空间的大小从而解决问题了。