> 文章列表 > JVM详解——内存结构

JVM详解——内存结构

JVM详解——内存结构

文章目录

  • 内存结构
    • 1、 运行时数据区
    • 2、虚拟机栈
    • 3、本地方法
    • 4、程序计数器
    • 5、 堆
    • 6、方法区
    • 7、运行时常量池
    • 8、内存溢出和内存泄漏
    • 9、 堆溢出

内存结构

JVM详解——内存结构

1、 运行时数据区

Java虚拟机在运行Java程序期间将管理的内存划分为不同的数据区,不同的区域负责不同的职能,有各自的生命周期,这些区域统称为运行时数据区。

从线程私有和共享的角度区分:

线程私有: 程序计数器、Java虚拟机栈、本地方法栈

线程共享: 堆、方法区

栈是运行时的单位,而堆是存储的单位​。

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪。


2、虚拟机栈

作用: 虚拟机栈也称为Java栈,主要管理Java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回。

每个线程都有自己的栈,栈中的数据以栈帧的格式存在,一个线程上正在执行的每个方法都有自己对应的一个栈帧,方法调用栈帧压栈,方法结束栈帧弹出。

栈帧的内部结构

  • 局部变量表

主要用于存储方法参数和定义在方法体内的局部变量(包括基本数据类型和对象引用)

  • 操作数栈

主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,在方法执行过程中,根据字节码指令,往操作数栈中写入数据或提取数据,即入栈、出栈

  • 动态链接

指向运行时常量池的方法引用

在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在 Class 文件的常量池中,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

  • 方法返回地址

用来存放调用方法的PC寄存器的值

方法结束的两种方式:正常执行、出现异常,非正常退出

  • 一些附加信息

携带与Java虚拟机相关的一些附加信息,比如对程序调试提供支持的信息。


3、本地方法栈

本地方法栈就是Java调用非Java代码的接口,也就是Native Method​本地方法。

作用:与操作系统交互,Java应用有时候需要依赖一些底层系统的支持,比如在拷贝数组时候使用系统拷贝。


4、程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器, 存储指向下一条指令的地址,即将执行的指令代码。

作用:CPU在切换线程时,JVM需要明确线程下一条应该执行什么字节码指令,而PC程序计数器中存储的就是下一条指令的地址,并且每个线程的执行进度不同,不同的线程需要有自己的PC程序计数器。


5、 堆

堆是Java虚拟机管理的内存中最大的一块,被所有线程共享。堆用来存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。

堆也是垃圾回收的主要区域,为了高效的进行垃圾回收,虚拟机把堆内存在逻辑上划分为三块区域

  • 新生代: 存放新创建的对象和没达到一定年龄的对象
  • 老年代: 被长时间使用的对象,老年代内存更大
  • 元空间(JDK1.8之前称永久代):存放一些方法中的临时对象

Java 虚拟机规范规定,Java 堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(通过 -Xmx​ 和 -Xms​ 控制),如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出 OutOfMemoryError​ 异常。

-Xmx: 堆的起始内存,默认初始化大小为 电脑内存/64

-Xmx​:堆的最大内存,默认初始化大小为 电脑内存/4


6、方法区

方法区是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,永久代是Hotspot虚拟机对方法区的实现(JDK8之后改为元空间)

JDK8 之前使用永久代实现方法区,容易内存溢出,因为永久代有 -XX:MaxPermSize ​​上限,即使不设置也有默认大小。JDK7 把放在永久代的字符串常量池、静态变量等移出保存到堆中,JDK8 中取消永久代,改用在本地内存中实现的元空间代替,把 类型信息、字段、方法、常量保存在本地内存的元空间中。


7、运行时常量池

运行时常量池是方法区的一部分,一个有效的class​字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含常量池表,用于存放各种字面量和对类型、域和方法的符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池,相对于 Class 文件常量池的另一个重要特征是动态性,Java 语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中。

比如:·String· 类的 intern()​ 方法,如果字符串常量池中存在对应得字面量,方法返回该字面量得地址;如果不存在,则创建一个对应得字面量放入运行时常量池,返回字面量地址。


8、内存溢出和内存泄漏

内存溢出OutOfMemory​,程序申请的内存超过JVM能够提供的内存大小,导致内存溢出,主要为堆内存溢出。

内存泄漏 Memory Leak​,无法释放已申请的内存,虚拟机不能再次使用该内存


9、 堆溢出

堆用于存储对象实例,只要不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收,随着对象数量的增加,程序需要的内存空间就会超出JVM分配的内存空间导致OOM程序崩溃(例如在 while 死循环中一直 new 创建实例)。

出现堆OOM的情况:

  • 堆的内存大小设置不当
  • 程序中存在内存泄漏问题,或应用中有大量占用内存的对象,并且无法及时释放

解决方案:

  • -Xms -Xmx ​​修改堆的内存大小
  • 通过内存监控软件去查找程序中的泄漏代码

线上排查方式:

  1. 获取内存的dump文件(1. 配置JVM启动参数,当触发了OOM异常时自动生成 2. 使用jmap工具生成)
  2. 使用MAT工具分析dump文件
    • 如果是内存泄漏,可以查看泄漏对象的GC Roots的引用链,通过类信息和引用链信息定位到代码位置,进行解决
    • 堆空间分配不足以满足业务需求,提升堆内存空间


参考文章:
‍1. https://pdai.tech/md/java/jvm/java-jvm-struct.html
2. https://blog.csdn.net/weixin_45629285/article/details/128050932