Skip to content

Use sun.misc.Unsafe only where Netty does - #53

Merged
merlimat merged 1 commit into
streamnative:masterfrom
merlimat:unsafe-only-with-netty
Oct 13, 2026
Merged

merlimat merged 1 commit into
streamnative:masterfrom
merlimat:unsafe-only-with-netty

Conversation

@merlimat

Copy link
Copy Markdown
Collaborator

Problem

Since JDK 24, the first call to one of sun.misc.Unsafe's memory-access methods prints a warning naming the caller (JEP 498). LightProtoCodec looks Unsafe up in its static initializer. An application whose Netty doesn't use Unsafe therefore gets that warning from LightProto:

  • Netty 4.2 on Java 25, where io.netty.noUnsafe=true is the default.
  • Any application started with -Dio.netty.noUnsafe=true.
  • Any application started with --sun-misc-unsafe-memory-access=warn|deny, which Netty 4.1.131 also honors.

Where Netty uses Unsafe (Netty 4.1 by default), Netty's own calls have printed the warning already, and the JVM prints it only once.

Example

Parsing or writing any LightProto message with -Dio.netty.noUnsafe=true on JDK 26:

WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by io.streamnative.lightproto.tests.LightProtoCodec (file:/.../tests/target/classes/)
WARNING: Please consider reporting this to the maintainers of class io.streamnative.lightproto.tests.LightProtoCodec
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release

The call comes from the codec's static initializer:

        try {
            Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
            ...
            offset = (long) objectFieldOffset.invoke(unsafe, String.class.getDeclaredField("value"));

Change

The codec looks Unsafe up only when Netty uses it (the try block is only re-indented):

        if (PlatformDependent.hasUnsafe()) {
            try {
                Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
                ...

Where Netty uses Unsafe, nothing else changes. Where it doesn't, the paths without Unsafe are taken. Two of them no longer allocate a temporary array per ASCII string:

// byte[] targets
s.getBytes(0, bytesCount, a, i);

// NIO views of direct buffers (messages above NIO_WRITE_MIN)
for (int k = 0; k < bytesCount; k++) {
    nb.put(i + k, (byte) s.charAt(k));
}

Both replace getBytes(ISO_8859_1) copies and were faster at every length measured, from 3 to 2000 chars. The NIO writer is allocation-free, so NioWriteTest's allocation check no longer skips -XX:-CompactStrings.

Why not remove Unsafe

The JDK has no public way to build a String around an existing byte[], or to read a String's bytes, without copying them. JDK 26's MemorySegment.getString/setString only handle NUL-terminated strings. Without Unsafe, the string paths got slower.

Measured on JDK 26 with interleaved A/B runs (4-6 forks per variant), as the change in time per op, where positive is slower:

Benchmark This PR Unsafe removed
3-char string read, pooled direct buffer +2.2% (noise) +24.8%
57-char string read, pooled direct buffer -0.6% +15.9%
MessageMetadata serialize +1.1% +6.8%
AddressBook serialize +0.3% +9.9%
Topic list 600 B / 6 KB, NIO-view write +0.5% / -0.7% +25% / +67%
gRPC parse, direct buffer view -1.2% +7.8%
gRPC parse, heap copy 0.0% -4.1%

With Netty's Unsafe off (-Dio.netty.noUnsafe=true), this PR trades the warning for the paths without Unsafe:

  • String reads: 9-18% faster. Pooled buffers are heap in that mode.
  • Array serialization: 7-9% slower.
  • NIO-view topic lists: 25-63% slower.
  • gRPC direct-view parse: 3% slower.

Testing

NoUnsafeTest starts a JVM with -Dio.netty.noUnsafe=true --sun-misc-unsafe-memory-access=warn. It round-trips strings through the heap, direct, NIO-view and gRPC-view paths and asserts that no Unsafe warning is printed. It fails on master with the warning above. It needs JDK 23+ for the flag, so CI's JDK 17 skips it.

mvn verify runs 584 tests in each of the two surefire executions (default and -XX:-CompactStrings), all passing.

Since JDK 24 the first call to a memory-access method of sun.misc.Unsafe
prints a warning that names the caller (JEP 498). LightProtoCodec looks
Unsafe up in its static initializer, so it prints that warning whenever
Netty doesn't use Unsafe itself: by default from Netty 4.2 on Java 25,
and with -Dio.netty.noUnsafe=true or --sun-misc-unsafe-memory-access
set to anything but allow. Where Netty uses it, Netty has printed the
warning already, and the JVM prints it only once.

The codec now looks Unsafe up only when PlatformDependent.hasUnsafe()
is true. Where it is, nothing else changes. Where it isn't, the paths
without Unsafe are taken, and two of them no longer allocate a
temporary array per ASCII string: arrays are written with
String.getBytes(int, int, byte[], int), and NIO views with one put per
char, which was faster than staging the bytes at every length measured.

Removing Unsafe outright was measured and rejected: the JDK can't build
a String around an existing array, or read a String's bytes, without a
copy, so string reads from direct buffers, string-dense serialization
and NIO-view writes of strings got 7-67% slower on JDK 26.
@merlimat
merlimat merged commit 8a6e9a4 into streamnative:master Oct 13, 2026
1 check passed
@merlimat
merlimat deleted the unsafe-only-with-netty branch October 13, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant