一、运行时数据区总览

二、JVM 内存分区及异常
1. 程序计数器(Program Counter Register)
-
作用:记录当前线程执行字节码的地址(行号),保证线程切换后能恢复到正确位置。
-
特点:线程私有,唯一无内存溢出的区域。
-
异常 :无。由 JVM 规范严格管理,不会发生内存溢出。
2. 虚拟机栈(Java Virtual Machine Stack)
-
作用:存储方法调用的栈帧(局部变量表、操作数栈、动态链接、方法出口等)。
-
特点:线程私有,栈帧的入栈/出栈对应方法的调用/结束。
-
异常:
-
StackOverflowError
:当栈深度超过 JVM 允许的阈值(如无限递归)。 -
OutOfMemoryError
:当栈尝试动态扩展但内存不足(少见,通常由操作系统限制导致)。
-
3. 本地方法栈(Native Method Stack)
-
作用:为 Native 方法(如 C/C++ 实现的方法)服务,功能类似虚拟机栈。
-
特点:线程私有,具体实现依赖 JVM 厂商。
-
异常 :同虚拟机栈,可能出现
StackOverflowError
或OutOfMemoryError
。
4. 堆(Heap)
-
作用:存储对象实例和数组(几乎所有对象都在堆中分配)。
-
特点:线程共享,垃圾回收(GC)的主要区域,分代管理(新生代、老年代)。
-
异常:
-
OutOfMemoryError: Java heap space
:对象过多或内存泄漏(如未释放大对象引用)导致堆空间耗尽。 -
OutOfMemoryError: GC Overhead limit exceeded
:GC 频繁执行但无法释放足够内存(通常因内存泄漏)。
-
5. 方法区(Method Area)
-
实现演变:
-
Java 7 及之前:永久代(PermGen),受 JVM 内存限制。
-
Java 8 及之后:元空间(Metaspace),使用本地内存(Native Memory)。
-
-
作用:存储类元信息(Class 结构)、常量池、静态变量、JIT 编译后的代码等。
-
异常:
-
OutOfMemoryError: PermGen space
(Java 7 及之前):加载过多类或大量动态代理类。 -
OutOfMemoryError: Metaspace
(Java 8 及之后):元空间本地内存不足。
-
6. 直接内存(Direct Memory)
-
作用 :通过
DirectByteBuffer
分配的非堆内存,用于 NIO 操作(如文件/网络 IO)。 -
特点:不受 JVM 堆限制,但受操作系统内存限制。
-
异常:
OutOfMemoryError: Direct buffer memory
:直接内存分配超过-XX:MaxDirectMemorySize
设置的值。
常见异常场景与解决方案
-
堆内存溢出(OOM):
-
原因:内存泄漏(如未关闭的集合引用)或堆设置过小。
-
解决 :检查代码,使用
-Xmx
增大堆大小,使用内存分析工具(如 MAT)定位泄漏。
-
-
栈溢出(StackOverflowError):
-
原因:无限递归或方法调用链过长。
-
解决 :修复递归终止条件,或通过
-Xss
增大栈容量(需谨慎)。
-
-
元空间溢出(Metaspace OOM):
-
原因:动态生成大量类(如反射、CGLib 代理)。
-
解决 :调整
-XX:MaxMetaspaceSize
,优化代码减少类加载。
-
-
直接内存溢出:
-
原因 :频繁分配未回收的
DirectByteBuffer
。 -
解决 :检查代码是否显式调用
System.gc()
或调整-XX:MaxDirectMemorySize
。
-
总结
分区 | 存储内容 | 线程属性 | 异常类型 |
---|---|---|---|
程序计数器 | 字节码执行地址 | 线程私有 | 无 |
虚拟机栈 | 方法栈帧 | 线程私有 | StackOverflowError/OOM |
本地方法栈 | Native 方法栈帧 | 线程私有 | StackOverflowError/OOM |
堆 | 对象实例 | 线程共享 | OOM: Java heap space/GC Overhead |
方法区(元空间) | 类元信息、常量池等 | 线程共享 | OOM: Metaspace/PermGen space |
直接内存 | NIO 缓冲区 | 线程共享 | OOM: Direct buffer memory |
理解这些分区及其异常有助于优化内存配置和排查问题,例如通过 JVM 参数调优(如 -Xmx
、-Xss
、-XX:MaxMetaspaceSize
)和代码优化避免内存泄漏。
三、附加说明
JVM 本地方法栈
主要作用
JVM 本地方法栈(Native Method Stack)是 Java 虚拟机(JVM)为执行本地方法(Native Method)而准备的内存区域。本地方法是使用非 Java 语言(如 C、C++)编写的方法,这些方法可以通过 Java 的本地接口(JNI,Java Native Interface)来调用。本地方法栈的主要作用是为本地方法的执行提供栈帧(Stack Frame),用于存储局部变量、操作数栈、动态链接和方法返回地址等信息,确保本地方法能够像 Java 方法一样按照栈的规则进行调用和返回。
存储内容
- 栈帧 :每个本地方法在执行时都会在本地方法栈中创建一个栈帧,栈帧是方法执行的基本单位。栈帧主要包含以下几个部分:
- 局部变量表:用于存储方法的局部变量,包括基本数据类型和对象引用。
- 操作数栈:用于在方法执行过程中进行数据的运算和传递。
- 动态链接:指向运行时常量池中该方法的引用,用于在方法调用时进行动态绑定。
- 方法返回地址:记录方法执行完毕后返回的位置。
示例说明
以下是一个简单的 Java 代码示例,使用了本地方法:
public class NativeMethodExample {
// 声明一个本地方法
public native void nativeMethod();
static {
// 加载本地库
System.loadLibrary("NativeLibrary");
}
public static void main(String[] args) {
NativeMethodExample example = new NativeMethodExample();
example.nativeMethod();
}
}
在这个示例中,nativeMethod()
是一个本地方法。当 main
方法调用 nativeMethod()
时,JVM 会在本地方法栈中为 nativeMethod()
创建一个栈帧,用于存储该方法执行过程中的相关信息。同时,JVM 会通过 JNI 调用本地库(如 NativeLibrary
)中的实现代码。
直接内存
主要作用
直接内存(Direct Memory)并不是 JVM 运行时数据区的一部分,但它也被频繁使用。直接内存的主要作用是提供一种直接访问系统物理内存的方式,避免了 Java 堆和本地内存之间的数据复制,从而提高了数据的读写性能。在一些需要进行大量数据传输的场景中,如网络编程、文件 I/O 等,使用直接内存可以显著减少内存复制的开销,提高程序的执行效率。
存储内容
直接内存主要存储一些需要与外部系统进行交互的数据,例如:
- 网络数据缓冲区:在网络编程中,使用直接内存作为网络数据的缓冲区,可以避免数据在 Java 堆和内核空间之间的多次复制,提高网络数据的传输效率。
- 文件映射缓冲区:在进行文件 I/O 操作时,可以使用直接内存将文件映射到内存中,直接对内存进行读写操作,从而提高文件的读写性能。
示例说明
以下是一个使用 ByteBuffer
分配直接内存的示例:
import java.nio.ByteBuffer;
public class DirectMemoryExample {
public static void main(String[] args) {
// 分配 1MB 的直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// 向直接内存中写入数据
for (int i = 0; i < 1024; i++) {
buffer.put((byte) i);
}
// 重置缓冲区的位置
buffer.flip();
// 从直接内存中读取数据
while (buffer.hasRemaining()) {
System.out.print(buffer.get() + " ");
}
}
}
在这个示例中,使用 ByteBuffer.allocateDirect()
方法分配了 1MB 的直接内存。通过直接内存进行数据的读写操作,可以避免数据在 Java 堆和本地内存之间的复制,提高了数据的读写效率。
需要注意的是,直接内存的分配和回收不受 JVM 垃圾回收机制的管理,需要手动调用 DirectByteBuffer
的 cleaner()
方法或者使用 System.gc()
触发系统的垃圾回收来释放直接内存。如果使用不当,可能会导致直接内存泄漏,影响系统的性能。
十、堆的公共内存并发分配内存时候使用到的CAS原理
在 Java 堆分配内存时,CAS(Compare-And-Swap)是一种重要的无锁算法,用于在多线程环境下安全地进行内存分配操作。下面详细介绍 Java 堆分配内存时用到的 CAS 原理。 ### 1. CAS 基本概念 CAS 是一种原子操作,它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当内存位置 V 的值与预期原值 A 相同时,才会将内存位置 V 的值更新为新值 B;否则,操作失败。这个过程是原子性的,即在执行过程中不会被其他线程中断。 ### 2. Java 堆内存分配场景中 CAS 的应用 在多线程环境下,多个线程可能同时请求在 Java 堆中分配内存。为了避免线程安全问题,传统的做法是使用锁机制,但锁会带来一定的性能开销。而 CAS 提供了一种无锁的解决方案,提高了内存分配的效率。 #### 2.1 线程本地分配缓冲(TLAB) Java 虚拟机(JVM)为每个线程分配了一个小的内存区域,称为线程本地分配缓冲(TLAB)。线程在 TLAB 内分配内存时,通常不需要使用 CAS 操作,因为 TLAB 是线程私有的,不会出现多线程竞争的情况。但当 TLAB 空间不足时,线程需要从堆的公共区域分配内存,这时就可能会使用 CAS 操作。 #### 2.2 公共堆内存分配 当线程的 TLAB 空间不足,需要从堆的公共区域分配内存时,多个线程可能会同时竞争同一块内存。为了保证内存分配的原子性,JVM 可以使用 CAS 操作。 以下是一个简化的示例,说明 CAS 在内存分配中的应用: ```java import java.util.concurrent.atomic.AtomicInteger; // 模拟 Java 堆内存管理器 class HeapMemoryManager { // 模拟堆内存指针,指向当前可分配内存的起始位置 private AtomicInteger memoryPointer; // 模拟堆内存大小 private final int heapSize; public HeapMemoryManager(int heapSize) { this.heapSize = heapSize; this.memoryPointer = new AtomicInteger(0); } // 分配指定大小的内存 public int allocate(int size) { while (true) { // 获取当前内存指针的值 int currentPointer = memoryPointer.get(); // 计算分配后的新指针位置 int newPointer = currentPointer + size; // 检查是否有足够的内存 if (newPointer > heapSize) { return -1; // 内存不足 } // 使用 CAS 操作尝试更新内存指针 if (memoryPointer.compareAndSet(currentPointer, newPointer)) { return currentPointer; // 分配成功,返回分配的内存起始地址 } // CAS 操作失败,说明有其他线程已经修改了内存指针,重试 } } } public class MemoryAllocationExample { public static void main(String[] args) { HeapMemoryManager manager = new HeapMemoryManager(1024); int address = manager.allocate(100); if (address != -1) { System.out.println("内存分配成功,起始地址: " + address); } else { System.out.println("内存不足,分配失败"); } } } ``` ### 3. 代码解释 - `AtomicInteger` 是 Java 提供的一个原子类,它内部使用 CAS 操作来保证对整数的原子更新。 - `memoryPointer` 表示当前可分配内存的起始位置。 - `allocate` 方法用于分配指定大小的内存。在方法内部,首先获取当前内存指针的值,然后计算分配后的新指针位置。接着,使用 `compareAndSet` 方法尝试更新内存指针。如果更新成功,说明内存分配成功;否则,说明有其他线程已经修改了内存指针,需要重试。 ### 4. CAS 的优缺点 #### 优点 - **无锁机制**:避免了锁的开销,提高了并发性能。 - **原子性**:保证了操作的原子性,避免了多线程竞争带来的线程安全问题。 #### 缺点 - **ABA 问题**:如果一个值从 A 变为 B,再从 B 变回 A,CAS 操作会认为值没有发生变化,从而可能导致意外的结果。 - **自旋开销**:如果 CAS 操作失败,线程需要不断重试,可能会消耗大量的 CPU 资源。 综上所述,CAS 在 Java 堆分配内存时起到了重要的作用,通过无锁机制提高了内存分配的效率。但在使用时需要注意其可能带来的问题,如 ABA 问题和自旋开销。