JVM的运行时数据区

运行时数据区结构

 方法区(JDK1.7)

JDK1.8以前,方法区位于堆内存中,大小固定,容易引发内存溢出(java.lang.OutOfMemoryError: PermGen space)。

为什么容易引发内存溢出?
  1. 方法区的内存大小固定不易扩展
  2. 类的卸载要满足(实际上很难都条件,所以基本不会回收,导致一些没用的类没法卸载):
    1. 该类的所有实例都被垃圾回收
    2. 加载该类的类加载器已经被垃圾回收等
  3. 一些框架会动态生成类,这些动态生成的类也会占用空间
元空间(JDK1.8)

元空间位于本地内存中,可以动态扩展(理论上取决于本地内存大小),减少内存溢出风险。

存储内容

元空间中主要存储了一下信息:

  1. 类的结构信息:类名,父类名,接口列表
  2. 字段信息:名称,数据类型,修饰符
  3. 方法信息:方法名,返回类型,参数列表,字节码指令(执行方法指令),异常信息表等
  4. 类常量池:
    1. 字面量:
      1. 字符串字面量(双引号内的字符串)
      2. 基本数据类型的值;
    2. 类中被final修饰的常量(字面量赋值,引用类型的话类常量池中存储的是符号引用)
运行时常量池

JVM加载类时,会将class文件常量池中的内容导入,并在程序运行时动态添加新的常量

存储内容
  1. 类常量池数据
  2. 符号引用:包括类的,字段的,方法等的符号引用,在运行时将这些符号引用解析为直接引用地址
  1. JVM读取到.java文件,Java编译器将.java文件编译成.class文件(字节码文件),此时的class文件存放在文件系统中(target目录下)
  2. 类加载器加载class文件,将类信息存放在元数据空间
  3. 运行时常量池是在类加载器加载类时创建的
  4. 每个类都有自己独立的运行时常量池,它的内容是由类常量池转换来的
堆 

JVM管理的最大一块内存区域,用于存储所有的实例对象和数组。它是垃圾回收(GC)的主要区域。堆的设计直接影响程序性能,内存利用率和稳定性。

存储内容
  1. 对象实例:
    1. 通过new关键字创建的对象(Object obj = new Object())
    2. 实例变量(成员变量)的值,包括基本类型数据和对象引用
  2. 数组:所有数组类型(int[],String[])的数据都存放在堆中
  3. 字符串常量池:存储字符串对象的引用
  4. 其他缓存对象:如缓存框架(EhCache、Guava Cache)中的对象,或静态集合(如static List)长期持有的对象
堆的分代结构
  1. 新生代
    1. Eden区:新对象首次被分配到的区域(大约80%的新生代空间)
    2. Survivor区(S0/S1):存活对象在Minor GC后复制到此处,S0,S1交替使用(各占10%)
  2. 老年代:多次GC后存活的对象存放至此(如长期缓存,全局配置对象)
  • 新生代的设计是为了快速回收短期存货对象(如局部变量,临时对象)
  • 多次GC后存活的对象晋升至老年代,可以减少GC对长期存活对象的扫描开销(如果长期存活对象依然存放在新生代,每次Minor GC时都会去扫描)
  • 大对象直接分配到老年代(若对象大小超过-XX:PretenureSizeThreshold参数值)
 垃圾回收算法
新生代

复制算法:将Eden区域和其中一个Survivor区(From区)的存活对象复制到另一个Survivor区(To区),清空原区内存。

优点是:垃圾回收后内存连续(无碎片),分配高效

缺点是:需要预留一块内存空间用于复制存活对象;存活对象较多时,复制成本增加

新生代触发GC:

  1. Eden区满:当新生代中的Eden区没有足够的空间分配给新对象的时候,会触发Minor GC(Java对象大多都是"朝生暮死",所以Minor GC很频繁,一般回收速度很快)
  2. Survivor区域的To区空间不足:这些对象会进入老年代,若老年代也无法容纳,会触发Full GC
  • 大对象会直接进入老年代:需要大量连续空间存储的对象(如长字符串,数组)会直接在老年代中分配,以免在新生代中频繁触发GC
  • 当Survivor区中相同年龄或更高年龄的对象所占用的空间总和超过Survivor区空间的一半时,这些对象就会被直接晋升到老年代
  • 当对象的年龄到达阈值的时候,会晋升到老年代
老年代

标记-清除算法:标记无用对象后清除(可能会产生内存碎片)

标记-整理算法:标记后整理内存,消除碎片

