02-JVM内存模型

type
status
date
slug
summary
tags
category
icon
password

1️⃣ 堆(Heap)

定义:堆是 JVM 运行时数据区的一部分,用于存储 对象实例数组。几乎所有通过new关键字创建的对象都在堆中分配内存。
特点
  • 堆是 JVM 中最大的内存区域,线程共享(多个线程访问同一堆)。
  • 垃圾回收(Garbage Collection, GC) 的主要管理区域。
  • 堆的大小可通过 JVM 参数配置(如-Xms设置初始堆大小,-Xmx设置最大堆大小)。
作用:存储动态分配的内存,例如类的实例对象、数组等。

1.新生代(Young Generation)

作用:存放新创建的对象,通常生命周期较短。
子区域
  • Eden 区
    • 新对象首先分配在 Eden 区。
    • 大多数对象在创建后很快变得不可达(例如,临时对象)。
  • Survivor 区(S0 和 S1)
    • 分为两个对等的区域:From Survivor(S0)To Survivor(S1)
    • 用于存放经过垃圾回收后仍存活的对象。
    • 每次垃圾回收时,Eden 和 From Survivor 中存活的对象被复制到 To Survivor(Minor GC,也叫 Young GC)。
    • S0S1 角色会互换(From 变成 To,To 变成 From)。
特点
  • 新生代占堆内存的较小部分(通常约为 1/3)。
  • 使用 复制算法(Copying Algorithm) 进行垃圾回收,效率较高,因为新生代对象存活率低。

2.老年代(Old Generation)

作用:存放生命周期较长的对象,例如缓存对象、单例对象等。
来源
  • 从新生代 Survivor 区经过多次 Minor GC 后仍存活的对象(通常达到一定年龄阈值,默认为 15 次)会被晋升(Promotion)到老年代。
  • 某些大对象(例如大数组)可能直接分配在老年代(通过-XX:PretenureSizeThreshold配置)。
特点
  • 老年代占堆内存的大部分(通常约为 2/3)。
  • 垃圾回收频率较低,但每次回收耗时较长(Major GCFull GC)。
  • 通常使用 标记-清除(Mark-Sweep)标记-整理(Mark-Compact) 算法。

3.永久代(PermGen,JDK 8 之前) / 元空间(Metaspace,JDK 8 及之后)

永久代(PermGen)
  • 作用:存储类元信息(Class Metadata,例如类结构、方法信息)、常量池(运行时常量池)、静态变量等。
  • 位置:在 JDK 8 之前,永久代是堆的一部分,受-XX:MaxPermSize参数限制。
  • 问题:容易因类加载过多(如动态代理、大量反射)导致 OutOfMemoryError: PermGen space
元空间(Metaspace)
  • 变化:从 JDK 8 开始,永久代被移除,类元信息存储在 本地内存(Native Memory) 中的元空间。
  • 特点
    • 元空间不再受堆大小限制,默认使用本地内存,最大值受操作系统限制。
    • 可通过-XX:MaxMetaspaceSize设置元空间最大值。
    • 减少了 PermGen 溢出的问题,但仍需关注类加载和卸载。
  • 作用:与永久代类似,存储类元信息、常量池等。

4.垃圾回收(GC)与堆的关系

堆是垃圾回收的核心区域,不同区域的垃圾回收策略不同:
Minor GC(Young GC)
  • 发生在新生代,回收 Eden 和 From Survivor 中的不可达对象。
  • 存活对象复制到 To Survivor 或晋升到老年代。
  • 速度快,暂停时间短(Stop-The-World 暂停)。
Major GC / Full GC
  • 涉及整个堆(新生代 + 老年代 + 元空间)。
  • 回收老年代和元空间中的不可达对象。
  • 耗时较长,可能导致较长的应用暂停。
触发条件
  • Eden 区满时触发 Minor GC。
  • 老年代空间不足、元空间不足或显式调用 System.gc() 可能触发 Full GC。

5.堆的配置和优化

堆的性能直接影响 Java 应用的性能,以下是常见配置参数:
Xms:初始堆大小(例如 -Xms512m)。
Xmx:最大堆大小(例如 -Xmx2048m)。
Xmn:新生代大小(例如 -Xmn256m)。
XX:SurvivorRatio:Eden 区与 Survivor 区的比例(默认 8,即 Eden:S0:S1 = 8:1:1)。
XX:MaxMetaspaceSize:元空间最大大小。
XX:+UseG1GC:启用 G1 垃圾回收器(适合大堆、低延迟场景)。
 
