Java虚拟机中的方法区(Method Area)用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器(JIT)编译后的代码缓存等数据。方法区中存储的内容其实可以粗略的看作.class
字节码文件加载到内存后的数据结构。
这里要注意的一点是,方法区是JVM规范中定义的一个概念,在实际的JVM实现中可能各不相同,例如在Java8的HotSpot中,常量池、类型信息和JIT代码缓存位于元空间(Metaspace),而静态变量和字符串常量池位于堆空间中。此外,JVM规范也没对方法区的垃圾回收进行明确规定,在不同JVM和不同的垃圾回收器下可能表现不同。
前面我们介绍过虚拟机栈、堆等,方法区和堆一样,是各个线程共享的内存区域。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,可能导致方法区的内存溢出。
在JDK7及之前的HotSpot中,方法区位于一个被称为永久代(Perm)的位置,JDK8及之后改为了元数据空间(Metaspace)。两者的区别是元数据空间不在JVM内存中,而是使用本地内存。在JDK7及之前的老工程中经常遇到加载了许多Jar包后永久代溢出的问题,不过在JDK8及之后由于元数据空间的内存分配方式发生了变化,因此已经很少遇到方法区内存不足的问题了。
JDK8及之后的HotSpot中,由于方法区使用元数据空间取代了永久代,因此配置参数也发生了变化。
-XX:MetaspaceSize:元数据空间起始值,由于使用了本地内存因此默认值依赖平台,Windows下64位JDK默认为21MB
-XX:MaxMetaspaceSize:元数据空间最大值,Windows下默认无限制
默认情况下,元数据空间的大小会自动在初始值和最大值之间调整。当第一次元数据空间达到MetaspaceSize
后就会触发Full GC,然后适当提升触发Full GC的阈值,但这个值不会超过MaxMetaspaceSize
。出于性能调优的考虑,在加载了大量框架、第三方库的大型工程中,我们可以直接给元数据空间指定一个较大的初始值,这样能够减少Full GC的次数。
如图所示是Java8的HotSpot中方法区的结构。这里注意Java7及之前的HotSpot,或是不同的虚拟机实现可能都和图中不完全一样,这里仅针对Java8来进行说明。
类型信息:对于每个加载的类(Class、Interface、Enum、Annotation)JVM都会在方法区存储一些信息,包括:1)类全限定名 2)父类的全限定名 3)类的修饰符 4)类直接接口的有序列表
字段信息:方法区保存了类中所有字段以及字段的声明顺序,包括字段名称、字段类型、字段修饰符。
方法信息:方法区还保存了类中所有方法的信息,包括:方法名,方法返回值类型,方法参数的数量和类型,方法修饰符,方法的字节码、操作数栈、局部变量表及大小,以及异常表。
非final的静态类变量:静态变量和类关联在一起,随着类的加载而加载,成为类数据中的一部分。
运行时常量池:前面我们查看过字节码文件中的常量池,常量池就是一个表格,虚拟机指令根据这张表格找到要执行的类、方法、参数类型、字符串字面量等信息。方法区中的运行时常量池就是其加载到内存中的数据结构,用于实现动态链接。
Java语言中字符串是一种比较特殊的类型,为了使字符串在运行过程中速度更快、更节省内存,JVM提供了字符串常量池的概念,相同的字符串会在字符串常量池中指向同一个地址。在Java8的HotSpot中,字符串常量池位于堆内存中。
下面例子代码我们拼接了三个字符串字面值,此时我们实际上在字符串常量池创建了一个字符串abc
,这是因为Java源代码在编译期间发生了编译期优化,下面代码和直接写String s = "abc"
是等价的,观察生成的字节码我们可以看出编译后的优化结果。
package com.gacfox.demo;
public class Main {
public static void main(String[] args) {
String s = "a" + "b" + "c";
}
}
0 ldc #2 <abc>
2 astore_1
3 return
不过如果上面代码中拼接操作存在变量,其具体的运行流程就不同了,存在变量时编译后会进行类似StringBuilder
的具体拼接运算。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类。
对常量池的垃圾回收策略很简单,只要常量池中的常量没有被引用就可以被回收,这和堆的回收行为一致。
卸载类就比较困难了,需要满足以下3个条件:
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法实际上Java虚拟机规范中并没有对方法区的垃圾回收做明确的规定,方法区可以不进行垃圾收集,事实上也确实存在未实现或未能完整实现方法区类型卸载的收集器存在。因为类型卸载的条件十分苛刻,然而这部分区域的回收有时又确实是必要的。在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常需要Java虚拟机具备类型卸载的能力,以保证不会对方法区内存造成过大的压力。