JVM 整理(三) 方法区+虚拟机栈

Java虚拟机(JVM)在运行Java程序时,会将其管理的内存划分为若干个不同的数据区域,这些区域各有分工,协同完成代码的执行。其中,方法区虚拟机栈是两个非常重要的内存区域,一个负责存储类的元数据,一个负责支撑方法的执行。理解它们的内部结构和运作原理,对于编写高质量代码、排查内存问题至关重要。本文将带你深入探索这两个区域。

一、方法区(Method Area)

1. 方法区存储什么?

方法区是所有线程共享 的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。简单来说,类加载完成后,类的元数据(如类名、访问修饰符、字段描述、方法描述等)就会存放在方法区。

《深入理解Java虚拟机》中给出了经典的描述:方法区存储了类信息、常量、静态变量、即时编译器编译后的代码缓存等。

  • 类信息:包括类的版本、字段、方法、接口等信息。

  • 常量池:存放编译期生成的各种字面量和符号引用。

  • 静态变量:类变量(static修饰)在方法区中有一份存储,被所有实例共享。

  • 即时编译器(JIT)编译后的代码:比如热点代码编译后的本地机器码。

2. 方法区的演进细节

方法区在JVM规范中只是一块逻辑上的区域,不同的虚拟机实现有不同的表现形式。对于HotSpot虚拟机来说,方法区的实现经历了较大的变化:

JDK版本 方法区实现 静态变量存储位置 字符串常量池位置
JDK 6及以前 永久代 方法区(永久代) 方法区(永久代)
JDK 7 永久代
JDK 8及以后 元空间
  • 永久代(PermGen) :在JDK 8之前,HotSpot使用永久代来实现方法区。永久代的大小是固定的,可以通过-XX:PermSize-XX:MaxPermSize调整。但永久代容易引发内存溢出(OutOfMemoryError: PermGen space),尤其是在动态生成大量类的场景(如JSP、OSGi)。

  • 元空间(Metaspace) :从JDK 8开始,永久代被移除,取而代之的是元空间 。元空间使用本地内存 (Native Memory),不再占用JVM堆内存,默认大小仅受本地内存限制。这大大减少了方法区内存溢出的概率。可以通过-XX:MetaspaceSize-XX:MaxMetaspaceSize调整。

静态变量去哪了?

在JDK 7及以后,静态变量和字符串常量池被移到了中。这意味着静态变量不再是方法区的一部分,而是存储在堆内存中,但逻辑上它们仍然属于类的元数据,只是物理存储位置发生了变化。这样做的目的是为了更好地进行垃圾回收,因为堆是GC的重点区域。

注意:虽然静态变量存储在了堆中,但它们依然被所有线程共享,生命周期与类相同。

3. 静态变量的共享特性

静态变量(类变量)被所有类实例共享,任何一个实例对静态变量的修改,其他实例都能看到。正因为这种共享特性,静态变量在多线程环境下需要考虑线程安全问题。

java 复制代码
public class StaticVarDemo {
    public static int count = 0;  // 静态变量,所有实例共享
}

二、虚拟机栈(Java Virtual Machine Stack)

1. 栈是什么?

虚拟机栈 (通常简称"栈")是线程私有 的,它的生命周期与线程相同。每个线程在创建时都会分配一个栈,用来存储该线程中方法调用时的数据。栈是方法执行的内存模型,每个方法被执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

栈的特点是先进后出(LIFO),每个方法从调用到执行完毕,对应着一个栈帧在虚拟机栈中的入栈和出栈。

2. 栈的运行原理

看一个简单的示例:

java 复制代码
public class StackDemo {
    public static void main(String[] args) {
        StackDemo demo = new StackDemo();
        demo.method2();
    }
    public void method2() {
        method3();
    }
    public void method3() {
        method4();
    }
    public void method4() {
        System.out.println("method4执行...");
    }
}

main方法启动时,JVM会为它创建一个栈帧(栈帧1)并压入栈中。接着main调用method2,栈帧2入栈;method2调用method3,栈帧3入栈;method3调用method4,栈帧4入栈。当method4执行完毕后,它的栈帧4首先出栈,然后依次是栈帧3、栈帧2、栈帧1。最终线程结束,栈释放。

这个过程直观地展示了"后进先出"的原则:最后被调用的方法最先结束。

3. 栈帧的内部结构

每个栈帧都包含以下几部分:

