方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作"非堆(Non-Heap)",目的是与Java堆区分开来。

方法区看作是一块独立于Java堆的内存空间

栈、堆、方法区交互关系

请输入图片描述

方法区的理解

  • 方法区与Java堆一样,是各个线程共享的内存区域
  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样都可以是不连续的
  • 方法区的大小根堆一样可以选择固定大小或者可扩展
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误
  • 关闭JVM就会释放这个区域的内存

      在JDK7及之前,习惯上把方法区称为永久代。JDK8开始使用元空间取代了永久代。
      本质上,方法区和永久代并不等价,仅仅是对HotSpot而言的。《Java虚拟机规范》对如何实现方法区不做统一要求,例如BEA JRockit不存在永久代的概念。现在来看当年使用永久代不是好的方法,导致Java程序更容易OOM。
      到了JDK8,废除了永久代,改用在本地内存中实现的元空间来代替。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代的最大区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。

设置方法区大小与OOM

      Jdk7及之前通过-XX:PermSize来设置永久代初始分配空间。默认值是20.75MB。通过-XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认是64MB,64位机器默认是82MB。当JVM加载的类信息容量超过了这个值,会报OutOfMemoryError:PermGenspace

      Jdk8及之后元数据去大小可以使用-XX:MetaspaceSize-XX:MaxMetaspaceSize指定,替代上述原有的两个参数。默认值依赖于平台,Windows下,-XX:MetaspaceSize是21MB,-XX:MaxMetaspaceSize的值是-1,表示没有限制。

使用CGlib使得方法区OOM

public class MethodAreaOOM{
    /**
     * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M jdk7
     * -XX:MaxMetaspaceSize=10M
     */
    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 o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(o,objects);
                }
            });
            enhancer.create();
        }
        /**
         * Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
         *     at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
         *     at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
         *     at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
         *     at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
         *     at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
         *     at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
         *     at unit02.JavaMethodAreaOOM2_9.main(JavaMethodAreaOOM2_9.java:28)
         */
    }
    static class OOMObject{

    }
}

运行时常量池

      运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
      JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项就像数组项一样通过索引访问。
      运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。

运行时常量池相对于Class文件常量池的一项重要特征是具备动态性,如String.intern()

演进细节

只有HotSpot才有永久代

jdk1.6及之前 有永久代,静态变量存放在永久代上
jdk1.7 有永久代,但已经逐步"去永久代",字符串常量池、静态变量移除,保存到堆中
jdk1.8及之后 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,当字符串常量池、静态变量仍在堆中

永久代为什么要被元空间替换?

  • 为永久代设置空间大小是很难确定的
  • 对永久代进行调优是很困难的

垃圾回收

      有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK11时期的ZGC收集器就不支持类卸载)。
      一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的Hotspot虚拟机对此区域未完全回收而导致内存泄漏。

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型

      方法区内常量池之中主要存放的两大类常量:字面量和符号引用。
      字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,创括下面三类常量:

  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符

      HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。回收废弃常量与回收Java堆中的对象非常类似。
      判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于"不再被使用的类"的条件就比较苛刻了。需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

      Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading-XX:+TraceClassUnLoading查看类加载和卸载信。
      在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

Last modification:September 9th, 2020 at 11:54 am
如果觉得我的文章对你有用,请随意赞赏