方法区

  • 方法区(Method Area)和Java堆一样,是各个线程共享的内存区域。
  • 《Java虚拟机规范》当中把方法区描述为堆的一个逻辑部分,但是它有一个别名叫“非堆”,目的是与Java堆区分开来。
  • jdk7以前,把方法区称为永久代。jdk8开始,使用元空间取代了永久代。

jdk7把原本存放在永久代的字符串常量池,静态变量等移到了堆中,直到jdk8才用元空间(Metaspace)替代了永久代,元空间在本地内存中。之所以放入本地内存我感觉就是加大了方法区的内存,避免了很多由于类加载过多导致的OOM。

  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,就会导致OOM。
  • 方法区同样存在垃圾收集。

这个垃圾收集的条件很苛刻也就为什么说是永久代一样,永久存在了,后续说明。

方法区大小设置

jdk7以前:

  • -XX:PermSize来设置永久代初始内存大小 默认20.75M

  • -XX:MaxPermiSize来设置永久代最大可分配空间。32位机器默认64M,64位机器82M

JVM加载的类信息容量超出最大值会报OutOfMemoryError:PermGen space

jdk8以后:

  • -XX:MetspaceSize和-XX:MaxMetaspaceSize设置元数据区大小。

windows下,-XX:MetspaceSize 默认为21M,-XX:MaxMetaspaceSize默认为-1就是没有限制。(这个-1我参考网上的都说的-1,但是我看自己的程序的默认参数不是-1。反正这个了解就好,没必要去记这个)

内存溢出错误java.lang.OutOfMemoryError:Metaspace

方法区的内部结构

方法区保存着被加载过的每一个类的信息,这些信息由类加载器在加载类的时候,从类的源文件抽取出来。

方法区保存的起始就是每个类的一个模板,Class的元数据。

这些信息有:

这些信息没有一个统一的标准,网上说什么的都有,这是我自己综合整理,我感觉这个比较全面。仅供参考啊

类型信息

对于每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

  • 类的完整有效名称(包名.类名)

比如java.lang.String

  • 类的直接父类的完整有效名

  • 类型直接实现接口的有序列表

一个类实现的接口可能有多个,多以存放在有序列表中。

  • 类的修饰符

比如public abstract


这个就像是查户口一样,看一下你家的详细地址(类的完整有效名称),你的父亲是谁(父类完整有效名称),你的工作(你实现的哪些接口),还有你是什么人(类的修饰符)。

字段信息

  • 字段修饰符(public、protect、private、default)
  • 字段声明的顺序
  • 字段的类型
  • 字段的名称

方法信息

  • 方法名称
  • 方法的返回类型(包括void)
  • 方法参数的类型、数目和顺序
  • 方法的修饰符(public、private、protected、static、final、synchronized、native、abstract)
  • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
  • 异常表(abstract和native方法除外)

前面从未说过异常表这次详细分析

包括异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获异常类的常量池索引

类的常量池/运行时常量池

类常量池(静态常量池)

  • class常量池用于存储编译器生成的各种字面量符号引用

红色的这几个我仅仅了解,无法说清楚,待我日后补充,其它的三种是网上常见的,并且只有这三种,基本上都是这样,我是参考《深入理解Java虚拟机》第三版 p218。

  • 每个class文件都有一个class常量池。

运行时常量池(动态常量池)

  • 方法区的一部分
  • 常量池表是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
  • 运行时常量池包含多种不同的常量,包括编译器就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里变为了真实地址。

实际上在解析阶段只有能确定唯一版本的方法才能将符号引用直接替换为直接引用,这里也就是前面的章节说过的非虚方法。

Byte,Short,Integer,Long,Character,Boolean这5中包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超过此范围仍然回去创建新的对象。这也就是包装类比较是否相等要使用equals()。(Double,Float没有实现常量池技术)

  • 运行时常量池相对于Class文件常量池的另外一个特征就是具备动态性。

String.intern()方法

非final修饰的类变量

  • 静态变量和类是关联在一起的,随着类的加载而加载。
  • 类变量被类的所有实例共享,即使没有类实例时也可以访问。

final型变量的值是在编译期就被确定了,因此被保存在常量池中。

对类加载器的引用

  • jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。

  • jvm在动态链接的时候需要这个信息。当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的。这对jvm区分名字空间的方式是至关重要的。

  • 比较两个类是否相等也需要比较类加载器是否是相同的。

  • 垃圾收集的时候也需要参考这个类的加载器是否不再使用!

对Class类的引用

jvm为每个加载的类都创建一个java.lang.Class的实例(存储在堆上)。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来, 因此,类的元数据里面保存了一个Class对象的引用;

方法表

引用:

为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还可以添加一些其他的数据结构,如方法表。jvm对每个加载的非虚拟类的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法。jvm可以通过方法表快速激活实例方法。(译者:这里的方法表与C++中的虚拟函数表一样,但java方法全都 是virtual的,自然也不用虚拟二字了。正像java宣称没有 指针了,其实java里全是指针。更安全只是加了更完备的检查机制,但这都是以牺牲效率为代价的,个人认为java的设计者 始终是把安全放在效率之上的,所有java才更适合于网络开发)

栈、堆、方法区的交互