3.1 局部变量表(Local Variables)

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。其容量以变量槽(Slot)为最小单位,每个Slot可以存放一个booleanbytecharshortintfloatreference(对象引用)或returnAddress类型的数据。对于64位的longdouble,会占用两个连续的Slot。

示例代码:

java 复制代码
public class LocalVarDemo {
    public static void main(String[] args) {
        int i = 100;
        String s = "hello";
        char c = 'c';
        Date date = new Date();
    }
}

编译后,可以通过javap -v LocalVarDemo.class查看字节码中的局部变量表(局部变量表在字节码中就已经存在,运行时加载到栈帧中):

复制代码
LocalVariableTable:
Start  Length  Slot  Name   Signature
    0      17     0  args   [Ljava/lang/String;
    2      15     1     i   I
    5      12     2     s   Ljava/lang/String;
    8       9     3     c   C
   11       6     4  date   Ljava/util/Date;

可以看到,每个局部变量在表中都有对应的条目,包括变量名、类型和所在槽位。

3.2 操作数栈(Operand Stack)

操作数栈是一个后进先出栈,用于存放方法执行过程中的中间计算结果。例如,执行int c = a + b;时,会先将a和b的值压入操作数栈,然后执行加法指令,将结果弹出并存入局部变量表。

3.3 动态链接(Dynamic Linking)

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程中的动态链接。例如,调用接口方法时,实际调用的实现类方法需要在运行时才能确定。

3.4 方法出口(Return Address)

方法正常返回或异常返回时,需要恢复调用者的状态,包括程序计数器的值等。这些信息保存在方法出口中。

4. 栈溢出(StackOverflowError)

由于每个线程的栈容量是有限的(可以通过-Xss参数设置),如果线程请求的栈深度大于虚拟机允许的深度,就会抛出StackOverflowError。最常见的触发场景是递归调用

java 复制代码
public class StackOOMDemo {
    public static int count = 1;
    public static void main(String[] args) {
        System.out.println(count++);
        main(args);  // 无限递归
    }
}

运行该程序会不断打印递增的count,直到栈空间耗尽,抛出StackOverflowError。可以通过JVM参数-Xss256k缩小栈大小,观察递归深度变小。

常见问题:

  • 垃圾回收是否涉及栈内存?

    不涉及。栈帧在方法调用结束后会自动弹出,不需要垃圾回收器介入。

  • 方法内的局部变量是线程安全的吗?

    如果该局部变量没有逃离方法的作用范围 (即没有返回给外部或赋值给共享变量),那么它是线程安全的。因为每个线程都有自己独立的栈帧,局部变量存储在其中,不会被其他线程访问。

    但如果局部变量是静态变量,或者作为参数传递给了其他线程,则可能发生线程安全问题。

三、总结

区域 共享性 存储内容 异常类型 关键特性
方法区 线程共享 类信息、常量、静态变量、即时编译代码 OutOfMemoryError 演进为元空间,静态变量移至堆
虚拟机栈 线程私有 栈帧(局部变量表、操作数栈、动态链接等) StackOverflowError 方法调用的LIFO模型

理解方法区和虚拟机栈,有助于我们分析类加载机制、内存分配、线程安全以及常见的异常问题。在开发中,注意控制递归深度、合理使用静态变量,以及了解不同JDK版本的内存变化,都能让我们的程序更加健壮。

希望本文能帮助你更好地掌握JVM的这两个核心内存区域。如有疑问,欢迎留言讨论!

相关推荐
NGC_66111 小时前
G1收集器
java·开发语言·jvm
森林里的程序猿猿2 小时前
垃圾收集器ParNew&CMS与底层标记三色标记算法
java·jvm·算法
He BianGu2 小时前
【笔记】在WPF中CommandManager的功能和应用场景详细介绍
笔记·wpf
穿过锁扣的风2 小时前
【完整带注释版】图像直方图绘制教程(OpenCV+Matplotlib)
笔记·python·opencv
超级璐璐2 小时前
fast-livo2修改笔记
笔记
IT19952 小时前
计算机理论文档阅读笔记-MQTT vs WebSocket
笔记·websocket·网络协议
ShineWinsu2 小时前
sqlite
jvm·数据库·oracle
猹叉叉(学习版)2 小时前
【ASP.NET CORE】 14. RabbitMQ、洋葱架构
笔记·后端·架构·c#·rabbitmq·asp.net·.netcore
左左右右左右摇晃2 小时前
JVM 整理(四) 堆
jvm·笔记