返回

JVM Native内存追踪: malloc vs mmap 原理详解与优化

Linux

JVM Native 内存追踪:mmap vs malloc

在 Java 虚拟机 (JVM) 的 native 内存追踪中,我们常常会看到 mallocmmap 这两个术语。它们代表 JVM 从操作系统申请内存的不同方式,了解它们之间的区别对于理解 JVM 内存使用情况和排查相关问题至关重要。

理解 malloc 和 mmap

简而言之,malloc 是 C 标准库提供的动态内存分配函数。当 JVM 需要小块内存时,会调用 malloc 向系统堆请求分配。而 mmap 是一种更底层的系统调用,可以将文件或其他资源映射到进程的地址空间中。对于大块内存分配,特别是有持久化需求的情况下, JVM 会倾向于使用 mmap

在 native 内存追踪报告中,malloc 对应的是通过 C 标准库的 malloc 分配的内存;mmap 通常用于映射堆、代码段、元空间(Metaspace)或其他需要映射的文件区域。 Committed memory 可以认为是实际占用的物理内存大小,与reserved memory 是不同的概念。

为什么会出现 malloc + mmap 等于 committed memory? 因为对于JVM而言, malloc 也是调用系统方法分配的内存,而这些最终都要反映在系统级的 committed 内存中, 这里的 malloc 是对堆区的小块内存分配。所以他们都包含在committed memory 内。

为什么会同时出现 malloc 和 mmap?

JVM 不会仅用一种方式申请内存, 而是根据内存的大小和用途选择最佳方式。 一般而言:

  • malloc: 适合分配少量内存,例如创建对象时所需的内存空间。它可以复用堆中已释放的小内存块,所以开销较小,分配效率高。
  • mmap: 用于映射内存,常见用途:
    • Java 堆 : 用于存储对象实例。使用 mmap 意味着这块区域实际上映射到某个匿名内存区域或临时文件。这部分通常占用较大的 committed memory.
    • 元空间(Metaspace) : 存放类和元数据信息。通过 mmap 映射,可以快速进行访问和更新。
    • 代码段 : 动态加载的 native 代码(JNI) 可能也会用mmap 映射到内存空间。
    • JNI Native 内存分配 : JNI中分配的内存可能会选择 mmap。

在实际运行中,你会看到如报告所示 mmap reserved 的内存通常大于 committed 内存, 这是因为 mmap 分配的空间常常先预留(reserved)一部分,而不是立即实际分配,这个预留的过程会占用系统的地址空间。 物理内存占用(committed)会随着实际需求而增长。 malloc 则没有此特点。

如何分析和优化 mmap/malloc 的使用

分析JVM 的 native 内存分配可以从以下几个角度入手。

1. 检查 Java 堆大小

  • 确认 Java 堆 (-Xms-Xmx) 设置是否合理。 过小的堆空间可能会频繁触发 GC ,导致内存压力增大,出现 mmap 的提交。
  • 检查最大堆内存 Xmx ,是否和报告中 Java Heap的 reserved/commited 值一致。
  • 操作步骤 : 修改启动参数: -Xms4g -Xmx4g (假设分配 4GB 堆)。

2. 分析 Metaspace 的使用情况

  • 元空间是类加载和方法区存放元数据的区域,如果其 committed 增长过快,应该分析应用是否有内存泄漏。 可以尝试调整其最大值,例如增大或限制Metaspace的使用大小,防止该区域的无限制增长。
  • 监控并排查 Metaspace 是否有加载太多类而造成占用过大的现象,检查是否有内存泄漏的风险。
  • 操作步骤 : 使用 JVM 参数 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m 来控制 Metaspace 的大小,并配合 JConsole 或 JMX 工具监控 Metaspace 占用。

3. 代码优化

  • 减少对象创建,尤其是在高频执行的代码路径上。 可以使用对象池, 缓存机制减少不必要的内存消耗。

  • 合理使用集合,选择适合的数据结构。

  • 注意 JNI 代码, 尽量减少在 JNI 中进行大量内存分配的操作,避免 JNI native 内存泄漏问题,导致 mmap 的异常占用。

  • 代码示例 :

    // 对象池的简单示例
    import java.util.concurrent.ArrayBlockingQueue;
    
    class ObjectPool<T> {
        private ArrayBlockingQueue<T> pool;
        private  ObjectGenerator<T>  generator ;
        public  interface ObjectGenerator <T>{
           T generate ();
        }
    
       public ObjectPool(int poolSize, ObjectGenerator <T>  generator){
           this.pool = new ArrayBlockingQueue<>(poolSize);
           this.generator = generator;
           for( int i = 0;i<poolSize;i++){
               pool.offer(generator.generate());
           }
       }
       public T acquire() throws InterruptedException{
          return pool.take();
       }
    
        public void release(T object){
            pool.offer(object);
        }
    
    }
    
    
    public class Example {
    
       static class  Data{
         //
        }
    
        public static void main(String[] args) throws InterruptedException {
            // 使用对象池
            ObjectPool <Data> pool  =  new ObjectPool(10 , ()->new Data());
             Data data =  pool.acquire();
            // process the object;
             pool.release(data);
    
    
            // 直接创建
           for( int i =0;i <10000 ; i++){
               Data data1 =  new Data(); //可能频繁GC,并且对象会被频繁回收
           }
        }
    
    }
    

4. 分析native代码分配

  • 对于分配的较大的 Native 内存, 可以通过跟踪JNI中的分配,来确定是由那些代码分配的,确定是否有泄露风险。
    * 也可以使用 Valgrind 等工具定位内存泄露或者未释放的问题。

5. 使用JVM参数调整Native memory setting.

  • 使用 JVM 参数调整堆外内存, Metaspace分配。例如,-XX:MaxDirectMemorySize 控制 DirectByteBuffer 能够申请的最大堆外内存大小.
  • jcmd <pid> VM.native_memory summary 可以打印当前应用的 native 内存使用情况,用于分析。
**命令行示例:** 

 ```bash
jcmd <pid> VM.native_memory summary # 显示当前JVM 的内存报告

 ```

安全提示

  • 生产环境不应直接使用默认参数。需要充分考虑生产环境负载,并且监控GC 以及 Native memory 使用。
  • 避免内存过度分配,限制相关内存区域的最大大小。
  • 及时排查代码中可能存在的内存泄漏。

分析 JVM 的 mallocmmap 内存分配, 是一个细致的工作。它能帮助理解内存分配行为,并识别可能存在的内存瓶颈,有助于优化 JVM 应用的内存使用和运行效率。