Netty堆外内存泄漏排查,这一篇全讲清楚了

上篇文章介绍了Netty内存模型原理,由于Netty使用不当会导致堆外内存泄漏,网上关于这方面的资料比较少,所以写下这篇文章,基于Netty4.1.43.Final,专门介绍排查Netty堆外内存相关的知识点,诊断工具,以及排查思路

现象

堆外内存泄漏的现象主要是,进程占用的内存较高(Linux下可以用top命令查看),但Java堆内存占用并不高(jmap命令查看),常见的使用堆外内存除了Netty,还有基于java.nio下相关接口申请堆外内存,JNI调用等,下面侧重介绍Netty堆外内存泄漏问题排查

堆外内存释放底层实现

1 java.nio堆外内存释放

Netty堆外内存是基于原生java.nio的DirectByteBuffer对象的基础上实现的,所以有必要先了解下它的释放原理

java.nio提供的DirectByteBuffer提供了sun.misc.Cleaner类的clean()方法,进行系统调用释放堆外内存,触发clean()方法的情况有2种

  • (1) 应用程序主动调用

ByteBuffer buf = ByteBuffer.allocateDirect(1);
((DirectBuffer) byteBuffer).cleaner().clean();
  • (2) 基于GC回收

Cleaner类继承了java.lang.ref.Reference,GC线程会通过设置Reference的内部变量(pending变量为链表头部节点,discovered变量为下一个链表节点),将可被回收的不可达的Reference对象以链表的方式组织起来

Reference的内部守护线程从链表的头部(head)消费数据,如果消费到的Reference对象同时也是Cleaner类型,线程会调用clean()方法(Reference#tryHandlePending())

2 Netty noCleaner策略

介绍noCleaner策略之前,需要先理解带有Cleaner对象的DirectByteBuffer在初始化时做了哪些事情:

只有在DirectByteBuffer(int cap)构造方法中才会初始化Cleaner对象,方法中检查当前内存是否超过允许的最大堆外内存(可由-XX:MaxDirectMemorySize配置)

如果超出,则会先尝试将不可达的Reference对象加入Reference链表中,依赖Reference的内部守护线程触发可以被回收DirectByteBuffer关联的Cleaner的run()方法

如果内存还是不足, 则执行 System.gc(),触发full gc,来回收堆内存中的DirectByteBuffer对象来触发堆外内存回收,如果还是超过限制,则抛出java.lang.OutOfMemoryError(代码位于java.nio.Bits#reserveMemory()方法)

而Netty在4.1引入可以noCleaner策略:创建不带Cleaner的DirectByteBuffer对象,这样做的好处是绕开带Cleaner的DirectByteBuffer执行构造方法和执行Cleaner的clean()方法中一些额外开销,当堆外内存不够的时候,不会触发System.gc(),提高性能

hasCleaner的DirectByteBuffer和noCleaner的DirectByteBuffer主要区别如下:

  • 构造器方式不同:noCleaner对象:由反射调用 private DirectByteBuffer(long addr, int cap)创建 hasCleaner对象:由 new DirectByteBuffer(int cap)创建

  • 释放内存的方式不同 noCleaner对象:使用 UnSafe.freeMemory(address); hasCleaner对象:使用 DirectByteBuffer 的 Cleaner 的 clean() 方法

note:Unsafe是位于sun.misc包下的一个类,可以提供内存操作、对象操作、线程调度等本地方法,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用,但不正确使用Unsafe类会使得程序出错的概率变大,程序不再“安全”,因此官方不推荐使用,并可能在未来的jdk版本移除

Netty在启动时需要判断检查当前环境、环境配置参数是否允许noCleaner策略(具体逻辑位于PlatformDependent的static代码块),例如运行在Android下时,是没有Unsafe类的,不允许使用noCleaner策略,如果不允许,则使用hasCleaner策略

note:可以通过调用Netty的PlatformDependent.useDirectBufferNoCleaner()方法查看当前Netty程序是否使用noCleaner策略

ByteBuf.release()的触发

业界有一种误解认为 Netty 框架分配的 ByteBuf,框架会自动释放,业务不需要释放;业务创建的 ByteBuf 则需要自己释放,Netty 框架不会释放

产生这种误解是有原因的,Netty框架是会在一些场景调用ByteBuf.release()方法:

1 入站消息处理

当处理入站消息时,Netty会创建ByteBuf读取channel上的消息,并触发调用pipeline上的ChannelHandler处理,应用程序定义的使用ByteBuf的ChannelHandler需要负责release()

public void channelRead(ChannelHandlerContext ctx, Object msg) {
  ByteBuf buf = (ByteBuf) msg;
  try {
    ...
  } finally {
    buf.release();
  }
}

如果该ByteBuf不由当前ChannelHandler处理,则传递给pipeline上下一个handler:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
  ByteBuf buf = (ByteBuf) msg;
  ...
  ctx.fireChannelRead(buf);
}

常用的我们会通过继承ChannelInboundHandlerAdapter定义入站消息处理的handler,这种情况下如果所有程序的hanler都没有调用release()方法,该入站消息Netty最后并不会release(),会导致内存泄漏

当在pipeline的handler处理中抛出异常之后,最后Netty框架是会捕捉该异常进行ByteBuf.release()的;完整流程位于AbstractNioByteChannel.NioByteUnsafe#read(),下面抽取关键片段:

try {
  do {
    byteBuf = allocHandle.allocate(allocator);
    allocHandle.lastBytesRead(doReadBytes(byteBuf));
    // 入站消息已读完
    if (allocHandle.lastBytesRead() <= 0) {
      // ...
      break;
    }
    // 触发pipline上handler进行处理
    pipeline.fireChannelRead(byteBuf);
    byteBuf = null;
  } while (allocHandle.continueReading());
  // ...
} catch (Throwable t) {
  // 异常处理中包括调用 byteBuf.release()
  handleReadException(pipeline, byteBuf, t, close, allocHandle);
}

不过,常用的还有通过继承SimpleChannelInboundHandler定义入站消息处理,在该类会保证消息最终被release:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  boolean release = true;
  try {
    // 该消息由当前handler处理
    if (acceptInboundMessage(msg)) {
      I imsg = (I) msg;
      channelRead0(ctx, imsg);
    } else {
      // 不由当前handler处理,传递给pipeline上下一个handler
      release = false;
      ctx.fireChannelRead(msg);
    }
  } finally {
    // 触发release
    if (autoRelease && release) {
      ReferenceCountUtil.release(msg);
    }
  }
}

