原生内存(堆外内存)

在JVM使用的内存中,通常堆消耗的部分最多,但是JVM也会为内部操作分配一些内部。这些非堆内存就是原生内存。

应用中可以通过JNI的malloc()类似的方法或者是使用NIO的API分配。JVM使用的原生内存和堆内存的总量,就是一个应用总的内存占用(Footprint)。

1、测量内存占用

unix系统中,像top和ps这样的程序可以给出基本数据;

window系统中,可以使用perfmon或VMMap;

何种平台,都需要看看进程实际分配的内存(这与保留的内存完全不同)。分配内存(也叫提交内存),保留内存(有时也叫作虚拟内存)。

分配内存和保留内存区别:

通过示例说明,JVM必须告知操作系统,它的堆可能需要多达2GB的内存,所以会保留这么多内存:操作系统承诺,当JVM因为要增加堆而尝试分配额外的内存时,这些内存时可以获取到的。最初分配的内存时512MB,而且这就是实际用到的全部内存。也即是提交内存。提交内存的量随堆的重新调整而波动;特别是,提交内存会随着堆的增加而相应增加。

线程栈是个例外。JVM每次创建线程时,操作系统分配一些原生内存来保存线程栈,向线程提交更多内存(至少要等到线程退出)。线程栈是在创建时全部分配的。

2、原生NIO缓冲区

开发者 可以通过JNI调用来分配原生内存,但是如果NIO字节缓冲区是通过allocateDirect()方法创建的,则也会分配原生内存。从性能角度看,原生字节缓冲区非常重要,因为它们支持原生代码和Java代码在不复制的情况下共享数据。

 调用allocateDirect()方法非常昂贵,所以应该尽可能重用直接字节缓冲区。理想的情况是,线程是独立的,而且每个线程持有一个直接字节缓存区作为线程局部变量。直接字节缓冲区的对象池可能更有用。

字节缓存区也可以切割管理。应用可以分配一个非常大的直接字节缓冲区,然后每个请求使用ByteBuffer类的slice()方法从中分配一部分。如果每次不能分配相同大小,这种方案也很难处理:会形成碎片。而且不像堆那样还可以对碎片进行压缩,字节缓冲区中的不同片段是无法压缩的,所以只有当所有片段大小都相同时,这种解决方案才好用。

直接字节缓冲区所分配的内存总量:可以通过设置-XX:MaxDirectMemorySize=N标志来指定。从JAVA7开始,这个标志默认值为0,以为没有限制(当然受制于地址空间大小,以及操作系统对进程的各种限制)。

3、原生内存跟踪

从java8开始,借助-XX:NativeMemoryTracking=off|summary|detail这个选项,JVM支持我们一窥它是如何分配原生内存的。原生内存跟踪(Native Memory Tracking,NMT)默认是关闭的(off)。如果开启了概要模式(summary)或详情模式(detail),可以随时通过jcmd命令获得原生内存的信息:

jcmd process_id VM.native_memory summary

如果JVM是使用-XX:+PrintNMTStatistics参数(默认false)启动的,它会在程序退出时打印原生内存分配信息。

示例:

package com.dxz.jvm;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 1堆溢出信息
 * @VM args:-verbose:gc -Xms20M -Xmx20M -XX:+PrintGCDetails
 * 
 * 2原生内存信息打印
 * @VM args:-verbose:gc -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UnlockDiagnosticVMOptions  -XX:+PrintNMTStatistics -XX:NativeMemoryTracking=summary
 */
public class HeapOutOfMemory {

