理解JVM运行时数据区(五)方法区

前言

在前面的文章里,对JVM运行时数据区里面的程序计数器Java虚拟机栈本地方法栈做了比较详细的讲解。接下来,我们来说说方法区

image.png

什么是方法区

我们先来看下《Java虚拟机规范》里面对方法区的定义: 《Java虚拟机规范》方法区连接

image.png

这里面对方法区进行了定义:

  • 它是一个线程共享的内存区域
  • 用于存储被虚拟机加载的类信息、字段信息、方法信息,以及方法和构造函数的代码,包括类和实际初始化以及接口初始化中使用的特殊方法(简单理解就是类的初始化方法<init>)
  • 在虚拟机启动时创建
  • 尽管在逻辑上是堆的一部分,但可以选择不进行垃圾回收或压缩它(可以看作是一块独立于Java堆的内存空间)
  • 方法区可以是固定大小,也可以根据计算的需要进行扩展
  • 方法区的内存不需要是连续的
  • 如果方法区中的内存无法满足分配请求,Java虚拟机就会抛出OutOfMemoryError

但是,由于《Java虚拟机规范》只规定了概念和作用,对如何实现方法区不做统一要求。因此不同的虚拟机有着不同的实现,甚至于相同的虚拟机在不同的版本上实现也不相同。比如HotSpot虚拟机,在jdk7之前、jdk7、jdk8及之后的版本对方法区的实现都不一样。

这边对方法区存储的信息做一个补充,这也是网上大部分文章描述的:方法区里面存储类信息字段信息方法信息常量静态变量即时编译器编译后的代码缓存等数据。

这段话如果放在jdk1.7之前没问题,但是放在现在的jdk版本上,它就有问题了,因为静态变量在jdk1.7的时候已经被移出方法区,放到了堆中了。因此此时方法区里面是没有静态变量了。

在jdk7及之前,习惯上把方法区称为永久代。从jdk8开始,使用元空间取代了永久代。

这边说下在之前为什么会习惯将方法区称为永久代,这是因为在HotSpot虚拟机上用永久代来实现方法区的逻辑(对于JRockit和J9虚拟机来说,是不存在"永久代"的概念的)。而HotSpot虚拟机是全世界使用最广泛的Java虚拟机,大部分java程序开发都是使用Hotspot虚拟机,因此在之前会习惯称方法区为永久代。

永久代和元空间的演进

永久代和元空间虽然都是对方法区的落地实现,但是二者不只是名字不同,数据区位置和内部结构也调整了,下面通过图片来展示永久代到元空间的演进。

jdk6及之前 image.png 在jdk6及以前,方法区使用永久代来实现,永久代放在堆空间中。此时里面放着类信息、字段信息、方法信息、常量、运行时常量池、静态变量、即时编译器编译后的代码缓存等数据

jdk7 image.png 从jdk7开始,静态变量和字符串常量池被移出永久代,并放入堆空间中。

jdk8 1738021e6a5d7e62_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0.awebp 到了jdk8的时候,永久代被彻底废除,采用元空间来实现方法区,此时元空间是放在本地内存中的。而原先永久代里面存放的数据也相应的存放到了元空间,不过静态变量和字符串常量池依旧放在堆空间中。

为什么会使用元空间替换掉永久代?

  • 由于使用永久代实现方法区的方式在后面发现了诸多问题,相比其他的虚拟机更容易出现OOM
    • 永久代调优困难
    • 垃圾回收效果不好
  • 永久代在虚拟机内部的堆空间中,本身大小就受到了限制,就算再大也无法突破堆空间的大小限制。
  • 元空间并不在虚拟机中,而是使用本地内存,因此默认情况下元空间的大小仅受本地内存的限制,虽然仍旧可能存在内存溢出,但是比原来出现的概率会更小。
  • Oracle收购了号称世界最快的JRockit虚拟机,并整合了JRockit虚拟机的优秀功能。既然JRockit使用的是比永久代更好的元空间,那就干脆去掉永久代,使用元空间。

方法区里面存放数据

上面我们讲到方法区里面存放着类信息、字段信息、方法信息、运行时常量池以及即时编译器编译后的代码缓存等信息。这边我找了一张图,以便大家能更直观的了解。

4fe9783d873c4fb69e61e55e97a30b85_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.awebp

类信息

当类被加载的时候,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会对频繁使用的代码即热点代码,在达到一定的使用次数后,会编译成本地平台相关的机器码,这样在下次执行的时候就能更快的运行。

  • 被多次调用的方法
  • 被多次执行的循环体

什么是运行时常量池

如果要了解运行时常量池,那么应当先了解下什么是常量池

我们先来看下一个简单的字节码文件:(屏幕有限,只能截这么大)

image.png

可以看到当字节码文件中有个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 是老年代的空间不足、永久代不足时才会触发。这就导致了字符串常量池回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低就会导致永久代内存不足。放到堆里能及时回收内存。

结束语

关于运行数据区的方法区就介绍到这里,感谢大家的阅读。文中如果有写的不好的地方,请在评论区指出,再次感谢。

系列文章

猜你喜欢

转载自juejin.im/post/7086113214236196894
今日推荐