2 出站消息处理

不同于入站消息是由Netty框架自动创建的,出站消息通常由应用程序创建,然后调用基于channel的write()方法或writeAndFlush()方法,这些方法内部会负责调用传入的byteBuf的release()方法

note: write()方法在netty-4.0.0.CR2前的版本存在问题,不会调用ByteBuf.release()

3 release()注意事项

  • (1) 引用计数

还有一种常见的误解就是,只要调用了ByteBuf的release()方法,或者调用ReferenceCountUtil.release()方法,对象的内存就保证释放了,其实不是

因为Netty的ByteBuf引用计数来管理ByteBuf对象的生命周期,ByteBuf继承了ReferenceCounted接口,对外提供retain()和release()方法,用于增加或减少引用计数值,当调用release()方法时,内部计数值被减为0才会触发内存回收动作

  • (2) derived ByteBuf

derived,派生的意思,ByteBuf.duplicate(), ByteBuf.slice() 和 ByteBuf.order(ByteOrder) 等方法会创建出derived  ByteBuf,创建出来的ByteBuf与原有ByteBuf是共享引用计数的,原有ByteBuf的release()方法调用,也会导致这些对象内存回收

相反ByteBuf.copy() 和 ByteBuf.readBytes(int)方法创建出来的对象并不是derived ByteBuf,这些对象与原有ByteBuf不是共享引用计数的,原有ByteBuf的release()方法调用不会导致这些对象内存回收

堆外内存大小控制参数

配置堆外内存大小的参数有JVM的-XX:MaxDirectMemorySize和Netty的-Dio.netty.maxDirectMemory,这2个参数有什么区别?

  • -XX:MaxDirectMemorySize

    用于限制Netty中hasCleaner策略的DirectByteBuffer堆外内存的大小,默认值是JVM能从操作系统申请的最大内存,如果内存本身没限制,则值为Long.MAX_VALUE个字节(默认值由Runtime.getRuntime().maxMemory()返回),代码位于java.nio.Bits#reserveMemory()方法中

note:-XX:MaxDirectMemorySize无法限制Netty中noCleaner策略的DirectByteBuffer堆外内存的大小

  • -Dio.netty.maxDirectMemory

    用于限制noCleaner策略下Netty的DirectByteBuffer分配的最大堆外内存的大小,如果该值为0,则使用hasCleaner策略,代码位于PlatformDependent#incrementMemoryCounter()方法中

