当前位置: 首页 > news >正文

JVM内存模型(运行时数据区)

目录

 编者想说

1、内存模型图

2、栈

3、程序计数器

3、堆

4、方法区(元空间)

5、本地方法栈(Native Method Stack)


 编者想说

        通过上一篇文章的对JVM的体系结构以及它的演化,我们对JVM有了一个比较清晰的认识,接下来我们将会深入其中的运行时数据区(也就是我们常说的JVM内存模型)进行更加深入的一个分析。

1、内存模型图

2、栈

首先,我们以下面这个程序为例来介绍一下栈这个内存区:

import java.util.Scanner;public class Test {public static void main(String[] args) {int a,b;Scanner sc = new Scanner(System.in);a = sc.nextInt();b = sc.nextInt();System.out.println("a+b="+add(a,b));}private static int add(int a, int b) {return a+b;}
}

在这个栈里面存储的叫栈帧(Stack Frame)​​,每个方法调用对应一个栈帧,包含:

局部变量表​:方法内的局部变量(基本类型、对象引用),比如上面程序中的a,b以及sc。

操作数栈​:方法执行时的临时操作数(如算术运算的中间结果a+b)。

动态链接​:指向方法区中该方法的类信息。

方法返回地址​:方法执行完毕后的返回位置。

下面是我对这个程序执行之后在栈内存中的一些动作做的一个图示,方便大家理解(如果有错误,请各位大佬严肃批评指正)。

此处,我要对上图做一些解释:

1、在创建a,b时,这两个临时变量是没有被赋值的,系统自动初始化值为0

2、sc是一个引用数据类型,因为sc是通过new关键字创建的,会指向运行时数据区中的堆内存,而此时栈内存中的sc其实指向堆内存中Scanner实例的地址(因为对象本身在堆中)。

3、a = sc.nextInt()和 b = sc.nextInt()这两个方法是通过sc读取我们输入的值并给a,b做一个赋值,调用nextInt()时,会临时压入nextInt方法的栈帧(执行完毕弹出),接着将值传给a,b,覆盖初始值0。

4、add(a,b)调用add()方法并传入a,b的值,在main栈帧的操作数栈中压入a,b的值然后创建add()方法的栈帧,里面包含局部变量表(分配两个int槽位,存储传入的a、b的拷贝值),操作数栈(计算a+b的结果,并暂存于此)。在add方法结束之后,弹出add方法的栈帧,返回值压入main方法的操作数栈。

​栈内存变量状态快照

