Java堆外內(nèi)存陷阱

最近在查一個(gè)堆外內(nèi)存泄露的問(wèn)題,通過(guò)-XX:MaxDirectMemorySize仍然限制不住堆外內(nèi)存的上漲,一直到機(jī)器物理內(nèi)存爆滿(mǎn),被oom killer。

上一篇關(guān)于MaxDirectMemorySize的設(shè)置中講過(guò)MaxDirectMemorySize限制了DirectByteBuffer的內(nèi)存申請(qǐng),但是它只是通過(guò)java.nio.Bits類(lèi)中reservedMemory,totalCapacity的值去做計(jì)算,真正申請(qǐng)堆外內(nèi)存空間的是sun.misc.Unsafe類(lèi)。也就是說(shuō),用戶(hù)完全可以通過(guò)其他方式使內(nèi)存泄露。

這里就遇到了一個(gè)由于句柄泄露導(dǎo)致內(nèi)存泄露的問(wèn)題。

88877.png

通過(guò)mat查看dump文件,
由于句柄泄露導(dǎo)致生成大量的EPollArrayWrapper對(duì)象。
這個(gè)對(duì)象占了多少內(nèi)存呢,來(lái)看下構(gòu)造函數(shù):

...省略大量代碼

static final int SIZE_EPOLLEVENT  = sizeofEPollEvent();
static final int NUM_EPOLLEVENTS  = Math.min(fdLimit(), 8192);

private static native int sizeofEPollEvent();
private static native int fdLimit();

EPollArrayWrapper() {
        // creates the epoll file descriptor
        epfd = epollCreate();

        // the epoll_event array passed to epoll_wait
        int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT;
        pollArray = new AllocatedNativeObject(allocationSize, true);
        pollArrayAddress = pollArray.address();

        for (int i=0; i<NUM_EPOLLEVENTS; i++) {
            putEventOps(i, 0);
            putData(i, 0L);
        }

        // create idle set
        idleSet = new HashSet<SelChImpl>();
}
...
class NativeObject {
  ...
  protected NativeObject(int var1, boolean var2) {
        if(!var2) {
            this.allocationAddress = unsafe.allocateMemory((long)var1);
            this.address = this.allocationAddress;
        } else {
            int var3 = pageSize();
            long var4 = unsafe.allocateMemory((long)(var1 + var3));
            this.allocationAddress = var4;
            this.address = var4 + (long)var3 - (var4 & (long)(var3 - 1));
        }
  }
  ...
}

EPollArrayWrapper在新建的時(shí)候創(chuàng)建了一個(gè)NativeObject,
而NativeObject通過(guò)unsafe.allocateMemory申請(qǐng)了堆外內(nèi)存。
其中allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT

  • NUM_EPOLLEVENTS
    min(句柄最大數(shù)量,8192),一般服務(wù)器句柄數(shù)設(shè)置的都比較大,這里選擇8192
  • SIZE_EPOLLEVENT
    native函數(shù),看下它的結(jié)構(gòu)
typedef union epoll_data {
        void *ptr;
         int fd;
         __uint32_t u32;
         __uint64_t u64;
     } epoll_data_t;//保存觸發(fā)事件的某個(gè)文件描述符相關(guān)的數(shù)據(jù)
     struct epoll_event {
         __uint32_t events;      /* epoll event */
         epoll_data_t data;      /* User data variable */
     };

epoll_data: union取最大值,64位整形,8字節(jié)
epoll_event: 4字節(jié)(32位整形)+4字節(jié)(補(bǔ)齊)+8字節(jié) = 16字節(jié)
16*8192 = 128k
可以看到,一個(gè)EpollSelector占了約128k堆外內(nèi)存,當(dāng)句柄泄露時(shí),堆外內(nèi)存會(huì)隨著句柄一起上漲。


總結(jié)

當(dāng)遇到內(nèi)存泄露的時(shí)候,首先要確定是堆內(nèi)泄露還堆外,最直接的方式就是設(shè)置Xmx,查看物理內(nèi)存占用是否超過(guò)JVM峰值,如果內(nèi)存總量一直保持在峰值,且fullGC后不下降,那么可以猜測(cè)是堆內(nèi)內(nèi)存泄露,可以通過(guò)dump文件來(lái)分析對(duì)象。
如果內(nèi)存總量超過(guò)Xmx,可以設(shè)置-XX:MaxDirectMemorySize,查看內(nèi)存總量是否等于堆內(nèi)+MaxDirectMemorySize,如果可以通過(guò)MaxDirectMemorySize限制內(nèi)存上漲,那么可以猜測(cè)是使用了DirectByteBuffer沒(méi)有合理回收導(dǎo)致的。如果還不能限制,那么就有可能是使用了Unsafe,需要看下第三方工具或中間件是否使用不當(dāng)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容