方法区概述
方法区主要用于存储JVM加载的每一个类的信息,包括类相关信息、字段信息、方法信息、常量池(运行时常量池、字符串常量池)、静态变量、方法表、编译器编译后的代码。
方法区是线程共享的;当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待;
方法区的大小是不固定的,JVM可以根据需要对方法区进行扩展,但如果类过多可能会导致方法区溢出,我认为这是JDK1.8以后将方法区挪到本地内存的原因之一。
类相关信息:public final class ClassStruct extends Object implements Serializable
- 修饰符(public final)
- 是类还是接口(class,interface)
- 类的全限定名(Test/ClassStruct.class)
- 直接父类的全限定名(java/lang/Object.class)
- 直接父接口的权限定名数组(java/io/Serializable)
字段信息:private String name
- 修饰符(pirvate)
- 字段类型(java/lang/String.class)
- 字段名(name)
方法信息:
- 方法声明的顺序
- 修饰符
- 方法返回值(java/lang/String.class)
- 方法名(getStatic_str)
- 局部变量和操作数栈大小
- 方法体的字节码(就是花括号里的内容)
- 异常表(throws Exception)
运行时常量池:每个class文件都维护一个常量池(class文件常量池),里面存放着编译时期生成的各种字面值和符号引用,该常量池的内容在类加载时被加载到方法区的运行时常量池。
字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。
对类加载器的引用:jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。
对class类的引用:jvm为每个加载的类都创建一个java.lang.Class的实例(存储在堆上)。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据(类的元数据)联系起来, 因此,类的元数据里面保存了一个Class对象的引用;
方法区的演变
JDK1.8以前,堆和方法区连在了一起,但这并不能说堆和方法区是一起的,它们在逻辑上依旧是分开的。但在物理上来说,它们又是连续的一块内存。
JDK1.7及以前的永久代
在JDK8以前,HotSpot JVM以永久代实现方法区,永久代和老年代,新生代一样在堆上实现。但是后来这种实现方法区的方式弊端逐渐显示出来。
- 永久代在堆中容易造成OOM(内存溢出),如果它的内存设置过大,那么就会使得老年代和新生代的内存不足,导致频繁GC;如果它的内存设置过小,因为在程序运行过程中,加载多少类是不可知的,所以在类很多的情况下,会导致内存溢出。
- 字符串常量池存放在永久代中,也容易造成永久代内存溢出。
JDK1.8以后的元空间
JDK1.7将字符串常量池从永久代中移出,单独存放在堆中,在JDK8中则完全将方法区移出永久代,改用元空间实现方法区,而类静态变量也随着Class类对象存放在堆中(成员变量一直都随类对象存在堆中)。剩下的都放在元空间里。而元空间也不再放在JVM内存中而是放在本地内存。