老年代触发GC

  1. 老年代空间不足:当老年代的空余空间不足以容纳从新生代晋升来的对象时,会触发Full GC
  2. 方法区(永久代或者元空间)空间不足,会触发Full GC
  3. 调用System.gc():调用后不一定会立即执行Full GC
分代收集

结合不同的垃圾回收算法,新生代和老年代采用不同的垃圾回收算法和策略进行垃圾回收,提高回收效率,减少STW(Stop-The-World)时间

如G1收集器将堆划分为多个Region,动态分代管理

复制算法详解
对象分配阶段

新对象被分配到Eden区域,如果Eden区域空间不足,则触发Minor GC。

垃圾回收阶段
  1. 标记存活对象:从根对象(GC Root)出发,标记所有可达对象
  2. 复制存活对象:将Eden区域和Survivor(From区域)的存活对象复制到Survivor(To区域)。存活对象每经历一次GC年龄+1
  3. 更新引用:更新所有指向复制对象的引用,使其指向新地址
  4. 交换角色:将Survivor的From区域和To区域互换,等待下次GC
  5. 清空Eden和From区:清空Eden区域和Survivor的From区域中剩余的对象
  6. 晋升老年代:如果存活对象的年龄超过阈值(15,-XX:PretenureSizeThreshold参数值),或者To区域空间不足,对象直接晋升到老年代
特点
  1. 垃圾回收后的内存空间是连续的,能提高分配效率
  2. 需要暂停应用线程(Stop-The-World)
  3. 需要预留内存空间,始终有一块内存空闲(可以通过调整Eden和S0/S1比例减少内存浪费)
  4. 如果存活对象过多,复制成本显著增加
  • 在复制存活对象时,有些收集器(ParNew收集器)会采用并行复制。多线程并行复制减少STW(Stop-The-World)时间
  • 可以通过调整Eden和S0/S1比例减少内存浪费
标记-清除算法详解
标记阶段
  1. 从根对象(GC Roots)出发,遍历所有直接引用对象
  2. 递归标记可达对象(间接或直接引用最终能在堆中找到对象)
    1. 通过深度优先搜索(DFS)或者广度优先搜索(BFS)遍历对象图,标记可达对象为"存活"
清除阶段
  1. 遍历堆内存,检查每个对象的标记状态
  2. 回收未标记对象:如果对象没有被标记(不可达),直接回收(释放内存);已标记的对象重置标记位,为下一次GC做准备
特点
  1. 回收后的内存是碎片化的(不连续),可能会引发内存碎片问题
  2. 标记和清除阶段会暂停所有的应用线程(Stop-The-World),可能会产生明显的延迟
  • 根对象(GC Roots)包括:线程栈中的局部变量表中的引用对象;本地方法栈中局部变量表的引用对象;元空间中的静态变量(引用类型),其他常量引用等
  • 标记阶段的标记实现方式:1. 对象头标记位:在对象头中设置标记位,标记是否存活

                                                   2.独立位图:使用单独的内存区域记录存活对象的地址,避                                                        免修改对象头

标记-整理算法详解
标记阶段
  1. 从GC Roots出发,遍历对象引用链,标记可达的对象
整理阶段
  1. 计算对象新地址:计算每个存活对象移动后的目标地址
  2. 更新对象引用:将存活对象更新后的地址更新到指向这些对象的引用
  3. 移动对象:将存活对象按顺序复制到目标地址,覆盖原有的内存区域
清除阶段
  1. 回收空闲内存:整理完成后,存活的对象集中在内存的一端,另一端的内存空间(包括原存活对象移动后留下的空隙)被一次性回收
特点
  1. 无内存碎片:内存连续分配,提升分配效率
  2. 内存利用率高:无需预留内存(对比复制算法)
  3. 高延迟:整理阶段需要移动对象并更新引用,导致长时间STW(Stop-The-World)
  4. 实现复杂:需要处理对象移动,引用更新,跨代引用(新生代对象中引用了老年代的对象)等问题
算法对比

吞吐量:系统在单位时间内处理的请求总数(包括TPS和QPS)

算法适用场景内存碎片吞吐量停顿时间
标记-整理老年代长(大堆时显著)
标记-清除老年代短(仅标记清除)
复制算法新生代短(低存活率时)

栈是线程私有的,每个线程在创建时都会分配独立的栈空间。线程独立管理自己的方法调用栈

线程私有:虚拟机栈,本地方法栈,PC寄存器(程序计数器)

虚拟机栈

