文章目录
- [第2章 Java内存区域与内存溢出异常](#第2章 Java内存区域与内存溢出异常)
-
- [2.0 个人感悟](#2.0 个人感悟)
- [2.1 概述](#2.1 概述)
- [2.2 运行时数据区](#2.2 运行时数据区)
-
- [2.2.1 程序计数器(Program Counter Register)](#2.2.1 程序计数器(Program Counter Register))
- [2.2.2 虚拟机栈(Virtual Machine Stack)](#2.2.2 虚拟机栈(Virtual Machine Stack))
- [2.2.3 本地方法栈(Native Method Stacks)](#2.2.3 本地方法栈(Native Method Stacks))
- [2.2.4 Java堆](#2.2.4 Java堆)
- [2.2.5 方法区(Method Area)](#2.2.5 方法区(Method Area))
- [2.2.6 运行时常量池(Runtime Constant Pool)](#2.2.6 运行时常量池(Runtime Constant Pool))
- [2.2.7 直接内存(Direct Memory)](#2.2.7 直接内存(Direct Memory))
- [2.3 HotSpot 虚拟机对象探秘](#2.3 HotSpot 虚拟机对象探秘)
-
- [2.3.1 对象的创建](#2.3.1 对象的创建)
- [2.3.2 对象的内存布局](#2.3.2 对象的内存布局)
- [2.3.3 对象的访问定位](#2.3.3 对象的访问定位)
- [2.4 实战:OutOfMemoryError异常](#2.4 实战:OutOfMemoryError异常)
-
- [2.4.1 Java堆溢出](#2.4.1 Java堆溢出)
- [2.4.2 直接内存溢出](#2.4.2 直接内存溢出)
- [2.5 知识点补充](#2.5 知识点补充)
-
- [2.5.1 构造函数和\<init\>()方法](#2.5.1 构造函数和<init>()方法)
第2章 Java内存区域与内存溢出异常
Java与C++之间有一堵由动态分配和垃圾收集技术所围成的高墙,墙外边的人想进去,墙里面的人却想出来。
2.0 个人感悟
- 这章主要讲了JVM的内存模型框架,各个区域是做什么的,哪些情况会发生溢出
- 心态很重要。看过记不住是常态,验证和猜想对不住是常态,找个资料找半天也是常态。工作的复杂度比学习高得多。调整好心态慢慢来
- 广度和深度的平衡。JVM的知识点很多,hotspot就有好多代,学习不可能面面俱到。先构建一个知识框架,实际遇到问题再详细钻研,可能是一个好的平衡状态
- 学知识的快乐和痛苦,有时候真的难以表达的。我在一些文章中感受到过,如果自己也能传递一些,那也是极好的
2.1 概述
java自动内存管理是把双刃剑。了解它,便于排查错误、修正问题。这一章主要介绍java内存区域的划分、作用和涉及到的异常。
2.2 运行时数据区
结构如图:

2.2.1 程序计数器(Program Counter Register)
介绍:
- 是一块较小的内存空间,是当前线程所执行的字节码的行号指示器
- 线程私有区域,每个线程都有自己独立的程序计数器
- 程序控制流的指示器,完成分支、循环、跳转、异常处理、线程恢复等基础流程控制
异常:
- 此区域《Java虚拟机规范》中没有规定OutOfMemoryError情况
2.2.2 虚拟机栈(Virtual Machine Stack)
介绍:
- 线程私有
- 每个方法被执行的时候,都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等
- 每一个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
- 局部变量表存放了编译器可知的各种java基本类型数据、对象引用。这些数据以局部变量槽(Slot)表示,其中64位的long和double类型的数据占用两个变量槽,其余的数据类型只占一个。
异常:
- 线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError
- 如果栈可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError
2.2.3 本地方法栈(Native Method Stacks)
介绍:
- 同虚拟机栈,服务于本地方法
异常:
- 同虚拟机栈
2.2.4 Java堆
介绍:
- 运行时数据区中内存最大的区域
- 存放对象实例
- 线程共享
- 可以处于物流商不连续的空间中,但逻辑上它应该被视为连续的
- 可以被实现为固定大小的,也可以是扩展的
异常:
- 无内存分配,并且也无法扩展时,抛出OutOfMemoryError异常
2.2.5 方法区(Method Area)
介绍:
- 线程共享
- 存储加载的类型信息、常量、静态变量、即时编译后的代码缓存数据
- 关于永久代说明:
永久代是垃圾分代回收理论中的概念;《Java虚拟机规范》中只定义了方法区,未约束其实现;永久代是方法区的一种实现方式,jdk6之前的hotspot就是这种实现,慢慢过度,到jdk8正式废除永久代,使用元空间代替
异常:
- 方法区无法满足新的内存分配需求时,将抛出OutOfMemonryError
2.2.6 运行时常量池(Runtime Constant Pool)
介绍:
- 运行时常量池是方法区的一部分
- Class文件包含的常量池表(Constant Pool Table),存放编译期生成的各种字面量与符号引用,在类加载后存放到方法区的运行时常量池中
- 具备动态性,运行期间也可以将新的常量放入池中
异常:
- 同方法区。因为是方法区的一部分
2.2.7 直接内存(Direct Memory)
介绍:
- 直接内存不是运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
- 比如NIO,底层是使用native方法直接分配堆外内存,Java堆中的DirectByteBuffer对象作为这块内存的引用。优点是可以避免在java堆中和native堆中来回复制数据(传统IO)。可以理解为在公司外租了个仓库
- 本机直接内存的分配不会受到Java堆大小限制,但会受到本机总内存大小显示及处理器寻址空间的限制
异常:
- 当使用的直接内存超过本机限制并且无法扩展时,出现OutOfMemoryError
2.3 HotSpot 虚拟机对象探秘
2.3.1 对象的创建
整体流程:
类加载检查 --> 分配内存 --> 初始化零值 -->设置对象信息 --> 执行<init>()方法--> 返回对象引用
1.类加载检查
遇到字节码new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个类是否已经被加载、解析和初始化过。
- 如果类尚未加载,则执行类加载过程
- 如果类已经加载,则进行下一步
2.分配内存
类加载检查完成后,则开始为对象分配内存。对象所需要的空间大小在类加载完成后便可以完全确定
**分配方式:
- 指针碰撞(Bump The Pointer)。假设堆内存空间绝对规整,那么分配内存仅仅是将指针向空闲空间方向挪动一段对象所需空间大小一样的距离
- 空闲列表(Free List)。如果堆中内存并不是规整的,已使用和空闲交错在一起,就需维护一个列表,用来记录哪些可用,哪些已被使用。在分配的时候,在列表中登记信息
线程安全方案: 由于创建对象的行为非常频繁,即使仅仅修改一个指针所指向位置,并发场景下也不是线程安全的。
- 同步处理。堆分配内存空间动作进行同步处理,实际上虚拟机是采用CAS(Compare And Swap,比较并交换)+重试机制保证更新操作的原子性
- 本地线程缓冲区(Thread Local Allocation Buffer,TLAB)。每个线程预先在堆中分配一小块私有内存,线程先在自己的TLAB中分配对象,TLAB用完后才需要同步锁定新的内存块
3.初始化零值
内存分配完成后,JVM需要将分配到的内存空间(不包含对象头)都初始化为零值(int为0,boolean为false,引用为null等)
4.设置对象信息
初始化零值后,还要堆对象进行必要的设置,通常有:对象的类型指针、哈希码、对象的GC分代年龄等
5. 执行<init>()方法
上面的工作都完成后,从JVM角度来说一个新的对象已经产生了。但从Java程序而言,对象的创建才刚开始,接下来要class文件中的<init>()方法,包括:
- 父类构造器的调用
- 示例变量初始化的代码
- 构造方法内的代码块
2.3.2 对象的内存布局
在HotSpot虚拟机中,对象在堆中的存储布局可以划分为3部分:对象头、实例数据和对齐填充
对象头(Header):主要包括两类信息
- 运行时数据(Mark Word)。如哈希码(HashCode)、GC分代年龄、锁状态标志、线程只有的锁、偏向线程ID、偏向时间戳等
- 类型指针。对象指向它的类型元数据的指针
- 数组长度。仅针对数组类型
实例数据 :对象真正存储的有效信息,即程序中所定义的各种类型的字段内容。
JVM会对字段存储顺序进行优化,字段存储顺序受虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在源码中定义的顺序影响。HotSpot虚拟机默认的分配顺序为longs/double、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Points),可以看到相同宽度的字段总是被分配到一起存放
对齐填充 : 并不是必然存在的,也没有特别的含义,它仅仅是占位符的作用。
HotSpot虚拟机的自动内存管理系统要求对象的初始地址必须是8字节的整数倍,换句话意思是任何对象的大小都必须是8字节的整数倍。么有对需要对齐填充来补齐
2.3.3 对象的访问定位
通过引用访问对象的主流方式有两种:
- 句柄访问:堆中划分出一块内存来作为句柄池,引用中存储的是句柄地址,句柄中包含了对象实例数据和类型数据的地址信息。
- 优点:对象被移动时(垃圾收集时移动对象是非常普遍的行为)时只用改变实例数指针,引用本身并不会被修改
- 缺点:方位对象需要二次定位,速度稍慢
- 直接指针:引用中直接存储对象的地址,对象中包含类型信息引用。
- 优点:速度快,节省了一次指针定位的开销
- 缺点:对象移动时需要同步跟新引用的地址信息
句柄访问示意

直接指针访问示意

2.4 实战:OutOfMemoryError异常
这里实战了下堆异常和直接内存溢出,书中示例了本地方法栈、方法区溢出,大家有兴趣可以看看。
2.4.1 Java堆溢出
构造异常思路: Java堆溢出的场景在上面说过,需要的内存超出堆的容量并且无法扩展时,会溢出。构造的思路,通过参数限制堆大小,然后代码不断创建对象。
遇到堆OOM分析思路:
- 定界:通过内存快照分析工具堆dump出的堆快照进行分析。确认是内存泄漏(Memeory Leak)还是内存溢出(Memeory Overflow)
- 内存泄漏:通过工具查看泄漏对象到GC Roots的引用链,找到没有被垃圾收集器回收的原因。
- 内存溢出:意味着内存空间中的对象应该存活,那么需要优化代码或者加大内存空间
实战 :
JVM参数:找不到设置可以看这个JVM学习问题记录(1) IDEA2025设置JVM启动参数
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
说明:
-Xms20m 最小堆大小设置20m
-Xmx20m 最大堆大小设置20m
-XX:+HeapDumpOnOutOfMemoryError 出现堆溢出时,dump出当前内存快照,便于分析
代码:
java
public class HeapOOM {
static class OOMObject {
}
public static void main() {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
控制台信息: 可以看到抛出了OOM,导出了快照文件,默认位置在项目路径下
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid16728.hprof ...
Heap dump file created [30206770 bytes in 0.014 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3513)
at java.base/java.util.Arrays.copyOf(Arrays.java:3482)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:244)
at java.base/java.util.ArrayList.add(ArrayList.java:483)
at java.base/java.util.ArrayList.add(ArrayList.java:496)
at jvm.chapter2.HeapOOM.main(HeapOOM.java:32)
Process finished with exit code 1
Eclipse Memory Analyzer结果也与预期一致

2.4.2 直接内存溢出
构造异常思路: 之前介绍过直接内存的含义,构造异常思路也简单,通过JVM参数限制直接内存大小,然后分配超出限制的内存。
实战:
JVM参数:
-Xmx20m -XX:MaxDirectMemorySize=10m
参数说明:
-Xmx20m 堆最大内存
-XX:MaxDirectMemorySize=10m 直接内存最大值
代码示例: 为了便于理解,这里使用的NIO方式,而不是书中的Unsafe示例。注意
ByteBuffer.allocateDirect()方法是向操作系统申请内存,不足就会抛出异常,而不是先操作内存
java
public class DirectMemoryOOM {
private static final int ONE_M = 1024 * 1024;
static void main() {
List<ByteBuffer> buffers = new ArrayList<ByteBuffer>();
int time = 0;
try {
while (true) {
ByteBuffer buffer = ByteBuffer.allocateDirect(ONE_M);
buffers.add(buffer);
System.out.println("Allocated 1MB direct buffer, total: " +
++time + " MB");
}
} catch (OutOfMemoryError e) {
System.err.println("OutOfMemoryError: " + e.getMessage());
e.printStackTrace();
}
}
}
运行结果:
Allocated 1MB direct buffer, total: 1 MB
Allocated 1MB direct buffer, total: 2 MB
Allocated 1MB direct buffer, total: 3 MB
Allocated 1MB direct buffer, total: 4 MB
Allocated 1MB direct buffer, total: 5 MB
Allocated 1MB direct buffer, total: 6 MB
Allocated 1MB direct buffer, total: 7 MB
Allocated 1MB direct buffer, total: 8 MB
Allocated 1MB direct buffer, total: 9 MB
Allocated 1MB direct buffer, total: 10 MB
OutOfMemoryError: Cannot reserve 1048576 bytes of direct buffer memory (allocated: 10485760, limit: 10485760)
java.lang.OutOfMemoryError: Cannot reserve 1048576 bytes of direct buffer memory (allocated: 10485760, limit: 10485760)
at java.base/java.nio.Bits.reserveMemory(Bits.java:178)
at java.base/java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:111)
at java.base/java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:360)
at jvm.chapter2.DirectMemoryOOM.main(DirectMemoryOOM.java:26)
2.5 知识点补充
2.5.1 构造函数和<init>()方法
构造方法:是java源代码中类的初始化方法
<init>()方法:是java编译器将源代码编译为字节码后,在.class文件中生成的方法
联系 :编译器会将以下三部分代码按顺序合并到<init>方法中:
- 父类构造器调用 :如果当前类不是
Object,会先插入super()或指定的父类构造函数调用 - 实例变量初始化和实例初始化块:按它们在源代码中出现的顺序插入对应的赋值代码
- 构造方法中的显式代码:程序员写在构造函数体中的语句