前言
在前面的文章里,对JVM运行时数据区里面的程序计数器、Java虚拟机栈和本地方法栈做了比较详细的讲解。接下来,我们来说说方法区。
什么是方法区
我们先来看下《Java虚拟机规范》里面对方法区的定义: 《Java虚拟机规范》方法区连接
这里面对方法区进行了定义:
- 它是一个线程共享的内存区域
- 用于存储被虚拟机加载的类信息、字段信息、方法信息,以及方法和构造函数的代码,包括类和实际初始化以及接口初始化中使用的特殊方法(简单理解就是类的初始化方法
<init>
) - 在虚拟机启动时创建
- 尽管在逻辑上是堆的一部分,但可以选择不进行垃圾回收或压缩它(可以看作是一块独立于Java堆的内存空间)
- 方法区可以是固定大小,也可以根据计算的需要进行扩展
- 方法区的内存不需要是连续的
- 如果方法区中的内存无法满足分配请求,Java虚拟机就会抛出OutOfMemoryError
但是,由于《Java虚拟机规范》只规定了概念和作用,对如何实现方法区不做统一要求。因此不同的虚拟机有着不同的实现,甚至于相同的虚拟机在不同的版本上实现也不相同。比如HotSpot虚拟机,在jdk7之前、jdk7、jdk8及之后的版本对方法区的实现都不一样。
这边对方法区存储的信息做一个补充,这也是网上大部分文章描述的:方法区里面存储类信息、字段信息、方法信息、常量、静态变量和即时编译器编译后的代码缓存等数据。
这段话如果放在jdk1.7之前没问题,但是放在现在的jdk版本上,它就有问题了,因为静态变量在jdk1.7的时候已经被移出方法区,放到了堆中了。因此此时方法区里面是没有静态变量了。
在jdk7及之前,习惯上把方法区称为永久代。从jdk8开始,使用元空间取代了永久代。
这边说下在之前为什么会习惯将方法区称为永久代,这是因为在HotSpot虚拟机上用永久代来实现方法区的逻辑(对于JRockit和J9虚拟机来说,是不存在"永久代"的概念的)。而HotSpot虚拟机是全世界使用最广泛的Java虚拟机,大部分java程序开发都是使用Hotspot虚拟机,因此在之前会习惯称方法区为永久代。
永久代和元空间的演进
永久代和元空间虽然都是对方法区的落地实现,但是二者不只是名字不同,数据区位置和内部结构也调整了,下面通过图片来展示永久代到元空间的演进。
jdk6及之前 在jdk6及以前,方法区使用永久代来实现,永久代放在堆空间中。此时里面放着类信息、字段信息、方法信息、常量、运行时常量池、静态变量、即时编译器编译后的代码缓存等数据
jdk7 从jdk7开始,静态变量和字符串常量池被移出永久代,并放入堆空间中。
jdk8 到了jdk8的时候,永久代被彻底废除,采用元空间来实现方法区,此时元空间是放在本地内存中的。而原先永久代里面存放的数据也相应的存放到了元空间,不过静态变量和字符串常量池依旧放在堆空间中。
为什么会使用元空间替换掉永久代?
- 由于使用永久代实现方法区的方式在后面发现了诸多问题,相比其他的虚拟机更容易出现OOM
- 永久代调优困难
- 垃圾回收效果不好
- 永久代在虚拟机内部的堆空间中,本身大小就受到了限制,就算再大也无法突破堆空间的大小限制。
- 元空间并不在虚拟机中,而是使用本地内存,因此默认情况下元空间的大小仅受本地内存的限制,虽然仍旧可能存在内存溢出,但是比原来出现的概率会更小。
- Oracle收购了号称世界最快的JRockit虚拟机,并整合了JRockit虚拟机的优秀功能。既然JRockit使用的是比永久代更好的元空间,那就干脆去掉永久代,使用元空间。
方法区里面存放数据
上面我们讲到方法区里面存放着类信息、字段信息、方法信息、运行时常量池以及即时编译器编译后的代码缓存等信息。这边我找了一张图,以便大家能更直观的了解。
类信息
当类被加载的时候,JVM会将被加载的类信息放在方法区里面,这边的类包括以几种:
- class 类
- interface 接口
- enum 枚举
- annotation 注解
存储的信息包括以下这些:
- 完整的有效名称,也可以说类的全限定名
- 直接父类的全限定名
- 修饰符(public、abstract、final的某个子集)
- 实现的所有接口的信息,这个是放在一个有序列表里面,因为可以实现多接口。
类加载器的引用
JVM必须知道一个类是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的引用作为类信息的一部分保存在方法区中。JVM在动态连接的时候需要这个信息。当解析一个类到另一个类的引用的时候,JVM需要保证这两个类的类加载器是相同的。这对JVM区分名字空间的方式是至关重要的。
Class实例的引用
JVM为每个加载的类都创建一个java.lang.Class的实例(存储在堆上)。而JVM必须以某种方式把Class的这个实例和存储在方法区中的类型数据(类元数据)联系起来。因此,类的元数据里面保存着一个Class对象的引用
字段信息或称为域信息
- 字段声明的顺序
- 字段名称
- 字段类型
- 字段的修饰符
注意: 域(Field) = 字段 = 属性 = 成员变量 ,这些说的都是一个意思
方法信息
- 方法名称
- 方法返回类型
- 方法参数的数量和类型
- 方法的修饰符
- 方法的字节码、操作数栈、局部变量表及大小(abstract和native除外)
- 异常表:每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类和常量池引用。(abstract 和 native 除外)
方法表
JVM对每个加载的非虚拟类的类信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法)JVM可以通过方法表快速的激活实例方法。
运行时常量池
在类加载时,也会将.class的字节码文件中的常量池载入到内存中,并保存在方法区中。我们常说的常量池就是只方法区中的运行时常量池。下面会简单对运行时常量池做一个描述。
即时编译期编译后的缓存代码(JIT代码缓存)
从字面意思理解就是代码缓存区,它缓存的是JIT(Just in Time)即时编译期编译的代码。JVM会对频繁使用的代码即热点代码,在达到一定的使用次数后,会编译成本地平台相关的机器码,这样在下次执行的时候就能更快的运行。
- 被多次调用的方法
- 被多次执行的循环体
什么是运行时常量池
如果要了解运行时常量池,那么应当先了解下什么是常量池。
我们先来看下一个简单的字节码文件:(屏幕有限,只能截这么大)
可以看到当字节码文件中有个Constant pool,这个是class文件中的常量池。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有个用于存放编译期生成的各种字面量和符号引用的常量池(Constant pool)。
字面量 :
- 文本字符串
- 被声明为final的常量值
- 基本数据类型值 (如果是int值的话 大于-32768并小于32767 是不会在常量池里面的,而是跟在字节码指令后面)
符号引用:
- 类和接口的全限定名 如
#9 = Class
- 字段的名称和描述符 如
#7 = Fieldref
- 方法的名称和描述符 如
#1 = Methodref
说完常量池,接下来来说说运行时常量池:
简单来说就是:JVM在完成类装载操作后,会将class文件中的常量载入到内存中,并保存在方法区。而放在方法区里的这块内存被称为运行时常量池。
实际上这个载入到内存中是很复杂的,这边只简单描述:
- class文件中的常量池内容会在类加载时进入方法区的运行时常量池中(并不是全部完整的放进去)
- 符号引用会解析成直接引用放在运行时常量池
- 如果是字符串的话,会先在堆中创建字符串对象实例,然后将该对象的引用放到字符串常量池中,最后将运行时常量池里的符号引用替换成直接引用
- java虚拟机为每个类和接口维护一个运行时常量池
关于常量池的描述,感兴趣的可以看这个Java中几种常量池的区分
字符串常量池为啥要放在堆中
jdk7中将字符串常量池放到了堆空间,因为永久代的回收效率低,在full gc 的时候才会触发,而full gc 是老年代的空间不足、永久代不足时才会触发。这就导致了字符串常量池回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低就会导致永久代内存不足。放到堆里能及时回收内存。
结束语
关于运行数据区的方法区就介绍到这里,感谢大家的阅读。文中如果有写的不好的地方,请在评论区指出,再次感谢。