当前位置:Java -> Java NIO:OutOfMemoryError

Java NIO:OutOfMemoryError

Java NIO(New Input/Output)是一个高性能的网络和文件处理API,使您可以进行非阻塞IO。非阻塞IO提供以下优势:

  • 并发性:NIO能够在不阻塞线程的情况下同时处理多个连接,从而提高并发性。
  • 异步编程:异步编程允许应用程序在等待IO操作完成时执行其他任务,提高整体效率。
  • 性能:非阻塞IO可以使用更少的线程管理更多的连接,减少处理并发请求所需的资源。

incident summary我们的一款应用程序正在利用NIO,但是在Java 11中运行时频繁遇到‘java.lang.OutOfMemoryError: Direct buffer memory’。然而,当我们升级到Java 17时,‘java.lang.OutOfMemoryError: Direct buffer memory’发生的频率大大降低。在本文中,我们想分享我们的发现和解决此问题的方法。

简单的Java NIO客户端

为了证明我们的案例,我们构建了一个简单的Spring Boot应用程序,用于异步上传图片。该应用程序利用Spring WebClient来连接REST API。Spring WebClient底层使用Java NIO技术来处理连接。以下是该应用程序的源代码。

public void webHeavyClientCall(Integer id,String url, String imagePath) {

        // Create a WebClient instance

        WebClient webClient = WebClient.create();

        // Prepare the image file

        File imageFile = new File(imagePath);

        // Perform the POST request with the image as a part of the request body

        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();

        body.add("file", new FileSystemResource(imageFile));

        System.out.println("Starting to post an image for Id"+id);
       
 webClient.post().uri(url).contentType(MediaType.MULTIPART_FORM_DATA).body(BodyInserters.fromMultipartData
(body))
                .retrieve().bodyToMono(String.class).subscribe(response -> {

                    System.out.println("Response Id"+id+ ":" + response);

        });

}


Java 11 NIO内存泄漏

我们在Java 11中执行了上述代码。经过大约15次迭代后,这个简单的应用程序开始抛出‘java.lang.OutOfMemoryError: Direct buffer memory’。以下是控制台中打印的输出。

Starting to post an image for Id0

Starting to post an image for Id1

Starting to post an image for Id2

Starting to post an image for Id3

Starting to post an image for Id4

Starting to post an image for Id5

Starting to post an image for Id6

Starting to post an image for Id7

Starting to post an image for Id8

Starting to post an image for Id9

Starting to post an image for Id10

Starting to post an image for Id11

Starting to post an image for Id12

Starting to post an image for Id13

Starting to post an image for Id14

2023-12-06 17:21:46.730  WARN 13804 --- [tor-http-nio-12] io.netty.util.concurrent.DefaultPromise  : An 
exception was thrown by reactor.ipc.netty.FutureMono$FutureSubscription.operationComplete()

reactor.core.Exceptions$ErrorCallbackNotImplemented: 
io.netty.channel.socket.ChannelOutputShutdownException: Channel output shutdown

Caused by: java.lang.OutOfMemoryError: Direct buffer memory

         at java.base/java.nio.Bits.reserveMemory(Bits.java:175) ~[na:na]

         at java.base/java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:118) ~[na:na]

         at java.base/java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:318) ~[na:na]

         at java.base/sun.nio.ch.Util.getTemporaryDirectBuffer(Util.java:242) ~[na:na]

         at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:164) ~[na:na]

         at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:130) ~[na:na]

         at java.base/sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:496) ~[na:na]

         at io.netty.channel.socket.nio.NioSocketChannel.doWrite(NioSocketChannel.java:418) ~[netty-
transport-4.1.23.Final.jar!/:4.1.23.Final]

        at io.netty.channel.AbstractChannel$AbstractUnsafe.flush0(AbstractChannel.java:934) ~[netty-
transport-4.1.23.Final.jar!/:4.1.23.Final]

    ... 18 common frames omitted


Java 17 NIO内存优化

我们在Java 17中执行了同样的程序。但是,为了在Java 17中运行这个程序,我们需要进行一些小的修改。以下是在Java 17上运行模拟上述行为的修改后的代码。

public void webHeavyClientCall(Integer id,String url, String imagePath) {

// Create a WebClient instance

WebClient webClient = WebClient.create();

// Prepare the image file

File imageFile = new File(imagePath);

// Perform the POST request with the image as a part of the request body

MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();

body.add("file", new FileSystemResource(imageFile));

System.out.println("Starting to post an image for Id"+id);

webClient.post().uri(url).contentType(MediaType.MULTIPART_FORM_DATA).body(BodyInserters.fromMultipartData
(body))                                                                 

   .retrieve().bodyToMono(String.class).subscribe(response -> {  
           System.out.println("Response Id"+id+ ":" + response);
      });

   }

}


升级后,内存使用有所改善。Java 17能够处理的NIO连接至少是Java 11的两倍。以下是控制台中的输出。您可以看到该应用程序能够进行50次连接迭代,之后才会出现‘java.lang.OutOfMemoryError’。另一方面,Java 11在15次连接后就失败了‘java.lang.OutOfMemoryError’

Starting to post an image for Id38

Starting to post an image for Id39

Starting to post an image for Id40

Starting to post an image for Id41

Starting to post an image for Id42

Starting to post an image for Id43

Starting to post an image for Id44

Starting to post an image for Id45

Starting to post an image for Id46

Starting to post an image for Id47

Starting to post an image for Id48

Starting to post an image for Id49

2023-12-12 14:49:38.421  WARN 59559 --- [ctor-http-nio-4] r.netty.http.client.HttpClientConnect           : 
[bfc8b2c8, L:/127.0.0.1:57435 ! R:localhost/127.0.0.1:8090] The connection observed an error

