说一说 JVM 内存区域划分,哪些区域可能发生 OOM

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

通常情况下,我们可以把 JVM 的内存区域划分为以下几个部分,其中,有的区域是以线程作为单位,而有的区域则是整个 JVM 进程唯一的:

  • 1.程序计数器

在 JVM 规范中,每个线程都有自己的程序计数器,并且任何时间一个线程只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 java 方法的 JVM 指令的地址;但是,如果正在执行的是本地方法,则未指定值。

  • 2.Java 虚拟机栈

虚拟机栈,早期也被称之为 Java 栈。每个线程在被创建时,都会创建一个虚拟机栈,其内部保存了一个个栈帧,对应着一次次 Java 方法的调用。

在上面的计数器中,我们提到了任何一个时间点,一个线程只能执行一个方法,也就是当前方法,类似的,任何一个时间点,一个线程只会有一个活动栈帧,通常叫当前帧,方法所在的类为当前类。如果说该方法中又调用了其他方法,对应的会创建新的栈帧,这个栈帧成为了新的当前帧,一直到它返回结果或者执行结束为止。

JVM 直接对 Java 栈的操作只有两个,即对栈帧的压栈出栈

栈帧中存储着局部变量表,操作数栈,动态链接,方法正常退出或者异常退出的定义等。

  • 3.堆

堆是 Java 内存管理的核心区域,用来放置 Java 实例对象。堆被所有的线程共享,在启动虚拟机时,我们可以通过指定 -Xmx 来指定最大的堆空间等指标。

堆也是垃圾回收器重点照顾的对象,所以堆内空间还会被不同的垃圾回收器进一步细分,其中最有名的就是新生代,老年代的划分。

  • 4.方法区

方法区也是被所有线程共享的一块内存区域,用于存储元数据,例如类的结构信息,以及对应的运行时常量池,字段,方法代码等。

在早期的 JVM 实现中,很多人习惯将方法区称为永久代,但是在 JDK 8 中已经将其删除,同时新增了元数据区

  • 5.运行时常量池

它是方法区的一部分。如果你有分析过反编译的类文件结构,你能看到版本号,字段,方法,父类,接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息。

  • 6.本地方法栈

它和虚拟机栈很类似,支持对本地方法的调用,同样也是每个线程创建一个。

下面上一张 JVM 内存区域划分的图:

哪些区域可能发生 OOM(Out of memory)?

内存溢出通俗的讲就是内存不够用了,并且 GC 通过垃圾回收也无法提供更多的内存。实际上除了程序计数器,其他区域都有可能发生 OOM, 简单总结如下:

  • 堆内存不足是最常见的 OOM 原因之一,抛出错误信息 java.lang.OutOfMemoryError:Java heap space,原因也不尽相同,可能是内存泄漏,也有可能是堆的大小设置不合理。

  • 对于虚拟机栈和本地方法栈,导致 OOM 一般为对方法自身不断的递归调用,且没有结束点,导致不断的压栈操作。类似这种情况,JVM 实际会抛出 StackOverFlowError , 但是如果 JVM 试图去拓展栈空间的时候,就会抛出 OOM.

  • 对于老版的 JDK, 因为永久代大小是有限的,并且 JVM 对老年代的内存回收非常不积极,所以当我们添加新的对象,老年代发生 OOM 的情况也非常常见。

  • 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的 OOM 有所改观,出现 OOM,异常信息则变成了:“java.lang.OutOfMemoryError: Metaspace”。