JVM 运行时数据区
线程私有区
该区域生命周期与线程生命周期完全相同,随线程创建而创建,随线程销毁而回收。
并且不与其他线程共享数据,不存在线程安全问题,也不需要 GC 介入。
程序计数器
用来记录当前线程正在执行的字节码指令和行号,执行引擎用它来读取下一条指令。
Java 多线程运行,CPU 会在不同线程之间来回切换时间片,如果不记录行号和执行的指令,当线程被挂起并重获得时间片执行时就不知道自己上次执行到哪儿了。
如果线程执行的是 Java 方法,计数器记录的是正在执行的虚拟机字节码的指令地址
如果线程执行的是 Native 方法,计数器的值则是 Undefined,因为底层机器指令是由 OS 直接管理
本地方法栈
为 JVM 执行 Native 方法的服务。 如 Thread.start() 底层的 start0() System.currentTimeMillis() 等等。
在 HotSpot 虚拟机中,直接将本地方法栈和 Java 虚拟机栈合二为一,不再做物理区分。
Java 虚拟机栈
用于存放单个线程的数据「方法,局部变量和操作数等」,一个虚拟机栈由多个栈帧组成。Frames 的生命周期与线程生命周期相同。因为栈内存不共享所以也是天然线程安全并且不参与 GC。

Frames 也可以被分配在 Heap 中
Frames 由以下组件构成:
- 局部变量表
- 操作数栈
- 当前类运行时的常量池引用 「动态链接」
- 方法返回地址
局部变量表
存放着:
- 方法参数
- 方法体内的局部变量
- 8 种基本数据类型、对象引用 (指向对中的指针) 和 returnAddress
操作数栈
用于在方法执行过充中存放中间计算结果、入参出参的 LIFO 栈
动态链接
每一个 Farmes 内部都包含一个指向常量池中该 Frames 所属方法的引用,在每次运行时将这些符号转化为直接调用的内存地址。
方法返回地址
存放调用该方法的指令的下一条指令地址,用于当前方法退出后恢复上层调用者的执行现场。
线程私有区参数设置
-Xss-Xss1m-Xss256k设置每个线程栈的大小, 设置栈大小过大会减少最多能创建线程数量,但是也可以提高最大方法调用深度。
常见的错误
StackOverflowError: 线程请求的栈深度大于虚拟机所允许的最大深度
OutOfMemoryError: 创建新线程时无法为其分配初始占空间、若栈允许动态扩容但扩容时没有足够的物理内存。
线程共享区
线程共享区是 JVM 中面积最大、逻辑最复杂、竞争最激烈的区域。 它的生命周期贯穿 JVM 进程始终,所有被创建的线程都可以访问这些区域。
线程共享区分为以下区域:
- 堆 Heap
- 方法区
堆
存储对象实例和数组,几乎所有的对象实例都在这里分配内存。比如 new 出来的对象。
分代模型
在 Serial、Parallel、CMS、G1 回收器下,堆分为年轻代和老代两部分。其中年轻代区域要大于老年代区域。
大部分对象生命周期很短,被创建不久后就会丢失引用,而存活下来的对象越有可能长期存在。

新生代
绝大多数对象生命周期极短,它被分为三块 Eden 区、Survivor 0 区和 Survivor 1 区。
- Eden 区: 占新生代的 4/5 最初的新对象都在这里分配
- Survivor 区: 各占 1/10,当 触发 Minor / Young GC 时 会把对象放入 Survior 区
对象流转
- 新创建的对象会进入 Eden 区,当 Eden 区装满后会触发 GC,将存活对象复制到 S0,年龄记为 1。
- 当下次 Eden 又满时,会把 Eden 和 S0 的存活对象复制到 S1 清空 Eden 和 S0 年龄再加 1。
- S0 和 S1 会不断互换 From 和 To 的角色,保持其中一个始终为空。
- 当对象年龄达到阈值(默认为 15,可以通过 -XX:MaxTenuringThreshold 控制)时晋升到老年代。
老年代
存放生命周期长久的对象 (长期存在的 Spring 单例 Bean、 缓存、大对象),空间不足时会触发 Major GC / Full GC,通常使用标记 - 清楚或者标记 - 整理算法。
大对象直接进入老年代的行为是由虚拟机动态决定的,它与具体使用的垃圾回收器和相关参数有关。大对象直接进入老年代是一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。
方法区
方法区存放的是类的结构信息,而不是具体的对象实例.
run-time constant pool, field and method data, and the code for methods and constructors, including the special methods used in class and interface initialization and in instance initialization.
运行时常量池
每个类被加载到内存后它的.class 文件中的常量池就会被放进去。
- 字面量: 代码中写死的值,如文本字符串敞亮、final 常量数值等.
- 符号引用:类和接口的全限定名 (java/lang/String)、方法多名称和描述符(入参、返回值类型)、字段的名称和描述符
运行时常量池可以在程序运行期间动态添加新常量 如 String.intern()
字段和方法数据
类结构的静态元数据描述:
- 字段数据,类中声明的变量、名字、类型、访问修饰符等等
- 方法数据:类中的方法、方法名、返回类型、参数列表、访问修饰符等等。
普通方法和构造器代码
存放真正执行的代码逻辑,这里存放的是编译后的字节码指令流。
StringTable 字符串常量池
字符串常量池是一个固定大小的哈希表,存储的是堆中字符串对象的引用,哈希桶的个数可以通过参数进行调节 (-XX:StringTableSize=60013).
在 JDK 7 之前 StringTable 存放在方法区 (永久代),在 JDK 7 之后移动到了堆中。
永久代的垃圾回收只有在 Full GC 时才会触发,回收条件很苛刻。
如果大量动态生成字符串并入池,旧可能会导致永久代内存耗尽
而移动到堆之后就可以接受到堆内高频的垃圾回收,即时清理没有被引用的字符串。
元空间
方法区是 JVM 规范,而永久代是 HotSpot 在 JDK8 之前对方法区的实现.
JDK 8 时 将方法区移出了 JVM 堆,直接放在 OS 的本地内存中,并命名为元空间。
