JVM 直接内存的使用与回收

1. 直接内存介绍

1.1 简介

直接内存主要被 Java NIO 使用,某种程度上也就是指DirectByteBuffer对象占用的堆外内存。DirectByteBuffer对象创建时会通过Unsafe类接口直接调用操作系统的malloc分配内存,然后将内存的起始地址和大小保存下来,据此就可以直接操作内存空间

DirectByteBuffer使用直接内存的原因有两点:

  1. 这块内存真正的分配并不在 Java 堆中,堆中只有一个很小的对象引用,这种方式能减轻 GC 压力
  2. 对于堆内对象,进行IO操作(Socket、文件读写)时需要先把对象复制一份到堆外内存再写入 Socket 或者文件,而当 DirectByteBuffer 就在堆外分配内存时可以省掉一次从堆内拷贝到堆外的操作,性能表现会更好
DirectByteBuffer(int cap) {
    
    
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    // 申请堆外内存,会显式调用 System.gc()
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
    
    
       // 内存空间的起始地址
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
    
    
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
    
    
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
    
    
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

1.2 直接内存的回收

需注意堆外内存并不直接控制于JVM,这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,而 Young GC 的时候只会将年轻代里不可达的DirectByteBuffer对象及其直接内存回收,如果这些对象大部分都晋升到了年老代,那么只能等到Full GC的时候才能彻底地回收DirectByteBuffer对象及其关联的堆外内存。因此,堆外内存的回收依赖于 Full GC

  • Full GC一般发生在年老代垃圾回收或者代码调用System.gc的时候,依靠年老代垃圾回收触发 Full GC,进而实现堆外内存的回收显然具有太大的不确定性。如果年老代一直不进行垃圾回收,那么堆外内存就得不到回收,机器的物理内存可能就会被慢慢耗光。为了避免这种情况发生,可以通过参数-XX:MaxDirectMemorySize来指定最大的直接内存大小,当其使用达到了阈值的时候将调用System.gc来做一次Full GC,从而完成可控的堆外内存回收。这样做的问题在于,堆外内存的回收依赖于代码调用 System.gc,而 JVM 参数-XX:+DisableExplicitGC会导致System.gc等于一个空函数,根本不会触发Full GC,这样在使用NettyNIO 框架时需注意是否会因为这个参数导致直接内存的泄露

  • -XX:MaxDirectMemorySize 参数没有指定的话,那么根据directMemory = Runtime.getRuntime().maxMemory()最大直接内存的值和堆内存大小差不多

  • NIO 中的 FileChannel、SocketChannel等Channel在通过IOUtil进行非DirectBuffer(如文件读写使用 HeapByteBuffer) IO读写操作时,底层会使用一个临时的DirectByteBuffer 来和系统进行真正的IO交互。为提高性能,使用完后这个临时的DirectByteBuffer会被缓存到ThreadLocal当直接使用 IOUtil 操作非DirectBuffer 的线程数较多或者 IO 操作的数据量较大时,会导致临时的DirectByteBuffer 占用大量堆外内存造成内存泄露-Djdk.nio.maxCachedBufferSize=500000(注意是字节数,不能用m、k、g)可以限制被缓存的DirectByteBuffer 的大小,超过这个限制的 DirectByteBuffer 不会被缓存到 ThreadLocalbufferCache 中,这样就能被GC正常回收掉

2. 查看直接内存

2.1 API 获取 MaxDirectMemory 的值

@Test
public void test() {
    
    

    // 申请直接内存,单位 B
    ByteBuffer.allocateDirect(60 * 1024 * 1024);
    // 运行时可用的最大内存
    System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024.0);
    // 获取 -XX:MaxDirectMemorySize
    System.out.println(VM.maxDirectMemory() / 1024.0 / 1024.0);
    // NIO 缓存池申请的直接内存大小,也就是 ByteBuffer 申请的内存
    System.out.println(SharedSecrets.getJavaNioAccess().getDirectBufferPool().getTotalCapacity() / 1024.0 / 1024.0);

}

2.2 NMT查看 DirectMemory 使用情况

JDK 1.7 之后使用 jmd 命令可以查看直接内存的使用情况,但是必须先通过 JVM 启动参数打开本地内存追踪(NMT),通常情况是使用命令 java -XX:NativeMemoryTracking=summary ..... -jar xxx.jar 来重新启动服务

-XX:NativeMemoryTracking=[off | summary | detail]
off: 默认关闭
summary | detail: 打开,会带来5%-10%的性能损耗,不建议在生产环境使用

经过添加启动参数重启后,就可以使用 jcm < pid > VM.native_memory scale=MB 命令来查看 Java 服务进程的内存占用了,一个使用示例如下:可知堆内存只有 64M,但是加上各种堆外内存占用后,整个 Java 进程申请的内存达到了 265M

扫描二维码关注公众号,回复: 12014384 查看本文章
  • committed 申请的可用内存并不是直接占用了物理内存,由于操作系统的内存管理是惰性的,对于已申请的内存虽然会分配地址空间,但并不会直接占用物理内存,真正使用的时候才会映射到实际的物理内存,所以 committed > res也是很可能的
// scale 表示显示内存的单位,默认为 KB,此处指定 MB
jcmd 4089 VM.native_memory scale=MB

4089:
Native Memory Tracking:
// 统计情况:保留的内存 1501M,实际申请为 265M
Total: reserved=1501MB, committed=265MB
                   // 堆内存大小 64M
-                 Java Heap (reserved=64MB, committed=64MB)
                            (mmap: reserved=64MB, committed=64MB)
                    // 加载 12939 个类,占用内存 82M,这部分在堆外 MetaSpace 中
-                     Class (reserved=1097MB, committed=82MB)
                            (classes #12939)
                            (malloc=11MB #19115)
                            (mmap: reserved=1086MB, committed=70MB)
                    // 总计 46 个线程,占用内存 45M,堆外栈内存
-                    Thread (reserved=45MB, committed=45MB)
                            (thread #46)
                            (stack: reserved=45MB, committed=45MB)
                     // JIT 编译产生的缓存代码,占用堆外内存 29M
-                      Code (reserved=249MB, committed=29MB)
                            (malloc=5MB #7681)
                            (mmap: reserved=244MB, committed=24MB)
                        // GC 占用 12M 内存,堆外
-                        GC (reserved=12MB, committed=12MB)
                            (malloc=10MB #255)
                            (mmap: reserved=2MB, committed=2MB)
                   // 通常为 NIO 使用的直接内存,占用 11M 堆外内存
-                  Internal (reserved=11MB, committed=11MB)
                            (malloc=11MB #16351)
                     // 常量池及字符串变量占用的内存, 18M 堆外内存
-                    Symbol (reserved=18MB, committed=18MB)
                            (malloc=15MB #163002)
                            (arena=3MB #1)
     // 开启 NMT 本身使用的内存,3M 堆外内存
-    Native Memory Tracking (reserved=3MB, committed=3MB)
                            (tracking overhead=3MB)

猜你喜欢

转载自blog.csdn.net/weixin_45505313/article/details/105310477
今日推荐