JVM内存区域划分
程序计数器 PC
记录下一条jvm指令的执行地址
特点:
每个线程有自己的程序计数器
不会存在内存溢出
虚拟机栈 JVM STACKS
线程运行所需要的内存空间,称为虚拟机栈
每个栈由多个栈帧组成,对应着每次方法调用的时候所占用的内存
每个线程只能有一个活动的栈帧,对应着当前正在执行的那个方法
栈帧
每个方法运行时所需要的内存
栈帧包括:参数,局部变量,返回地址
问题
垃圾回收是否涉及栈内存
不涉及,不需要对栈内存发起回收处理
作用域绑定:栈内存用于存储方法的调用帧(局部变量、方法参数等)。当方法执行结束时(正常返回或抛出异常),其对应的整个栈帧会自动弹出(LIFO 原则),内存立即释放。
无需外部干预:栈内存的分配和回收完全由编译器/解释器通过指令指针和栈指针硬件机制管理,效率极高(仅需移动指针位置)。
栈内存分配越大越好吗?
不是,会限制线程数目
资源浪费:每个线程独占栈空间,若设置过大(如 10MB),高并发场景下(1000 线程 → 10GB)易耗尽物理内存。
溢出风险:栈深度由递归层级/局部变量体积决定。即使总空间大,单次方法调用链过长仍会触发 StackOverflowError(例如无限递归)。
方法内的局部变量是不是线程安全的
是的
核心原因在于 每个线程都有自己独立的栈(Stack)。
独立副本: 当一个方法被多个线程同时执行时,每个线程都会在自己的栈上为该方法的局部变量创建一个独立的副本。这些副本是互相隔离的,一个线程修改它的局部变量副本不会影响到其他线程的局部变量副本。
栈内存: 局部变量通常存储在线程的栈内存中。当方法执行完毕,这些局部变量就会被销毁。
这个是作为方法参数传递进来的,这个就不再是线程私有的了,所以这个局部变量StringBuilder不是线程安全的
这个逃离了方法作用域,也不是线程安全的
线程运行诊断
本地方法栈
为本地方法提供运行的内存空间
堆
通过new关键字,创建对象都会使用堆内存
特点:
- 是线程共享的,堆中的对象都是需要考虑线程安全问题
- 有垃圾回收机制
堆内存诊断
package cn.meowrain.heaptest;
import java.util.concurrent.TimeUnit;
public class HeapTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("....1");
TimeUnit.SECONDS.sleep(20);
System.out.println("....2");
byte[] array = new byte[1024 * 1024 * 10];
TimeUnit.SECONDS.sleep(10);
System.out.println("....3");
array = null;
System.gc();
TimeUnit.SECONDS.sleep(30);
}
}
方法区
方法区是JVM中一个被所有线程共享的内存区域
存储内容: 用于存储每个类的结构信息:
- 运行时常量池
- 字段和方法的数据
- 方法和构造函数的代码(字节码)
- 用于类和实例初始化,接口初始化的特殊方法
主要特性
生命周期:方法区在虚拟机启动时被创建,并伴随整个虚拟机的生命周期。
与堆的关系:规范中提到,方法区在逻辑上是堆(Heap)的一部分。但是,具体的实现可以选择不进行垃圾回收或内存整理。这给了虚拟机实现者很大的灵活性,也是为什么历史上会有永久代(PermGen)和元空间(Metaspace)这些不同实现的原因。
内存管理:
方法区的内存可以是不连续的。
其大小可以是固定的,也可以根据需要进行动态扩展和收缩。
开发者或用户通常可以通过JVM参数来控制方法区的初始大小、最大值和最小值。
异常情况
如果方法区无法满足内存分配请求(比如加载了过多的类,导致空间不足),Java虚拟机将会抛出 OutOfMemoryError 异常。
元空间
一、元空间(Metaspace)到底是什么?
元空间是 Java 8 引入的一个虚拟机内存区域,用来替代之前的永久代(Permanent Generation, PermGen)。
它的核心作用是:存储类的元数据(Class Metadata)。
最重要的一点是:元空间使用的是本地内存(Native Memory),而不是 JVM 堆内存。 这是它和永久代最根本的区别。
二、元空间里具体存储了什么?
元空间存储的是 JVM 加载的每一个类的“静态”描述信息。主要包括以下内容:
类的结构信息(Klass Metadata):
类的全限定名。
类的修饰符(public, abstract, final 等)。
父类和所有实现接口的信息。
字段信息:每个字段的名称、类型、修饰符。
方法信息:每个方法的名称、返回类型、参数列表、修饰符、以及最重要的——方法的字节码(Bytecode)。
运行时常量池(Runtime Constant Pool):
这是 .class 文件中常量池(Constant Pool)在运行时的内存表示。
它存放了编译期生成的各种字面量(如文本字符串、final 常量值)和符号引用(如类和接口的全限定名、字段和方法的名称和描述符)。
注意:从 Java 7 开始,字符串常量池(String Pool)已经从永久代(后来的元空间)移到了 Java 堆中。所以元空间里只包含对字符串常量的引用。
JIT 编译后的代码缓存(JIT-compiled Code Cache):
当 JVM 的即时编译器(Just-In-Time Compiler)将热点方法(被频繁调用的方法)的字节码编译成本地机器码后,这些优化过的机器码也会被缓存在元空间中,以提高执行效率。
注解(Annotations):
类、方法、字段上的注解信息。
永久代
可以把它理解为 JVM 在 Java 8 之前的 “类的养老院” 。当一个类被加载后,它的“身份信息”和一些“终身携带的行李”就会被安置在这个养老院里,理论上会一直待下去。
但问题是,这个养老院的地盘是固定的,如果住进来的类太多,养老院就会“爆满”,导致整个系统崩溃。这就是为什么它在 Java 8 中被“拆除”,并由更现代化的元空间 (Metaspace) 所取代。
方法区是由永久代/元空间实现的
方法区是JVM规范中定义的一个逻辑区域,规定用来存储类相关的信息(比如类的结构、常量池、方法字节码、等)。
在 HotSpot 虚拟机中,Java 7及以前用**永久代 (PermGen)来实现方法区,Java 8及以后则用元空间 (Metaspace)**来实现方法区。
运行时常量池
什么是常量池?
(base) PS D:\project\rocket-mq-demo\target\classes\cn\meowrain\heaptest> javap -v .\HeapTest.class
Classfile /D:/project/rocket-mq-demo/target/classes/cn/meowrain/heaptest/HeapTest.class
Last modified 2025年6月17日; size 568 bytes
SHA-256 checksum e83faf4d213359c779c37fb31bd23cd3b402dd4b7fd8d9bfb487de48e803b361
Compiled from "HeapTest.java"
public class cn.meowrain.heaptest.HeapTest
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // cn/meowrain/heaptest/HeapTest
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // Hello Java
#14 = Utf8 Hello Java
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // cn/meowrain/heaptest/HeapTest
#22 = Utf8 cn/meowrain/heaptest/HeapTest
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 LocalVariableTable
#26 = Utf8 this
#27 = Utf8 Lcn/meowrain/heaptest/HeapTest;
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 args
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 SourceFile
#33 = Utf8 HeapTest.java
{
public cn.meowrain.heaptest.HeapTest();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/meowrain/heaptest/HeapTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello Java
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HeapTest.java"
常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息
运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会被放入运行时常量池,把里面的符号地址变为真实地址
StringTable
StringTable指的是字符串常量池,这是一个用来存储字符串实例的特殊内存区域
Java中的字符串是不可变对象,为了优化性能和节省内存,JVM使用了字符串常量池机制,所有用字面量方式创建的字符串,如String str=“hello”;
都会被存储在方法区中的字符串常量池中
package cn.meowrain.heaptest;
public class HeapTest {
public static void main(String[] args) {
String a = "a"; // 常量池
String b = "b"; // 常量池
String ab = a + b; // 堆对象 底层是StringBuilder StringBuilder最后会调用toString方法,返回一个new String() 因此创建在堆上
String ab1 = "a" + "b"; //编译器优化,等于String ab1 = "ab" 在常量池
String ab2 = "ab"; // 常量池
String ab3 = ab.intern(); // 如果池中有"ab",返回池中的引用,否则直接返回堆堆内存中的引用
System.out.println(ab == ab1); // false
System.out.println(ab1 == ab2); // true
System.out.println(ab3 == ab2); // true
}
}
String x2 = new StringBuilder().append("c").append("d").toString();
x2.intern();
String x1 = "cd";
System.out.println(x1 == x2); // true
String x3 = new String("cd"); //先在字符串常量池创建"cd",后创建String对象在堆上
x3.intern();//查找字符串常量池里面有没有cd,发现有,返回常量值
// JVM 发现常量池中已经存在 "cd"(在第1步创建的)。
// 于是 intern() 方法返回了常量池中那个 "cd" 的引用。
// 但是!这个返回值没有被赋给任何变量,所以 x2 变量本身没有变,它仍然指向堆上的对象。
// x1 指向 -> 字符串常量池中的 "cd" 对象。
// x2 指向 -> 堆内存中的 "cd" 对象。
// 它们的内存地址不同。
System.out.println(x3 == x1); // false