Java虚拟机JVM在运行Java程序时会将其管理的内存划分为若干个不同的数据区域这些区域各有分工协同完成代码的执行。其中方法区和虚拟机栈是两个非常重要的内存区域一个负责存储类的元数据一个负责支撑方法的执行。理解它们的内部结构和运作原理对于编写高质量代码、排查内存问题至关重要。本文将带你深入探索这两个区域。一、方法区Method Area1. 方法区存储什么方法区是所有线程共享的内存区域它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。简单来说类加载完成后类的元数据如类名、访问修饰符、字段描述、方法描述等就会存放在方法区。《深入理解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. 静态变量的共享特性静态变量类变量被所有类实例共享任何一个实例对静态变量的修改其他实例都能看到。正因为这种共享特性静态变量在多线程环境下需要考虑线程安全问题。public class StaticVarDemo { public static int count 0; // 静态变量所有实例共享 }二、虚拟机栈Java Virtual Machine Stack1. 栈是什么虚拟机栈通常简称“栈”是线程私有的它的生命周期与线程相同。每个线程在创建时都会分配一个栈用来存储该线程中方法调用时的数据。栈是方法执行的内存模型每个方法被执行时都会创建一个栈帧Stack Frame用于存储局部变量表、操作数栈、动态链接、方法出口等信息。栈的特点是先进后出LIFO每个方法从调用到执行完毕对应着一个栈帧在虚拟机栈中的入栈和出栈。2. 栈的运行原理看一个简单的示例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可以存放一个boolean、byte、char、short、int、float、reference对象引用或returnAddress类型的数据。对于64位的long和double会占用两个连续的Slot。示例代码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。最常见的触发场景是递归调用。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的这两个核心内存区域。如有疑问欢迎留言讨论
JVM 整理(三) 方法区+虚拟机栈
Java虚拟机JVM在运行Java程序时会将其管理的内存划分为若干个不同的数据区域这些区域各有分工协同完成代码的执行。其中方法区和虚拟机栈是两个非常重要的内存区域一个负责存储类的元数据一个负责支撑方法的执行。理解它们的内部结构和运作原理对于编写高质量代码、排查内存问题至关重要。本文将带你深入探索这两个区域。一、方法区Method Area1. 方法区存储什么方法区是所有线程共享的内存区域它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。简单来说类加载完成后类的元数据如类名、访问修饰符、字段描述、方法描述等就会存放在方法区。《深入理解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. 静态变量的共享特性静态变量类变量被所有类实例共享任何一个实例对静态变量的修改其他实例都能看到。正因为这种共享特性静态变量在多线程环境下需要考虑线程安全问题。public class StaticVarDemo { public static int count 0; // 静态变量所有实例共享 }二、虚拟机栈Java Virtual Machine Stack1. 栈是什么虚拟机栈通常简称“栈”是线程私有的它的生命周期与线程相同。每个线程在创建时都会分配一个栈用来存储该线程中方法调用时的数据。栈是方法执行的内存模型每个方法被执行时都会创建一个栈帧Stack Frame用于存储局部变量表、操作数栈、动态链接、方法出口等信息。栈的特点是先进后出LIFO每个方法从调用到执行完毕对应着一个栈帧在虚拟机栈中的入栈和出栈。2. 栈的运行原理看一个简单的示例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可以存放一个boolean、byte、char、short、int、float、reference对象引用或returnAddress类型的数据。对于64位的long和double会占用两个连续的Slot。示例代码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。最常见的触发场景是递归调用。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的这两个核心内存区域。如有疑问欢迎留言讨论