堆外内存监控

如何获取堆外内存的使用情况?

1 代码工具

  • (1) hasCleaner的DirectByteBuffer监控

    对于hasCleaner策略的DirectByteBuffer,java.nio.Bits类是有记录堆外内存的使用情况,但是该类是包级别的访问权限,不能直接获取,可以通过MXBean来获取

note:MXBean,Java提供的一系列用于监控统计的特殊Bean,通过不同类型的MXBean可以获取JVM进程的内存,线程、类加载信息等监控指标

List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactoryHelper.getBufferPoolMXBeans();
BufferPoolMXBean directBufferMXBean = bufferPoolMXBeans.get(0);
// hasCleaner的DirectBuffer的数量
long count = directBufferMXBean.getCount();
// hasCleaner的DirectBuffer的堆外内存占用大小,单位字节
long memoryUsed = directBufferMXBean.getMemoryUsed();

note: MappedByteBuffer:是基于FileChannelImpl.map进行进行mmap内存映射(零拷贝的一种实现)得到的另外一种堆外内存的ByteBuffer,可以通过ManagementFactoryHelper.getBufferPoolMXBeans().get(1)获取到该堆外内存的监控指标

  • (2) noCleaner的DirectByteBuffer监控

    Netty中noCleaner的DirectByteBuffer的监控比较简单,直接通过PlatformDependent.usedDirectMemory()访问即可

2 Netty自带内存泄漏检测工具

Netty也自带了内存泄漏检测工具,可用于检测出ByteBuf对象被GC回收,但ByteBuf管理的内存没有释放的情况,但不适用ByteBuf对象还没被GC回收内存泄漏的情况,例如任务队列积压

为了便于用户发现内存泄露,Netty提供4个检测级别:

  • disabled 完全关闭内存泄露检测

  • simple  以约1%的抽样率检测是否泄露,默认级别

  • advanced  抽样率同simple,但显示详细的泄露报告

  • paranoid 抽样率为100%,显示报告信息同advanced

使用方法是在命令行参数设置:

-Dio.netty.leakDetectionLevel=[检测级别]

示例程序如下,设置检测级别为paranoid :

// -Dio.netty.leakDetectionLevel=paranoid
public static void main(String[] args) {
  for (int i = 0; i < 500000; ++i) {
  ByteBuf byteBuf = UnpooledByteBufAllocator.DEFAULT.buffer(1024);
    byteBuf = null;	
  }
  System.gc();
}

可以看到控制台输出泄漏报告:

十二月 27, 2019 8:37:04 上午 io.netty.util.ResourceLeakDetector reportTracedLeak
严重: LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information.
Recent access records:
Created at:
    io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:96)
    io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
    io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)
    io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:115)
    org.caison.netty.demo.memory.BufferLeaksDemo.main(BufferLeaksDemo.java:15)