代码执行位置栈帧局部变量表槽位(索引)存储内容
int a, b;main0 (args)方法参数(未使用)
1 (a)int初始值0
2 (b)int初始值0
Scanner sc = ...main3 (sc)指向堆中Scanner的引用
a = sc.nextInt()main1 (a)键盘输入值(如5
b = sc.nextInt()main2 (b)键盘输入值(如3
add(a, b)内部add0 (a)maina的值拷贝
1 (b)mainb的值拷贝

 在弄清楚栈之后,我们要开始介绍程序计数器了

3、程序计数器

​​ 程序计数器(Program Counter Register)是JVM运行时数据区中一个非常核心但容易被忽视的组件。它的作用可以用一句话概括:​

记录当前线程正在执行的字节码指令的地址(行号)​,相当于代码执行的“书签”,确保线程切换或方法调用后能准确恢复到执行位置。

​程序计数器的作用​

1. ​线程执行的“导航仪”​

  • 每个线程独立拥有一个程序计数器,互不干扰。
  • 存储的是下一条待执行指令的地址​(字节码的行号偏移量)。
    • 例如:当前执行到main方法的第5行字节码,PC寄存器就保存5

2. ​方法调用时的“存档点”​

  • 当线程调用新方法(如add(a, b))时:
    1. PC寄存器会暂存当前方法的执行位置(如main方法的第10行)。
    2. 切换到新方法的起始指令地址(如add方法的第0行)。
    3. 方法返回时,根据PC寄存器保存的地址恢复执行。

3. ​Native方法的特殊处理

  • 如果线程执行的是native方法(如JNI调用),PC寄存器的值为undefined
    • 因为native方法的执行由本地代码(如C++)控制,不在JVM字节码范围内。

4.​为什么需要程序计数器?​

  • 线程切换恢复​:CPU时间片轮转时,线程可能被挂起,PC寄存器确保恢复后继续执行正确位置。假设线程A执行compute()PC=6时被挂起,线程B开始执行。当线程A恢复时,程序计数器会准确恢复到PC=6,继续执行iadd指令。
  • 方法调用/返回​:嵌套调用方法时(如A→B→C),PC寄存器保存调用链的返回路径。
  • 避免指令混乱​:无PC寄存器时,多线程执行可能导致代码位置错乱。

5.​与操作数栈的区别

程序计数器操作数栈
只存指令地址(数字),不存数据存方法执行的临时数据(如a+b的结果)
线程切换依赖它线程切换无关
永远不溢出可能栈溢出(StackOverflowError

​以我们上面的那个程序为例:

public static void main(String[] args) {int a = sc.nextInt();  // 假设字节码行号10int b = sc.nextInt();  // 行号15int result = add(a, b); // 行号20
}
  1. 线程执行到int a = sc.nextInt()时,PC寄存器保存10
  2. 调用nextInt()方法时,PC寄存器暂存15(下一条指令地址),跳转到nextInt的字节码起始位置。
  3. nextInt执行完毕返回后,PC恢复为15,继续执行。​

唯一无OOM的区域​:程序计数器是JVM规范中唯一不会发生OutOfMemoryError的区域。

线程私有​:每个线程独立存储,生命周期与线程相同。

性能关键​:JIT编译器会优化PC寄存器的使用,减少指令定位开销。

程序计数器虽然不直接存储业务数据,但它是JVM多线程执行和指令顺序控制的基础,类似于CPU中的指令指针(EIP/RIP)。

3、堆

JVM的堆内存(Heap Memory)是Java程序运行时数据区域中最重要的部分之一,主要用于存储对象实例和数组。堆内存是所有线程共享的内存区域,在JVM启动时创建。

以下是堆的内存结构图

堆内存的主要特点

  1. 对象存储区域​:几乎所有通过new关键字创建的对象实例都存储在堆中
  2. 垃圾回收的主要区域​:GC(垃圾收集器)主要管理堆内存
  3. 线程共享​:所有线程共享堆内存
  4. 动态分配​:堆的大小可以在JVM启动时指定,也可以动态扩展

堆内存的分代结构

现代JVM通常将堆内存划分为几个不同的代(Generation),以便更高效地进行垃圾回收:

1. 年轻代(Young Generation)

  • Eden区​:新创建的对象首先分配在Eden区
  • Survivor区​:分为From Survivor和To Survivor两个区域,用于存放从Eden区经过GC后存活的对象
  • 年轻代使用复制算法进行垃圾回收(Minor GC)

2. 老年代(Old Generation/Tenured Generation)

  • 存放长期存活的对象
  • 当对象在年轻代经历一定次数的GC后仍然存活,会被晋升到老年代
  • 老年代使用标记-清除或标记-整理算法进行垃圾回收(Major GC/Full GC)

3. 永久代/元空间(PermGen/Metaspace)

  • Java 8之前称为永久代(PermGen),Java 8及以后改为元空间(Metaspace)
  • 存储类元数据、方法区信息等
  • 元空间使用本地内存(Native Memory)而非堆内存

堆内存相关参数

  • -Xms:初始堆大小
  • -Xmx:最大堆大小
  • -Xmn:年轻代大小
  • -XX:NewRatio:老年代与年轻代的比例
  • -XX:SurvivorRatio:Eden区与Survivor区的比例

堆内存溢出

当堆内存不足时,会抛出OutOfMemoryError错误,常见原因包括:

  • 内存泄漏
  • 堆大小设置不合理
  • 对象生命周期过长

4、方法区(元空间)

方法区(Method Area)是JVM规范定义的一个逻辑内存区域,在Java 8之前被称为永久代(PermGen)​,从Java 8开始被元空间(Metaspace)​取代。

核心概念

  1. 存储内容​:

    • 类元数据(Class metadata):类的结构信息(字段、方法、构造器等)
    • 运行时常量池(Runtime Constant Pool)
    • 静态变量(Static variables)
    • 即时编译器(JIT)编译后的代码
    • 方法字节码
  2. 与堆的关系​:

    • 在Java 7及之前:方法区是堆的逻辑部分,物理上位于堆内存中
    • 从Java 8开始:元空间使用本地内存(Native Memory),不再属于堆内存

永久代 vs 元空间

特性永久代(PermGen)元空间(Metaspace)
位置JVM堆内存的一部分本地内存(Native Memory)
大小限制-XX:MaxPermSize限制默认只受系统可用内存限制
垃圾回收Full GC时回收可单独触发元空间GC
内存溢出java.lang.OutOfMemoryError: PermGen spacejava.lang.OutOfMemoryError: Metaspace
调优参数-XX:PermSize, -XX:MaxPermSize-XX:MetaspaceSize, -XX:MaxMetaspaceSize

元空间关键特性

  1. 动态扩展​:

    • 默认情况下,元空间会根据应用需求动态调整大小
    • 避免了永久代固定大小导致的OutOfMemoryError
  2. 类元数据生命周期​:

    • 与类加载器(ClassLoader)生命周期绑定
    • 当类加载器被回收时,其加载的类元数据也会被回收
  3. 内存管理​:

    • 使用mmap而非malloc来分配内存
    • 采用块(Chunk)分配策略,提高内存利用率

相关JVM参数

# 初始元空间大小(并非初始就分配,而是首次GC的阈值)
-XX:MetaspaceSize=64M# 最大元空间大小(默认基本无限制)
-XX:MaxMetaspaceSize=256M# 元空间扩容时增加的幅度(默认约20%)
-XX:MinMetaspaceFreeRatio=40
-XX:MaxMetaspaceFreeRatio=70# 启用类元数据的并行卸载(JDK 12+)
-XX:+ClassUnloadingWithConcurrentMark

常见问题

  1. 元空间内存泄漏​:

    • 通常由未关闭的类加载器引起
    • 常见于动态生成类(如使用ASM、CGLIB)的应用
  2. 调优建议​:

    • 监控元空间使用情况(JVisualVM、JConsole等工具)
    • 对于大量使用动态代理的应用,适当增加MaxMetaspaceSize
    • 避免创建过多类加载器
  3. 性能影响​:

    • 元空间GC会导致应用暂停(但比永久代Full GC影响小)
    • 过大的元空间可能影响Native Memory其他组件的使用

5、本地方法栈(Native Method Stack)

本地方法栈是JVM内存结构中一个专门为本地方法(Native Method)​服务的内存区域,与Java虚拟机栈类似但服务于不同的目的。

核心概念

  1. 基本定义​:

    • 为JVM运行本地方法(Native Method)​服务的内存区域
    • 每个线程在调用本地方法时会创建独立的本地方法栈
    • 存储本地方法的调用状态、参数、局部变量等

举例:

//定义一个线程对象并开启
Thread t = new Thread(); 
t.start();                

那么在这个start()方法的底层就有一个本地方法:

  1. 与虚拟机栈的区别​:

    特性Java虚拟机栈本地方法栈
    服务对象Java方法本地方法(用native修饰的方法)
    实现规范JVM规范明确要求由JVM实现者自行决定
    语言类型Java语言实现通常用C/C++实现
    异常类型StackOverflowError/OutOfMemoryError由操作系统决定

关键特性

  1. 内存分配​:

    • 线程私有,生命周期与线程相同
    • 大小可以通过-Xss参数设置(与Java虚拟机栈共享同一参数)
    • 在HotSpot JVM实现中,本地方法栈和虚拟机栈是合二为一的
  2. 运行机制​:

    • 当线程调用native方法时:
      1. 在本地方法栈中创建栈帧
      2. 动态链接到本地方法接口(JNI)
      3. 执行本地方法实现(通常位于.dll.so文件中)
      4. 返回结果后栈帧出栈
  3. 重要限制​:

    • 栈深度限制(可能抛出StackOverflowError)
    • 内存分配失败(可能抛出OutOfMemoryError)
    • 本地方法执行错误可能导致JVM崩溃(因为超出了JVM控制范围)

常见问题与调优

  1. StackOverflowError​:

    • 本地方法递归调用层次过深
    • 解决方案:增大栈大小(-Xss参数),或优化本地方法实现
  2. 内存泄漏​:

    • 本地方法中分配的内存未正确释放
    • 特别注意事项:通过JNI New创建的Java对象需要特别管理
  3. 性能调优​:

    # 设置线程栈大小(包括本地方法栈)
    -Xss1m# 对于大量使用本地方法的应用,可能需要增大该值
    -Xss2m
  4. 安全风险​:

    • 本地方法栈可能成为攻击入口(缓冲区溢出等)
    • 建议:对关键本地方法进行严格的安全审计

注意事项

  1. HotSpot JVM的实现特点:

    • 在主流JVM实现(如HotSpot)中,本地方法栈和Java虚拟机栈是合并的
    • 通过-Xoss参数设置本地方法栈大小的功能在HotSpot中无效
  2. 平台差异性:

    • 不同操作系统对本地方法栈的支持可能有差异
    • 32位/64位系统的默认栈大小不同
  3. 调试建议:

    • 使用-XX:+PrintFlagsFinal查看实际栈大小
    • 对于本地方法问题,需要结合系统级调试工具(如gdb)

相关文章:

  • 力扣面试150题--二叉树的层平均值
  • CppCon 2014 学习:HOW UBISOFT MONTREAL DEVELOPS GAMES FOR MULTICORE
  • 7.CircuitBreaker断路器
  • DALI DT6与DALI DT8介绍
  • 嵌入式开发学习日志(linux系统编程--进程(4)——线程锁)Day30
  • 界面控件DevExpress WinForms中文教程:Banded Grid View - 如何固定Bands?
  • ESP32对接巴法云实现配网
  • IntelliJ IDEA 中进行背景设置
  • Python使用
  • 【工作笔记】 WSL开启报错
  • 参数化建模(三):SOLIDWORKS中的参数化应用实例
  • docker部署自动化测试环境笔记
  • (21)量子计算对密码学的影响
  • Redis持久化机制
  • 力扣HOT100之动态规划:322. 零钱兑换
  • 【大模型】情绪对话模型项目研发
  • 区域未停留检测算法AI智能分析网关V4打造铁道/工厂/机场等场景应用方案
  • 2025 年 Solana 生态全景分析:它如何从以太坊「高速替代方案」成长为成熟的基础设施?
  • 换ip是换网络的意思吗?怎么换ip地址
  • write和read命令中的通道号指南
  • 亚马逊网站建设历程/电商网站平台搭建
  • 平罗门户网站建设/企业网站设计思路
  • 东莞企业网站推广多少钱/优化设计三年级上册答案
  • 网站怎样才有流量/网上营销怎么做
  • wordpress数据库名/短视频seo排名
  • 开网站建设/淘宝网店代运营正规公司