跳至主要內容
JVM 内存模型

JVM 内存模型

YUDI-Corgi原创JavaJavaJVM大约 9 分钟

什么是 JVM

概念

JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,是一种用于计算设备的规范、能够运行 Java 字节码的虚拟机,拥有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。

引入 Java 虚拟机后,Java 语言开发的程序在不同平台上运行时不需要重新编译。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的字节码(.class),就可以在多种平台上不加修改地运行。对于 JVM,除了 Oracle,也有其它的开源或闭源实现。(摘自维基百科open in new window

理解

Java 是一门可跨平台的语言,但其本身并不能跨平台,而是通过 JVM 来实现。是通过 JVM 将编译好的文件解释成平台系统(Mac、Linux、Windows 等)可执行的机器码,然后系统加以运行,实现 “一次编译,到处运行” 的效果。

学习 JVM,可以围绕四个部分进行:

  • 类的加载机制
  • 内存模型
  • GC 算法、垃圾回收
  • GC 分析、命令调优

本篇着重于 JVM 内存模型的讲解,作为 Java 开发人员,平常或多或少会有这样的疑问,堆内存空间应当设置多大?OOM(OutOfMemoryError)异常到底涉及了运行时数据内存哪块区域?JVM 内存调优从哪里入手?

在理解了 JVM 内存模型后,我们就可以知道,平常编写的 Java 文件信息是如何被 JVM 管理、存放,Java 方法执行时生成的变量、返回结果等 JVM 又是如何操作,堆内存大小如何设置等等。

JVM 内存模型

Java 程序的开发,并不需要像 C/C++ 开发一样时刻关注内存的释放,而是全权交由 JVM 去管理,JVM 内存模型主要是指运行时内存模型,分为 线程私有线程共享 数据区两大类:

  • 线程私有:程序计数器、虚拟机栈、本地方法栈;
  • 线程共享:Java 堆(Heap)、方法区(包含运行时常量池)。

线程私有数据区域生命周期与线程相同,依赖用户线程的启动(结束)而创建(销毁);线程共享数据区域则随虚拟机的启动(关闭)而创建(销毁)。

JVM 内存模型结构图(绿色共享,橙色私有):

内存模型
内存模型

程序计数器(私有)

程序计数器是一块较小的内存空间,是当前线程所执行的字节码的行号指示器,每个线程都有一个独立的计数器,互不影响。通过该计数器,JVM 解释器就知道下一步要执行的字节码指令位置,而分支、循环、跳转、异常处理、线程恢复等基础功能也都依赖于该计数器来实现。

如果线程正在执行的是一个 Java 方法,则计数器记录的是正在执行的虚拟机字节码指令的地址,若为 Native 方法,则计数器的值为空(Undefined),并且该内存区域是唯一一个在虚拟机中没有规定任何 OOM 异常情况的区域。

虚拟机栈(私有)

是描述 Java 方法执行的内存模型,每个方法(不含 Native)在执行时都会创建一个栈帧,方法执行过程,就对应着虚拟机栈的入栈到出栈的过程。(现在明白平常用 IDE 调试时点击 Drop Frame 是回退到方法执行前的原因了吧 (●’◡’●))

栈帧(Stack Frame)结构

  • 局部变量表
  • 操作栈
  • 动态链接
  • 方法返回地址
  • 额外附加信息

栈帧是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

异常(Exception)

JVM 规范规定该区域有两种异常:

  • StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出;
  • OutOfMemoryError:当 JVM 动态扩展到无法申请足够内存时抛出。

本地方法栈(私有)

本地方法栈的作用其实与虚拟机栈类似,区别只在于 本地方法栈是为 Native 方法 服务而虚拟机栈是为 Java 方法 服务。虚拟机规范中对本地方法栈中的方法使用的语言、方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。有的虚拟机实现也将本地方法栈和虚拟机栈合并,如 HotSpot 虚拟机。

对于异常抛出规范,也与虚拟机栈相同,分别是 StackOverFlowErrorOutOfMemoryError

堆(共享)

Java 堆(Heap)是 JVM 管理的最大的一块内存,也是 GC(Garbage Collection,垃圾收集)的重点照顾对象,存放的是几乎所有的对象实例和数组数据。(JIT 编译器有栈上分配、标量替换等优化技术的实现导致部分对象实例数据不存在 Java 堆,而是栈内存)

由于主流 JVM 实现对于 GC 采用分代收集算法,因此从 GC 角度来看,Java 堆可分为:

  • 新生代(Eden 区、From Servivor 区、To Servivor 区)
  • 老年代

可以简单的理解新生代主要是存放新创建的对象,而老年代则是存放生命周期或存活时间较长的对象。并且 JVM 在新生代的 Eden 区开辟了一小块内存区域,即 分配缓冲区(TLAB - Thread-local allocation buffer,线程私有),因为 Java 程序中很多对象都是小对象且用过即丢,不存在线程共享和适合被快速 GC ,所以小对象通常会被 JVM 优先分配在 TLAB 上,好处是分配内存效率高。

异常(Exception)

JVM 规范规定该区域可抛出异常: OutOfMemoryError,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时抛出。

方法区(共享)

方法区主要存放的是虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等,而 GC 在此区域出现频率较低,主要针对的是常量池的回收和类型的卸载,GC 不会在主程序运行期对方法区进行清理,所以容易随着加载的 Class 增多导致类膨胀,从而引发 OutOfMemoryError 异常。

方法区也常被称为**“永久代(Permanent Generation)”**,这是因为 **HotSpot 虚拟机(Sun JDK 和 Open JDK 自带的虚拟机实现)**的设计团队选择把 GC 分代收集扩展至方法区(也可以理解为是用永久代方法实现了方法区),从而在 GC 方面与 Java 堆保持一致。

在 Java 8 后,永久代则被替换为**“元空间(Metaspace)”**,当然了,后者依然是基于 HotSpot 虚拟机,两者区别主要在于:元空间并不在虚拟机中,而是使用本地内存。因此元空间的大小仅受本地内存限制,基本不存在 OOM 异常问题。并且类的元数据放入本地内存,字符串池和静态变量等则放入到 Java 堆中。

运行时常量池

作为方法区的一部分,用于存放编译期生产的各种字面量和符号引用,运行时常量池除了编译期产生的 Class 文件的常量池,还可以在运行期间,将新的常量加入常量池,比如 String 类的 intern() 方法。该区域不会抛出 OutOfMemoryError 异常。

  • 字面量:与 Java 语言层面的常量概念相近,包含文本字符串、声明为 final 的常量值等;
  • 符号引用:编译语言层面的概念,包括以下三类:
    1. 类和接口的全限定名;
    2. 字段的名称和描述符;
    3. 方法的名称和描述符。

补充

关于堆、栈内存的大小设置,可以通过 IDE(IDEA、Eclipse)或 Web 容器(Tomcat )等来配置,可通过下图来了解参数所控制的区域分别是什么。

堆栈设置
堆栈设置

参数控制:

  • -Xms:设置堆的最小空间大小;
  • -Xmx:设置堆的最大空间大小;
  • -XX:NewSize:设置新生代最小空间大小;
  • -XX:MaxNewSize:设置新生代最大空间大小;
  • -XX:PermSize:设置永久代最小空间大小;(JDK 8 后无效)
  • -XX:MaxPermSize:设置永久代最大空间大小;(JDK 8 后无效)
  • -XX:MetaspaceSize:设置元空间最小空间大小;(JDK 8 后有效)
  • -XX:MaxMetaspaceSize:设置元空间最大空间大小;(JDK 8 后有效)
  • -Xss:设置每个线程的堆栈大小。

总结

本篇主要说明 JVM 内存结构及其概念,意在让大伙了解 JVM 结构是个什么样子,而了解 JVM 是如何管理内存、如何处理 Java 程序运行所产生的数据,可以启发我们在内存管理、性能分析和调优方面的思维。作为 Java 开发者,想要强化个人的技术,扩展自己的思维,JVM 是一道必须攻破的关卡。

参考资料

花絮

本篇是我第一次编写的博客文章,之所以以 JVM 知识为开头,也是因为对于 JVM 的内容一直都是仅凭记忆,就导致容易遗忘,而且很多知识点都是零零散散,没有连结成知识网,也没有做一些知识记录,加上本人也想通过搭建一个博客网站,来总结自己一路学习的技术和知识,并分享给别人,因此便有了这个开头。

博客的编写,尤其是技术类,要考虑的细节还是挺多,如技术原理、常用实现方式、个人实际使用经验、流行程度、更新迭代等。总的来说,这对于我个人的知识总结能力也大有脾益,对一门新技术,我崇尚的是从不会、到了解掌握、再到能讲解给别人听让别人理解。

路漫漫其修远兮,吾将上下而求索。