dump排查内存异常 java_记一次生产环境netty堆外内存占用异常排查

外网linux centos 下 ,java8 ,然后用的netty4.1.18版本,在线玩家非常少,而且日志中还记录到出现过DirectMemory oom 也就是实际占用内存大于过4g

top命令显示占用了3.1g内存

实际上启动参数配置的:Xmx2048m -XX:MaxDirectMemorySize=2048,

通过jmap -heap命令打印如下:

9cd10a295cd593b91281cb482e936a49.png

top命令显示内存占用

7a5139dff98ee1b23006b2c87a44c9cc.png

打印的java进程启动配置参数

41480e568bd3551b05682277506d99bc.png

jmap命令显示的java进程占用的堆内存

堆内存的使用没什么异常

然后再通过 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对象查起:

50f379db63ea4e9d64fddb1a8eef1dab.png

PooledUnsafeDirectByteBuf对象

a2161097047568f632acb74cb174881b.png

PooledUnsafeDirectByteBuf对象信息

0414059a24b19db441825aca2d7ebfc2.png
6310613e729aa602763533067c4707c7.png
29917504e51e98ef9827d55886d5c5c3.png

发现实际上都是在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() 方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值