如果应用占用的内存在不断地增长,但是堆和元空间等区域都没有明显变化,那么大概率可能和堆外内存的使用有关。所以我们应该考虑有哪些情况会使用到堆外内存。
那么,有哪些场景会使用到堆外内存呢?
Java NIO的ByteBuffer.allocateDirect在使用直接内存时,如果没有正确释放或及时回收,可能导致内存泄露。
当我们使用ByteBuffer buffer = ByteBuffer.allocateDirect(1024)
分配内存时,会在堆外占用1k的内存,同时会在堆上创建一个ByteBuffer对象,当然这个对象只占用一个对象的指针引用的大小。
当我们在做堆内存分析的时候,如果发现堆上有大量的ByteBuffer对象,虽然他们自身占用的内存空间很小,但是他们关联的堆外内存可能会很大。所以这时候就需要考虑ByteBuffer导致的内存泄露的问题。
除了NIO会使用堆外内存,很多缓存框架也会用堆外内存的,如Ehcache、MapDB、OHC等,他们都会使用堆外内存来提升存储空间以及减少对GC的影响。
我们都知道,这些缓存是有一些缓存过期策略的,但是如果没有设置合理,那么就可能导致长期无法被清理,大量占用堆外内存。
现在, 很多日志框架都支持Memory Mapped File Appender(内存映射)这种日志记录方式,它利用了操作系统的内存映射文件特性来实现日志写入。这种方法通过将文件内容映射到进程的地址空间,允许应用程序像访问内存一样直接访问文件内容,从而提高了写入性能。
在使用内存映射记录日志时,它将日志先写入到映射到内存的文件中。这个过程利用内存映射文件的高效性,减少了实际的磁盘I/O操作。然后再由操作系统定时将内存中的更改同步回磁盘文件。
而这部分使用的内存就是堆外内存。随着日志文件的增长,映射到内存中的大小也会增长。所以会导致我们看到的应用占用的内存的增长。
还有需要注意的是,除了我们应用程序自己的日志,很多框架或者web容器也会有自己的日志缓存的机制。
如果应用程序通过Java本地接口(JNI)调用本地代码或库,那么这些本地操作可能会分配额外的内存,而这部分内存不会在JVM堆内存或元空间中反映出来。所以,也需要检查一下程序中是不是有这种代码。
除了堆内内存、堆外内存,还需要考虑一下栈内存,每个线程都有自己的线程栈,随着线程数量的增加,线程栈所占用的内存也会增加。这方面也需要考虑一下。