java内存区域
Java 内存区域
编者注:本文为历史博文归档;涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效,阅读时请注意时效性。
Java 虚拟机(JVM)管理的内存区域主要分为以下五部分:
- 线程共享区域:方法区(Method Area)、堆(Heap)。
- 线程私有区域:程序计数器(Program Counter Register)、本地方法栈(Native Method Stack)、虚拟机栈(VM Stack)。

程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
- 每一条 JVM 线程都有自己的程序计数器,各条线程之间互不影响、独立存储,这类内存区域被称为“线程私有”内存。
- 在任意时刻,一条 JVM 线程只会执行一个方法的代码,该方法称为该线程的当前方法(Current Method)。
- 如果该方法是 Java 方法,程序计数器保存的是 JVM 正在执行的字节码指令的地址。
- 如果该方法是 native 方法,程序计数器的值则为 undefined(未定义)。
- 此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何
OutOfMemoryError情况的区域。
说明:Java 虚拟机的多线程是通过线程轮流切换并分配处理器时间来实现的。
Java 虚拟机栈(Java Virtual Machine Stack)
与程序计数器一样,Java 虚拟机栈也是线程私有的。每一个 JVM 线程都有自己的虚拟机栈,栈与线程同时创建,其生命周期与线程相同。
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈(如表达式 a+b 的语法树计算、进栈出栈操作)、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- JVM 栈可以被实现成固定大小,也可以根据计算动态扩展。
- 如果采用固定大小的 JVM 栈设计,那么每一条线程的 JVM 栈容量应该在线程创建时独立地选定。JVM 实现应该提供调节 JVM 栈初始容量的手段。
- 如果采用动态扩展和收缩的 JVM 栈方式,应该提供调节最大、最小容量的手段。
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出
StackOverflowError。 - 如果 JVM 栈可以动态扩展,但在尝试扩展时无法申请到足够的内存,则抛出
OutOfMemoryError。
本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈作用相似,后者为虚拟机执行 Java 方法服务,而前者为虚拟机用到的 Native 方法服务。
- 虚拟机规范对于本地方法栈中方法使用的语言、使用方式和数据结构没有强制规定。
- 有的虚拟机(比如 HotSpot)直接把本地方法栈和虚拟机栈合二为一。
- 该区域抛出的异常类型与虚拟机栈相同(
StackOverflowError或OutOfMemoryError)。
Java 堆(Java Heap)
Java 堆是虚拟机管理的内存中最大的一块,同时也是被所有线程所共享的。它在虚拟机启动时创建,其主要作用是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
- 堆中的对象被自动管理,也就是俗称的 GC(Garbage Collector,垃圾收集器) 所管理。开发者无需操心销毁回收的事儿。
- Java 堆的容量可以是固定大小,也可以随着需求动态扩展(通过
-Xms和-Xmx参数),并在不需要过多空间时自动收缩。 - Java 堆所使用的内存不需要保证是物理连续的,只要逻辑上是连续的即可。
- JVM 实现应当提供给程序员调节 Java 堆初始容量的手段;对于可动态扩展和收缩的堆,则应当提供调节其最大和最小容量的手段。
- 如果堆中没有内存完成实例分配并且堆也无法扩展,就会抛出
OutOfMemoryError。
方法区(Method Area)
方法区跟堆一样是被各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 虽然这个区域被虚拟机规范描述为堆的一个逻辑部分,但是它的别名叫“非堆”,用来与堆做区别。
- 方法区在虚拟机启动的时候创建。
- 方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。
- 方法区在实际内存空间中可以是不连续的。
- Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段;对于可以动态扩展和收缩的方法区,则应当提供调节其最大、最小容量的手段。
- 当方法区无法满足内存分配需求时,就会抛出
OutOfMemoryError。
示例:堆、栈和方法区之间的关系
以下代码示例说明了对象实例、类信息与引用变量在不同内存区域的分布:
public class Test2 {
public static void main(String[] args) {
Test2 t2 = new Test2();
// JVM 将 Test2 类信息加载到方法区
// new Test2() 实例保存在堆区
// Test2 引用 t2 保存在栈区
}
}运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。
- 对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。
- 一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
- 运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性。Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。这种特性被开发人员利用得比较多的便是
String类的intern()方法。 - 既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出
OutOfMemoryError异常。
直接内存(Direct Memory)
直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现。
- JDK 1.4 加入的 NIO 中,
ByteBuffer有个方法是allocateDirect(int capacity)。这是一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存。 - 然后通过一个存储在 Java 堆里面的
DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 - 显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括 RAM 及 SWAP 区或者分页文件)的大小及处理器寻址空间的限制。
- 服务器管理员配置虚拟机参数时,一般会根据实际内存设置
-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
参考资料
说明:本文内容主要基于 JDK 1.8 之前的经典 Java 内存模型(如永久代实现方法区)。在 JDK 1.8 及后续版本中,方法区的具体实现已变更为元空间(Metaspace),永久代已被移除,但内存区域的逻辑划分(如堆、栈、方法区概念)依然适用。具体参数与行为请以当前使用的 JDK 版本官方文档为准。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/java-nei-cun-qu-yu.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。