JVM内存区域

在Java中,内存区域分为静态内存区域和运行时内存区域。

  • 静态内存区域:是在程序编译时就已经分配好的内存区域,用于存储程序中的静态变量常量。静态区在程序运行期间一直存在,直到程序结束才会被释放。

    包括以下两个部分:

    • 静态变量存储区域(Static Variable Storage Area): 这是用于存储静态变量的区域。静态变量在类加载时被分配,它们的值在整个程序运行期间保持不变,可以通过类名直接访问。
    • 常量存储区域(Constant Storage Area): 常量存储区域用于存储编译时的常量数据,如字符串常量、数值常量等。这些常量值在编译阶段就确定,不会发生变化。
  • 运行时内存区域:运行时数据区域是在程序运行时动态分配和管理的,用于存储对象实例方法调用栈等与程序运行相关的数据

    主要包括以下区域:

    • 堆(Heap):用于存储对象实例和数组。堆中的数据是在运行时动态分配和释放的,它是程序运行期间最主要的内存区域。
    • 虚拟机栈(Stack):简称栈,用于存储方法调用的局部变量、方法参数、返回值和操作数栈。栈中的数据在方法的调用和返回过程中被分配和释放。
    • 方法区(Method Area):用于存储类的元数据、静态变量、常量池等信息。在旧版的 JVM 中,方法区也被称为永久代,但在 Java 8+ 中被元空间取代。
    • 程序计数器(Program Counter):用于记录当前线程所执行的字节码指令的地址。每个线程都有自己独立的程序计数器。
    • 本地方法栈(Native Method Stack):类似于栈,但用于执行本地方法(Native Method)。
    • 运行时常量池(Runtime Constant Pool):用于存储类文件中的常量信息,在运行时可以解析成实际的内存地址或引用。
    • 直接内存(Direct Memory):并不是 JVM 内部的一部分,但通过 NIO 库可以在堆之外直接分配内存,用于提高 I/O 操作性能。

下面对Java运行时内存区域进行详解 。

运行时内存区域

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。Java 堆是程序运行时最主要的内存区域,用于存储动态创建的对象和数组。合理管理和配置堆内存对于程序的性能和稳定性非常重要。

堆区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

堆主要有以下三个特性 :

  1. 动态分配: 堆是动态分配的内存区域,程序在运行时创建的对象都会被分配到堆中。通过 new 关键字创建的对象实例,以及数组,都存储在堆上。
  2. 垃圾回收: Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。因为当堆内存中的对象实例在不再被引用时,会成为垃圾对象,Java 的垃圾回收机制会定期清理这些不再被引用的对象,释放占用的内存。
  3. 分代策略: 从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆内存通常根据对象的生命周期被分为不同的代:
    1. 年轻代(Young Generation):年轻代用于存放新创建的对象 。
    2. 老年代(Old Generation):老年代用于存放经过多次垃圾回收仍然存活的对象。
    3. 永久代(1.7之中)。JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。

堆易发生的错误(OOM 异常):

堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:

  1. **java.lang.OutOfMemoryError: GC Overhead Limit Exceeded**:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  2. java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置。

方法区 / 元空间

在 Java 运行时数据区域中,方法区(Method Area)是一块用于存储类的元数据、静态变量、常量池等信息的内存区域。方法区在 JVM 启动时被创建,它是各个线程共享的区域,用于存储所有类的结构信息和方法代码。

永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

为什么JDK1.8用元空间 (MetaSpace)替代永久代 (PermGen) ?

  1. 内存溢出问题。整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
  2. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
  3. 性能优化: 元空间采用了本地内存(native memory)来存储类的元数据等信息,这种方式更加灵活,避免了一些永久代的性能问题,如永久代的垃圾回收开销。

虚拟机栈

虚拟机栈(Java Virtual Machine Stack)是每个线程私有的内存区域,用于存储方法调用的局部变量、方法参数、返回值和操作数栈等信息。它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。

栈的组成