优化建议
根据应用特点调整新生代和老年代比例。
监控 GC 频率和暂停时间,减少 Full GC。
使用工具(如 VisualVM、JProfiler)分析堆使用情况,定位内存泄漏。

2️⃣ 栈(Stack)

定义:栈是 JVM 运行时数据区的一部分,用于存储 方法调用局部变量。每个线程在 JVM 中都有自己的 线程栈(Thread Stack),也叫 虚拟机栈(Java Virtual Machine Stack)
特点
  • 线程私有:每个线程有独立的栈,互不干扰。
  • 后进先出(LIFO):栈按方法调用顺序压栈和出栈。
  • 大小可通过 JVM 参数配置(如 -Xss,设置每个线程栈的大小,例如 -Xss1m)。
作用
存储方法执行时的 局部变量(包括基本类型变量和对象引用)。
管理 方法调用栈帧(Stack Frame),包括方法参数、返回值、操作数栈等。
支持方法递归和动态调用。

1.栈的结构

栈由多个 栈帧(Stack Frame) 组成,每个栈帧对应一次方法调用。栈帧包含以下部分:
局部变量表(Local Variable Table)
  • 存储方法中的局部变量,包括:
    • 基本数据类型(int、long、double 等)的值。
    • 对象引用(指向堆中的对象地址)。
    • 方法参数和 this(非静态方法中)。
  • 大小在编译期确定,单位为槽(Slot),32 位类型占 1 槽,64 位类型(如 long、double)占 2 槽。
操作数栈(Operand Stack)
  • 用于方法执行时的临时计算,例如算术运算、方法调用时的参数传递。
  • 基于栈的结构,操作数压入和弹出。
动态链接(Dynamic Linking)
  • 用于解析符号引用(例如方法调用、字段访问)到实际内存地址。
  • 涉及运行时常量池(在堆的元空间中,JDK 8+)。
方法返回地址(Return Address)
  • 记录方法执行完成后返回的指令地址。
  • 支持正常返回(return)或异常退出。

2.栈的工作机制

压栈(Push)
  • 每次方法调用,JVM 创建一个新的栈帧并压入当前线程的栈顶。
  • 栈帧初始化局部变量表、操作数栈等。
出栈(Pop)
  • 方法执行结束(正常返回或抛出异常),栈帧被弹出,恢复调用者的栈帧。
  • 局部变量和操作数栈的内容被销毁。
栈溢出(StackOverflowError)
  • 如果方法调用层次过深(如无限递归),栈空间不足,抛出 StackOverflowError
  • 可通过增大 -Xss 或优化代码(如避免深递归)解决。
线程隔离
  • 每个线程的栈独立,线程 A 的栈帧不会影响线程 B。

3.栈与堆的对比和关联

存储内容
  • :存储局部变量(基本类型值、对象引用)、方法调用信息。
  • :存储对象实例、数组、类元信息(元空间,JDK 8+)。
内存管理
  • :自动管理,方法结束时栈帧弹出,内存立即释放,无需垃圾回收。
  • :动态分配,由垃圾回收器管理,回收不可达对象。
生命周期
  • :局部变量随方法调用结束而销毁,生命周期短。
  • :对象可能在多个方法间共享,生命周期可能较长(新生代 → 老年代)。
关联
  • 栈中的对象引用(如 String s)指向堆中的对象实例。

4.栈的配置和优化

栈大小配置
  • Xss:设置每个线程的栈大小(默认 1MB,视 JVM 和操作系统而定)。
  • 例如:-Xss512k 减小栈大小,适合线程数多但方法调用浅的场景。
优化建议
  • 避免深递归:将递归改为迭代(如用循环处理文件路径)。
  • 减少局部变量:合并不必要的临时变量,减少栈帧占用。
  • 监控栈溢出:使用工具(如 JVisualVM)分析线程栈深度,定位 StackOverflowError
与堆的协同优化
  • 栈中的对象引用可能导致堆中对象无法被 GC 回收(例如,长时间持有引用)。

3️⃣ 方法区(Method Area)

