读书《深入理解Java虚拟机》JVM内存模型

84 阅读4分钟
graph LR
    A["JVM内存"]
    B["运行时数据区"]
    C["线程私有"]
    D["线程共享"]
    E["虚拟机栈"]
    F["方法区"]
    G["堆"]
    A --> B
    A --> 直接内存
    B --> C
    B --> D
    C --> E
    C --> 本地方法栈
    C --> 程序计数器
    D --> G
    D --> F

JVM在执行Java程序时会为其划分一块内存区域,这块内存区域又可以划分为运行时数据区和直接内存。

运行时数据区中有线程私有(虚拟机栈、本地方法栈和程序计数器)和线程共享(堆和方法区)两大类,线程共享意味着线程不安全,在并发编程中需要注意。

JVM内存模型.png

程序计数器

  • 当前线程所执行字节码的行号指示器;
  • JVM的多线程是通过线程切换并分配时间片执行来实现的。在任何一个时刻,一个 cpu 只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器;
  • 不会产生 OOM 异常。

虚拟机栈

  • 描述 Java 方法执行的内存模型:每个方法在执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  • 通常说的 Java 栈是指虚拟机栈,或虚拟机栈中的局部变量表部分。

  • 发生在这个区域的两种异常状况:

    • StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度时抛出;
    • OutOfMemoryError:虚拟机栈在动态扩展过程中无法获取足够内存时抛出。

局部变量表

  • 局部变量表存放了编译期可知的各种基本数据类型、对象引用类型和 returnAddress 类型。
  • 局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

本地方法栈

  • 与虚拟机栈作用类似,但本地方法栈是为 Native 方法服务。
  • HotSpot VM 直接把本地方法栈和虚拟机栈合二为一,使用同一块区域调用栈帧的压入和弹出操作,可以减少数据结构复杂性,提高代码执行的一致性和性能,在内存管理和异常处理上也更加简洁。

  • 几乎所有对象实例和数组都要在堆上分配,堆是JVM管理的最大一块内存,也是垃圾回收的主要区域。(栈上分配和标量替换不会在堆上分配对象)
  • 根据分代收集算法,现代收集器将堆内存划分为老年代和年轻代,默认占比为 2:1。年轻代被分为 Eden 区和 Survivor 区,Survivor 区又被分为 S0 和 S2 两个区域。Eden 区和 S0 区和 S1 区默认占比 8:1:1。划分的目的是更好地回收内存,更快的分配内存,与存放的内容无关。
  • 如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OOM 异常。
graph LR
    A["老年代(占比2/3)"]
    B["年轻代(占比1/3)"]
    C["Eden(占比4/5)"]
    D["Survivor(占比1/5)"]
    E["S0(占比1/2)"]
    F["S1(占比1/2)"]
    堆 --> A
    堆 --> B
    B --> C
    B --> D
    D --> E
    D --> F

方法区

  • 非堆,用于存储被 JVM 加载的类信息、常量、静态常量、即时编译器编译后的代码等数据,是存储类元数据的重要区域;
  • 在 HotSpot JVM 中曾通过“永久代”管理方法区,以便使用与堆相同的垃圾收集机制,但这会带来内存溢出等问题。后来使用本地内存实现方法区;
  • 方法区的内存回收主要针对常量池和类的卸载,但回收效果不理想,且类型卸载的条件十分严格。如果方法区内存不足,会抛出 OOM;
  • 运行时常量池是方法区中的一个区域,用于存储已被加载的类文件中的字面量和符号引用,并且在运行时可以动态添加新的常量,若常量池无法扩展则可能导致 OOM。

直接内存

  • 直接内存不在 JVM 规范定义的内存区域中,但实践中重要且可能导致 OOM;
  • JDK 1.4 引入的 NIO 允许 Java 通过 DirectByteBuffer 直接访问堆外内存,提高性能;
  • 直接内存分配不受Java堆大小限制,但受限于系统总内存和寻址能力;
  • 在配置虚拟机参数时容易忽略直接内存,导致各个内存区域总和大于物理内存限制,进而导致 OOM。