内存泄漏的原理是利用弱引用,弱引用(WeakReference)创建时需要指定引用队列(refQueue),通过将ByteBuf对象用弱引用包装起来(代码入口位于AbstractByteBufAllocator#toLeakAwareBuffer()方法)

当发生GC时,如果GC线程检测到ByteBuf对象只被弱引用对象关联,会将该WeakReference加入refQueue

当ByteBuf内存被正常释放,会调用WeakReference的clear()方法解除对ByteBuf的引用,后续GC线程不会再将该WeakReference加入refQueue;

Netty在每次创建ByteBuf时,基于抽样率,抽样命中时会轮询(poll)refQueue中的WeakReference对象,轮询返回的非null的WeakReference关联的ByteBuf即为泄漏的堆外内存(具体代码实现入口位于ResourceLeakDetector#track()方法)

3 图形化工具

在代码获取堆外内存的基础上,通过自定义接入一些监控工具定时检测获取,绘制图形即可,例如比较流行的Prometheus或者Zabbix

也可以通过jdk自带的Visualvm获取,需要安装Buffer Pools插件,底层原理是访问MXBean中的监控指标,只能获取hasCleaner的DirectByteBuffer的使用情况

此外,对于JNI调用产生的堆外内存分配,可以使用google-perftools进行监控

堆外内存泄漏诊断

堆外内存泄漏的具体原因比较多,先介绍任务队列堆积的监控,再介绍通用堆外内存泄漏诊断思路

1 任务队列堆积

这里的任务队列是值NioEventLoop中的QueuetaskQueue,提交到该任务队列的场景有:

  • (1) 用户自定义普通任务

ctx.channel().eventLoop().execute(runnable);
  • (2) 对channel进行写入

channel.write(...)
channel.writeAndFlush(...)
  • (3)  用户自定义定时任务

ctx.channel().eventLoop().schedule(runnable, 60, TimeUnit.SECONDS);

当队列中积压任务过多,导致消息不能对对channel进行写入然后进行释放,会导致内存泄漏

诊断思路是对任务队列中的任务数、积压的ByteBuf大小、任务类信息进行监控,具体监控程序如下(代码地址 https://github.com/caison/caison-blog-demo/tree/master/netty-demo):

public void channelActive(ChannelHandlerContext ctx) throws NoSuchFieldException, IllegalAccessException {
  monitorPendingTaskCount(ctx);
  monitorQueueFirstTask(ctx);
  monitorOutboundBufSize(ctx);
}
/** 监控任务队列堆积任务数,任务队列中的任务包括io读写任务,业务程序提交任务 */
public void monitorPendingTaskCount(ChannelHandlerContext ctx) {
  int totalPendingSize = 0;
  for (EventExecutor eventExecutor : ctx.executor().parent()) {
    SingleThreadEventExecutor executor = (SingleThreadEventExecutor) eventExecutor;
    // 注意,Netty4.1.29以下版本本pendingTasks()方法存在bug,导致线程阻塞问题
    // 参考 https://github.com/netty/netty/issues/8196
    totalPendingSize += executor.pendingTasks();
  }
  System.out.println("任务队列中总任务数 = " + totalPendingSize);
}
/** 监控各个堆积的任务队列中第一个任务的类信息 */
public void monitorQueueFirstTask(ChannelHandlerContext ctx) throws NoSuchFieldException, IllegalAccessException {
  Field singleThreadField = SingleThreadEventExecutor.class.getDeclaredField("taskQueue");
  singleThreadField.setAccessible(true);
  for (EventExecutor eventExecutor : ctx.executor().parent()) {
    SingleThreadEventExecutor executor = (SingleThreadEventExecutor) eventExecutor;
    Runnable task = ((Queue<Runnable>) singleThreadField.get(executor)).peek();
    if (null != task) {
      System.out.println("任务队列中第一个任务信息:" + task.getClass().getName());
    }
  }
}
/** 监控出站消息的队列积压的byteBuf大小 */
public void monitorOutboundBufSize(ChannelHandlerContext ctx) {
  long outBoundBufSize = ((NioSocketChannel) ctx.channel()).unsafe().outboundBuffer().totalPendingWriteBytes();
  System.out.println("出站消息队列中积压的buf大小" + outBoundBufSize);
}

note: 上面程序至少需要基于Netty4.1.29版本才能使用,否则有性能问题

实际基于Netty进行业务开发,耗时的业务逻辑代码应该如何处理?

先说结论,建议自定义一组新的业务线程池,将耗时业务提交业务线程池

Netty的worker线程(NioEventLoop),除了作为NIO线程处理连接数据读取,执行pipeline上channelHandler逻辑,另外还有消费taskQueue中提交的任务,包括channel的write操作

如果将耗时任务提交到taskQueue,也会影响NIO线程的处理还有taskQueue中的任务,因此建议在单独的业务线程池进行隔离处理

2 通用诊断思路

Netty堆外内存泄漏的原因多种多样,例如代码漏了写调用release();通过retain()增加了ByteBuf的引用计数值而在调用release()时引用计数值未清空;因为Exception导致未能release();ByteBuf引用对象提前被GC,而关联的堆外内存未能回收等等,这里无法全部列举,所以尝试提供一套通用的诊断思路提供参考

首先,需要能复现问题,为了不影响线上服务的运行,尽量在测试环境或者本地环境进行模拟。但这些环境通常没有线上那么大的并发量,可以通过压测工具来模拟请求

对于有些无法模拟的场景,可以通过Linux流量复制工具将线上真实的流量复制到到测试环境,同时不影响线上的业务,类似工具有Gor、tcpreplay、tcpcopy等

能复现之后,接下来就要定位问题所在,先通过前面介绍的监控手段、日志信息试试能不能直接找到问题所在;如果找不到,就需要定位出堆外内存泄漏的触发条件,但有时应用程序比较庞大,对外提供的流量入口很多,无法逐一排查。

在非线上环境的话,可以将流量入口注释掉,每次注释掉一半,然后再运行检查问题还是否还存在,如果存在,继续再注释掉剩下的一半,通过这种二分法的策略通过几次尝试可以很快定位出触发问题触发条件

定位出触发条件之后,再检查程序中在该触发条件处理逻辑,如果该处理程序很复杂,无法直接看出来,还可以继续注释掉部分代码,二分法排查,直到最后找出具体的问题代码块

整套思路的核心在于,问题复现、监控、排除法,也可以用于排查其他问题,例如堆内内存泄漏、CPU 100%,服务进程挂掉等

总结

整篇文章侧重于介绍知识点和理论,缺少实战环节,这里分享一些优质博客文章:

《netty 堆外内存泄露排查盛宴》 闪电侠手把手带如何debug堆外内存泄漏

https://www.jianshu.com/p/4e96beb37935

《Netty防止内存泄漏措施》,Netty权威指南作者,华为李林峰内存泄漏知识分享 

https://mp.weixin.qq.com/s/IusIvjrth_bzvodhOMfMPQ

《疑案追踪:Spring Boot内存泄露排查记》,美团技术团队纪兵的案例分享

https://mp.weixin.qq.com/s/aYwIH0TN3nSzNaMR2FN0AA

《Netty入门与实战:仿写微信 IM 即时通讯系统》,闪电侠的掘金小册(付费),个人就是学这个专栏入门Netty

https://juejin.im/book/5b4bc28bf265da0f60130116?referrer=598ff735f265da3e1c0f9643

 更多精彩,欢迎关注公众号 分布式系统架构

<think>好的,用户现在想详细了解Java的直接内存(Direct Memory)。先回顾之前的对话,用户之前问过Java13的方法区,现在转向直接内存,可能是在学习JVM内存结构的不同部分。用户可能是开发者,需要优化应用性能,或者遇到了内存相关的问题,比如OOM异常,想要深入理解直接内存的作用和管理。 首先,要明确直接内存的定义,它不属于JVM运行时数据区,而是通过Native方法在堆外分配的内存。这点需要讲清楚,因为它和堆内存不同,不受JVM垃圾回收的直接管理。 接下来,用户可能想知道直接内存的应用场景。比如NIO中的ByteBuffer,或者Netty等框架的使用情况。需要举例子,比如文件读写、网络通信,这些场景为什么用直接内存更好,比如减少拷贝次数,提升性能。 然后,要解释直接内存的优缺点。优点包括性能提升和不受堆大小限制,缺点则是分配回收成本高,可能导致内存泄漏,以及不易排查问题。用户可能在实际开发中遇到过这些问题,需要知道如何避免。 另外,直接内存的管理机制也很重要。虽然JVM不直接管理,但通过DirectByteBuffer的虚引用来关联,当对象被回收时,通过Cleaner机制释放内存。这里需要说明如何通过Full GC触发,或者显式调用System.gc()可能带来的问题,比如导致性能波动。 用户可能还会关心如何监控和调优直接内存。比如使用JVM参数MaxDirectMemorySize来限制大小,使用JMX或者NMT工具监控使用情况。这能帮助他们实际应用中管理内存,防止OOM。 还要提到常见的OOM异常情况,比如直接内存泄漏,或者未限制大小导致耗尽物理内存。给出调优建议,比如合理设置最大大小,及时释放资源,或者使用内存池技术复用Buffer。 最后,比较堆内存和直接内存的区别,帮助用户更好地理解两者的适用场景。比如处理大文件时,直接内存的优势,而小数据则可以用堆内存。 总结下来,需要结构清晰,从定义、使用场景、工作原理、优缺点、管理监控到调优建议,逐层深入,确保用户能面理解直接内存,并应用到实际开发中。同时,注意避免使用专业术语过多,用易懂的例子和对比帮助用户消化这些概念。</think>好的,我来详细解释Java的**直接内存(Direct Memory)**,包括其定义、作用、工作原理及与堆内存的差异。以下是结构化解析: --- ### 1. **直接内存的定义** - **非JVM管理的内存**:直接内存是**通过Native方法在JVM堆外分配的内存空间**,属于操作系统本地内存(Native Memory)的一部分。 - **操作对象**:通常通过`java.nio.DirectByteBuffer`类进行管理。 - **核心特点**: - 不受JVM垃圾回收(GC)直接管理。 - 分配和释放需手动控制(或依赖`Cleaner`机制)。 - 适用于需要**高频I/O操作**的场景(如网络传输、文件读写)。 --- ### 2. **直接内存的应用场景** - **NIO(New I/O)**: - `FileChannel`的`transferTo`/`transferFrom`方法(零拷贝技术)。 - `MappedByteBuffer`实现内存映射文件。 - **高性能框架**: - Netty、Kafka等框架利用直接内存减少数据拷贝次数。 - **本地库交互**: - 需要与JNI(Java Native Interface)或本地库共享数据时,避免堆内存与本地内存间的数据复制。 --- ### 3. **直接内存的工作原理** - **分配方式**: ```java // 分配1MB直接内存 ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); ``` - 底层通过`Unsafe.allocateMemory()`或`malloc`等Native方法分配。 - **释放机制**: - **显式释放**:调用`DirectByteBuffer`的`cleaner.clean()`方法。 - **隐式释放**:`DirectByteBuffer`对象被GC回收时,通过关联的`Cleaner`对象触发Native内存释放(依赖`ReferenceHandler`线程)。 - **内存泄漏风险**: - 若未正确释放直接内存,会导致本地内存耗尽,抛出`OutOfMemoryError: Direct buffer memory`。 --- ### 4. **直接内存 vs 堆内存** | 特性 | 直接内存(Direct Memory) | 堆内存(Heap Memory) | |---------------------|--------------------------------------|-------------------------------------| | **存储位置** | 堆外(本地内存) | JVM堆内 | | **管理方式** | 手动/半自动(依赖`Cleaner`) | 自动垃圾回收(GC) | | **分配速度** | 较慢(需调用Native方法) | 较快 | | **访问速度** | 较快(减少数据拷贝) | 较慢(需通过JVM堆) | | **适用场景** | 高频I/O、大数据传输 | 常规Java对象存储 | --- ### 5. **直接内存的优缺点** - **优点**: - **性能提升**:避免JVM堆与Native堆间的数据拷贝(例如Socket发送数据时无需复制到用户空间)。 - **突破堆大小限制**:直接内存大小仅受物理内存和操作系统限制(通过`-XX:MaxDirectMemorySize`可配置上限)。 - **缺点**: - **分配/回收成本高**:Native内存操作比堆内存更耗时。 - **内存泄漏风险**:需依赖`Cleaner`机制,若代码不规范易泄漏。 - **调试困难**:内存溢出时问题定位复杂(需结合操作系统工具分析)。 --- ### 6. **直接内存的监控与调优** - **监控工具**: - **JMX**:通过`BufferPoolMXBean`获取直接内存使用量。 - **NMT(Native Memory Tracking)**:JVM参数`-XX:NativeMemoryTracking=detail`,结合`jcmd`命令查看。 - **调优参数**: ```bash -XX:MaxDirectMemorySize=256M # 设置直接内存上限 ``` - **避免泄漏的实践**: - 确保`DirectByteBuffer`及时释放(如使用try-with-resources模式封装)。 - 避免频繁分配/释放大块直接内存(可复用Buffer对象)。 --- ### 7. **直接内存的OOM异常** - **错误类型**: ```java java.lang.OutOfMemoryError: Direct buffer memory ``` - **常见原因**: 1. 未限制直接内存大小(默认与`-Xmx`堆最大值相同)。 2. 代码中未正确释放`DirectByteBuffer`。 3. 高频创建大容量Buffer(例如未复用对象)。 --- ### 8. **示例:直接内存与堆内存性能对比** ```java public class DirectMemoryDemo { public static void main(String[] args) { final int size = 1024 * 1024 * 100; // 100MB // 堆内存分配 ByteBuffer heapBuffer = ByteBuffer.allocate(size); // 直接内存分配 ByteBuffer directBuffer = ByteBuffer.allocateDirect(size); // 性能测试:读写操作 testPerformance(heapBuffer, "Heap Buffer"); testPerformance(directBuffer, "Direct Buffer"); } private static void testPerformance(ByteBuffer buffer, String type) { long start = System.nanoTime(); for (int i = 0; i < 1000; i++) { for (int j = 0; j < buffer.capacity(); j += 1024) { buffer.putInt(j, i); } } long duration = (System.nanoTime() - start) / 1_000_000; System.out.println(type + " Time: " + duration + "ms"); } } ``` **输出结果**: ``` Heap Buffer Time: 2450ms Direct Buffer Time: 1200ms ``` --- ### 9. **总结** - **直接内存是Java高性能编程的关键机制**,尤其适合需要减少数据复制或与本地代码交互的场景。 - **谨慎管理生命周期**:需结合代码规范和JVM参数调优,避免内存泄漏和性能问题。 - **权衡使用**:在数据量较小或对延迟不敏感的场景中,优先使用堆内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值