上图是对象访问定位的两种方式,更详细的内容可见 对象访问定位,同时它也间接的反映出了三者之间的关系!!
可以用一个更加直接的形式表示三者之间的关系,假设下面的代码定义在一个方法中,如下图所示:
「方法区」和「堆」一样,它们都是线程共享的,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据 (经典版本下 -> JDK7 之前)
虽然《Java 虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作「非堆」,目的是与 Java 堆区分开来
在 对象内存布局 中说过,一个对象是根据相应的类生成的,如Object obj = new Object()
;而一个类中主要包含:常量、类变量 (静态变量)、实例变量、方法
有些东西属于类,而非对象;换句话说,有些东西所有对象都一样,比如:常量、类变量、方法
所以也就没必要为每个对象都花内存空间去存储这些东西,而是采用一种共享的思想,把这些属于类的东西专门存放在一个地方,每个对象中就不用存了,直接在对象中用一个指针指向这些属于类的数据即可
在 JDK7 之前,专门存放这些东西的地方就是方法区,之后的版本有些许变化,后面详细讨论!
《Java 虚拟机规范》对方法区的约束是非常宽松的:
相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代 (JDK7 及以前版本) 的名字一样「永久」存在了
这个区域的内存回收目标主要是针对「常量池的回收」和「对类型的卸载」,关于方法区的垃圾回收详细内容可见 回收方法区
一般来说这个区域的回收效果比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时候又确实是必要的
写在前面!!这里介绍的内部结构是基于 JDK6,如下图所示。之后的版本会发生变化,下面会详细阐述这些变化 HotSpot 中方法区的演进
下面详细介绍方法区中主要的部分中都包含了哪些内容!以一个具体的类为例展开阐述:
public class Son extends Father implements Comparable<Integer> {
public int a = 20;
public static int b = 30;
public int compareTo(Integer o) {
return 0;
}
public static void main(String[] args) {
Son son = new Son();
}
}
对每个加载的类型 (类 class、接口 interface、枚举 enum、注解 annotation),Java 虚拟机必须在方法区中存储以下类型信息:
Son
类在方法区中的类型信息如下:
public class com.lfool.myself.Son extends com.lfool.myself.Father implements java.lang.Comparable<java.lang.Integer> // 全类名、父类、接口
flags: ACC_PUBLIC, ACC_SUPER // 修饰符
Java 虚拟机必须在方法区中保存类型的所有域 (字段/属性) 的相关信息以及域的声明顺序:
Son
类在方法区中的域信息如下:
public int a; // 修饰符 类型 名称
descriptor: I
flags: ACC_PUBLIC
public static int b; // 修饰符 类型 名称
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
Java 虚拟机必须保存所有方法的以下信息,同域信息一样包括声明顺序:
方法名称
方法的返回类型 (或void)
方法参数的数量和类型 (按顺序)
方法的修饰符 (public、private、protected、static、final、synchronized、native、abstract)
方法的字节码 (bytecodes)、操作数栈、局部变量表的大小 (abstract 和 native 方法除外)
异常表 (abstract 和 native方法除外)
Son
类在方法区中的方法信息如下:
public static void main(java.lang.String[]); // 方法修饰符 返回类型 名称 参数
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code: // 字节码
stack=2, locals=2, args_size=1 // 操作数栈 局部变量表大小
0: new #3 // class com/lfool/myself/Son
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: return
LineNumberTable:
line 18: 0
line 19: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 son Lcom/lfool/myself/Son;
静态变量其实是属于域信息,之所以单独拿拎出来,是因为它在 HotSpot 的演进中发生了变化,下面会详细阐述具体变化 HotSpot 中方法区的演进
JDK8 的 HotSpot 虚拟机中静态变量在 Java 堆中,为此特意做了一个实验,详细可见 实战「静态变量」「实例变量」「局部变量」
该部分单独有一篇文章阐述,详情可见 运行时常量池
为了内容的完整性,还总结了字符串常量池,比较硬核,详情可见 字符串常量池
在 堆的内存结构 部分介绍过 (-∞, JDK7] 和 [JDK8, +∞) 中方法区的区别,无非就是「永久代」和「元空间」的区别,那「方法区」和它们俩到底是什么关系呢?
首先「方法区」是一个广义的概念,而「永久代」和「元空间」是「方法区」的一种实现!!类似于接口和实现类的关系
之所以该部分的小标题是「HotSpot 中方法区的演进」,而不是「方法区的演进」,有两个原因:
基于上述原因,该部分主要分析 HotSpot 中方法区的演进,先给出发展的三个重要时间节点:
考虑到 HotSpot 的发展,在 JDK6 的时候开发团队就有放弃永久代,逐步改为采用本地内存来实现方法区的计划了
到了 JDK7 的 HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移至 Java 堆中
而到了 JDK8,终于完全废弃了永久代的概念,改用与 JRockit、J9 一样在本地内存中实现的元空间来代替,把 JDK7 中永久代还剩余的内容 (主要是类型信息) 全部移到元空间中
注意:JDK8 中,字符串常量池、静态变量依旧还是在 Java 堆中,并不在方法区!!
分别用三个图来表示三个时间节点的方法区内部情况,如下图所示:
👉 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 不需要配置永久代呢?
首先元空间被移动到了一个和堆不相连的本地内存区域,按照元空间的默认配置,它的大小只受限于本地内存大小,这样就不容易发生方法区溢出
JDK7 将字符串常量池 (StringTable) 放到了 Java 堆中!!
因为永久代回收效率低,在 Full GC 时才会触发,而 Full GC 只有在老年代、永久代空间不足时才会触发
这就导致 StringTable 的回收效率不高,在开发中会有大量字符串被创建,回收效率低,导致永久代空间不足;放到 Java 堆中,能及时回收!
上面说过「方法区」可以选择固定大小或者可扩展的内存
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,这也是初始的高水位线
MaxMetaspaceSize
时,适当提高该值;如果释放空间过多,则适当降低该值-XX:MetaspaceSize
设置为一个相对较高的值-XX:MetaspaceSize
即可根据《Java 虚拟机规范》,如果方法区无法满足新的内存分配需求时,将会抛出 OutOfMemoryError (OOM) 异常,下面用代码演示一下抛异常:
xxxxxxxxxx
/**
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
**/
public class MethodOOM extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
MethodOOM oom = new MethodOOM();
for (int i = 0; i < 10000; i++) {
// 创建 ClassWriter 对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
// 指明版本号、修饰符、类名、包名、父类、接口
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = classWriter.toByteArray();
// 类的加载
oom.defineClass("Class" + i, code, 0, code.length);
j++;
}
} finally {
System.out.println(j);
}
}
}
// result
3276
Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:757)
at java.lang.ClassLoader.defineClass(ClassLoader.java:636)
at com.lfool.myself.MethodOOM.main(MethodOOM.java:25)