JVM篇:内存分区及作用及各部分可能发生的异常

一、运行时数据区总览

二、JVM 内存分区及异常

1. 程序计数器(Program Counter Register)
  • 作用:记录当前线程执行字节码的地址(行号),保证线程切换后能恢复到正确位置。

  • 特点:线程私有,唯一无内存溢出的区域。

  • 异常。由 JVM 规范严格管理,不会发生内存溢出。


2. 虚拟机栈(Java Virtual Machine Stack)
  • 作用:存储方法调用的栈帧(局部变量表、操作数栈、动态链接、方法出口等)。

  • 特点:线程私有,栈帧的入栈/出栈对应方法的调用/结束。

  • 异常

    • StackOverflowError:当栈深度超过 JVM 允许的阈值(如无限递归)。

    • OutOfMemoryError:当栈尝试动态扩展但内存不足(少见,通常由操作系统限制导致)。


3. 本地方法栈(Native Method Stack)
  • 作用:为 Native 方法(如 C/C++ 实现的方法)服务,功能类似虚拟机栈。

  • 特点:线程私有,具体实现依赖 JVM 厂商。

  • 异常 :同虚拟机栈,可能出现 StackOverflowErrorOutOfMemoryError


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 设置的值。

常见异常场景与解决方案

  1. 堆内存溢出(OOM)

    • 原因:内存泄漏(如未关闭的集合引用)或堆设置过小。

    • 解决 :检查代码,使用 -Xmx 增大堆大小,使用内存分析工具(如 MAT)定位泄漏。

  2. 栈溢出(StackOverflowError)

    • 原因:无限递归或方法调用链过长。

    • 解决 :修复递归终止条件,或通过 -Xss 增大栈容量(需谨慎)。

  3. 元空间溢出(Metaspace OOM)

    • 原因:动态生成大量类(如反射、CGLib 代理)。

    • 解决 :调整 -XX:MaxMetaspaceSize,优化代码减少类加载。

  4. 直接内存溢出

    • 原因 :频繁分配未回收的 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 垃圾回收机制的管理,需要手动调用 DirectByteBuffercleaner() 方法或者使用 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 问题和自旋开销。

相关推荐
xiaolingting3 小时前
JVM层面的JAVA类和实例(Klass-OOP)
java·jvm·oop·klass·instanceklass·class对象
神仙别闹10 小时前
基于Python+Sqlite实现的选课系统
jvm·python·sqlite
上分小子2.014 小时前
jvm-Java虚拟机
java·开发语言·jvm
5xidixi14 小时前
JAVA EE初阶 JVM
java·jvm·java-ee
北城以南没有天16 小时前
排查JVM的一些命令
jvm
BUG研究员_1 天前
JVM深入理解
java·jvm·学习
小梁不秃捏1 天前
JVM 类加载器深度解析(含实战案例)
jvm·类加载器
Anarkh_Lee1 天前
图解JVM-2. 类加载子系统
java·jvm·后端
舰长1151 天前
快速定位并优化CPU 与 JVM 内存性能瓶颈
jvm