运行时内存详解
运行时数据区总体组成上节已经介绍过。接下来需要讨论的问题有两个:
- 有哪些数据区会出现内存溢出?
- JDK不同版本间运行时内存区域的实现有哪些变化?
1. 哪些数据区会内存溢出?
先给出答案,会出现内存溢出(OOM)的区域有:
- 本地方法栈
- 虚拟机栈 ^6c721d
- 方法区
- 堆 ^947181
- 直接内存(准确来说,不属于JVM运行时数据区)
也就是说只有程序计数器不会出现内存溢出。
1.1 堆内存溢出
也是最常见的内存溢出。指的是在堆上分配的对象空间超过了堆的最大大小,导致内存溢出。溢出时,会抛出OutOfMemoryError,并提示Java Heap Space导致,也就是常说的OOM。如下图所示:

可以使用
-Xmx参数设置堆的最大大小,如:-Xmx500m,即代表设置堆的最大内存大小为500m。
1.2 栈内存溢出
是第二常见的内存溢出情况。指的是所有栈帧的占用超过了栈空间的最大值。溢出后会抛出 StackOverflowError 。如下图所示:

栈空间的最大值可以使用
-Xss进行设置,如-Xss512k代表最大值为512k。
1.3 方法区内存溢出
方法区溢出是指方法区中存放的内容,如类的元信息超过了方法区内存的最大值。超过后,会抛出 OutOfMemoryError,并提示Metaspace溢出或者PermGen Space溢出。
由于JDK7之前的版本,方法区使用永久代实现,JDK8及以后使用元空间实现。因此:
JDK7之前,可以使用-XX:MaxPermSize={Value}来配置最大值,
JDK8及之后,使用-XX:MaxMetaspaceSize={Value}来实现
1.4 直接内存
直接内存的溢出是指申请的直接内存空间超过了最大值,溢出后会抛出 OutOfMemoryError ,并提示Direct buffer memory区域出现溢出。如图:

可以使用
-XX:MaxDirectMemorySize={Value}设置最大值。
1.5 程序计数器为什么不会出现内存溢出?
程序计数器就是一个用于存储当前线程执行的字节码位置的寄存器。
- 占用的内存大小是固定的,即在32位JVM中占4个字节,在64位JVM中占8个字节。
- 程序计数器不涉及对象的销毁和创建,因此它的大小不会因为对象的创建和销毁而影响。
2. JDK不同版本间运行时内存区域的实现有哪些变化?
JDK的内存区域在各个版本中的实现整体保持一致,主要区别在于方法区。由于方法区是《Java虚拟机规范》中设计的虚拟概念,因此每款Java虚拟机在实现上都各不相同。本文中,主要讨论HotSpot的设计。
在HotSpot中:
- JDK7及其之前的版本中,将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机的参数来控制。
- JDK8及其之后的版本中,将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,如果不手动设置其最大大小,那么只要不超过操作系统的承受上限,可以一直分配。
如下图所示:

2.1 为什么使用元空间来代替永久代?
- 方法区空间可以动态分配,在不超过操作系统上限的情况下,可以一直分配,不回出现OOM的问题。如果在永久代,由于其存在于堆上,有堆内存上限。
比如,在启动项目时,无法预估class类到底有多少,这些类的元信息、常量等到底会占用多大内存,如果预估错误就可能造成内存不足或者内存浪费,需要对永久代不断调优。而改用元空间,就可以避免这个问题。
- 提高 Full GC 的效率。 在永久代中,Full GC 的触发比较频繁,而且效率较低。因为永久代中存放了很多 JVM 需要的类信息,这些数据大多数是不会被清理的,所以 Full GC 往往无法回收多少空间。但在元空间模型中,由于字符串常量池已移至堆中,静态变量也移至 Java 堆或者本地内存,因此可以更有效地进行垃圾回收,避免了因频繁的 Full GC 导致的性能影响。
2.2 字符串常量位置的变化
在早起,字符串常量池属于运行时常量池的一部分,他们的存储位置也是一致的。后续做出了调整,将字符串常量池和运行时常量池做了拆分。迭代步骤如下:
- JDK7之前: 运行时常量池逻辑包含字符串常量池,hotspot虚拟机对方法区的实现位永久代;
- JDK7: 字符串常量池被从方法去拿到堆中,运行时常量池剩下的东西还在永久代中;
- JDK8之后: hotspot移除了永久代改用元空间实现,字符串常量池依然在堆中。
2.3 字符串常量池为什么移动到堆中?
字符串常量池移动到堆中的原因如下:
- 垃圾回收优化: 由于字符串常量池的回收逻辑与对象的回收逻辑类似,当内存不足时,如果字符串常量池中的常量不被使用就可以被回收。而方法区中的类的元信息回收逻辑更加复杂。将字符串常量池移动到堆中后,可以利用对象的垃圾回收器对其进行回收,提高GC效率;
- 使方法区空间大小更加可控: 由于项目中,类的元信息不会占用过大的空间,因此方法区的空间一般设置的上限都不会太大。如果字符串常量池在方法区中存储,如果大量地向常量池中存放字符串常量,就可能造成方法区的空间大小不可控;
- 对
intern()方法进行优化: 在JDK6中,intern()方法会把第一次遇到的字符串实例复制到永久代的字符串常量池中。JDK7之后,把字符串常量池放入堆中之后,便可以将这个复制的操作省掉,提高了intern()方法的效率。