在JVM中,栈帧是调用方法的基本单元,每当一个方法被调用时,就会创建一个栈帧压入当前线程的虚拟机栈中;方法执行完毕,栈帧会被弹出。

栈帧的结构

局部变量表

存储方法参数和方法内部定义的局部变量

数据结构:

  1. 以索引为单位的数组,索引从0开始
  2. 基本类型(如 int,float)和对象引用(如Object)占一个索引位(槽位),long和double占两个连续的索引位(槽位)
  3. 实例方法(非static)的索引0存储的是this引用,后续一次为方法参数和局部变量

示例:

public int calculate(int x, int y) {
    long sum = x + y;
    return (int) sum;
}

局部变量表结构

索引内容类型
0this对象引用
1xint
2yint
3-4sumlong

操作数栈

存储字节码指令的操作数,用于执行计算、方法调用和返回结果

数据结构:

  1. 后进先出(LIFO)的栈结构,最大深度在编译时确定
  2. 字节码指令(如iaddinvokevirtual)从栈顶取出操作数,并将结果压回栈顶

示例

int a = 10;
int b = 20;
int c = a + b;
  • 操作数栈执行流程

    1. bipush 10 → 栈顶压入10

    2. istore_1 → 栈顶值10存入局部变量表索引1(变量a)。

    3. bipush 20 → 栈顶压入20

    4. istore_2 → 栈顶值20存入索引2(变量b)。

    5. iload_1 → 加载a的值10到栈顶。

    6. iload_2 → 加载b的值20到栈顶。

    7. iadd → 弹出1020,计算结果30压入栈顶。

    8. istore_3 → 栈顶值30存入索引3(变量c)。

动态连接

将符号引用(Symbolic References)转换为直接引用(Direct References)

实现机制:

  • 符号引用:类、方法、字段的名称描述(如java/lang/Object.toString()
  • 直接引用:目标方法在内存中的实际地址(如方法入口指针)
  • 解析过程可能在类加载阶段(静态解析)或运行时(动态解析,如多态方法调用)

示例

多态方法调用:

Animal animal = new Dog();
animal.speak(); // 动态链接到Dog类的speak()方法

方法返回地址

记录方法执行结束后应返回的位置

两种情况:

  1. 正常返回:程序计数器(PC寄存器)记录调用方法的下一条指令地址
  2. 异常返回:异常处理表(Exception Table)决定跳转位置

示例:

调用方法A()内部调用B():

void A() {
    B(); // PC记录此处地址+1
    System.out.println("A done");
}

B()执行完毕,根据返回地址回到System.out.println("A done") 

其他附加信息

异常处理表(Exception Table):

  • 记录try-catch块的范围和异常处理器入口
  • 发生异常时,JVM查找匹配的catch块并跳转

运行时常量池引用:

指向当前方法所属类的常量池,用于动态链接时的符号解析

特点
  • 线程私有:每个线程独立管理栈帧,避免并发冲突
  • 轻量高效:基于栈的操作模型简化指令集设计,适合跨平台执行
  • 动态扩展:支持方法嵌套调用,深度由栈大小限制
  • 内存隔离:栈帧随方法调用自动创建/销毁,内存管理简单可靠
本地方法栈

独立于虚拟机栈,专门用于本地方法的调用,与Java虚拟机栈功能类似,但处理的是非Java语言(如C或C++)编写的方法

HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的。这意味着它们共享相同的栈空间

特点
  1. 线程私有:与虚拟机栈类似,每个线程独立分配
  2. 避免本地方法执行干扰Java代码的栈管理
程序计数器(PC寄存器)

JVM中的程序计数器(Program Counter Register)是一块较小的内存空间,主要用于存储当前线程正在执行的字节码指令的地址

  1. 线程切换的恢复点:在多线程环境下,CPU会在不同线程之间进行切换。程序计数器记录了每个线程当前执行的位置,当线程切换回来时,可以根据程序计数器的值继续从正确的位置执行
  2. 方法调用和返回的地址记录:程序计数器会记录方法调用和返回的地址,确保方法执行完成后能够正确返回到调用方法的下一条指令
  3. 异常处理的恢复点:当发生异常时,程序计数器记录了异常发生时的位置,便于异常处理器在处理后恢复到异常发生前的执行位置
在多线程的环境下,CPU切换线程时,会保存当前线程的上下文信息(包括寄存器信息、栈信息等)

切换回当前线程时,根据上下文信息继续执行

过度的上下文切换会导致程序的性能下降

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值