程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Java底层 - JVM

    • JVM - Java体系结构
    • JVM - 类加载子系统
    • JVM - 运行时数据区概述及线程
    • JVM - 程序计数器
    • JVM - 虚拟机栈
    • JVM - 本地方法接口
    • JVM - 本地方法栈
    • JVM - 堆 (Heap)
    • JVM - 方法区
      • 1. 引言:运行时数据区的最后一块拼图
      • 2. 栈、堆、方法区的交互关系
      • 3. 方法区的核心理解
        • 3.1 方法区的概念与规范
        • 3.2 HotSpot JVM 中方法区的实现演进
      • 4. 设置方法区大小与 OOM
        • 4.1 JDK 7 及之前的永久代参数
        • 4.2 JDK 8 及之后的元空间参数
        • 4.3 模拟方法区 OOM
        • 4.4 如何解决方法区 OOM
      • 5. 方法区的内部结构
        • 5.1 类型信息 (Type Information)
        • 5.2 域信息 (Field Information / 成员变量信息)
        • 5.3 方法信息 (Method Information)
        • 5.4 运行时常量池 (Runtime Constant Pool)
        • 5.5 静态变量 (Static Variables)
        • 5.6 即时编译器编译后的代码缓存 (JIT Code Cache)
        • 5.7 内部结构反编译示例
        • 5.8 non-final 的类变量 (静态变量) 访问
        • 5.9 全局常量 (static final)
        • 5.10 字符串常量池 (String Table / String Pool)
      • 6. 方法区使用示例(字节码执行流程)
      • 7. 方法区的演进细节再讨论
        • 7.1 为什么要用元空间替换永久代?
        • 7.2 StringTable (字符串常量池) 位置调整的原因
        • 7.3 静态变量 (Static Variables) 的存放位置演变
      • 8. 方法区的垃圾回收
      • 9. 方法区小结
      • 10. 常见面试题回顾
    • JVM - 对象实例化内存布局
    • JVM - 直接内存管理
    • JVM - 执行引擎
    • JVM - 字符串常量池 (StringTable)
    • JVM - 垃圾回收概述
    • JVM - 垃圾回收相关算法
    • JVM - 垃圾回收相关概念
    • JVM - 垃圾回收器
    • JVM - Class文件结构
    • JVM - 字节码指令集与解析
    • JVM - 类的加载过程详解
    • JVM - 再谈类的加载器
    • JVM - 调优概述
    • JVM - 监控及诊断工具cmd
    • JVM - 监控及诊断工具GUI
    • JVM - 运行时参数
    • JVM - 分析GC日志
  • Java底层
  • Java底层 - JVM
scholar
2024-01-20
目录

JVM - 方法区

# 1. 引言:运行时数据区的最后一块拼图

在 JVM 的运行时数据区中,我们已经探讨了程序计数器、虚拟机栈、本地方法栈和堆。本次我们将深入研究最后一部分——方法区 (Method Area)。

JVM 运行时数据区概览

从线程共享的角度来看,方法区与堆一样,是所有线程共享的区域。而虚拟机栈、本地方法栈和程序计数器是线程私有的。

线程共享与私有区域 (回顾:ThreadLocal 通过为每个线程提供独立的变量副本,解决了线程共享区域中的并发安全问题,常用于数据库连接管理和会话管理。)

# 2. 栈、堆、方法区的交互关系

理解方法区的作用,需要先明确它与栈、堆之间的协作关系,特别是对象创建和访问的过程。

考虑以下代码:

Person person = new Person();
1

这行简单的代码涉及了 JVM 运行时数据区的三个核心部分:

栈、堆、方法区的交互

  1. 方法区 (Method Area):
    • 存储 Person 类的类型信息(类的结构、字段、方法、构造函数、常量池等)。当类加载器加载 Person.class 文件时,这些信息就被放入方法区。
  2. Java 栈 (Java Stack):
    • 当前线程的栈帧中,局部变量表会存储 person 这个引用变量。它只是一个地址或句柄,指向堆中的对象。
  3. Java 堆 (Java Heap):
    • new Person() 这个操作会在堆中创建一个 Person 类的实例对象。对象包含了实例变量等数据。

关键联系:堆中的 Person 对象内部通常会包含一个指向方法区中 Person 类类型信息的指针。这个指针使得 JVM 能够知道这个对象是由哪个类创建的,从而可以访问类的方法、静态变量等信息。

# 3. 方法区的核心理解

# 3.1 方法区的概念与规范