    public static void main(String[] args) {
        List<TestObject> cases = new ArrayList<TestObject>();
        HeapOutOfMemory heapOutOfMemory = new HeapOutOfMemory();
        int i = 0;
        while (i <10) {
            cases.add(heapOutOfMemory.new TestObject());
            i++;
        }
        try {
            TimeUnit.SECONDS.sleep(60);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public class TestObject {
        private double a = 34.53;
        private Integer b = 9999999;
    }
}

 

用jcmd

D:\workspace\study\target>jcmd 11532 VM.native_memory summary
11532:

Native Memory Tracking:

Total: reserved=1361449KB, committed=62425KB
-                 Java Heap (reserved=20480KB, committed=20480KB)
                            (mmap: reserved=20480KB, committed=20480KB)

-                     Class (reserved=1062013KB, committed=10109KB)
                            (classes #421)
                            (malloc=5245KB #154)
                            (mmap: reserved=1056768KB, committed=4864KB)

-                    Thread (reserved=15423KB, committed=15423KB)
                            (thread #16)
                            (stack: reserved=15360KB, committed=15360KB)
                            (malloc=45KB #82)
                            (arena=18KB #30)

-                      Code (reserved=249636KB, committed=2572KB)
                            (malloc=36KB #314)
                            (mmap: reserved=249600KB, committed=2536KB)

-                        GC (reserved=6667KB, committed=6611KB)
                            (malloc=5771KB #117)
                            (mmap: reserved=896KB, committed=840KB)

-                  Compiler (reserved=132KB, committed=132KB)
                            (malloc=2KB #22)
                            (arena=131KB #3)

-                  Internal (reserved=5380KB, committed=5380KB)
                            (malloc=5316KB #1365)
                            (mmap: reserved=64KB, committed=64KB)

-                    Symbol (reserved=1495KB, committed=1495KB)
                            (malloc=943KB #111)
                            (arena=552KB #1)

-    Native Memory Tracking (reserved=38KB, committed=38KB)
                            (malloc=3KB #37)
                            (tracking overhead=35KB)

-               Arena Chunk (reserved=185KB, committed=185KB)
                            (malloc=185KB)


D:\workspace\study\target>

 

说明:

Total: reserved=1361449KB(保留内存), committed=62425KB(提交内存)
-                 Java Heap (reserved=20480KB(20MB,同参数设置一致), committed=20480KB)
                            (mmap: reserved=20480KB, committed=20480KB)
堆使用情况:
Java Heap (reserved=20480KB, committed=20480KB)
                            (mmap: reserved=20480KB, committed=20480KB)
保留内存为20MB与设置的相同。

-                     Class (reserved=1062013KB, committed=10109KB)
                            (classes #421)
                            (malloc=5245KB #154)
                            (mmap: reserved=1056768KB, committed=4864KB)
用于保存类的元数据的原生内存。classes #421是实际用于保存程序中的421个类而占用的内存相比,JVM保留的内存要更多。

-                    Thread (reserved=15423KB, committed=15423KB)
                            (thread #16)
                            (stack: reserved=15360KB, committed=15360KB)
                            (malloc=45KB #82)
                            (arena=18KB #30)
thread #16,表示16个线程,分配的总内存有15423KB,平均一个线程是1MB。

-                      Code (reserved=249636KB, committed=2572KB)
                            (malloc=36KB #314)
                            (mmap: reserved=249600KB, committed=2536KB)
JIT的代码缓存:根据上面的类数量来的,所以内存占用不是很多。

-                        GC (reserved=6667KB, committed=6611KB)
                            (malloc=5771KB #117)
                            (mmap: reserved=896KB, committed=840KB)
GC算法的处理锁使用的一些堆外空间。


-                  Compiler (reserved=132KB, committed=132KB)
                            (malloc=2KB #22)
                            (arena=131KB #3)
这个区域是供编译器自身操作使用的,这与生成的代码放在代码缓存中是不同的。

-                    Symbol (reserved=1495KB, committed=1495KB)
                            (malloc=943KB #111)
                            (arena=552KB #1)

保留字符串(Interned String)的引用与符号表引用放在这里。

-    Native Memory Tracking (reserved=38KB, committed=38KB)
                            (malloc=3KB #37)
                            (tracking overhead=35KB)
NMT本身的操作也需要一些空间。


NMT跟踪

NMT也支持跟踪内存分配随时间的变化情况。如果JVM在启动时启用了NMT,可以使用如下命令确定内存的基线使用情况:

jcmd process_id VM.native_memory baseline

4、针对不同操作系统优化JVM

JVM可以利用一些调优选项来优化操作系统内存的使用。

4.1、大页

java支持-XX:+UseLargepages选项。其默认值跟具体的操作系统配置有关。在windows上,必须在操作系统中启动大页。(默认使用常规页)

在linux上,UseLargepages默认不会启用,要支持大页,需要配置一下操作系统。

在Solaris上,不要什么操作系统方面配置,默认启用大页。

linux大页

linux透明页

大页大小

 

5、压缩的oop
oop:ordinary object pointer,即普通对象指针,JVM将其用作对象引用的句柄。

 

### ### JVM 堆外内存溢出排查方法 JVM 堆外内存溢出(Off-Heap Memory Leak)是指 Java 应用程序在堆之外的内存区域(如直接内存、元空间、线程栈等)持续增长,最终导致系统内存耗尽或抛出 `OutOfMemoryError`。由于堆外内存不受垃圾回收机制直接管理,其排查难度通常高于堆内存问题。 #### 1. 使用 Native Memory Tracking(NMT)监控原生内存 JVM 提供了 `-XX:NativeMemoryTracking` 参数用于跟踪 JVM 内部及本地库的内存使用情况,可帮助识别堆外内存的分配源头。例如: ```bash -XX:NativeMemoryTracking=summary ``` 启动应用后,可通过 `jcmd` 命令查看内存使用摘要: ```bash jcmd <pid> VM.native_memory summary ``` 输出内容将展示 JVM 各个子系统(如 Java Heap、Class、Thread、Code、GC 等)的内存使用情况,帮助定位异常增长的模块[^4]。 #### 2. 分析堆外内存分配源头 堆外内存主要包括以下几种类型: - **直接内存(Direct Buffer)**:通过 `ByteBuffer.allocateDirect()` 分配,常用于 NIO 操作,不受堆内存限制。 - **元空间(Metaspace)**:存放类元数据,替代了永久代(PermGen),默认无上限,需通过 `-XX:MaxMetaspaceSize` 设置上限。 - **线程栈(Thread Stack)**:每个线程默认分配一定大小的栈空间(如 1MB),线程数过多可能导致内存溢出。 - **JNI 或 native 代码分配的内存**:如第三方库或 native 方法中未释放的资源。 通过 NMT 的详细输出,可以判断是哪一部分内存增长异常,进而结合代码逻辑分析是否为内存泄漏[^4]。 #### 3. 生成并分析堆外内存快照 对于某些堆外内存泄漏问题,如直接内存泄漏,可通过工具捕获堆外内存快照。例如: - 使用 `jmap` 查看堆内存对象分布(不适用于堆外内存)。 - 使用 `jstack` 分析线程栈,查看是否有大量线程创建或阻塞。 - 使用 `Native Memory Tracker` 的 `summary` 或 `detailed` 模式获取更细粒度的内存分配信息。 #### 4. 控制堆外内存使用 由于堆外内存无法通过 JVM 参数直接限制,建议采取以下措施: - 对直接内存设置上限,使用 `-Dio.netty.maxDirectMemory`(适用于 Netty 等框架)。 - 在容器化部署时(如 Kubernetes),通过 `limit` 限制 Pod 的总内存,防止因堆外内存导致 OOMKilled。 - 避免在业务节点中进行大量计算或频繁创建线程,建议将业务节点与计算节点分离部署,确保业务稳定性[^4]。 #### 5. 工具辅助分析 - **VisualVM**:支持查看线程、堆内存、GC 情况,也可加载 heap dump 文件。 - **JProfiler**:提供堆外内存分析功能,支持实时监控线程栈和直接内存分配。 - **MAT(Memory Analyzer)**:主要用于堆内存分析,但结合线程栈信息也可辅助排查堆外问题。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值