栈由一个个栈帧组成,而每个栈帧中都拥有:

  1. 局部变量表 : 用于存储方法调用过程中使用的局部变量、方法参数和返回值。局部变量表的容量是提前确定的,根据方法中定义的局部变量和方法参数的数量来确定。局部变量可以是基本数据类型(如整数、浮点数)或者对象引用。
  2. 操作数栈:用于执行方法中的操作。操作数栈用于保存方法的中间和最后的计算结果。
  3. 动态链接:主要服务一个方法需要调用其他方法的场景。动态链接的作用在类加载时,方法的符号引用会被解析为直接引用,以确定方法的实际入口地址。
  4. 方法返回地址:返回地址是指向方法被调用后要返回的地址。在方法调用前,调用指令会将返回地址压入栈帧,以便在方法返回时恢复调用位置。不同的方法调用可能会有不同的返回地址。

栈的特性

  1. 线程私有: 每个线程在运行时都会有一个对应的虚拟机栈,这意味着虚拟机栈的数据是线程私有的,不同线程之间的栈数据不共享。
  2. 方法调用: 方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。除了一些 Native 方法调用是通过本地方法栈实现的(后面会提到),其他所有的 Java 方法调用都是通过栈来实现的。
  3. 方法嵌套: 如果一个方法内部又调用了其他方法,会在虚拟机栈上逐层创建新的栈帧。方法调用的层级关系由栈帧的压栈和弹栈操作来管理。

栈易发生的错误(StackOverflowError 、OOM异常):

  • StackOverFlowError 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

本地方法栈

Java 虚拟机本地方法栈(Java Virtual Machine Native Stack)类似于虚拟机栈,但用于执行本地方法(Native Method)。本地方法是使用其他语言(如 C、C++)编写的方法,通过 Java 本地接口(Java Native Interface,JNI)与 Java 代码交互。在 HotSpot 虚拟机中本地方法栈和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowErrorOutOfMemoryError 两种错误。

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

为了线程切换后能恢复到正确的执行位置,因此每条线程都需要有一个独立的程序计数器,即程序计数器为线程私有的。

程序计数器的特性

  1. 代码的流程控制。字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 线程切换恢复。在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
  3. 计数器溢出: 程序计数器的值通常是一个较小的数值。当计数器的值达到最大值后,会循环回到0。因此程序计数器不会引起内存溢出,因为程序计数器是线程私有的,不涉及堆内存。

直接内存(非运行时数据区的一部分)

直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。直接内存的分配和释放不受垃圾回收的控制,因此可以用于处理大量的 I/O 操作和内存缓冲区,提高程序性能。

以下是对 Java 直接内存的更详细理解:

  1. 使用场景: 直接内存主要用于处理 I/O 操作,如文件读写、网络通信等。它通常被用作内存映射文件、网络数据传输的缓冲区,以及一些需要高性能的数据操作场景。
  2. 分配方式: 直接内存的分配不是通过 Java 虚拟机堆内存管理机制实现的,而是通过操作系统的本地调用(如 malloc 函数)来直接分配内存。这使得直接内存的分配和释放比 Java 堆内存更加高效。
  3. 堆外内存: 直接内存位于堆外,即不受 Java 堆的大小限制。因此,它可以用于分配大量的内存,而不会影响 Java 堆的使用。
  4. 垃圾回收: 直接内存的分配和释放不受垃圾回收的影响,因此可以避免垃圾回收带来的性能开销。然而,这也意味着开发人员需要手动管理直接内存的分配和释放,以防止内存泄漏。
  5. 内存映射文件: 直接内存可以用于内存映射文件,通过将文件的一部分映射到内存中,实现文件的高效读写。
  6. NIO 和 I/O 操作: Java 的 NIO 库(New I/O)使用直接内存来提供高效的通道(Channel)和缓冲区(Buffer)操作,适用于高性能的 I/O 操作。
  7. 性能优化: 直接内存的分配和释放效率高,适用于一些对性能要求较高的场景,如网络编程、图像处理等。