官方定义 (参考 Java SE 8 JVM Specification §2.5.4): 方法区是 JVM 内存区域的一部分,用于存储每个类的结构信息,例如运行时常量池 (Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的代码,包括在类和实例初始化以及接口初始化中使用的特殊方法。

关键特性:

  1. 线程共享:与堆一样,方法区被该虚拟机中所有线程共享。
  2. 创建时机:方法区在虚拟机启动时创建。
  3. 逻辑上属于堆?:
    • 《Java虚拟机规范》提到:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”
    • 但在 HotSpot JVM 的实现中,为了区分管理,方法区通常被称为 Non-Heap (非堆)。因此,实践中我们通常将方法区视为独立于 Java 堆的内存空间。 方法区与堆的逻辑关系
  4. 物理连续性:和堆一样,方法区的物理内存空间可以不连续。
  5. 大小可调:方法区的大小可以是固定的,也可以是可扩展的。
  6. 内存溢出 (OOM):如果方法区无法满足内存分配请求(例如加载过多的类),JVM 将抛出 OutOfMemoryError。
    • JDK 7 及之前:java.lang.OutOfMemoryError: PermGen space (永久代空间不足)
    • JDK 8 及之后:java.lang.OutOfMemoryError: Metaspace (元空间不足)
    • 常见触发场景:加载大量第三方 JAR 包、部署过多的 Web 应用(如 Tomcat)、大量使用动态代理或反射生成类。
  7. 关闭时释放:方法区的内存在 JVM 关闭时会被释放。

示例:方法区加载大量类

一个看似简单的 Java 程序,在运行时会加载许多核心库的类。

// 文件名: MethodAreaDemo.java
public class MethodAreaDemo {
    public static void main(String[] args) {
        System.out.println("start...");
        try {
            // 保持进程存活足够长时间,以便观察
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end...");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

使用 JVisualVM 等工具监控此程序的“类”加载情况,可以看到即使是这个简单程序,也加载了数千个类(包括 JDK 核心类库)。

JVisualVM 显示加载的类数量 (一个简单的应用也可能加载 1600+ 个类)

# 3.2 HotSpot JVM 中方法区的实现演进

重要概念区分:

  • 方法区 (Method Area):这是 JVM 规范中定义的逻辑内存区域。
  • 永久代 (Permanent Generation, PermGen):这是 HotSpot JVM 在 JDK 7 及之前版本中对方法区的具体实现。它使用JVM管理的堆内存的一部分。
  • 元空间 (Metaspace):这是 HotSpot JVM 在 JDK 8 及之后版本中对方法区的具体实现。它使用的是本地内存 (Native Memory),不再占用堆空间。

演进历程:

方法区实现的演变

  • JDK 1.7 及之前 (永久代 PermGen):
    • 方法区通过永久代实现,位于堆内存中。
    • 受到 JVM 堆大小参数 (-Xmx) 和永久代专用参数 (-XX:PermSize, -XX:MaxPermSize) 的限制。
    • 缺点:
      • 永久代大小难以设定,容易导致 PermGen space OOM。
      • 永久代的垃圾回收(主要是类卸载)条件苛刻且效率不高,容易造成内存泄漏。Full GC 时才会回收。
  • JDK 1.8 及之后 (元空间 Metaspace):
    • 完全废弃永久代,改用元空间实现方法区。
    • 元空间使用本地内存 (Native Memory),与堆内存隔离。
    • 优点:
      • 解决了永久代 OOM 的主要问题:默认情况下,元空间大小只受可用本地内存限制,不易溢出。
      • 简化了调优:不再需要精细调整永久代大小。
    • 存储内容调整:永久代中的一些数据(如字符串常量池、静态变量)在 JDK 7 和 8 的演变中被移到了堆中,元空间主要存储类的元数据本身。

方法区/永久代/元空间与 GC 的关系 (无论是永久代还是元空间,如果无法满足内存分配,都会抛出 OOM)

# 4. 设置方法区大小与 OOM

虽然元空间默认使用本地内存,但仍可对其进行大小限制和调优。

# 4.1 JDK 7 及之前的永久代参数

  • -XX:PermSize=<size>:设置永久代的初始分配空间。默认值约 20.75M。
  • -XX:MaxPermSize=<size>:设置永久代的最大可分配空间。32 位系统默认 64M,64 位系统默认 82M。
  • OOM:当加载的类信息所需空间超过 -XX:MaxPermSize 时,抛出 java.lang.OutOfMemoryError: PermGen space。

永久代大小参数

# 4.2 JDK 8 及之后的元空间参数

  • -XX:MetaspaceSize=<size>:设置元空间的初始大小(不是初始容量,而是初始高水位线 (Initial High Watermark))。达到这个值会触发第一次 Full GC(用于卸载不再使用的类)。默认值依赖平台,64 位服务器 JVM 通常约 21MB。
    • GC 触发与调整:GC 后,JVM 会根据释放的空间动态调整这个高水位线。如果释放空间不足,会提高水位线(不超过 -XX:MaxMetaspaceSize);如果释放过多,会降低水位线。
    • 调优建议:如果初始值设置过低,可能导致频繁的 Full GC。建议根据应用实际情况,将其设置为一个较高的值(如 -XX:MetaspaceSize=256m)以减少启动初期的 GC 次数。
  • -XX:MaxMetaspaceSize=<size>:设置元空间的最大容量。默认值是 -1,表示没有限制(只受可用本地内存限制)。
    • 建议设置:虽然默认无限制,但在生产环境中强烈建议设置一个上限(如 -XX:MaxMetaspaceSize=1g 或更高,取决于应用和物理内存),以防止因类加载失控导致耗尽服务器所有内存。
  • OOM:如果元空间大小达到了 -XX:MaxMetaspaceSize 的限制,并且 Full GC 后仍无法分配所需空间,抛出 java.lang.OutOfMemoryError: Metaspace。

# 4.3 模拟方法区 OOM

通过动态生成大量类来填满方法区(永久代或元空间)。

// 文件名: OOMTest.java
// 需要引入 ASM 库来动态生成类,例如:
// import jdk.internal.org.objectweb.asm.ClassWriter; // JDK 自带(不推荐直接使用内部 API)
// import jdk.internal.org.objectweb.asm.Opcodes;
// 或者使用 Maven/Gradle 引入 org.ow2.asm:asm

import jdk.internal.org.objectweb.asm.ClassWriter; // 仅为示例,生产环境应使用标准 ASM 库
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * 模拟方法区 OOM
 * JDK 7: -XX:PermSize=10m -XX:MaxPermSize=10m
 * JDK 8+: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 */
public class OOMTest extends ClassLoader { // 继承 ClassLoader 以便调用 defineClass
    public static void main(String[] args) {
        int j = 0; // 计数成功加载的类的数量
        try {
            OOMTest test = new OOMTest();
            for (int i = 0; i < 100000; i++) { // 尝试加载大量类
                // 1. 使用 ASM 创建类的字节码
                ClassWriter classWriter = new ClassWriter(0); // 0 表示不需要计算 maxs 和 frames
                // 定义类的基本信息:版本(V1_8), 访问权限(public), 类名("Class"+i), 包(null), 父类(Object), 接口(null)
                classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 生成类的字节码 byte 数组
                byte[] code = classWriter.toByteArray();

                // 2. 使用 ClassLoader 加载生成的类
                // defineClass 是 ClassLoader 的 protected 方法,用于将 byte 数组转换为 Class 对象
                test.defineClass("Class" + i, code, 0, code.length);
                j++;

                // 可选:稍微打印进度,观察加载过程
                // if (j % 1000 == 0) {
                //     System.out.println("Loaded " + j + " classes");
                // }
            }
        } finally {
            // 打印最终成功加载的类的数量
            System.out.println("Successfully loaded classes: " + j);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

运行情况 1:不设置上限 (JDK 8+)

使用默认 JVM 参数。程序会尝试加载完所有类(取决于循环次数)。 输出示例:

Successfully loaded classes: 100000
1

运行情况 2:设置小上限 (JDK 8+) VM Options: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m

运行后程序会在加载一定数量的类后因元空间不足而抛出 OOM。 输出示例:

Successfully loaded classes: 8531
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756) // 行号可能因JDK版本变化
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at OOMTest.main(OOMTest.java:31) // 指向 defineClass 调用行
1
2
3
4
5
6

(如果使用 JDK 7 并设置 -XX:MaxPermSize=10m,会抛出 PermGen space OOM)

# 4.4 如何解决方法区 OOM

解决 PermGen space 或 Metaspace OOM 的步骤:

  1. 分析 Dump 文件:
    • 出现 OOM 时,使用 JVM 参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<path> 生成堆转储快照(Heap Dump)。虽然 OOM 是方法区,但 Heap Dump 中通常也包含了类加载器的信息,有助于分析。
    • 使用内存分析工具(如 Eclipse MAT, JProfiler, VisualVM)打开 Dump 文件。
  2. 区分内存泄漏 (Memory Leak) 与 内存溢出 (Memory Overflow):
    • 内存泄漏:某些类或类加载器本应被卸载回收,但由于存在意外的强引用(例如被长期存活的对象、线程、集合等持有),导致 GC 无法回收,最终占满了方法区。
    • 内存溢出:程序确实需要加载如此多的类,超出了设置的方法区容量上限。
  3. 处理内存泄漏:
    • 通过分析工具查找占用内存最多的类或类加载器实例。
    • 查看这些实例到 GC Roots 的引用链,找出是哪个不该存在的引用阻止了它们的回收。
    • 定位到代码中产生这些引用的地方并修复逻辑。常见原因包括:
      • 集合类持有对象引用未清理。
      • 监听器、回调未注销。
      • 自定义类加载器使用不当,其自身或加载的类无法被回收。
      • ThreadLocal 使用后未调用 remove()。
  4. 处理内存溢出:
    • 如果确认不存在泄漏,确实是应用需要加载大量类:
      • 增大方法区容量:调整 -XX:MaxPermSize (JDK 7) 或 -XX:MaxMetaspaceSize (JDK 8+)。确保物理内存充足。
      • 优化代码:检查是否可以减少动态类的生成(如优化动态代理使用),是否可以合并或移除不必要的依赖库。
      • 检查应用服务器配置:如果部署了大量应用,考虑是否超出服务器承载能力。

# 5. 方法区的内部结构

方法区主要存储的是已被 JVM 加载的类相关信息。

方法区内部结构概览

根据《深入理解Java虚拟机》的描述,方法区存储内容主要包括:

  • 类型信息 (Type Information)
  • 域信息 (Field Information)
  • 方法信息 (Method Information)
  • 运行时常量池 (Runtime Constant Pool)
  • 静态变量 (Static Variables) (JDK 7+ 移到堆中,但逻辑上仍与类关联)
  • 即时编译器 (JIT) 编译后的代码缓存 (Code Cache)

方法区存储内容细化

# 5.1 类型信息 (Type Information)

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

  • 完整有效名称 (Fully Qualified Name):例如 java.lang.String。
  • 直接父类的完整有效名称:对于 Object 类或接口,此项为空。
  • 类型的修饰符 (Modifiers):例如 public, abstract, final 等。
  • 直接实现的接口列表 (Interfaces):一个有序列表,包含该类型直接实现的所有接口的完整有效名称。

# 5.2 域信息 (Field Information / 成员变量信息)

JVM 必须保存类型中所有字段(成员变量)的相关信息,并保持声明顺序:

  • 域名称 (Field Name)。
  • 域类型 (Field Type):例如 int, java.lang.String。
  • 域修饰符 (Field Modifiers):例如 public, private, static, final, volatile, transient 等。

# 5.3 方法信息 (Method Information)

JVM 必须保存类型中所有方法的信息,并保持声明顺序:

  • 方法名称 (Method Name)。
  • 返回类型 (Return Type):例如 void, int, java.lang.String。
  • 参数数量和类型 (Parameter Types):按声明顺序排列。
  • 方法修饰符 (Method Modifiers):例如 public, private, static, final, synchronized, native, abstract。
  • 方法的字节码 (Bytecodes):实际的可执行代码(抽象方法和本地方法除外)。
  • 操作数栈大小 (Operand Stack Size) 和 局部变量表大小 (Local Variable Table Size)(抽象和本地方法除外)。
  • 异常表 (Exception Table):描述方法中 try-catch 块的信息(抽象和本地方法除外)。
    • 每个异常处理条目包含:try 块的起始和结束指令位置、catch 块的起始指令位置、需要捕获的异常类型的常量池索引。

# 5.4 运行时常量池 (Runtime Constant Pool)

这是方法区中非常重要的一部分。

  • 来源:每个 .class 文件都有一个常量池表 (Constant Pool Table),它是 Class 文件结构的一部分,存储了编译时产生的各种字面量 (Literals) 和 符号引用 (Symbolic References)。当类被加载到 JVM 时,这个 Class 文件常量池的内容会被转存到方法区,形成运行时常量池。
  • 内容:
    • 字面量:如文本字符串 ("hello"), final 常量值 (如 final int MAX = 100; 中的 100)。
    • 符号引用:一种编译时的符号表示,用于描述代码中引用的目标,还未解析为直接内存地址。包括:
      • 类和接口的全限定名。
      • 字段的名称和描述符(类型)。
      • 方法的名称和描述符(参数类型和返回类型)。
  • 与 Class 文件常量池的区别:
    • 时机:Class 文件常量池是静态的(编译时确定),运行时常量池是动态的(类加载后创建,运行时可能变化)。
    • 地址:运行时常量池中的符号引用在解析 (Resolution) 阶段会被替换为直接引用 (Direct References),即指向内存中实际数据(如对象地址、方法代码入口)的指针或偏移量。
    • 动态性:运行时常量池具有动态性。例如,String.intern() 方法可以在运行时将新的字符串添加到字符串常量池(它是运行时常量池的一部分)。
  • 作用:运行时常量池是 JVM 执行字节码指令(如 ldc, getfield, putfield, invokevirtual)时查找常量、类、字段、方法信息的关键数据结构。

常量池 (Constant Pool) vs 运行时常量池 (Runtime Constant Pool)

  • 常量池:存在于 .class 文件中,包含字面量和符号引用。是静态的。
  • 运行时常量池:存在于方法区中,是类加载后常量池的运行时表示。包含解析后的直接引用(也可能保留符号引用待运行时解析),具有动态性。

方法区包含运行时常量池 Class 文件结构与常量池

为什么需要常量池? Java 字节码文件需要引用大量外部信息(类名、方法名、字段名、字符串等)。如果将这些信息直接硬编码在字节码指令中,会使字节码文件变得非常臃肿。常量池提供了一种间接引用机制:字节码指令只包含指向常量池中某个条目的索引,JVM 在运行时通过索引查找常量池即可获得所需信息,大大减小了 .class 文件的大小。

运行时常量池的 OOM:如果运行时常量池所需内存超过方法区限制,也会抛出 OutOfMemoryError。

# 5.5 静态变量 (Static Variables)

  • 逻辑归属:静态变量是类级别的变量,与类本身关联,被所有实例共享。从逻辑上看,它们是类信息的一部分,属于方法区管理。
  • 物理存储位置 (HotSpot):
    • JDK 6 及之前:静态变量存储在永久代(方法区的实现)中。
    • JDK 7:静态变量从永久代移到了 Java 堆中。
    • JDK 8 及之后:永久代被元空间取代,但静态变量仍然存储在 Java 堆中。
  • 访问:即使静态变量的物理存储位置在堆中,访问它们仍然需要通过方法区中的类信息来定位。

# 5.6 即时编译器编译后的代码缓存 (JIT Code Cache)

  • JVM 的执行引擎在运行时会将热点代码(经常执行的方法或循环)通过 JIT 编译器编译成本地机器码,以提高执行效率。
  • 这些编译后的本地机器码会被缓存起来,这个缓存区域通常也位于方法区(或一个与之关联的区域)。

# 5.7 内部结构反编译示例

通过 javap -v 查看字节码,可以直观了解方法区存储的部分信息。

// 文件名: MethodInnerStrucTest.java
import java.io.Serializable;

/**
 * 测试方法区的内部构成
 */
public class MethodInnerStrucTest extends Object implements Comparable<String>, Serializable {
    // 实例变量 (存储在堆对象中,但其描述信息在方法区)
    public int num = 10;
    // 类变量 (静态变量) - JDK 7+ 实际存储在堆,但描述信息和引用在方法区关联的 Class 对象中
    private static String str = "测试方法的内部结构";

    // 构造器 (方法信息存储在方法区)
    public MethodInnerStrucTest() {}

    // 实例方法 (方法信息存储在方法区)
    public void test1() {
        int count = 20; // 局部变量,存储在栈帧中
        System.out.println("count = " + count);
    }

    // 静态方法 (方法信息存储在方法区)
    public static int test2(int cal) { // cal 是局部变量
        int result = 0; // 局部变量
        try {
            int value = 30; // 局部变量
            result = value / cal;
        } catch (Exception e) { // e 是局部变量
            e.printStackTrace();
        }
        return result;
    }

    // 实现接口方法 (方法信息存储在方法区)
    @Override
    public int compareTo(String o) { // o 是局部变量
        return 0;
    }

    // 静态代码块 (编译后成为 <clinit> 方法,方法信息存储在方法区)
    static {
        // str 的初始化在此进行
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

使用命令 javap -v -p MethodInnerStrucTest.class 反编译后的部分内容(已在问题中提供,此处不再重复),可以清晰看到:

  • 类信息:public class com.youngkbt.java.MethodInnerStrucTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
  • 常量池 (Constant pool):大量的条目,包括类引用 (#17 = Class #69 // com/youngkbt/java/MethodInnerStrucTest), 字段引用 (#2 = Fieldref #17.#53 // com/youngkbt/java/MethodInnerStrucTest.num:I), 方法引用 (#1 = Methodref #18.#52 // java/lang/Object."<init>":()V), 字符串字面量 (#6 = String #57 // count =) 等。
  • 域信息:public int num;, private static java.lang.String str; 及其描述符和标志。
  • 方法信息:包括构造器 <init>, test1, test2, compareTo 以及编译器生成的桥接方法 compareTo(Object) 和类初始化方法 <clinit>。每个方法都包含描述符、标志、字节码 (Code 属性)、行号表、局部变量表、异常表(如有)等。

# 5.8 non-final 的类变量 (静态变量) 访问

静态变量与类本身关联,可以通过类名直接访问,也可以通过对象引用访问(但不推荐,可能引起误解)。

// 文件名: MethodAreaTest.java
public class MethodAreaTest {
    public static void main(String[] args) {
        Order order = null; // order 引用为 null
        order.hello(); // 编译通过,运行时也正常!调用静态方法不需要对象实例
        System.out.println(order.count); // 编译通过,运行时正常!访问静态变量不需要对象实例
        // 上面两行虽然用了 null 引用,但因为访问的是 static 成员,
        // 编译器和 JVM 会解析为 Order.hello() 和 Order.count
    }
}

class Order {
    public static int count = 1; // 静态变量
    public static final int number = 2; // 静态常量
    public static void hello() { // 静态方法
        System.out.println("hello!");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

输出:

hello!
1
1
2

这证明了静态成员是属于类的,不依赖于具体的对象实例。

# 5.9 全局常量 (static final)

使用 static final 修饰的变量是全局常量。

  • 基本类型/String字面量常量:如果在声明时就用字面量初始化(例如 public static final int NUMBER = 2; 或 public static final String GREETING = "Hello";),并且这个值在编译期可知,那么这个常量的值会直接嵌入到使用该常量的类的字节码中(常量传播优化),并且在类的常量池中也会存储这个字面量值。访问这种常量通常不会触发类的初始化。
  • 引用类型/运行时计算的常量:如果 static final 变量是引用类型,或者其值需要在运行时计算(例如 static final Date NOW = new Date();),那么它仍然是一个静态变量,其初始化在类的 <clinit> 方法中进行,值存储在堆中(如果是对象的话),引用存储在与 Class 对象关联的地方(堆中)。

字节码对比:

对于 Order 类中的 count 和 number:

// javap 输出片段
public static int count; // 普通静态变量
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

public static final int number; // 静态常量 (基本类型)
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2 // <<-- 关键!值在编译时已确定并写入 class 文件
1
2
3
4
5
6
7
8
9

可以看到 number 有一个 ConstantValue 属性,表明其值在编译期就确定了。

# 5.10 字符串常量池 (String Table / String Pool)

  • 运行时常量池的一部分:字符串常量池是运行时常量池的一个特例,专门用于存储字符串字面量和通过 String.intern() 方法添加的字符串。
  • 目的:复用相同的字符串对象,节省内存。当代码中出现字符串字面量(如 "abc")时,JVM 会检查字符串常量池中是否已存在等值的字符串。如果存在,就直接返回池中对象的引用;如果不存在,就在池中创建一个新的字符串对象,并返回其引用。
  • 位置变化:
    • JDK 6及之前:字符串常量池位于永久代中。
    • JDK 7:字符串常量池从永久代移到了 Java 堆中。
    • JDK 8及之后:字符串常量池仍然在 Java 堆中。
  • 详细内容:请参考专门的 JVM - StringTable字符串常量池 文档。

# 6. 方法区使用示例(字节码执行流程)

回顾之前的代码示例及其字节码执行过程,可以更清晰地理解方法区(特别是运行时常量池)的作用。

// 文件名: MethodAreaDemo.java (简化版)
public class MethodAreaDemo {
    public static void main(String args[]) {
        int x = 500;
        int y = 100;
        int a = x / y; // 5
        int b = 50;
        System.out.println(a + b); // 输出 55
    }
}
1
2
3
4
5
6
7
8
9
10

字节码执行流程图示 (结合常量池和操作数栈/局部变量表):

  1. sipush 500:将 short 型整数 500 推到操作数栈顶。 sipush 500
  2. istore_1:将操作数栈顶的 int 值 (500) 存入局部变量表索引为 1 的位置 (即变量 x)。 istore_1
  3. bipush 100:将 byte 型整数 100 推到操作数栈顶。
  4. istore_2:将操作数栈顶的 int 值 (100) 存入局部变量表索引为 2 的位置 (即变量 y)。
  5. iload_1:将局部变量表索引 1 的 int 值 (500) 推到操作数栈顶。
  6. iload_2:将局部变量表索引 2 的 int 值 (100) 推到操作数栈顶。 iload_1, iload_2
  7. idiv:将操作数栈顶的两个 int 值相除 (100 / 500,注意栈是后进先出,所以是 y / x -> 100/500 = 0.2,整数除法结果为 0,啊这里例子和字节码似乎有点对不上,按字节码是 500/100 = 5),并将结果 (5) 压入栈顶。 idiv (图中结果是 5,说明是 500/100)
  8. istore_3:将操作数栈顶的 int 值 (5) 存入局部变量表索引为 3 的位置 (即变量 a)。
  9. bipush 50:将 byte 型整数 50 推到操作数栈顶。
  10. istore 4:将操作数栈顶的 int 值 (50) 存入局部变量表索引为 4 的位置 (即变量 b)。
  11. getstatic #2:获取静态字段 java.lang.System.out。#2 是指向运行时常量池中 System.out 字段符号引用的索引。JVM 解析该符号引用,找到 System.out 对应的 PrintStream 对象引用,并将其压入操作数栈。 getstatic
  12. iload_3:将局部变量表索引 3 的 int 值 (5) 推到操作数栈顶。
  13. iload 4:将局部变量表索引 4 的 int 值 (50) 推到操作数栈顶。
  14. iadd:将操作数栈顶的两个 int 值 (5 和 50) 相加,结果 (55) 压入栈顶。
  15. invokevirtual #3:调用实例方法 java.io.PrintStream.println(I)V。#3 是指向运行时常量池中该方法符号引用的索引。JVM 解析引用,找到方法入口。操作数栈顶的 55 作为参数,PrintStream 对象引用作为调用者,执行方法。 invokevirtual
  16. return:main 方法执行完毕,返回。 return

程序计数器 (PC Register) 在整个过程中始终指向下一条要执行的指令的地址,确保流程控制(包括方法调用返回、线程切换恢复)的正确性。

# 7. 方法区的演进细节再讨论

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

  1. 移除 HotSpot 特有实现,向 JRockit 看齐:永久代是 HotSpot JVM 的一个特定实现,而 Oracle 收购的 BEA JRockit JVM 并无此概念。移除永久代有助于整合两者。
  2. 解决永久代大小设置难题:为永久代分配多大空间是一个老大难问题。设置小了,动态加载类多时易 OOM;设置大了,浪费内存。元空间使用本地内存,默认只受物理内存限制,大大降低了 OOM 风险。
  3. 改善 Full GC 性能:永久代的回收(特别是类卸载)通常发生在 Full GC 时,且效率不高。将类的元数据移到与堆隔离的本地内存,理论上可以为未来更优化的类元数据管理和回收机制(虽然目前元空间的回收仍主要依赖 Full GC)打下基础,并可能减少 Full GC 对堆内对象回收的影响。

# 7.2 StringTable (字符串常量池) 位置调整的原因

  • JDK 7 将 StringTable 移到堆中:
    • 主要原因:永久代的垃圾回收效率太低(通常只在 Full GC 时回收)。
    • 问题:应用程序通常会创建大量字符串对象,如果这些字符串都进入永久代的 StringTable,并且回收不及时,很容易导致 PermGen space OOM。
    • 解决方案:将 StringTable 移到堆中,这样它就可以参与更频繁的 Minor GC 和 Major GC,使得不再被引用的字符串常量能更快地被回收,缓解了永久代的压力。

# 7.3 静态变量 (Static Variables) 的存放位置演变

  • 核心结论:静态变量引用的对象实例 (即 new 出来的对象实体) 始终都存储在 Java 堆中。发生变化的是静态变量这个引用本身存放在哪里。
  • 演变过程 (HotSpot):
    • JDK 6 及之前:静态变量引用和类信息一起存放在永久代。
    • JDK 7:静态变量引用从永久代移到了 Java 堆中,与该类对应的 java.lang.Class 对象实例关联在一起(Class 对象本身也存放在堆中)。
    • JDK 8 及之后:静态变量引用仍然在 Java 堆中,与 Class 对象关联。

示例验证:

// 文件名: StaticFieldTest.java
/**
 * 演示静态变量引用的对象实例在堆中
 * JDK 7 VM Options: -Xms200m -Xmx200m -XX:PermSize=100m -XX:MaxPermSize=100m -XX:+PrintGCDetails
 * JDK 8 VM Options: -Xms200m -Xmx200m -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m -XX:+PrintGCDetails
 */
public class StaticFieldTest {
    // 创建一个 100MB 的 byte 数组,并用静态变量 arr 引用它
    private static byte[] arr = new byte[1024 * 1024 * 100]; // 100MB

    public static void main(String[] args) {
        System.out.println("Static byte array allocated.");
        // 保持运行以便观察内存
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 打印 arr 引用,防止被 JIT 优化掉
        System.out.println(StaticFieldTest.arr.length);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

使用 JVisualVM 或 GC 日志分析:

  • JDK 7 环境: JDK 7 静态变量对象在堆 (可以看到 Old Gen (老年代) 被占用了约 100MB,而 Perm Gen 使用量很小)
  • JDK 8 环境: JDK 8 静态变量对象在堆 (同样,Old Gen 被占用了约 100MB,Metaspace 使用量很小)

深入分析 (使用 JHSDB - JDK 9+):

《深入理解 Java 虚拟机》书中通过 JHSDB 工具分析了类似场景:

// 文件名: StaticObjTest.java (来自书中案例)
public class StaticObjTest {
    static class Test {
        static ObjectHolder staticObj = new ObjectHolder(); // 静态变量引用
        ObjectHolder instanceObj = new ObjectHolder();      // 实例变量引用

        void foo() {
            ObjectHolder localObj = new ObjectHolder();     // 局部变量引用
            System.out.println("done");
        }
    }

    private static class ObjectHolder {} // 空对象,用于占位

    public static void main(String[] args) {
        Test test = new StaticObjTest.Test();
        test.foo();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

分析结论:

  1. staticObj, instanceObj, localObj 这三个引用变量所指向的 ObjectHolder 对象实例,本身都创建在 Java 堆中。 三个对象实例都在堆中
  2. staticObj 这个引用变量本身存储在哪里?书中通过 JHSDB 发现它存储在一个 java.lang.Class 对象实例的字段里。 静态引用在 Class 对象中
  3. 最终结论 (JDK 7+):HotSpot JVM 选择将静态变量引用与对应的 java.lang.Class 对象实例一起存放在 Java 堆中。

# 8. 方法区的垃圾回收

方法区(无论是永久代还是元空间)确实存在垃圾回收,尽管其回收条件和效率与堆不同。

回收目标:

  1. 常量池中废弃的常量:
    • 主要指运行时常量池中的字面量和不再被任何地方引用的符号引用。
    • 回收条件:只要常量没有被任何代码或对象引用,就可以被回收。
    • 回收机制:类似于堆中对象的回收。相对简单。
  2. 不再使用的类型 (Unused Classes/Types):
    • 这是方法区回收的主要难点和重点。
    • 回收条件(非常苛刻,需同时满足):
      • 1. 该类的所有实例都已经被回收:Java 堆中不存在该类及其任何子类的对象实例。
      • 2. 加载该类的类加载器 (ClassLoader) 已经被回收:这是最难满足的条件之一。JVM 自带的启动类加载器、扩展类加载器、应用类加载器通常很难被回收。只有在使用了自定义类加载器的场景下(如 OSGi, JSP 热加载, 动态代理等),当自定义类加载器本身可以被回收时,它加载的类才有可能被卸载。
      • 3. 该类对应的 java.lang.Class 对象没有在任何地方被引用:无法通过反射等方式访问该类。
    • 回收行为:JVM 允许对满足上述三个条件的无用类进行回收,但不保证一定会回收。
    • 控制参数:
      • -Xnoclassgc:可以禁用类的卸载(不推荐)。
      • -verbose:class:打印类加载和卸载信息。
      • -XX:+TraceClassLoading / -XX:+TraceClassUnloading:跟踪类加载/卸载的详细过程。
  • 必要性:在大量使用反射、动态代理、CGLib、热部署、OSGi 等场景中,类的卸载能力至关重要,否则会因不断加载新版本的类而导致方法区内存溢出。

# 9. 方法区小结

方法区在 JVM 结构中的位置总结

  • 定义:方法区是 JVM 规范定义的线程共享内存区域,用于存储类信息、运行时常量池、JIT 代码缓存等。
  • 实现:HotSpot JVM 中,JDK 7 前用永久代(堆内),JDK 8+ 用元空间(本地内存)。
  • 演进:元空间取代永久代解决了大小设定难题和部分 OOM 问题,并将字符串常量池、静态变量移至堆中。
  • 内部核心:运行时常量池是 Class 文件常量池的运行时表示,包含字面量和解析后的直接/符号引用,支持动态性。
  • 垃圾回收:方法区会进行垃圾回收,主要回收废弃常量和不再使用的类,类的卸载条件非常苛刻。
  • OOM:当方法区空间不足时,会抛出 PermGen space (JDK 7-) 或 Metaspace (JDK 8+) OOM。

# 10. 常见面试题回顾

(此处列出的面试题是对原文的整理,可作为复习要点)

  • JVM 内存模型/结构:包含哪些区域?(堆、栈、方法区、程序计数器、本地方法栈)
  • 各区域作用:分别存储什么数据?(类信息、对象实例、线程执行状态、本地方法调用状态等)
  • 栈与堆的区别:生命周期、存储内容、共享性、空间大小、异常类型(StackOverflowError vs OutOfMemoryError)。
  • 堆的结构:新生代(Eden, S0, S1)、老年代。为什么要分代?
  • 新生代内部比例:Eden vs Survivor 默认比例 (8:1:1)?为什么需要两个 Survivor 区?(复制算法需要)
  • 对象晋升老年代的条件:年龄阈值、动态年龄判断、大对象、Survivor 空间不足。
  • Java 8 内存模型变化:元空间取代永久代,存储位置(本地内存),字符串常量池和静态变量仍在堆中。
  • 方法区/永久代/元空间是否发生 GC:是。回收什么?(废弃常量、无用类)。类卸载条件?
  • JVM 内存分区:结合具体场景解释各区域交互(如对象创建过程)。
编辑此页 (opens new window)
上次更新: 2025/04/05, 20:16:54
JVM - 堆 (Heap)
JVM - 对象实例化内存布局

← JVM - 堆 (Heap) JVM - 对象实例化内存布局→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式