对于 C/C++ 开发者而言,内存是一项令人头痛的"权利",开发者有权对每个对象分配不同的内存,只要他们想,但能力越大责任自然也越大,分配了的内存如果不及时收回,如果下一个对象没有空间对其进行分配,导致内存泄漏和内存溢出问题。
而对于 Java 开发者而言,JVM 存在自动内存管理机制,开发者也就不用再去纠结内存的分配和回收,但如果 Java 程序出现内存泄漏和内存溢出的问题而不知道怎么解决就头疼了,所以需要了解 JVM 是如何使用内存,如何分配内存的。
运行时数据区域
当 Java 程序开始运行时,JVM 会将 OS 分配给 Java 的内存空间分成若干个不同的块,而每个块都具有各自的用途

程序计数器(Program Counter Register)
每个线程都有一个私有的程序计数器
程序计数器像是乐队的指挥家,在线程中去管理代码执行,分支,循环,跳转,异常处理,都需要程序计数器来完成,一般而言,程序计数器中不会产生 OutOfMemoryError(内存溢出错误)
原因是因为:
- 空间固定且极小:它所占用的内存大小在虚拟机实现时就已经确定(通常就是一个指针的大小),并且不随程序运行而改变。
- 存储内容简单:它仅存储一个指向方法区中字节码的地址(或偏移量),不存储复杂的、可动态增长的数据结构。
Java 虚拟机栈(Java VM Stack)
每个方法被执行时,JVM 都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、 方法出口等信息。每一个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈从入栈到出栈的过程
局部变量表会在编译时期创建,当方法入栈后会根据局部变量表来分配局部变量槽,当进入一个方法,这个方法需要在栈帧中分配多大的局部变量是完全确定的,所以一般而言运行时一个方法内的局部变量槽的个数是不变的
当线程请求的栈深度大于虚拟机所允许的深度会抛出 StackOverflowError(栈溢出错误)
如果栈的大小是可变的,那么当栈扩展时无法申请到足够的内存就会产生 OOM
局部变量表
局部变量表内存放了编译期可知的各种 JVM 基本数据类型、对象引用类型和 returnAddress类型
- 基本数据类型: 四类八种,byte、 short、 int、 long、 float、 double、 char、 boolean
- 对象引用类型: reference 类型,一个指向对象首地址的指针,也可能是指向一个代表对象的句柄或者其他于此对象相关的位置
- returnAddress: 指向了一条字节码指令的地址,也就是调用方法的返回值返回地址
局部变量槽
数据类型在局部变量表中的存储空间以局部变量槽表示,其中 64 位的 long 和 double 会用到两个局部变量槽
本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈相似,不过虚拟机栈执行的是 Java 方法也就是字节码服务,而本地方法栈则是为虚拟机使用本地方法服务,本地方法服务既是 Java 线程请求 OS 服务的方法服务代码
和虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常
Java 堆(Java Heap)
堆是所有 Java 线程之间共享的一块大的内存区域,所有的对象实例以及数组几乎都在堆上分配。
Java 堆是垃圾收集器管理的内存区域。
从内存分配的角度来看,在 Java 堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB),以提升对象分配时的效率。
将 Java 堆细分只是为了更好地回收内存或者更快地分配内存。
Java 堆的空间大小是可变的。
当堆扩展时申请不到空间会抛出 OutOfMemoryError 异常
方法区(Method Area)
Java 8 开始,废除了永久代,改用元空间
元空间是方法区的一个实现,方法区是元空间的抽象,二者的关系接口与实现的关系,或者说是抽象概念与具体落地之间的关系。
方法区和堆一样是线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区存放的是"设计图纸"(类元信息),堆中存放的是根据图纸造出来的"实物"(对象实例)。
方法区包括以下内容
- 类信息(Class Metadata)
这是最基础的"图纸"信息,JVM 加载一个 .class 文件后,会提取以下信息存入:
类的全限定名:比如 com.example.Person。
父类的全限定名:比如 java.lang.Object。
类的修饰符:是 public?abstract?还是 final?
接口列表:这个类实现了哪些接口。 - 运行时常量池(Runtime Constant Pool)
这是方法区里非常重要的一部分。
字面量:代码中写的死数值,比如文本字符串 "Hello World"、整数 100、final 常量等。
符号引用:这是代码逻辑的"占位符"。比如代码里写了 Date d = new Date(),在编译时,字节码里并不知道 Date 类的真实物理内存地址,只能用"java.util.Date"这个符号字符串来代替。等到运行时,JVM 会去常量池查表,把这个符号引用替换成真实的内存入口地址。 - 静态变量(Static Variables)
被 static 关键字修饰的变量。
特点:它们不属于某个具体的对象(不属于饼干),而是属于类本身(属于模具的属性,比如模具的生产日期)。
注:在 JDK 7 及以后的 HotSpot 虚拟机实现中,静态变量和字符串常量池从方法区移到了堆中,但在逻辑规范上,它们仍属于方法区的一部分。 - 方法的代码(Method Code)
这是真正的"操作指南"。
类中定义的所有方法(包括构造方法、普通方法)的字节码指令都存在这里。
当你调用 p.sayHello() 时,线程的程序计数器就会指向这里面的地址,开始一行行读取指令执行。
当方法区申请不到空间时会抛出 OutOfMemoryError 异常
直接内存(Direct Memory)
直接内存不是虚拟机运行时数据区的一部分,但是这部分内存也会被频繁地使用。
例如 Java 在使用 NIO 后分配的堆外内存就位于直接内存中,这样子提高了性能
不过直接内存在动态修改时如果申请不到空间会产生 OutOfMemoryError 异常