通常情况下,我们可以把 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”。