目录
[程序计数器(PC 寄存器)](#程序计数器(PC 寄存器))
[Java 虚拟机栈](#Java 虚拟机栈)
[Java 虚拟机栈的特点](#Java 虚拟机栈的特点)
[本地方法栈(Native Method Stack)](#本地方法栈(Native Method Stack))
程序计数器(PC 寄存器)
定义
程序计数器(PC 寄存器)是 JVM 中的一块小型内存区域,专门用于存储当前线程正在执行的字节码指令的地址。若当前线程正在执行的是本地方法,此时程序计数器的值将为 undefined
,表示没有指向任何有效的字节码指令。
作用
- 指令流控制:程序计数器是字节码解释器的核心组件之一。它通过指向当前执行的字节码指令,确保程序能够顺序地读取并执行指令,从而实现控制流的管理。
- 线程切换支持:在多线程环境中,程序计数器为每个线程提供独立的执行位置记录。这使得当线程被切换时,能够准确地恢复到上次执行的状态,确保程序的连续性和一致性。
特点
- 内存空间:程序计数器占用的内存空间相对较小,通常只有几百字节。这种小巧的设计使得它能够快速访问,减少了对系统资源的消耗。
- 线程私有性:每个线程都有自己的程序计数器,线程之间的计数器是相互独立的。这种设计确保了多线程环境下的执行状态不会相互干扰,增强了线程的安全性。
- 生命周期:程序计数器的生命周期与线程密切相关。它在线程创建时被分配,并在线程结束时被销毁。这种动态的生命周期管理使得程序计数器能够有效地支持线程的并发执行。
- 内存异常 :程序计数器是 JVM 中唯一一个不会引发
OutOfMemoryError
的内存区域。这是因为它的内存需求是固定的,不会动态扩展,因此在内存管理上具有极高的稳定性。
Java 虚拟机栈
定义
Java 虚拟机栈是 JVM 中用于描述 Java 方法运行过程的内存模型。每当一个 Java 方法被调用时,JVM 会为其创建一个称为"栈帧"的区域,以存储该方法执行过程中的相关信息。
结构
- 每个线程的内存 :
- 每个线程在运行时都会有自己的虚拟机栈(JVM Stack),用于存储该线程执行方法时所需的内存。
- 栈帧(Frame) :
- 虚拟机栈由多个栈帧组成 。每个栈帧对应于一次方法调用,包含该方法执行所需的所有信息。
- 活动栈帧 :
- 在任何时刻,每个线程只能有一个活动栈帧 。这个活动栈帧表示当前正在执行的方法。其他方法的栈帧会在该方法被调用时被压入栈中,形成一个栈结构。
栈帧的结构
-
局部变量表(Local Variable Table) :存储方法的参数 和局部变量。
-
操作数栈(Operand Stack):用于存放计算过程中的中间结果和操作数,支持方法的计算和操作。
-
动态链接(Dynamic Linking):存储对常量池中方法和字段的引用,以支持方法调用和字段访问。
-
方法返回地址:指定方法执行完毕后,程序控制流返回的位置。
栈帧的生命周期
栈帧的生命周期与方法的调用和返回密切相关:
- 方法调用:当一个方法被调用时,JVM会为该方法创建一个新的栈帧,并将其压入当前线程的调用栈中。此时,局部变量表和操作数栈被初始化。
- 方法执行:方法在执行过程中,局部变量和操作数会在栈帧中进行操作。计算结果会存储在操作数栈中,直到需要返回值。
- 方法返回:当方法执行完毕后,JVM会将返回值(如果有)放入操作数栈中,并根据返回地址将控制权转回到调用该方法的位置。随后,当前栈帧会被弹出,释放其占用的内存。
JVM压栈出栈过程
在Java虚拟机(JVM)中,方法的调用和执行依赖于栈内存的压栈和出栈操作。每当一个方法被调用时,JVM会创建一个新的栈帧并将其压入当前线程的栈中。当方法执行完毕时,对应的栈帧会被弹出。
压栈过程
- 当一个方法被调用时,JVM会为该方法创建一个新的栈帧。
- 栈帧包含了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
- 新创建的栈帧会被压入当前线程的栈中,成为栈的栈顶元素。
- 方法的参数和局部变量会被复制到新栈帧的局部变量表中。
- 操作数栈和动态链接会被初始化。
- 方法返回地址会被保存,用于在方法执行完毕后返回到调用点。
出栈过程
- 当一个方法执行完毕(正常返回或抛出异常)时,对应的栈帧会被弹出栈。
- 如果方法有返回值,返回值会被压入调用者栈帧的操作数栈中。
- 程序会根据保存的返回地址跳转回调用点,继续执行。
- 被弹出的栈帧会被销毁,释放其占用的内存空间。
Java 虚拟机栈的特点
- 运行速度:Java 虚拟机栈的运行速度非常快,仅次于程序计数器。
- 局部变量表:局部变量表在栈帧创建时分配,大小在编译时确定,运行过程中不会改变。
- 异常处理 :
- StackOverflowError:当栈深度超过最大限制时抛出。
- OutOfMemoryError:当栈允许动态扩展但内存用尽时抛出。
Java 虚拟机栈是线程私有的,随着线程的创建而创建,随着线程的结束而销毁。即使出现 StackOverflowError
,内存空间仍可能有剩余。
常见运行时异常
NullPointerException
- 空指针引用异常ClassCastException
- 类型强制转换异常IllegalArgumentException
- 传递非法参数异常ArithmeticException
- 算术运算异常ArrayStoreException
- 向数组中存放不兼容对象异常IndexOutOfBoundsException
- 下标越界异常NegativeArraySizeException
- 创建负大小数组错误异常NumberFormatException
- 数字格式异常SecurityException
- 安全异常UnsupportedOperationException
- 不支持的操作异常
本地方法栈(Native Method Stack)
定义
用于支持 Java 程序调用本地方法(Native Method)。本地方法是用其他语言(如 C 或 C++)编写的方法。
作用
本地方法栈的主要作用是为每个线程提供一个独立的栈空间,用于存储本地方法的调用信息和执行状态。它与 Java 虚拟机栈类似,但专门用于处理本地方法的调用。
结构
本地方法栈的结构与 Java 虚拟机栈相似,主要包括以下几个部分:
- 栈帧:每个本地方法调用都会创建一个栈帧,存储该方法的参数、局部变量、返回地址等信息。
- 局部变量表:存储本地方法的参数和局部变量。
- 操作数栈:用于存放计算过程中间结果。
- 返回地址:指向调用本地方法的地方,以便在方法执行完毕后返回。
生命周期
- 创建:当线程调用本地方法时,JVM 会为该方法创建一个新的栈帧并将其压入本地方法栈中。
- 执行:本地方法的执行过程与 Java 方法类似,执行完毕后,栈帧会被弹出,控制权返回到调用该方法的位置。
特点
- 线程私有:每个线程都有自己的本地方法栈,栈之间是相互独立的。
- 与 Java 栈的区别:本地方法栈专门用于处理本地方法的调用,而 Java 虚拟机栈则用于处理 Java 方法的调用。
- 异常处理 :如果本地方法栈的大小不够,可能会抛出
StackOverflowError
,如果内存不足,则会抛出OutOfMemoryError
。
堆
定义
堆是 Java 虚拟机(JVM)中用于存放对象的内存区域。几乎所有的对象实例 和数组 都是在堆中分配的。堆是 Java 内存管理的核心部分,负责动态分配内存。
特点
- 线程共享:整个 JVM 只有一个堆,所有线程都可以访问同一个堆。这与程序计数器、Java 虚拟机栈和本地方法栈不同,后者是线程私有的。
- 创建时机:堆在 JVM 启动时被创建,并在整个应用程序的生命周期内存在。
- 垃圾回收的主要场所:堆是垃圾回收器的主要工作区域,负责管理对象的生命周期和内存的回收。
- 逻辑连续性:虽然堆可以物理上不连续,但在逻辑上应被视为连续的内存空间。这使得 JVM 可以高效地管理内存。
- 内存溢出 :当堆内存使用量超过了堆内存的最大容量 时,会抛出
OutOfMemoryError
异常。堆的大小可以根据需要进行调整,JVM 支持动态扩展。 - 同步问题:由于堆是共享的,访问堆中的对象时需要注意线程安全性,确保方法和属性的一致性。
新生代、老年代
新生代(Young Generation)
定义 :
新生代是堆的一部分,专门用于存放新创建的对象。由于大多数对象的生命周期较短,因此新生代的设计目的是为了优化内存分配和垃圾回收。
结构 :
新生代通常分为三个区域:
- Eden 区 :几乎所有新创建的对象都在 Eden 区分配内存。
- From Survivor 区 :存放经过一次垃圾回收后仍然存活的对象。
- To Survivor 区:在进行垃圾回收时,From Survivor 区的对象会被复制到 To Survivor 区。
新生代与老年代的默认空间比例为 1:2
垃圾回收 :
新生代的垃圾回收称为 Minor GC。由于新生代中的对象大多数是短生命周期的,Minor GC 的频率较高,回收速度也相对较快。通过复制算法,新生代能够有效地清理不再使用的对象。
老年代(Old Generation)
定义 :
老年代是堆的另一部分,用于存放生命周期较长的对象。经过多次垃圾回收仍然存活的对象会被晋升到老年代。
特点:
- 老年代的空间通常较大,垃圾回收的频率相对较低,主要是因为老年代中的对象大多数是长期存活的。
- 老年代的垃圾回收称为 Full GC,通常比 Minor GC 更耗时,因为它需要检查和回收更多的对象。
垃圾回收策略 :
老年代的垃圾回收通常使用标记-清除或标记-整理算法。由于老年代中的对象存活时间较长,因此其回收策略更加谨慎,以避免频繁的回收操作。
对象分配过程
- 对象首先在 Eden 区分配 :当使用
new
关键字创建对象时,对象会被分配到 Eden 区。Eden 区有大小限制。 - Minor GC 触发:当 Eden 区空间不足时,会触发 Minor GC。Minor GC 会将 Eden 区和 From Survivor 区中存活的对象复制到 To Survivor 区。
- 对象晋升 Survivor 区:经过一次 Minor GC 后,仍然存活的对象会被移动到 Survivor 区。如果 Survivor 区空间不足,对象会直接晋升到老年代。
- 对象晋升老年代 :如果对象在 Survivor 区经历了多次 Minor GC 仍然存活,它会被移动到老年代。默认情况下,对象在 Survivor 区经历 15 次 Minor GC 后会被晋升到老年代。
总结
- 新生代:用于存放新创建的对象,主要通过 Minor GC 进行垃圾回收,回收频率高。
- 老年代:用于存放生命周期较长的对象,进行 Full GC 进行垃圾回收,回收频率低。
方法区
用于存储类的元数据、常量池、静态变量和即时编译器编译后的代码等信息。方法区是所有线程共享的内存区域。
存储内容
- 类的元数据 :
- 包含类的结构信息,如类名、父类、接口、字段、方法、访问修饰符等。
- 每个类在加载时会在方法区中创建一个 Class 对象,用于描述该类的相关信息。
- 常量池 :
- 存储编译时生成的各种字面量和符号引用,包括字符串常量、基本数据类型的常量等。
- 常量池可以提高内存使用效率,避免重复创建相同的常量。
- 静态变量 :
- 类的静态变量(即用
static
修饰的变量)存储在方法区中。 - 静态变量在类加载时被分配内存,并在整个类的生命周期内保持存在。
- 类的静态变量(即用
- 即时编译器编译后的代码 :
- 在 JIT(Just-In-Time)编译过程中,编译后的代码会存储在方法区中,以便快速执行。
特点
- 线程共享:方法区是所有线程共享的内存区域,多个线程可以同时访问其中的数据。
- 永久代:方法区中的信息一般需要长期存在,因此在堆的划分方法中,把方法区称为"永久代"。
- 内存回收效率低:方法区中的信息一般需要长期存在,回收一遍之后可能只有少量信息无效。主要回收目标是对常量池的回收和对类型的卸载。
- 内存管理灵活:Java 虚拟机规范对方法区的要求比较宽松。允许固定大小、动态扩展,也允许不实现垃圾回收。
元空间-----是方法区的实现
在 Java 8 之前,方法区的实现是永久代(PermGen),而从 Java 8 开始,元空间替换了永久代。并且元空间使用的是本地内存,而不是 JVM 堆内存。这意味着方法区的内容不再存储在虚拟机的堆内存中,而是使用操作系统的本地内存。
内存位置:
Java 8 之前 :方法区是 JVM 堆内存的一部分,存储类的元数据、常量池、静态变量等信息。在这种情况下,方法区的大小是固定的,容易导致 OutOfMemoryError
,尤其是在加载大量类时。
Java 8 及之后 :方法区被元空间替代,元空间使用本地内存(Native Memory),不再受 JVM 堆大小的限制。元空间的大小可以动态扩展,允许加载更多的类。
动态扩展:
元空间的大小可以通过 JVM 参数 -XX:MetaspaceSize
和 -XX:MaxMetaspaceSize
进行配置。与方法区不同,元空间的大小可以根据需要进行动态调整。
垃圾回收:
在 Java 8 之前,方法区中的数据会随着 JVM 的垃圾回收而回收,主要是对常量池的回收和对类型的卸载。
在 Java 8 及之后,元空间中的数据会在类卸载时进行垃圾回收,类的卸载通常发生在类加载器被回收时。