方法区

堆、栈、方法区的关系

首先来梳理一下「」「虚拟机栈」「方法区」三者之间的关系:

10

上图是对象访问定位的两种方式,更详细的内容可见 对象访问定位,同时它也间接的反映出了三者之间的关系!!

可以用一个更加直接的形式表示三者之间的关系,假设下面的代码定义在一个方法中,如下图所示:

1

对方法区的理解

「方法区」和「堆」一样,它们都是线程共享的,它用于存储已被虚拟机加载的类型信息常量静态变量即时编译器编译后的代码缓存等数据 (经典版本下 -> JDK7 之前)

虽然《Java 虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作「非堆」,目的是与 Java 堆区分开来

对象内存布局 中说过,一个对象是根据相应的类生成的,如Object obj = new Object();而一个类中主要包含:常量、类变量 (静态变量)、实例变量、方法

有些东西属于类,而非对象;换句话说,有些东西所有对象都一样,比如:常量、类变量、方法

所以也就没必要为每个对象都花内存空间去存储这些东西,而是采用一种共享的思想,把这些属于类的东西专门存放在一个地方,每个对象中就不用存了,直接在对象中用一个指针指向这些属于类的数据即可

在 JDK7 之前,专门存放这些东西的地方就是方法区,之后的版本有些许变化,后面详细讨论!

《Java 虚拟机规范》对方法区的约束是非常宽松的:

相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代 (JDK7 及以前版本) 的名字一样「永久」存在了

这个区域的内存回收目标主要是针对「常量池的回收」和「对类型的卸载」,关于方法区的垃圾回收详细内容可见 回收方法区

一般来说这个区域的回收效果比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时候又确实是必要的

方法区的内部结构

写在前面!!这里介绍的内部结构是基于 JDK6,如下图所示。之后的版本会发生变化,下面会详细阐述这些变化 HotSpot 中方法区的演进

4

下面详细介绍方法区中主要的部分中都包含了哪些内容!以一个具体的类为例展开阐述:

类型信息

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

Son类在方法区中的类型信息如下:

域信息

Java 虚拟机必须在方法区中保存类型的所有域 (字段/属性) 的相关信息以及域的声明顺序:

Son类在方法区中的域信息如下:

方法信息

Java 虚拟机必须保存所有方法的以下信息,同域信息一样包括声明顺序:

Son类在方法区中的方法信息如下:

静态变量

静态变量其实是属于域信息,之所以单独拿拎出来,是因为它在 HotSpot 的演进中发生了变化,下面会详细阐述具体变化 HotSpot 中方法区的演进

JDK8 的 HotSpot 虚拟机中静态变量在 Java 堆中,为此特意做了一个实验,详细可见 实战「静态变量」「实例变量」「局部变量」

运行时常量池

该部分单独有一篇文章阐述,详情可见 运行时常量池

为了内容的完整性,还总结了字符串常量池,比较硬核,详情可见 字符串常量池

HotSpot 中方法区的演进

堆的内存结构 部分介绍过 (-∞, JDK7] 和 [JDK8, +∞) 中方法区的区别,无非就是「永久代」和「元空间」的区别,那「方法区」和它们俩到底是什么关系呢?

首先「方法区」是一个广义的概念,而「永久代」和「元空间」是「方法区」的一种实现!!类似于接口和实现类的关系

之所以该部分的小标题是「HotSpot 中方法区的演进」,而不是「方法区的演进」,有两个原因:

基于上述原因,该部分主要分析 HotSpot 中方法区的演进,先给出发展的三个重要时间节点:

2

考虑到 HotSpot 的发展,在 JDK6 的时候开发团队就有放弃永久代,逐步改为采用本地内存来实现方法区的计划了

到了 JDK7 的 HotSpot,已经把原本放在永久代的字符串常量池静态变量等移至 Java 堆中

而到了 JDK8,终于完全废弃了永久代的概念,改用与 JRockit、J9 一样在本地内存中实现的元空间来代替,把 JDK7 中永久代还剩余的内容 (主要是类型信息) 全部移到元空间中

注意:JDK8 中,字符串常量池静态变量依旧还是在 Java 堆中,并不在方法区!!

分别用三个图来表示三个时间节点的方法区内部情况,如下图所示:

3

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

👉 JEP 122: Remove the Permanent Generation

Motivation: This is part of the JRockit and HotSpot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.

解释:这是 JRockit 和 HotSpot 融合工作的一部分。JRockit 客户不需要配置永久代 (因为JRockit没有永久代),习惯不配置永久代,所以 HotSpot 去掉了永久代

这个官方解释略显敷衍,那为什么 JRockit 不需要配置永久代呢?

首先元空间被移动到了一个和堆不相连的本地内存区域,按照元空间的默认配置,它的大小只受限于本地内存大小,这样就不容易发生方法区溢出

StringTable 为什么被调整到了 Java 堆?

JDK7 将字符串常量池 (StringTable) 放到了 Java 堆中!!

因为永久代回收效率低,在 Full GC 时才会触发,而 Full GC 只有在老年代、永久代空间不足时才会触发

这就导致 StringTable 的回收效率不高,在开发中会有大量字符串被创建,回收效率低,导致永久代空间不足;放到 Java 堆中,能及时回收!

设置方法区大小与 OOM

设置方法区大小

上面说过「方法区」可以选择固定大小或者可扩展的内存

JDK7 及以前

JDK7 及以前「方法区」的实现方式为「永久代」,具体见上面 HotSpot 中方法区的演进

-XX:PermSize=size:设置永久代初始分配空间。默认值为 20.75MB

-XX:MaxPermSize=size:设置永久代最大可分配空间。32 位机器默认值为 64MB;64 位机器默认值为 82MB

当 JVM 加载的类信息容量超过了这个值,虚拟机将会抛出异常OutOfMemoryError:PermGen space

JDK8 及以后

JDK8 及以后「方法区」的实现方式为「元空间」,具体见上面 HotSpot 中方法区的演进

-XX:MetaspaceSize=size:设置元空间初始分配空间

-XX:MaxMetaspaceSize=size:设置元空间最大可分配空间,默认是 -1,即不受限制,或者说只受限于本地内存大小

与永久代不同,如果不指定大小,默认情况下,虚拟机会消耗所有的可用系统内存。如果元空间发生溢出,虚拟机将会抛出异常OutOfMemoryError:Metaspace

对于一个 64 位的服务器端 JVM 来说,其默认的-XX:MetaspaceSize值为 21MB,这也是初始的高水位线

方法区 OOM

根据《Java 虚拟机规范》,如果方法区无法满足新的内存分配需求时,将会抛出 OutOfMemoryError (OOM) 异常,下面用代码演示一下抛异常: