引言

在 Java 应用的内存管理中,Heap 、No-Heap 和 Off-Heap 是开发者优化性能和资源管理时不可忽视的关键组成部分。它们在 JVM 的运行中扮演着不同的角色,负责存储不同类型的数据结构和对象。随着现代应用程序的复杂性和规模不断提升,合理地分配和管理这三类内存,不仅可以提高系统的效率,还能在高并发、大数据处理等场景下有效避免性能瓶颈。

Heap 是 Java 应用最常使用的内存区域,所有动态创建的对象都存储在这里。然而,频繁的垃圾回收(GC)操作有时会带来延迟,影响应用的响应时间。为此,No-Heap 提供了一个独立的区域,用于存储类的元数据、线程栈和方法区数据,确保 JVM 稳定高效运行。而 Off-Heap 则是一个独立于 JVM 的内存空间,适合存储大数据和长生命周期的对象,减少垃圾回收的干扰。

理解和合理运用这三者之间的关系,能够帮助开发者在不同的应用场景中充分发挥内存管理的优势,实现高效的 Java 应用。

概览

以下是对 Heap、No-Heap 和 Off-Heap 三者在常见属性、功能和应用场景方面的对比:

属性/功能 Heap No-Heap Off-Heap
定义 JVM 中存储对象实例的内存区域 JVM 内的非堆内存区域(方法区、线程栈等) JVM 外部的内存,由开发者手动管理
管理方式 由 JVM 和 GC 自动管理 JVM 自行管理,开发者不可直接控制 手动分配和释放,独立于 JVM
GC 影响 受垃圾回收机制影响,可能导致性能抖动 不参与垃圾回收,减少 GC 开销 不受 GC 影响,提高高并发和大数据处理性能
存储内容 动态创建的对象实例 类的元数据、方法字节码、静态变量、线程栈等 大数据对象、长生命周期数据(如缓存、IO 数据)
性能 受 GC 影响,可能引发 STW 事件 性能较高,无 GC 影响 一般与类加载和线程栈管理相关
分配方式 自动分配,代码中通过 new 关键字创建对象 JVM 启动时分配,使用 JVM 内部机制 通过 ByteBuffer.allocateDirect() 或 JNI 分配
应用场景 普通 Java 对象存储,适合大多数业务逻辑处理 类加载、静态变量存储、线程栈管理 高性能缓存、I/O 操作、大数据处理,高并发系统

基础知识

Heap(堆内存)

Heap(堆内存)是 Java 虚拟机(JVM)用来存储所有对象实例和数组的内存区域。Java 中的对象在运行时通过 new 关键字动态创建,默认会存放在堆中。堆内存分为多个区域,用于管理对象的生命周期和垃圾回收机制。常见的区域包括:

堆内存的大小可以通过 JVM 参数 -Xms-Xmx 来手动配置,以适应不同的应用需求。

Heap 是 Java 中最常用的内存区域,适用于各种需要动态分配内存的场景。常见的使用场景包括:

heap 内存之所以这么常用,因为以下优点:

  1. 自动管理内存:JVM 自动管理堆内存中的对象分配和释放,开发者无需显式释放内存,避免了手动管理的复杂性。
  2. 方便对象共享:堆中的对象可以被多个线程访问和共享,适合多线程环境。
  3. 垃圾回收机制:JVM 提供了自动垃圾回收(GC)机制,自动清理不再使用的对象,减少内存泄漏风险。

虽然 heap 内存有很多的优点,但也不可避免存在下面几项缺点:

  1. GC 影响性能:当堆内存较大时,频繁的垃圾回收会导致应用程序出现性能抖动,尤其在大数据处理和高并发场景中,GC 停顿可能影响系统响应速度。
  2. 延迟问题:在高负载环境下,GC 可能会带来延迟,尤其是 Full GC 可能暂停整个应用的执行,造成卡顿。
  3. 内存碎片化:随着对象频繁分配和回收,堆内存可能出现碎片化,影响内存的高效利用。

Heap 内存在 Java 开发中占据核心地位,其便捷的对象存储方式和自动化内存管理非常适合大多数业务场景。然而,随着系统规模的扩大和并发量的增加,堆内存的垃圾回收开销可能成为性能瓶颈,需要结合 Off-Heap 等优化手段进行调整。

Off-Heap(堆外内存)

Off-Heap 是指 JVM 外部的内存,即不在 JVM 的堆区管理下的内存空间。通常由开发者手动管理,比如通过 DirectByteBufferUnsafe 类或使用第三方库(如 Netty、RocksDB)来分配和释放内存。

下面是 off-heap 的主要特性:

off-heap 内存的主要使用场景如下:

Off-Heap 优点:

Off-Heap 缺点:

No-Heap(非堆内存)

No-Heap(非堆内存)是 JVM 之外的内存区域,主要用于存储类元数据、静态变量、线程栈等信息。在 Java 8 之后,元空间(Metaspace)取代了早期的永久代(PermGen),成为 No-Heap 的重要部分。

