1

  是挺久没有“宠爱”我们netty小婊贝了,最近又开始搞事了。
006cSBLKgy1fgmvvjictnj30j60foabh.jpg

于是,趁机探究了下MXBean关于direct memory的监控原理。

一、问题

  通过arthas dashboard中的direct,visualvm的MBeans以及jmx获取的grafana监控都无法看到直接内存有大量占用,但实际上应用已经在OOM的边缘。

image.png

image.png

······

那难道拿这个“小可爱”没法了吗?只能祭出终极绝招:添加jvm启动参数-XX:NativeMemoryTracking=detail

简单说一下NMT,NMT(本地内存跟踪)是JDK自带的功能,方法简单,可以用它跟踪JVM内存使用情况。使用方法:

1. 启用NMT(启动加参数):-XX:NativeMemoryTracking=detail
2. 简单的执行命令:jcmd $pid VM.native_memory detail

即可打印:Java Heap、Class、Thread、Code、GC、Compiler、Internal、Symbol、Native Memory Tracking、Arena Chunk 内存可用、已占用情况。

其中:Java Heap即-Xmx的设置和占用情况;
Thread包含线程数量和线程本身占用内存(-Xss乘以线程数量)的情况;
Internal包含直接内存(Direct Memory,受-XX:MaxDirectMemorySize限制)

二、MXBean监控直接内存案例

  一般我们可以使用MXBean通过下面的方式获取直接内存的使用情况:

private static final Logger LOG = LoggerFactory.getLogger(MemoryStatics.class);
    // direct memory
    public static BufferPoolMXBean directMBean;
    // mapped memory
    public static BufferPoolMXBean mappedMXBean;

    static {
        List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class);

        for (BufferPoolMXBean mbean : bufferPoolMXBeans) {
            if (mbean.getName().equals("direct")) {
                directMBean= mbean;
            } else {
                mappedMXBean = mbean;
            }
        }
    }
    public void monitor() {

        while (true) {
            assert directMBean != null;
            LOG.info("buffer pool name: " + directMBean);
            LOG.info("memory used: " + directMBean.getMemoryUsed());
            LOG.info("max memory : " + directMBean.getTotalCapacity());
            LOG.info("contain buffers : " + directMBean.getCount());
            LOG.info("---------------------------------------");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

arthas的mbean命令和visualvm的MBeans中的BufferPool原理上也是类似的。

三、BufferPoolMXBean底层原理

  上面的案例中,通过getMemoryUsed、getTotalCapacity、getCount方法获取实际direct memory的使用情况。而真正的实现类是JDK的sun.management.ManagementFactoryHelper(JDK8) 【可以直接打印mbean对象,可以看见类似对象信息:sun.management.ManagementFactoryHelper$1@de2b39c】:

getBufferPoolMXBeans.png

而上面这段代码可以看到有getName、getCount、getTotalCapacity等方法,其真正实现都是在java.nio.Bits中,实现如下:

BIts获取内存大小.png

Bits类中有几个静态成员,简单说明:

// 最大直接内存,默认和-Xmx配置的大小一样
private static volatile long maxMemory = VM.maxDirectMemory();
// 已使用的直接内存大小
private static final AtomicLong reservedMemory = new AtomicLong();
// 总的直接内存大小,小于等于maxMemory
private static final AtomicLong totalCapacity = new AtomicLong();

在使用java.nio.ByteBuffer.allocateDirect或者DirectByteBuffer主构造函数时,每次分配直接内存都会调用Bits类进行实际分配并更新reservedMemorytotalCapacity等值。

关于DirectByteBuffer可以看之前的一篇《从HotSpot源码理解DirectByteBuffer》

DirectByteBuffer主构造函数如下:
DirectByteBuffer主构造函数.png

netty的直接内存分配

  简单说明下netty的直接内存分配。netty直接内存分配最终是通过PlatformDependent实现的,这个类有个原子类的静态成员DIRECT_MEMORY_COUNTER独立进行内存使用记录,不依赖Bits.

// PlatformDependent直接内存记录器
private static final AtomicLong DIRECT_MEMORY_COUNTER;

每次要申请一块新的direct memory的时候, 它就调用incrementMemoryCounter方法去增加 DIRECT_MEMORY_COUNTER 的值:

PlatformDependent内存分配.png

最终是通过反射实例化DirectByteBuffer,用的是下面的构造函数:

netty用的DirectByteBuffer构造函数.png

PlatformDependent0反射实例化代码:

PlatformDependent0反射实例化DirectByteBuffer.png

因此netty所分配的直接内存大小,常规的监控手段无法监测,包括arthas、visualvm中的MBean、以及其他所有通过jmx获取直接内存数据的手段。

如果要监控,只能自定义实现,从PlatformDependent入手,网上已有不少资料。


开翻挖掘机
225 声望26 粉丝

不忘初心❤️,且行且思考