最近在查一個(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)題。

通過(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)。