Skip to content

Commit

Permalink
深入理解Java虚拟机-运行时数据区
Browse files Browse the repository at this point in the history
  • Loading branch information
geekyspace committed Aug 14, 2024
1 parent e559451 commit bea599a
Showing 1 changed file with 89 additions and 40 deletions.
129 changes: 89 additions & 40 deletions src/md/jvm/part2/runtime-data-areas.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,77 +10,126 @@ tag: JVM

# 运行时数据区

> **运行时数据区**是指Java虚拟机(JVM)在运行Java程序时用于存储数据的内存区域,分为程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区五个部分。
> **运行时数据区**是指在运行程序时存储数据的内存区域。分为程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区五个部分。
![Java虚拟机运行时数据区](https://img.geekyspace.cn/pictures/2024/202408102247073.png)

* **线程私有:**
* **程序计数器** - 存储线程执行位置
* **虚拟机栈** - 存储Java方法调用与执行过程的数据
* **本地方法栈** - 存储本地方法的执行数据
* **线程共享:**
* **** - 主要存储对象
* **方法区** - 存储类/方法/字段等定义(元)数据
* **运行时常量区** - 保存常量static数据

## 程序计数器

> **程序计数器**(Program Counter Register)是一块较小的内存空间,可以看做当前线程所执行字节码的行号指示器。
> 每个线程都有一个独立的程序计数器,用于记录线程执行的位置,以便线程切换后能恢复到正确的位置。
> **程序计数器**是线程私有的,用于记录当前程序执行的字节码指定位置。
**知识点:**

1. 线程私有
2. 不会被垃圾回收
3. 访问速度最快(JVM内存区域中)
4. 占用内存少,不会出现`OutOfMemoryError`
5. 执行Java方法时,记录的是字节码指令地址
6. 执行Native方法时,记录为未定义(`undefined`

**思考以下问题,加强理解:**

* Java方法执行时,程序计数器记录字节码指令地址
* Native方法执行时,程序计数器为空(Undefined)
* 此区域不抛出`OutOfMemoryError`异常
1. 程序计数器如何保证线程能够准确地恢复到之前的执行位置?
2. 字节码执行与程序计数器的关系?

## Java虚拟机栈
## 虚拟机栈

> **Java虚拟机栈**(Java Virtual Machine Stack)是线程私有的内存区域,其生命周期与线程相同。
> 它描述了方法执行的内存模型。当方法被执行时,JVM 会为该方法同步创建一个**栈帧**(Stack Frame)。
> **虚拟机栈**是线程私有的内存区域,其生命周期与线程相同。
> 它描述了**方法执行的内存模型**。当方法被执行时,JVM会为该方法同步创建一个==栈帧(Stack Frame)==
* 每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
* 可以通过参数`-Xss`来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
* 栈帧用于存储局部变量、操作数栈、动态链接、方法出口等信息。
**知识点:**

![栈帧的概念结构](https://img.geekyspace.cn/pictures/2024/202408112312001.png)
1. 线程私有
2. 不会被垃圾回收
3. 访问速度仅次于程序计数器
4. 栈大小可设置,限制深度:
* 推荐固定大小设置(`-Xss 数值[k|m|g]`),达到上限,抛出`StackOverflowError`
* 动态扩展,可用内存不足时,抛出`OutOfMemoryError`

* **局部变量表(Local Variable Table):** 方法执行时用于存储方法参数和局部变量的一块内存空间。
* **操作数栈(Operand Stack):** 后进先出(LIFO)结构,用于方法执行时存储执行指令产生中间结果。
* **动态链接(Dynamic Linking):** 指在方法调用时,将符号引用转换为直接引用的过程。
* **方法返回地址(Return Address):** 指方法调用后返回位置的地址。
**栈帧的内部结构:**

![栈帧的概念结构](https://pdai.tech/images/jvm/jvm/0082zybply1gc8tjehg8bj318m0lbtbu.jpg)

* **局部变量表:** 用于存储方法中的局部变量和参数。
* **操作数栈:** 后进先出(LIFO)结构,用于方法执行时存储执行指令产生中间结果。
* **动态链接:** 指在方法调用时,将符号引用转换为直接引用的过程。
* **方法返回地址:** 指方法调用后返回位置的地址。

## 本地方法栈

> 本地方法栈(Native Method Stack)与虚拟机栈功能非常相似,其区别在于:
> * 虚拟机栈为虚拟机执行Java方法(字节码)服务
> * 本地方法栈则为虚拟机执行本地(Native)方法服务
> **本地方法栈**是线程私有,与虚拟机栈功能相似。其中虚拟机栈为Java方法(字节码)服务,本地方法栈则为Native方法服务。
HotSpot虚拟机把虚拟机栈和本地方法栈合二为一
* HotSpot虚拟机把虚拟机栈和本地方法栈合二为一。
* 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出`StackOverflowError``OutOfMemoryError`异常。

## Java堆

> Java堆(Java Heap)是虚拟机管理的内存中最大的一块,线程共享,并在虚拟机启动时创建。
> **Java堆**是虚拟机管理的内存中最大的一块,线程共享,并在虚拟机启动时创建。
> 它的唯一目的是存放对象实例,几乎所有的对象实例以及数组都在堆上分配。
**堆内存模型:**

![堆内存模型](https://img.geekyspace.cn/pictures/2024/202407172004168.png)

* **新生代 (Young Generation):**
* 通常由Eden区和两个Survivor区(被称为from/to或s0/s1)组成,默认比例是`8:1:1`
* 大多数新创建的对象都在新生代分配内存,被填满时会触发一次`Minor GC`(小型垃圾回收)。
* 使用的垃圾收集算法通常是复制算法,将存活的对象复制到Survivor区,然后清理Eden区和使用过的Survivor区。
* **老年代 (Old Generation / Tenured Generation):**
* 通过多次新生代垃圾收集后任然存活的对象会被晋升到老年代。
* 老年代的对象相对来说生命周期较长,垃圾回收(`Major GC``Full GC`)相对较少,一旦发生,会比`Minor GC`耗时。
现代垃圾收集器采用分代收集理论进行设计,因此堆内存被划分为多个区域,包括:

* **新生代:**
* 存放生命周期较短的对象
* 通常由Eden区和两个Survivor区(被称为from/to或s0/s1)组成,默认比例是`8:1:1`
* 填满时触发`Minor GC`(小型垃圾回收)
* 采用复制算法,将存活的对象复制到Survivor区,然后清理Eden区和使用过的Survivor区。
* **老年代:**
* 存放生命周期较长,或多次垃圾收集后任然存活的对象
* 填满时触发`Major GC``Full GC`,耗时严重
* 使用的垃圾收集算法通常是标记-清除算法或标记-整理算法。
* **元空间 (Metaspace):**
* 从JDK8开始,永久代(PermGen)被元空间取代。
* 元空间使用的是本地内存,而不是JVM堆内存。
* 元空间的大小仅受本地内存限制,但可以通过参数进行调整。
* **永久代(PermGen):**
* 存放Class元数据,包括类结构、方法、字段信息等
* 属于“堆”的一部分,无法扩展时会抛出`OutOfMemoryError`异常
* 通过命令`-Xms`设置初始堆大小,`-Xmx`设定最大堆大小
* 从JDK8开始,被元空间(Metaspace)取代,称为“非堆”,使用的是本地内存

[DigitalOcean——Java (JVM) 内存模型 - Java 中的内存管理](https://www.digitalocean.com/community/tutorials/java-jvm-memory-model-memory-management-in-java)

## 方法区

> 方法区(Method Area)是JVM规范中定义的一个概念,用于被虚拟机加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据
> **方法区**是JVM规范中的一个逻辑区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
在Java8之前,方法区被称为“**永久代**(PermGen)”。

从Java8开始,方法区的实现被改为**元空间**(Metaspace),元空间使用的是本地内存,而不是像永久代那样在JVM的堆内存中分配。
* 在Java7的时候,方法区被称为“**永久代**(PermGen)”。
* 从Java8开始,方法区的实现被改为**元空间**(Metaspace),元空间使用的是本地内存,而不是像永久代那样在JVM的堆内存中分配。

## 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。
> **运行时常量池**是方法区的一部分,用于存放编译期生成的各种字面量与符号引用,支持在运行时动态添加新的常量。
* **字面量:** 表示固定的数据值,如整数、浮点数、字符串等常量。
* **符号引用:** 一组符号,用于描述所引用的目标,包括类和接口的全限定名、字段和方法的名称。

**知识点:**

1. 具备动态性,如`String.intern()`方法将字符串对象添加到运行时常量池中。
2. 会产生`OutOfMemoryError`异常

**思考以下问题,加强理解:**

1. Class常量池与运行时常量池的关系?

## 直接内存

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
**直接内存**并不是虚拟机运行时数据区的一部分,也未在《Java虚拟机规范》中明确定义。
然而,由于其频繁使用且可能导致`OutOfMemoryError`异常,值得在此进行讨论。

## 直接内存
**关键点:**
- **NIO的引入:** JDK 1.4引入了NIO(New Input/Output)类,通过通道(Channel)和缓冲区(Buffer)实现了一种新的I/O方式。它使用本地(Native)函数库直接分配堆外内存,并通过在Java堆中的`DirectByteBuffer`对象进行引用和操作。
- **性能优势:** 这种方法能够显著提高性能,因为它避免了在Java堆和本地堆之间的数据复制,从而加快了I/O操作。
- **内存限制:** 虽然直接内存的分配不受Java堆大小的限制,但仍受到本机总内存(包括物理内存、SWAP分区或分页文件)大小和处理器寻址空间的限制。
- **配置问题:** 在配置虚拟机参数(如`-Xmx`)时,管理员通常会根据实际物理内存来设置Java堆的大小,但可能忽略直接内存的占用。如果各个内存区域的总和超过了物理内存限制,可能在动态扩展时导致`OutOfMemoryError`异常。
-

0 comments on commit bea599a

Please sign in to comment.