JVM 方法区

1. 前言

本节主要讲解运行时数据区的方法区。本节主要知识点如下:

  • 了解方法区的作用及意义,为本节的基础知识;
  • 了解方法区存放数据类型,为本节重点内容之一;
  • 了解运行时常量池,我们在学习Class文件结构的时候,也学习过常量池结构,那么运行时常量池本节课程会进行讲解;
  • 了解方法区与堆内存结构的关系,以JDK 1.8 版本为分界线,进行对比讲解,为本节重点内容之一。

2. 什么是方法区

定义:方法区,也称非堆(Non-Heap),是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field 等元数据对象、static-final 常量、static 变量、JIT 编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域 “运行时常量池”。

Tips:对于运行时常量池,后文会有讲解。

对于习惯在 HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为 “永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为 HotSpot 虚拟机的设计团队选择把 GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如 BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。

3. 方法区存放的数据

在讲解方法区内存放的数据之前,我们先通过示意图来直观的看下,方法区存放的数据与堆内存之间的关系。如下图所示:

图片描述

从图中可以看到,方法区存放了 ClassLoader 对象的引用,也存放了一个到类对象的引用,这两个引用的对象实例会存放到堆内存中。从上图我们就可以简单的了解到方法区存放的数据是什么,接下来,我们对存放的数据类型进行解释。

  • 类型全限定名:全限定名为 package 路径与类名称组合起来的路径;
  • 类型的直接超类的全限定名:父类或超类的全限定名;
  • 类型是类类型还是接口类型:判定当前类是 Class 还是接口 Interface;
  • 类型的访问修饰符:判断修饰符,如 pulic,private 等;
  • 类型的常量池:这部分会在下文进行讲解;
  • 字段信息:类中字段的信息;
  • 方法信息:类中方法的信息;
  • 静态变量:类中的静态变量信息;
  • 一个到类 ClassLoader 的引用:对 ClassLoader 的引用,这个引用指向对内存;
  • 一个到 Class 类的引用:对对象实例的引用,这个引用指向对内存。

4. 运行时常量池

我们先来回顾下Class 文件结构中的常量池的相关知识。

Class 文件中的常量池
在 Class 文件结构中,最头的 4 个字节用于存储 Megic Number,用于确定一个文件是否能被 JVM 接受,再接着 4 个字节用于存储版本号,前 2 个字节存储次版本号,后 2 个存储主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个 u2 类型的数据 (constant_pool_count) 存储常量池容量计数值。

常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References)。更加具体的知识,同学们可以翻看之前相关的小节内容。

运行时常量池:我们回到正题,来看下运行时常量池。

Tips:其实 Class 文件中的常量池与运行时常量池的关系非常容易理解,Class 文件中的常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。简单总结来说,编译器使用 Class 文件中的常量池,运行期使用运行时常量池。

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是 String 类的 intern() 方法。

5. 常量池的优势

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。

  • 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
  • 节省运行时间:比较字符串时,==equals () 快。对于两个引用变量,只用 == 判断引用是否相等,也就可以判断实际值是否相等。

6. 方法区内存变更

图片描述

方法区的实现,虚拟机规范中并未明确规定,目前有 2 种比较主流的实现方式:

HotSpot 虚拟机 1.8之前:在 JDK1.6 及之前版本,HotSpot 使用 “永久代(permanent generation)” 的概念作为实现,即将 GC 分代收集扩展至方法区。这种实现比较偷懒,可以不必为方法区编写专门的内存管理,但带来的后果是容易碰到内存溢出的问题(因为永久代有 - XX:MaxPermSize 的上限)。

在 JDK1.7,HotSpot 逐渐改变方法区的实现方式,如 1.7 版本移除了方法区中的字符串常量池,但为发生本质的变化。

HotSpot 虚拟机 1.8之后:1.8 版本中移除了方法区并使用 metaspace(元数据空间)作为替代实现。metaspace 占用系统内存,也就是说,只要不碰触到系统内存上限,方法区会有足够的内存空间。但这不意味着我们不对方法区进行限制,如果方法区无限膨胀,最终会导致系统崩溃。

7. 小结

本节主要讲解了运行时数据区里边的方法区,方法区是一块共享内存区域,在运行时数据区占据着十分重要的位置。我们了解了方法区里边存储的数据类型,也了解到了方法区的作用,同时了解了方法区内存的版本变更,通篇皆为重点知识,学习者需要用心学习。