JVM学习打卡day04--方法区
目录
一、方法区的定义
方法区(Method Area) 是 JVM 运行时数据区中线程共享的一块内存。它存储每个类的结构信息,包括:
运行时常量池(Runtime Constant Pool):类的常量(字面量、符号引用)
字段信息:字段名、类型、修饰符
方法信息:方法名、返回类型、参数、修饰符、字节码指令
特殊方法:实例初始化器 <init>、类初始化器 <clinit>
静态变量(JDK 7 及之后存放在堆中,逻辑上仍属于方法区)
类加载器引用(用于类卸载)
实现位置(以 HotSpot 为例):
JDK 7 及以前:永久代(Permanent Generation),受 -XX:MaxPermSize 限制
JDK 8 及以后:元空间(Metaspace),使用本地内存(Native Memory),默认无上限
关键特点:
逻辑上属于堆的一部分(但通常实现为独立区域)
生命周期与 JVM 一致(除非类被卸载)
所有线程共享同一个方法区
二、方法区与虚拟机栈的核心区别
| 维度 | 方法区 | 虚拟机栈(VM Stack) |
|---|---|---|
| 线程模型 | 线程共享(所有线程能访问同一个类的方法字节码) | 线程私有(每个线程有自己的栈) |
| 存储内容 | 类的静态结构:字节码、常量池、字段信息、静态变量 | 方法的动态状态:栈帧(局部变量表、操作数栈、动态链接、返回地址) |
| 生命周期 | JVM 启动时创建,JVM 退出时销毁(类可能被卸载) | 线程创建时创建,线程结束时销毁;栈帧随方法调用而创建、返回而销毁 |
| 关联对象 | 存放类型信息(java.lang.Class 对象引用的元数据) |
存放运行时数据(如局部变量 int a = 3 的值) |
| 内存异常 | OutOfMemoryError(元空间/永久代耗尽) |
StackOverflowError(栈深度超限)OutOfMemoryError(动态扩展失败) |
| 管理粒度 | 以类为单位分配/回收 |
以栈帧为单位分配/回收 |
三、方法区的内存溢出
本质:方法区(JDK 8+ 元空间 / JDK 7- 永久代)无法再容纳新的类元数据。
常见原因:
动态生成类过多:CGLIB、JSP、Java Proxy、大量反射生成的代理类
大量 JSP 或模板文件(第一次访问时编译为 Servlet 类)
OSGi、热部署等频繁加载自定义类加载器(类卸载失败导致元数据堆积)
字符串常量池溢出(仅 JDK 6 及以前,因为常量池在永久代)
不同 JDK 版本的溢出信息:
| JDK 版本 | 区域 | 错误信息 |
|---|---|---|
| JDK 6 及以前 | 永久代 | java.lang.OutOfMemoryError: PermGen space |
| JDK 7 | 部分移至堆(如静态变量),常量池在堆 | 仍可能出现 PermGen space(若未禁用-XX:MaxPermSize) |
| JDK 8+ | 元空间(本地内存) | java.lang.OutOfMemoryError: Metaspace |
快速定位:
增加元空间/永久代上限:-XX:MaxMetaspaceSize / -XX:MaxPermSize
使用 -XX:+TraceClassLoading -XX:+TraceClassUnloading 观察类加载/卸载
怀疑动态代理时,检查是否有 ProxyGenerator 大量生成类
四、运行时常量池
1. 什么是常量池?为什么需要它?
这里所说的“常量池”通常指 class 文件常量池(Constant Pool),它是 .class 字节码文件中的一个核心数据结构。它就像一个符号仓库,存储了编译期确定的各种常量:包括字面量(整数、浮点数、字符串字面量的字符序列)和符号引用(类名、方法名、字段名及描述符)。字节码指令需要用到这些信息时,通过一个2字节的索引指向常量池中的条目。
为什么要搞这么个池子?
省空间:代码中多次出现的相同字符串或类名,在常量池中只存一份,字节码指令只需要一个短小的索引。
支持动态链接:符号引用在类加载时才会被解析为真实的内存地址(直接引用),无需提前硬编码。
解耦合:常量池作为中介,让字节码与具体内存布局解耦,便于运行时优化和重定位。
注意:class 文件常量池是静态存储在磁盘上的数据,它不属于 JVM 运行时内存。
2. 运行时常量池
运行时常量池是类加载后,将 class 文件常量池中的内容拷贝并解析到方法区(JDK 8+ 元空间)后的运行时版本。它保留了所有符号引用和字面量,并支持动态添加(如 String.intern())。在解析阶段,部分符号引用会被替换为直接引用(例如方法调用地址)。
每个类加载后都会在方法区拥有自己对应的运行时常量池。它是方法区的一部分,属于 JVM 运行时内存。
与字符串常量池的关系
字符串常量池(StringTable)是一个全局的哈希表,用于缓存字符串对象,避免重复创建。它在 JDK 7 及以后被移到了堆中(JDK 6 及以前在方法区的永久代)。
-
运行时常量池中存储的是字符串字面量的符号引用(例如
CONSTANT_String),当第一次使用该字面量时,JVM 会去字符串常量池中查找或创建对应的 String 对象,然后将运行时常量池中的符号引用更新为直接引用(指向堆中的 String 对象)。 -
因此,字符串常量池不是运行时常量池的一部分(物理上独立),但两者通过引用关联。
总结三者的关系:
| 名称 | 存储位置 | 生命周期 | 内容 |
|---|---|---|---|
| class 文件常量池 | .class 文件(磁盘) |
编译期 | 字面量、符号引用 |
| 运行时常量池 | 方法区(JDK8+ 元空间) | 类加载到 JVM 退出 | 符号引用、字面量、动态添加的常量 |
| 字符串常量池 | 堆(JDK7+) | JVM 全局 | 字符串对象的引用(缓存) |
3.具体例子:类的字节码及常量池展示
Java 源代码(Demo.java)
java
public class Demo {
private String name = "Tom";
public void sayHello() {
System.out.println("Hello " + name);
}
}
编译并查看常量池
bash
javac Demo.java javap -v Demo.class
精简后的字节码输出(仅常量池部分)
text
Constant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = String #21 // "Tom" #3 = Fieldref #5.#22 // Demo.name:Ljava/lang/String; #4 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #5 = Class #25 // Demo #6 = Class #26 // java/lang/Object #7 = Utf8 name #8 = Utf8 Ljava/lang/String; #9 = Utf8 <init> #10 = Utf8 ()V #11 = Utf8 Code #12 = Utf8 LineNumberTable #13 = Utf8 sayHello #14 = Utf8 ()V #15 = Utf8 StackMapTable #16 = Utf8 SourceFile #17 = Utf8 Demo.java #18 = Utf8 "Tom" #19 = Utf8 Hello #20 = NameAndType #9:#10 // "<init>":()V #21 = Utf8 Tom #22 = NameAndType #7:#8 // name:Ljava/lang/String; #23 = Class #27 // java/lang/System #24 = NameAndType #28:#29 // out:Ljava/io/PrintStream; #25 = Utf8 Demo #26 = Utf8 java/lang/Object #27 = Utf8 java/lang/System #28 = Utf8 out #29 = Utf8 Ljava/io/PrintStream;
常量池中的信息分类
| 类型 | 示例条目 | 说明 |
|---|---|---|
| 字面量(Utf8) | #21 = Utf8 Tom |
字符串 "Tom" 的实际内容 |
| 类引用(Class) | #5 = Class Demo |
当前类 Demo 的符号引用 |
| 字段引用(Fieldref) | #3 = Fieldref Demo.name |
字段 name 所属类和类型 |
| 方法引用(Methodref) | #1 = Methodref Object.<init> |
构造方法的符号引用 |
| 名称&类型(NameAndType) | #22 = name:Ljava/lang/String |
字段名+描述符 |
字节码指令(例如 ldc #2)会从常量池中取出 #2 的字符串 "Tom",压入操作数栈。运行时常量池就是在类加载时把这些表格原样放入方法区,并允许 JVM 动态更新某些条目(如解析后直接把 Methodref 替换为直接调用地址)。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)