外网linux centos 下 ,java8 ,然后用的netty4.1.18版本,在线玩家非常少,而且日志中还记录到出现过DirectMemory oom 也就是实际占用内存大于过4g
top命令显示占用了3.1g内存
实际上启动参数配置的:Xmx2048m -XX:MaxDirectMemorySize=2048,
通过jmap -heap命令打印如下:
堆内存的使用没什么异常
然后再通过 jmap -histo:live 命令部分信息如下:
num #instances #bytes class name
----------------------------------------------
1: 11969 104112784 [B
2: 137435 16710288 [C
3: 76585 7081000 [Ljava.lang.Object;
4: 70256 6566336 [Ljava.util.HashMap$Node;
5: 197173 6309536 java.util.HashMap$Node
6: 79449 3813552 java.util.HashMap
7: 84042 3361680 com.linlongyx.h5.webgame.proto.internal.WalkEvent
8: 139197 3340728 java.util.ArrayList
9: 134742 3233808 java.lang.String
10: 65147 2084704 java.util.concurrent.ConcurrentHashMap$Node
11: 85347 2048328 java.util.concurrent.ConcurrentLinkedQueue$Node
12: 41623 1664920 java.util.TreeMap$Entry
13: 45486 1455552 com.linlongyx.h5.webgame.config.bean.RewardBean
14: 12016 1329528 java.lang.Class
15: 6516 1219216 [I
16: 27971 1118840 java.util.LinkedHashMap$Entry
17: 16871 1079744 io.netty.channel.ChannelOutboundBuffer$Entry
18: 59694 955104 java.lang.Integer
19: 28847 923104 io.netty.util.Recycler$DefaultHandle
20: 10033 882904 io.netty.buffer.PooledUnsafeDirectByteBuf
21: 1799 867568 [Ljava.util.concurrent.ConcurrentHashMap$Node;
22: 21263 850520 com.linlongyx.h5.webgame.config.bean.DropBagBean$DropRewardBean
23: 50128 802048 java.lang.Character
24: 19850 794000 java.lang.ref.Finalizer
25: 8875 781000 java.lang.reflect.Method
26: 1170 767520 io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueue
27: 3269 557080 [J
28: 49 543640 [Ljava.nio.ByteBuffer;
29: 21882 525168 java.lang.Long
30: 7939 508096 java.util.concurrent.ConcurrentHashMap
31: 3291 500232 com.linlongyx.h5.webgame.config.bean.ItemBean
32: 20026 480624 com.linlongyx.h5.webgame.config.bean.AttrBean
33: 5715 457200 java.lang.reflect.Constructor
34: 1001 392392 com.alibaba.druid.stat.JdbcSqlStat
35: 955 382000 com.linlongyx.h5.webgame.common.scene.actor.MonsterActor
36: 7800 374400 com.linlongyx.h5.webgame.app.godBeastBag.GodBeastBagEntity$$EnhancerByCGLIB$$280fb42d
37: 9200 368000 com.linlongyx.h5.webgame.app.godBeastBag.GodBeastBagEntity
38: 236 279744 [Lio.netty.util.Recycler$DefaultHandle;
39: 4577 256312 java.util.LinkedHashMap
40: 15760 252160 java.lang.Object
41: 5838 233520 java.lang.ref.SoftReference
42: 60 232640 [Lio.netty.buffer.PoolSubpage;
43: 7187 229984 com.linlongyx.h5.webgame.proto.binary.struct.GridInfo
44: 7 229488 [Ljava.util.concurrent.ForkJoinTask;
45: 4601 220848 com.linlongyx.h5.webgame.app.bag.BagEntity
46: 3932 220192 com.linlongyx.h5.webgame.app.bag.BagEntity$$EnhancerByCGLIB$$9c4a9d2f
47: 10783 215192 [Ljava.lang.Class;
堆外内存占用异常,jmap 中:
17: 16871 1079744 io.netty.channel.ChannelOutboundBuffer$Entry
18: 59694 955104 java.lang.Integer
19: 28847 923104 io.netty.util.Recycler$DefaultHandle
20: 10033 882904 io.netty.buffer.PooledUnsafeDirectByteBuf
这几个数据有问题,正常的情况下,PooledUnsafeDirectByteBuf 对象是不会那么多的。感觉是没有自动回收到。
红色字体,表示可能存在问题的实例,毕竟在线才10多人,PooledUnsafeDirectByteBuf 对象的数量太多了。
然后,再用jmap -dump:live,format=b,file=jmap.bin pid 命令把堆内存都dump出来分析,然后再用
jhat -J-mx1024m /data/jmap.bin 命令生成页面进行访问:http://localhost:7000/
先从PooledUnsafeDirectByteBuf对象查起:
发现实际上都是在Recycler类中等待被回收,但是为什么没有被回收呢?
也就是PooledUnsafeDirectByteBuf 对象已经进入到了,Recycler.Stack中
而且 hasBeenRecycled (Z) : true
ChannelOutboundBuffer$Entry对象
最终也在Recycler$DefaultHandler中而且对象还很多,也就是也没有释放
从源码中看到:
Entry 对象的产生只有在:
/**
* Add given message to this {@link ChannelOutboundBuffer}. The given {@link ChannelPromise} will be notified once
* the message was written.
*/
public void addMessage(Object msg, int size, ChannelPromise promise) {
Entry entry = Entry.newInstance(msg, size, total(msg), promise);
if (tailEntry == null) {
flushedEntry = null;
} else {
Entry tail = tailEntry;
tail.next = entry;
}
tailEntry = entry;
if (unflushedEntry == null) {
unflushedEntry = entry;
}
// increment pending bytes after adding message to the unflushed arrays.
// See https://github.com/netty/netty/issues/1619
incrementPendingOutboundBytes(entry.pendingSize, false);
}
添加发送消息对象的时候,才会调用。
AbstractChannel 类中的 write方法调用
@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
// If the outboundBuffer is null we know the channel was closed and so
// need to fail the future right away. If it is not null the handling of the rest
// will be done in flush0()
// See https://github.com/netty/netty/issues/2362
safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);
// release message now to prevent resource-leak
ReferenceCountUtil.release(msg);
return;
}
int size;
try {
msg = filterOutboundMessage(msg);
size = pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable t) {
safeSetFailure(promise, t);
ReferenceCountUtil.release(msg);
return;
}
outboundBuffer.addMessage(msg, size, promise);
}
然后最终在:DefaultChannelPipeline.HeadContext.unsafe
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}
然后看Recycler源码发现:
static final class DefaultHandleimplements Handle {
private int lastRecycledId;
private int recycleId;
boolean hasBeenRecycled;
private Stack> stack;
private Object value;
DefaultHandle(Stack> stack) {
this.stack = stack;
}
@Override
public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
stack.push(this);
}
}
回收调用的是 stack.push(this);
void push(DefaultHandle> item) {
Thread currentThread = Thread.currentThread();
if (threadRef.get() == currentThread) {
// The current Thread is the thread that belongs to the Stack, we can try to push the object now.
pushNow(item);
} else {
// The current Thread is not the one that belongs to the Stack
// (or the Thread that belonged to the Stack was collected already), we need to signal that the push
// happens later.
pushLater(item, currentThread);
}
}
然后发现如果不是当前线程不会直接回收而是,延迟回收。
private void pushLater(DefaultHandle> item, Thread thread) {
// we don't want to have a ref to the queue as the value in our weak map
// so we null it out; to ensure there are no races with restoring it later
// we impose a memory ordering here (no-op on x86)
Map, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();
WeakOrderQueue queue = delayedRecycled.get(this);
if (queue == null) {
if (delayedRecycled.size() >= maxDelayedQueues) {
// Add a dummy queue so we know we should drop the object
delayedRecycled.put(this, WeakOrderQueue.DUMMY);
return;
}
// Check if we already reached the maximum number of delayed queues and if we can allocate at all.
if ((queue = WeakOrderQueue.allocate(this, thread)) == null) {
// drop object
return;
}
delayedRecycled.put(this, queue);
} else if (queue == WeakOrderQueue.DUMMY) {
// drop object
return;
}
queue.add(item);
}
也就是会放入到延迟的队列中,
从上面的代码分析中,还是得不出来,为什么会出现这个问题。
源代码中真正释放directMemory 地方是在:
PlatformDependent
/**
* Try to deallocate the specified direct {@link ByteBuffer}. Please note this method does nothing if
* the current platform does not support this operation or the specified buffer is not a direct buffer.
*/
public static void freeDirectBuffer(ByteBuffer buffer) {
CLEANER.freeDirectBuffer(buffer);
}
然后调用的地方:
PoolArena.DirectArena
@Override
protected void destroyChunk(PoolChunk chunk) {
if (PlatformDependent.useDirectBufferNoCleaner()) {
PlatformDependent.freeDirectNoCleaner(chunk.memory);
} else {
PlatformDependent.freeDirectBuffer(chunk.memory);
}
}
PooledByteBuf
@Override
protected final void deallocate() {
if (handle >= 0) {
final long handle = this.handle;
this.handle = -1;
memory = null;
tmpNioBuf = null;
chunk.arena.free(chunk, handle, maxLength, cache);
chunk = null;
recycle();
}
}
deallocate()方法,在WrappedCompositeByteBuf,AbstractReferenceCountedByteBuf两个类中调用
PooledByteBuf extends AbstractReferenceCountedByteBuf
AbstractReferenceCountedByteBuf 相关方法
@Override
public boolean release() {
return release0(1);
}
@Override
public boolean release(int decrement) {
return release0(checkPositive(decrement, "decrement"));
}
private boolean release0(int decrement) {
int oldRef = refCntUpdater.getAndAdd(this, -decrement);
if (oldRef == decrement) {
deallocate();
return true;
} else if (oldRef < decrement || oldRef - decrement > oldRef) {
// Ensure we don't over-release, and avoid underflow.
refCntUpdater.getAndAdd(this, decrement);
throw new IllegalReferenceCountException(oldRef, decrement);
}
return false;
}
使用release 方法的地方就很多了,很多handler write 的方法最后都会调用。
经过源码分析,以及断点调试:
long maxDirectMemory = SystemPropertyUtil.getLong("io.netty.maxDirectMemory", -1);
if (maxDirectMemory == 0 || !hasUnsafe() || !PlatformDependent0.hasDirectBufferNoCleanerConstructor()) {
USE_DIRECT_BUFFER_NO_CLEANER = false;
DIRECT_MEMORY_COUNTER = null;
} else {
USE_DIRECT_BUFFER_NO_CLEANER = true;
if (maxDirectMemory < 0) {
maxDirectMemory = maxDirectMemory0();
if (maxDirectMemory <= 0) {
DIRECT_MEMORY_COUNTER = null;
} else {
DIRECT_MEMORY_COUNTER = new AtomicLong();
}
} else {
DIRECT_MEMORY_COUNTER = new AtomicLong();
}
}
DIRECT_MEMORY_LIMIT = maxDirectMemory;
netty用的直接内存是:USE_DIRECT_BUFFER_NO_CLEANER = true;
也就是有计算的,DIRECT_MEMORY_LIMIT 如果没有配置的话,会取jdk启动参数的,
private static long maxDirectMemory0() {
long maxDirectMemory = 0;
ClassLoader systemClassLoader = null;
try {
// Try to get from sun.misc.VM.maxDirectMemory() which should be most accurate.
systemClassLoader = getSystemClassLoader();
Class> vmClass = Class.forName("sun.misc.VM", true, systemClassLoader);
Method m = vmClass.getDeclaredMethod("maxDirectMemory");
maxDirectMemory = ((Number) m.invoke(null)).longValue();
} catch (Throwable ignored) {
// Ignore
}
if (maxDirectMemory > 0) {
return maxDirectMemory;
}
try {
// Now try to get the JVM option (-XX:MaxDirectMemorySize) and parse it.
// Note that we are using reflection because Android doesn't have these classes.
Class> mgmtFactoryClass = Class.forName(
"java.lang.management.ManagementFactory", true, systemClassLoader);
Class> runtimeClass = Class.forName(
"java.lang.management.RuntimeMXBean", true, systemClassLoader);
Object runtime = mgmtFactoryClass.getDeclaredMethod("getRuntimeMXBean").invoke(null);
@SuppressWarnings("unchecked")
List vmArgs = (List) runtimeClass.getDeclaredMethod("getInputArguments").invoke(runtime);
for (int i = vmArgs.size() - 1; i >= 0; i --) {
Matcher m = MAX_DIRECT_MEMORY_SIZE_ARG_PATTERN.matcher(vmArgs.get(i));
if (!m.matches()) {
continue;
}
maxDirectMemory = Long.parseLong(m.group(1));
switch (m.group(2).charAt(0)) {
case 'k': case 'K':
maxDirectMemory *= 1024;
break;
case 'm': case 'M':
maxDirectMemory *= 1024 * 1024;
break;
case 'g': case 'G':
maxDirectMemory *= 1024 * 1024 * 1024;
break;
}
break;
}
} catch (Throwable ignored) {
// Ignore
}
if (maxDirectMemory <= 0) {
maxDirectMemory = Runtime.getRuntime().maxMemory();
logger.debug("maxDirectMemory: {} bytes (maybe)", maxDirectMemory);
} else {
logger.debug("maxDirectMemory: {} bytes", maxDirectMemory);
}
return maxDirectMemory;
}
所以直接内存的分配,释放有下面两个方法控制:
/**
* Allocate a new {@link ByteBuffer} with the given {@code capacity}. {@link ByteBuffer}s allocated with
* this method MUST be deallocated via {@link #freeDirectNoCleaner(ByteBuffer)}.
*/
public static ByteBuffer allocateDirectNoCleaner(int capacity) {
assert USE_DIRECT_BUFFER_NO_CLEANER;
incrementMemoryCounter(capacity);
try {
return PlatformDependent0.allocateDirectNoCleaner(capacity);
} catch (Throwable e) {
decrementMemoryCounter(capacity);
throwException(e);
return null;
}
}
/**
* This method MUST only be called for {@link ByteBuffer}s that were allocated via
* {@link #allocateDirectNoCleaner(int)}.
*/
public static void freeDirectNoCleaner(ByteBuffer buffer) {
assert USE_DIRECT_BUFFER_NO_CLEANER;
int capacity = buffer.capacity();
PlatformDependent0.freeMemory(PlatformDependent0.directBufferAddress(buffer));
decrementMemoryCounter(capacity);
}
freeDirectNoCleaner 方法的调用最终会在:PooledByteBuf中调用:
@Override
protected final void deallocate() {
if (handle >= 0) {
final long handle = this.handle;
this.handle = -1;
memory = null;
tmpNioBuf = null;
chunk.arena.free(chunk, handle, maxLength, cache);
chunk = null;
recycle();
}
}
进一步会在:AbstractReferenceCountedByteBuf类中调用
@Override
public boolean release() {
return release0(1);
}
@Override
public boolean release(int decrement) {
return release0(checkPositive(decrement, "decrement"));
}
private boolean release0(int decrement) {
int oldRef = refCntUpdater.getAndAdd(this, -decrement);
if (oldRef == decrement) {
deallocate();
return true;
} else if (oldRef < decrement || oldRef - decrement > oldRef) {
// Ensure we don't over-release, and avoid underflow.
refCntUpdater.getAndAdd(this, decrement);
throw new IllegalReferenceCountException(oldRef, decrement);
}
return false;
}
最终到达 handler 与ByteBuf(PooledByteBufAllocator --PooledUnsafeDirectByteBuf)中调用
也就是收,出现PooledUnsafeDirectByteBuf 没有被释放只能是没有调用 release() 方法