定义:方法区是 JVM 运行时数据区的一部分,用于存储 类元信息运行时常量池。它是 JVM 规范定义的逻辑区域,实际实现因 JVM 版本和供应商而异。
特点
  • 线程共享:方法区是所有线程共享的内存区域,与堆类似。
  • 逻辑区域:方法区是 JVM 规范的一部分,具体实现可能不同(例如,JDK 7 及之前的永久代,JDK 8 及之后的元空间)。
  • 垃圾回收:方法区可能进行垃圾回收(例如,卸载不再使用的类),但频率较低。
作用
  • 存储类元信息,包括类结构、方法代码、字段信息等。
  • 存储运行时常量池,包含字符串字面量、符号引用等。
  • 存储静态变量(JDK 7 之前,JDK 7+ 静态变量移至堆)。

1.方法区的内容

类元信息
  • 类的全限定名、父类、接口、修饰符。
  • 方法的字节码(Code 属性,包含方法的指令序列)。
  • 字段信息(名称、类型、修饰符)。
  • 类的常量池(Class Constant Pool,编译期生成的符号引用)。
运行时常量池(Runtime Constant Pool)
  • 每个类或接口的常量池在运行时的表示形式。
  • 包含:
    • 字面量(Literal,例如字符串字面量 "hello"、数字常量)。
    • 符号引用(Symbolic Reference,例如类、方法、字段的引用)。
  • 在类加载的解析阶段,符号引用会被解析为直接引用(指向实际内存地址)。
静态变量(Static Variables,JDK 7 之前)
  • 类的静态变量(static 修饰)存储在方法区。
  • 从 JDK 7 开始,静态变量移至堆中(通常在类的实例对象中)。

2.方法区与堆、栈的关联

与堆的关联
  • 方法区的运行时常量池引用堆中的对象(例如,字符串字面量指向堆中的 String 实例)。
  • JDK 7+,静态变量和字符串常量池存储在堆中(通常在老年代)。
  • 例如,代码:
    • String fileName = item.getFilename().toLowerCase();
    • String 类元信息(如 toLowerCase 方法的字节码)存储在方法区(元空间)。
    • fileName引用的String对象存储在堆中(Eden 区)。
与栈的关联
  • 栈中的栈帧(方法调用)通过动态链接访问方法区的类元信息和运行时常量池。
  • 例如,调用 StringUtils.isNotBlank 时,栈帧中的动态链接解析到方法区中 StringUtils类的字节码。
交互流程
  • 类加载时,JVM 将类元信息加载到方法区。
  • 方法调用时,栈帧从方法区获取方法字节码并执行。
  • 对象创建时,栈中的引用指向堆中的实例,实例的类信息引用方法区的元信息。

4️⃣ 程序计数器(Program Counter)

定义:程序计数器是 JVM 运行时数据区的一部分,每个线程私有,用于存储当前线程正在执行的 字节码指令地址
特点
  • 线程私有:每个线程有独立的程序计数器,互不干扰。
  • 小内存:占用极小的内存空间,仅存储一个地址值。
  • 无垃圾回收:程序计数器不存储对象引用,因此不需要垃圾回收。
  • 无溢出异常:不像栈(可能抛出 StackOverflowError)或堆(可能抛出 OutOfMemoryError),程序计数器不会溢出。
作用
  • 记录当前线程执行的字节码指令地址,指导 CPU 执行下一条指令。
  • 支持线程切换(上下文切换),保存和恢复线程的执行位置。
  • 对于非 native 方法,存储 JVM 字节码地址;对于 native 方法,值未定义(通常为空)。

1.程序计数器的工作机制

指令地址存储
  • JVM 执行引擎从程序计数器读取当前指令的地址,获取方法区中的字节码指令(如 invokevirtualiload)。
  • 执行指令后,程序计数器更新为下一条指令的地址(顺序执行或跳转,如 if 分支、goto)。
线程切换
  • 多线程环境下,JVM 通过时间片轮转调度线程。
  • 线程挂起时,程序计数器保存当前指令地址;恢复时,从该地址继续执行。
  • 代码中可能涉及多线程(如 Redis 操作),每个线程的程序计数器独立跟踪其执行位置。
特殊情况
  • 非 native 方法:程序计数器指向方法区中方法的字节码地址。
  • Native 方法:程序计数器不存储有效地址,因为 native 方法由本地代码(C/C++)执行,JVM 不直接控制。

2.程序计数器与堆、栈、方法区的关联

