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)。
- S0 和 S1 角色会互换(From 变成 To,To 变成 From)。
特点:
- 新生代占堆内存的较小部分(通常约为 1/3)。
- 使用 复制算法(Copying Algorithm) 进行垃圾回收,效率较高,因为新生代对象存活率低。
2.老年代(Old Generation)
作用:存放生命周期较长的对象,例如缓存对象、单例对象等。
来源:
- 从新生代 Survivor 区经过多次 Minor GC 后仍存活的对象(通常达到一定年龄阈值,默认为 15 次)会被晋升(Promotion)到老年代。
- 某些大对象(例如大数组)可能直接分配在老年代(通过
-XX:PretenureSizeThreshold
配置)。
特点:
- 老年代占堆内存的大部分(通常约为 2/3)。
- 垃圾回收频率较低,但每次回收耗时较长(Major GC 或 Full 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 类元信息(如
toLowerCase
方法的字节码)存储在方法区(元空间)。 fileName
引用的String对象存储在堆中(Eden 区)。
String fileName = item.getFilename().toLowerCase();
与栈的关联:
- 栈中的栈帧(方法调用)通过动态链接访问方法区的类元信息和运行时常量池。
- 例如,调用
StringUtils.isNotBlank
时,栈帧中的动态链接解析到方法区中StringUtils
类的字节码。
交互流程:
- 类加载时,JVM 将类元信息加载到方法区。
- 方法调用时,栈帧从方法区获取方法字节码并执行。
- 对象创建时,栈中的引用指向堆中的实例,实例的类信息引用方法区的元信息。
4️⃣ 程序计数器(Program Counter)
定义:程序计数器是 JVM 运行时数据区的一部分,每个线程私有,用于存储当前线程正在执行的 字节码指令地址。
特点:
- 线程私有:每个线程有独立的程序计数器,互不干扰。
- 小内存:占用极小的内存空间,仅存储一个地址值。
- 无垃圾回收:程序计数器不存储对象引用,因此不需要垃圾回收。
- 无溢出异常:不像栈(可能抛出
StackOverflowError
)或堆(可能抛出OutOfMemoryError
),程序计数器不会溢出。
作用:
- 记录当前线程执行的字节码指令地址,指导 CPU 执行下一条指令。
- 支持线程切换(上下文切换),保存和恢复线程的执行位置。
- 对于非 native 方法,存储 JVM 字节码地址;对于 native 方法,值未定义(通常为空)。
1.程序计数器的工作机制
指令地址存储:
- JVM 执行引擎从程序计数器读取当前指令的地址,获取方法区中的字节码指令(如
invokevirtual
、iload
)。
- 执行指令后,程序计数器更新为下一条指令的地址(顺序执行或跳转,如 if 分支、goto)。
线程切换:
- 多线程环境下,JVM 通过时间片轮转调度线程。
- 线程挂起时,程序计数器保存当前指令地址;恢复时,从该地址继续执行。
- 代码中可能涉及多线程(如 Redis 操作),每个线程的程序计数器独立跟踪其执行位置。
特殊情况:
- 非 native 方法:程序计数器指向方法区中方法的字节码地址。
- Native 方法:程序计数器不存储有效地址,因为 native 方法由本地代码(C/C++)执行,JVM 不直接控制。
2.程序计数器与堆、栈、方法区的关联
与栈(Stack):
- 栈存储方法调用的栈帧(包括局部变量表、操作数栈)。
- 程序计数器记录当前栈帧执行的字节码指令地址。
- 例如:
- 栈帧存储
fileName
(局部变量)和方法调用信息。 - 程序计数器指向
toLowerCase
方法的字节码指令地址。
String fileName = item.getFilename().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...