No-Heap(非堆内存)的主要使用场景涉及存储 Java 虚拟机运行所需的元数据、线程栈和静态变量。它的使用场景主要体现在以下几方面:

  1. 类元数据存储:No-Heap 中的元空间(Metaspace)用来存储类的定义、方法元数据等信息。适用于大规模类加载场景,如微服务架构或动态生成大量类的框架(如 Hibernate、Spring)。
  2. 多线程应用:每个线程都有独立的栈空间存储线程调用栈信息,因此 No-Heap 对并发较高的应用至关重要,特别是在高并发场景下,如 Web 服务器和大型分布式系统。
  3. 静态变量管理:静态变量不存储在堆内存中,而是在 No-Heap 区域,适用于需要共享数据的应用场景,如全局配置、缓存类静态资源等。
  4. JVM 性能调优:对于 JVM 调优,合理配置 No-Heap 区域的元空间和栈大小可以防止内存溢出,并提高系统性能。

No-Heap 优点:

No-Heap 缺点:

申请内存实践

要向 Heap、Off-Heap 和 No-Heap 这三种内存区域申请内存,可以通过不同的方法来操作,以下是对应的具体代码示例:

Heap 内存申请

Heap 内存是 JVM 默认分配的内存区域,通常用于分配 Java 对象的内存。要向 Heap 申请内存,只需要创建 Java 对象即可,所有对象默认存储在堆中,由 JVM 垃圾回收器(GC)管理。

下面是个使用案例:

public class HeapMemoryExample {
    public static void main(String[] args) {
        // 创建对象,分配在 Heap 内存中
        String[] largeArray = new String[1000000];  // 分配大量对象,占用 heap
        for (int i = 0; i < largeArray.length; i++) {
            largeArray[i] = "String number " + i;  // 每个对象存储在 heap 内存中
        }
        System.out.println("在堆中为对象申请内存");
    }
}

Off-Heap 内存申请

Off-Heap 内存指的是不在 JVM 堆内存中分配的内存,通常是通过 Java NIO 的 DirectByteBuffer 或使用 Unsafe 类进行手动管理。堆外内存通常用于需要高性能的 I/O 操作或避免垃圾回收影响的场景。

使用 DirectByteBuffer 分配 Off-Heap 内存:

import java.nio.ByteBuffer;

public class OffHeapMemoryExample {
    public static void main(String[] args) {
        // 分配 10 MB 的堆外内存
        ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
        // 向堆外内存中写数据
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put((byte) i);
        }
        System.out.println("在堆外内存中申请内存");
    }
}

tips

释放堆外内存方式:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
sun.misc.Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
cleaner.clean();  // 立即释放堆外内存

还可以使用使用 Unsafe 类分配 Off-Heap 内存(不推荐用于生产环境):

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class OffHeapMemoryWithUnsafe {
    private static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        Unsafe unsafe = getUnsafe();
        long memoryAddress = unsafe.allocateMemory(1024 * 1024);  // 分配 1 MB 堆外内存
        unsafe.setMemory(memoryAddress, 1024 * 1024, (byte) 0);   // 初始化为 0
        System.out.println("不安全分配的堆外内存。");

        // 释放堆外内存
        unsafe.freeMemory(memoryAddress);
        System.out.println("已释放不安全的堆外内存。");
    }
}

tips

No-Heap 内存申请

No-Heap 内存包括 Metaspace、线程栈(Thread Stack)和 代码缓存(Code Cache)。Java 类的元数据存储在 Metaspace 中,而每个线程都有独立的栈空间。No-Heap 内存通常由 JVM 在运行时自动管理,开发者不能直接控制其分配。

在 Java 8 及以上版本,Metaspace 用于存储类的元数据。当类加载器加载一个类时,会将该类的元数据信息存放到 Metaspace 中。要增加 Metaspace 的使用,可以加载大量类。

import java.util.ArrayList;
import javassist.ClassPool;

public class MetaspaceMemoryExample {
    public static void main(String[] args) {
        // 使用 Javassist 工具库动态创建类,占用 Metaspace 空间
        ClassPool classPool = ClassPool.getDefault();
        ArrayList<Class<?>> classes = new ArrayList<>();

        try {
            for (int i = 0; i < 10000; i++) {
                // 动态创建类
                Class<?> newClass = classPool.makeClass("Class" + i).toClass();
                classes.add(newClass);  // 将类加载到 JVM Metaspace 中
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("通过加载许多类来分配元空间内存。");
    }
}

tips

使用线程栈来占用 no-heap 内存:每个 Java 线程启动时,JVM 会为其分配线程栈。线程栈大小可以通过 JVM 参数 -Xss 配置。增加线程栈的使用,可以通过创建大量线程来实现。

public class ThreadStackExample {
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(10000);  // 让线程等待,消耗栈内存
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        System.out.println("线程启动,堆栈内存分配。");
    }
}

tips

FunTester 原创精华

【连载】从 Java 开始性能测试


↙↙↙阅读原文可查看相关链接,并与作者交流