与栈(Stack)
  • 栈存储方法调用的栈帧(包括局部变量表、操作数栈)。
  • 程序计数器记录当前栈帧执行的字节码指令地址。
  • 例如:
    • String fileName = item.getFilename().toLowerCase();
    • 栈帧存储 fileName(局部变量)和方法调用信息。
    • 程序计数器指向 toLowerCase 方法的字节码指令地址。
与堆(Heap)
  • 堆存储对象实例(如 String 对象)。
  • 程序计数器间接影响堆,通过执行指令(如 new 创建对象)触发堆内存分配。
  • 例如,new String() 的指令地址由程序计数器记录,对象分配在堆的 Eden 区。
与方法区(Method Area)
  • 方法区存储类元信息和字节码。
  • 程序计数器指向方法区中当前方法的字节码地址。
  • 例如,RedisUtils.getCacheObject 的字节码存储在方法区(元空间),程序计数器记录执行到的指令位置。

5️⃣ 本地方法栈(Native Method Stack)

定义:本地方法栈是 JVM 运行时数据区的一部分,线程私有,用于支持 本地方法(Native Method) 的执行。本地方法是用非 Java 语言(通常是 C/C++)实现的,通过 Java Native Interface(JNI)调用。
特点
  • 线程私有:每个线程有独立的本地方法栈,互不干扰。
  • 动态分配:栈大小由 JVM 实现决定,部分 JVM(如 HotSpot)将本地方法栈与虚拟机栈(Java 方法栈)合并。
  • 可能溢出:与虚拟机栈类似,可能抛出 StackOverflowError(栈帧过多)或 OutOfMemoryError(栈空间不足)。
作用
  • 管理本地方法调用的栈帧,存储本地方法的局部变量、参数、返回值等。
  • 支持 JNI 调用,例如访问操作系统资源(如文件、网络)或硬件功能。
  • 为 Java 调用非 Java 代码提供运行环境。

1.本地方法栈的工作机制

栈帧管理
  • 每次调用本地方法,JVM 在本地方法栈中创建栈帧,存储:
    • 局部变量(类似 Java 方法栈的局部变量表)。
    • 方法参数和返回值。
    • 操作数栈(视本地方法实现而定)。
  • 本地方法执行由本地代码(C/C++)控制,栈帧内容由本地代码管理。
与 Java 方法栈的区别
  • Java 方法栈:处理 Java 字节码方法,栈帧结构由 JVM 严格定义(包括局部变量表、操作数栈、动态链接等)。
  • 本地方法栈:处理本地方法,栈帧结构由本地代码实现决定,JVM 不直接控制。
栈溢出
  • 如果本地方法调用层次过深(如递归调用 C 函数),可能抛出 StackOverflowError
  • 如果栈空间不足(例如,创建过多线程),可能抛出 OutOfMemoryError
HotSpot JVM 实现
  • 在 Oracle 的 HotSpot JVM 中,本地方法栈和 Java 方法栈(虚拟机栈)通常合并为一个栈,统一由 -Xss 参数控制(例如 -Xss1m 设置线程栈大小)。
  • 合并后,Java 方法和本地方法的栈帧在同一线程栈中管理。

2.本地方法栈与堆、栈、方法区、程序计数器的关联

与堆(Heap)
  • 本地方法可能通过 JNI 访问堆中的 Java 对象(例如,String 或数组)。
  • 例如,调用本地方法修改堆中的 String 对象,本地方法栈存储 JNI 传递的对象引用,实际对象在堆中。
与栈(Java 方法栈)
  • Java 方法调用本地方法时,Java 方法栈帧记录调用点,本地方法栈创建新栈帧执行本地代码。
  • 返回 Java 方法后,本地方法栈帧弹出,Java 方法栈恢复执行。
  • 例如,代码可能调用 System.getProperty(底层通过 JNI 实现),涉及 Java 方法栈到本地方法栈的切换。
与方法区(Method Area)
  • 方法区存储本地方法的符号引用(通过 JNI 定义的 native 方法签名)。
  • 本地方法栈执行时,可能通过 JNI 访问方法区的类元信息。
与程序计数器(Program Counter)
  • 对于本地方法,程序计数器通常不存储有效地址(值未定义),因为本地方法的指令由 C/C++ 控制,而非 JVM 字节码。
  • Java 方法调用本地方法时,程序计数器记录调用指令地址,待本地方法返回后继续执行。
 
上一篇
01-JVM类加载机制
下一篇
03-JVM对象创建与内存分配
Loading...