Recently, we've added a test to the Vert.x core test suite that requires some memory resources (a few dozen MB). The build passes with all transports except io_uring.
We captured a heap dump on OutOfMemoryError. Here's what we found.
When Vert.x creates a NetClient, it registers a Java cleaner task (in case the user does not close the client properly).
This task holds a reference to the corresponding Vert.x instance, even when the client is closed properly.
eclipse-vertx/vert.x#6109 is going to address the problem.
Regardless of this Vert.x specific issue, we noticed that after Vert.x / Netty are closed, each IoUringIoHandler still holds a MsgHdrMemoryArray of 1024 entries that retains about 600kB.
When the ioHandler is destroyed, the array is released:
|
msgHdrMemoryArray.release(); |
Each entry is released as well:
|
void release() { |
|
assert !released; |
|
released = true; |
|
for (MsgHdrMemory hdr: hdrs) { |
|
hdr.release(); |
|
} |
|
msgHdrMemoryArrayMemoryCleanable.clean(); |
|
} |
But that doesn't do anything since all entries are created with null cleanables:
|
this.msgHdrMemoryCleanable = null; |
|
this.socketAddrMemoryCleanable = null; |
|
this.iovMemoryCleanable = null; |
|
this.cmsgDataMemoryCleanable = null; |
And the release method is:
|
void release() { |
|
if (msgHdrMemoryCleanable != null) { |
|
msgHdrMemoryCleanable.clean(); |
|
} |
|
if (socketAddrMemoryCleanable != null) { |
|
socketAddrMemoryCleanable.clean(); |
|
} |
|
if (iovMemoryCleanable != null) { |
|
iovMemoryCleanable.clean(); |
|
} |
|
if (cmsgDataMemoryCleanable != null) { |
|
cmsgDataMemoryCleanable.clean(); |
|
} |
|
} |
So, for each closed Vert.x // Netty instance, about 8 event loops retain their own ioHandler, which retains about 600kB. In our test suite, when the OOM happens, we have about 200 closed Vert.x // Netty instances hold by the global Vert.x cleaner. That adds up to about a gigabyte 😅
Again, this is going to be addressed in Vert.x by eclipse-vertx/vert.x#6109, but perhaps could we improve things on the Netty side as well?
How about filling the array with nulls when MsgHdrMemoryArray.release() is invoked?
void release() {
assert !released;
released = true;
Arrays.fill(hdrs, null);
msgHdrMemoryArrayMemoryCleanable.clean();
}
Wouldn't the GC get a chance to do its job earlier?
Perhaps this happens because we run the test suite with JDK11, that doesn't have the offsetSlice method?
|
public static ByteBuffer offsetSlice(ByteBuffer buffer, int index, int length) { |
|
if (PlatformDependent0.hasOffsetSliceMethod()) { |
|
return PlatformDependent0.offsetSlice(buffer, index, length); |
|
} else { |
|
return ((ByteBuffer) buffer.duplicate().clear().position(index).limit(index + length)).slice(); |
|
} |
|
} |
Recently, we've added a test to the Vert.x core test suite that requires some memory resources (a few dozen MB). The build passes with all transports except io_uring.
We captured a heap dump on
OutOfMemoryError. Here's what we found.When Vert.x creates a
NetClient, it registers a Java cleaner task (in case the user does not close the client properly).This task holds a reference to the corresponding Vert.x instance, even when the client is closed properly.
eclipse-vertx/vert.x#6109 is going to address the problem.
Regardless of this Vert.x specific issue, we noticed that after Vert.x / Netty are closed, each
IoUringIoHandlerstill holds aMsgHdrMemoryArrayof 1024 entries that retains about 600kB.When the
ioHandleris destroyed, the array is released:netty/transport-classes-io_uring/src/main/java/io/netty/channel/uring/IoUringIoHandler.java
Line 476 in 93c5a40
Each entry is released as well:
netty/transport-classes-io_uring/src/main/java/io/netty/channel/uring/MsgHdrMemoryArray.java
Lines 85 to 92 in 0306ded
But that doesn't do anything since all entries are created with
nullcleanables:netty/transport-classes-io_uring/src/main/java/io/netty/channel/uring/MsgHdrMemory.java
Lines 52 to 55 in 0306ded
And the release method is:
netty/transport-classes-io_uring/src/main/java/io/netty/channel/uring/MsgHdrMemory.java
Lines 177 to 190 in 0306ded
So, for each closed Vert.x // Netty instance, about 8 event loops retain their own
ioHandler, which retains about 600kB. In our test suite, when the OOM happens, we have about 200 closed Vert.x // Netty instances hold by the global Vert.x cleaner. That adds up to about a gigabyte 😅Again, this is going to be addressed in Vert.x by eclipse-vertx/vert.x#6109, but perhaps could we improve things on the Netty side as well?
How about filling the array with nulls when
MsgHdrMemoryArray.release()is invoked?Wouldn't the GC get a chance to do its job earlier?
Perhaps this happens because we run the test suite with JDK11, that doesn't have the offsetSlice method?
netty/common/src/main/java/io/netty/util/internal/PlatformDependent.java
Lines 1068 to 1074 in 6b16cda