目录

一、方法区的定义

二、方法区与虚拟机栈的核心区别

三、方法区的内存溢出

四、运行时常量池

1. 什么是常量池?为什么需要它?

2. 运行时常量池

3.具体例子:类的字节码及常量池展示

Java 源代码(Demo.java)

编译并查看常量池

精简后的字节码输出(仅常量池部分)

常量池中的信息分类


一、方法区的定义

方法区(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 替换为直接调用地址)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