reactor.netty.ReactorNetty$InternalNettyException: java.lang.OutOfMemoryError: Cannot reserve 4096 bytes 
of direct buffer memory (allocated: 202956, limit: 204800)

Caused by: java.lang.OutOfMemoryError: Cannot reserve 4096 bytes of direct buffer memory (allocated: 
202956, limit: 204800)

    at java.base/java.nio.Bits.reserveMemory(Bits.java:178) ~[na:na]

    at java.base/java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:121) ~[na:na]

    at java.base/java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:332) ~[na:na]

    at io.netty.buffer.UnpooledDirectByteBuf.allocateDirect(UnpooledDirectByteBuf.java:104) ~[netty-
buffer-4.1.73.Final.jar!/:4.1.73.Final]

    at io.netty.buffer.UnpooledDirectByteBuf.<init>(UnpooledDirectByteBuf.java:64) ~[netty-buffer
-4.1.73.Final.jar!/:4.1.73.Final]

    at io.netty.buffer.UnpooledUnsafeDirectByteBuf.<init>(UnpooledUnsafeDirectByteBuf.java:41) ~
[netty-buffer-4.1.73.Final.jar!/:4.1.73.Final]

    at io.netty.buffer.UnsafeByteBufUtil.newUnsafeDirectByteBuf(UnsafeByteBufUtil.java:634) ~[netty-
buffer-4.1.73.Final.jar!/:4.1.73.Final]

    at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:397) ~
[netty-buffer-4.1.73.Final.jar!/:4.1.73.Final]

    at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:188) ~
[netty-buffer-4.1.73.Final.jar!/:4.1.73.Final]

    at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:179) ~
[netty-buffer-4.1.73.Final.jar!/:4.1.73.Final]

    at io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:116) ~[netty-
buffer-4.1.73.Final.jar!/:4.1.73.Final]

    at org.springframework.core.io.buffer.NettyDataBufferFactory.allocateBuffer(NettyDataBufferFactory.java:71)
 ~[spring-core-5.3.15.jar!/:5.3.15]

    at org.springframework.core.io.buffer.DataBufferUtils$ReadCompletionHandler.request(DataBufferUtils.java:945
) ~[spring-core-5.3.15.jar!/:5.3.15]


解决‘OutOfMemoryError: Direct buffer memory’

为了解决这个问题,我们使用了yCrash监控工具。该工具能够在生产环境中预测故障发生前的情况。一旦它在环境中预测到故障,它会捕获环境中的360°故障排除工件,分析它们,并立即生成根本原因分析报告。它捕获的工件包括垃圾回收日志、线程转储、堆替代、netstat、vmstat、iostat、top、top -H、dmesg、内核参数和磁盘使用情况…

您可以在这里注册并开始使用该工具的免费版。

yCrash服务器分析了示例应用程序并提供了明确的问题指示和建议。以下是yCrash为SpringBoot WebClient应用程序生成的事故摘要报告。您可以注意到,yCrash清楚地指出了错误,并提出了必要的建议来纠正问题。

Incident Summary Report from yCrash 图1:yCrash的事件摘要报告

垃圾回收分析报告

yCrash的垃圾回收(GC)分析报告显示,连续运行了Full GCs(请参见下面的截图)。当GC运行时,整个应用程序暂停,不会处理任何事务。整个应用程序都将变得无响应。在SpringBoot WebClient应用程序崩溃并出现OutOfMemoryError之前,我们观察到了无响应的行为。

yCrash report pointing our Consecutive Full GC problem 图2:yCrash报告指出连续的Full GC问题

日志分析报告Outofmemoryerror:直接缓冲器内存

yCrash的应用程序日志分析报告显示,应用程序遭受了'java.lang.OutOfMemoryError: Direct buffer memory'(请参见下面的截图),导致了应用程序崩溃。

yCrash日志报告指向java.lang.OutOfMemoryError: Direct buffer memory 图3: yCrash日志报告指向java.lang.OutOfMemoryError: Direct buffer memory

为什么Java NIO应用程序遭受Outofmemoryerror

Java NIO对象存储在JVM本地内存的“直接缓冲区内存”区域。(注:JVM中存在不同的内存区域。要了解它们,您可以观看此视频片段)。当我们执行上述两个程序时,我们设置了直接缓冲区内存大小为200k(即,XX:MaxDirectMemorySize=200k)。在分配了200k直接缓冲区内存的情况下,Java 11只能执行15次迭代,而Java 17可以执行50次迭代。这清楚地表明了JDK团队在Java 17版本中所做的优化。

WebClient对象存储在本地区域的直接内存区域 图4: WebClient对象存储在本地区域的直接内存区域

在Java 11或以下版本增加:Xx:Maxdirectmemorysize

因此,如果您的应用程序正在利用Java NIO并在Java 11或更低版本上运行,并且遇到‘java.lang.OutOfMemoryError: Direct buffer memory’,那么您面前有几种解决方案:

  1. 考虑分配更高的直接缓冲区内存大小。
  2. 考虑升级到Java 17或更高版本。

由于升级到Java 17需要更多依赖项,我们使用JVM参数-XX:MaxDirectMemorySize=1000k将直接内存大小增加到更高的值。做出这一更改后,应用程序的Java 11版本能够成功运行而不出现任何错误。

结论

在本文中,我们讨论了Java 11中由Java NIO引起的‘java.lang.OutOfMemoryError: Direct buffer memory’以及解决这个问题的潜在解决方案。希望对您有所帮助。

推荐阅读: 百度面经(6)

本文链接: Java NIO:OutOfMemoryError