JVM基础之JVM内存模型详解

目录

  • Java运行时数据区
    • 程序计数器
    • 虚拟机栈
    • 本地方法栈
    • 方法区
    • 运行时常量池
    • 字符串常量池
  • 面试题
    • 为什么递归会导致StackOverFlowError?
    • 元空间(MetaSpace)和永久代(PermGen)的区别
    • 元空间替代永久代的优势
    • JVM内存模型中堆和栈的区别
    • JDK1.7为什么要将字符串常量池移动到堆中?
    • 不同JDK版本之间的intern()方法有什么区别?

JVM内存模型是JVM中的一个重要知识点,掌握内存模型有助于了解Java内存分配机制以及后续的GC机制,在实际项目中能够对常见OOM错误进行排查。

JVM内存模型

此处JVM内存模型与Java内存模型(JMM)是两个不同的概念。关于Java内存模型JMM的介绍,请前往我的这篇文章

Java运行时数据区

从图中可以看出:

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

线程共享:堆、元空间

注:元空间(MetaSpace)是JDK1.8之后加入的。在JDK1.8之前,它是方法区。具体将在下文介绍。

接下来我们具体来看运行时数据区的内容。

程序计数器

程序计数器记录当前线程执行的位置,是唯一一个不会出现OutOfMemoryError的内存区域。它的生命周期:随着线程的创建而创建,随着线程的结束而死亡。

  • 当前线程所执行的字节码行号指示器(物理上不存在,是逻辑上的)。
  • 改变计数器的值来选取下一条需要执行的字节码指令。
  • 和线程是一对一的关系即:线程私有
  • 对Java方法计数,如果是native方法则计数器值为undefined
  • 不会发生内存泄露(唯一一个不会出现OutOfMemoryError的内存区域)。

虚拟机栈

虚拟机栈由一个个栈帧(Stack Frame)组成,每个栈帧中包含局部变量表、操作数栈、动态链接、返回地址。虚拟机栈是线程私有的。局部变量表主要存放的就是基本数据类型和对象引用。虚拟机栈可能会出现StackOverflowErrorOutOfMemoryError错误。它的生命周期与程序计数器相同:随着线程的创建而创建,随着线程的结束而死亡。

栈是JVM运行时数据区域的一个核心,除了一些native方法的调用是通过本地方法栈实现的,其他所有的Java方法调用都是通过栈配合实现的。

调用方法会有一个对应的栈帧被压入栈中,每一个方法调用结束后,会将栈帧弹出。

栈的结构示意图如下:

栈

局部变量表: 主要存放基本数据类型(boolean、byte、char、short、int、float、long、double...)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

操作数栈: 用于存放方法执行过程中产生的中间计算结果,它充当中转站的作用。另外,计算过程中产生的临时变量也会放在操作数栈中。

动态链接: 主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。

本地方法栈

本地方法栈与虚拟机栈类似,虚拟机栈为执行Java方法提供服务,本地方法栈为执行native本地方法提供服务。本地方法栈也是线程私有的。

本地方法在执行时也会在本地方法栈处创建一个栈帧,方法执行完毕后响应的栈帧会弹出栈释放内存空间。本地方法栈也会出现StackOverflowErrorOutOfMemoryError错误。

堆是虚拟机管理内存中最大的一块,它是线程的共享区域,随着虚拟机的启动而创建。堆是对象实例的分配区域,GC管理的主要区域。在垃圾回收时,堆会被划分为新生代和老年代,其中新生代又被划分为Eden、From Survivor、To Survivor、Old区域。此处我们将在GC机制的文章中详细进行分析。

方法区

用于存放已被加载的类信息、常量、静态变量等。JDK1.6之前方法区的实现是永久代,JDK1.8时方法区实现是元空间

关于永久代和元空间我们将在下文进行介绍。

运行时常量池

运行时常量池的功能类似于传统的编程语言的符号表。在Class文件的常量池中,存放着用于保存编译时生成的字面量和符号引用。在类加载后,Class文件常量池会放在运行时常量池中。运行时常量池也是方法区的一部分,它受方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError

字符串常量池

字符串常量池是JVM为了提升性能和减少内存消耗针对String类专门开辟的一块区域,主要是为了避免字符串的重复创建造成资源的浪费。在JDK1.7之前,字符串常量池存放在永久代,JDK1.7后字符串常量池和静态变量移动到Java堆中。JDK1.8永久代的实现变成元空间。

面试题

为什么递归会导致StackOverFlowError?

答:递归过深会造成栈帧中被压入过多的栈而占用太多空间,导致栈空间过深,而线程请求栈的深度是有限的,当线程请求栈深度超过当前Java虚拟机的最大深度时,就会引发java.lang.StackOverFlowError异常。补充:当虚拟机栈过多则会引发java.lang.OutOfMemoryError异常。

元空间(MetaSpace)和永久代(PermGen)的区别

答:元空间使用本地内存,而永久代使用的是JVM内存。

整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

元空间替代永久代的优势

答:

  • 字符串常量池存在永久代中,容易出现性能问题和内存溢出。而元空间使用直接内存,只受到本机可用内存限制,溢出概率小。
  • 由于类和方法的信息大小难以确定,元空间将根据运行时应用程序需求动态调整大小,而永久代不会,确定大小十分困难。
  • 元空间有利于减轻GC负担,而永久代会为GC带来不必要的复杂性。当系统中加载的类、反射的类和调用方法较多时,永久代空间不足就会触发Full GC。用元空间可以降低Full GC频率,减少GC负担。
  • 元空间方便与HotSpot和其他JVM如Jrockit进行集成。(Jrockit没有永久代)

JVM内存模型中堆和栈的区别

答:

  • 管理方式:栈自动释放,堆需要通过GC来释放。
  • 空间大小:栈比堆小。JVM管理的内存中,堆是最大的一块。
  • 碎片相关:栈产生的内存碎片远小于堆。
  • 分配方式:栈支持静态分配和动态分配,堆仅支持动态分配。
  • 效率:栈的效率比堆高。

JDK1.7为什么要将字符串常量池移动到堆中?

答:因为永久代的GC回收效率太低,只有当Full GC时才会执行。这就造成Java程序中有大量被创建的字符串等待被回收。将字符串移动到堆中,能够及时地回收字符串内存。

不同JDK版本之间的intern()方法有什么区别?

  • JDK1.6:当调用intern()方法时,如果字符串常量池中已经存在该字符串的对象,则直接返回池中该字符串的引用。否则将此字符串对象添加到字符串常量池中,并返回该字符串的引用。
  • JDK1.6+:当调用intern()方法时,如果字符串常量池中已经存在该字符串的对象,则直接返回池中该字符串的引用(此处与JDK1.6相同)。否则,如果该字符串对象已经存在于Java堆中,则将堆中此对象的引用添加到字符串常量池中,并返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。
JavaJVM
2025 © Yeliheng的技术小站 版权所有