sun.misc.Unsafe in Java: Deprecation, VarHandle and FFM API

sun.misc.Unsafe explained for Java 25 with what it does, the JEP 471 and 498 deprecation steps, warnings, and how to move to VarHandle and the FFM API.

Timeline of sun.misc.Unsafe from Java 9 to Java 26 with VarHandle in Java 9, FFM API in Java 22, deprecation for removal in Java 23, runtime warnings by default in Java 24 and still warnings in Java 25 and 26, followed by deny and removal in later releases

The class sun.misc.Unsafe is an internal JDK class that reads and writes memory without bounds or type checks, allocates memory outside the Java heap and performs compare-and-swap operations on fields. Its memory-access methods are deprecated for removal since Java 23 (JEP 471), and since Java 24 the JVM prints a warning the first time one of them runs (JEP 498).

Libraries such as serializers, network frameworks and concurrent data structures used Unsafe for speed before the JDK had a safe API for the same work. Today, field access and compare-and-swap go through VarHandle (Java 9), and off-heap memory goes through the Foreign Function and Memory (FFM) API, final since Java 22 (JEP 454).

The following example writes and reads a long outside the heap, once with Unsafe and once with the FFM API.

Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

long address = unsafe.allocateMemory(8);
unsafe.putLong(address, 42L);                                  // deprecated for removal
long oldWay = unsafe.getLong(address);                         // 42
unsafe.freeMemory(address);

MemorySegment segment = Arena.ofAuto().allocate(ValueLayout.JAVA_LONG);
segment.set(ValueLayout.JAVA_LONG, 0, 42L);
long newWay = segment.get(ValueLayout.JAVA_LONG, 0);           // 42

Notice that the Unsafe version works with a raw address that nothing checks, whereas the MemorySegment knows its size and who frees it. We look at what Unsafe offers and how code gets an instance, follow its deprecation from Java 23 to Java 27, and replace each use case with VarHandle or the FFM API.

1. What sun.misc.Unsafe Provides

The class lives in the module jdk.unsupported, which the JDK kept open after Java 9 so that existing libraries could still run. It is not part of the Java SE API, has no official Javadoc, and its methods can crash the JVM when they get a wrong address or offset. The JDK has its own copy, jdk.internal.misc.Unsafe, which application code cannot access.

On Java 25, 67 of the 70 public method names of sun.misc.Unsafe are deprecated for removal. Only allocateInstance(), pageSize() and throwException() carry no deprecation. The methods fall into a few groups, and each group has a supported replacement.

GroupExample methodsDeprecated for removal sinceReplacement
Field access and CAS on objectsobjectFieldOffset, getInt, compareAndSwapInt, getAndAddIntJava 18 (offsets), Java 23 (access)VarHandle, atomic classes
Array element accessarrayBaseOffset, arrayIndexScaleJava 23MethodHandles.arrayElementVarHandle()
Off-heap memoryallocateMemory, putLong(address, …), copyMemory, freeMemoryJava 23Arena and MemorySegment (FFM API)
Fences and thread parkingfullFence, park, unparkJava 22VarHandle.fullFence(), LockSupport
Direct buffer cleanupinvokeCleanerJava 23Arena.close() for FFM segments
Object creation without a constructorallocateInstanceNot deprecatedA constructor, factory or serialization framework

2. Getting an Unsafe Instance

The static method Unsafe.getUnsafe() exists, but it throws SecurityException unless the caller was loaded by the boot class loader, which is never the case for application code. Libraries read the private static field theUnsafe with reflection instead, and the module jdk.unsupported allows that access.

Unsafe direct = Unsafe.getUnsafe();                            // SecurityException: Unsafe

Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe instance = (Unsafe) theUnsafe.get(null);
int pageSize = instance.pageSize();                            // 4096

The compiler already warns at the import, with warning: Unsafe is internal proprietary API and may be removed in a future release. That warning has existed for years, and unlike normal deprecation warnings, @SuppressWarnings cannot turn it off.

3. Deprecation Timeline and Runtime Warnings

The JDK removes Unsafe in steps, so that library authors have several releases to move to the replacements. JEP 471 lists the steps, and each one lands in a separate release.

Timeline of sun.misc.Unsafe from Java 9 to Java 26 with VarHandle in Java 9, FFM API in Java 22, deprecation for removal in Java 23, runtime warnings by default in Java 24 and still warnings in Java 25 and 26, followed by deny and removal in later releases
Unsafe still works on Java 25 and 26 with a warning, and the next steps make the memory-access methods throw and then remove them
Java versionChange
Java 9VarHandle added (JEP 193); sun.misc.Unsafe moves to the module jdk.unsupported
Java 22FFM API final (JEP 454); fences and park/unpark deprecated for removal
Java 23All memory-access methods deprecated for removal; option –sun-misc-unsafe-memory-access added with default allow
Java 24Default becomes warn, one warning on the first call (JEP 498)
Java 25 to Java 27Default is still warn
Later releaseDefault deny, calls throw UnsupportedOperationException; after that, the methods are removed

The following program uses Unsafe for a counter, and on Java 25 it prints four warning lines on the first call before its own output.

java -cp classes LegacyCounter
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by LegacyCounter (file:/home/dev/app/classes/)
WARNING: Please consider reporting this to the maintainers of class LegacyCounter
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
views = 1

The option –sun-misc-unsafe-memory-access takes four values. The value allow hides the warning, warn prints it once, debug prints one line with a stack trace for every call, and deny makes every call fail the way it will in a future release.

java --sun-misc-unsafe-memory-access=deny -cp classes LegacyCounter
Exception in thread "main" java.lang.UnsupportedOperationException: objectFieldOffset
	at jdk.unsupported/sun.misc.Unsafe.beforeMemoryAccessSlow(Unsafe.java:1822)
	at jdk.unsupported/sun.misc.Unsafe.beforeMemoryAccess(Unsafe.java:1787)
	at jdk.unsupported/sun.misc.Unsafe.objectFieldOffset(Unsafe.java:905)

Running the test suite with deny today shows which code paths will break when the default changes. The flag allow only hides the problem until the methods are gone.

4. Replacing Field Access and CAS With VarHandle

A VarHandle is a typed reference to a field, a static field or an array element that supports plain, volatile, acquire/release and atomic access. It replaces the pair of objectFieldOffset() and compareAndSwapInt(), and the JVM checks the type and the receiver on every call, so a wrong object throws an exception instead of corrupting memory.

A web analytics service counts page views in millions of small PageStats objects. One AtomicInteger per field would double the number of objects, so the class keeps a plain volatile int and updates it through one static VarHandle, which is how libraries used Unsafe before.

static class PageStats {
    private static final VarHandle VIEWS;
    static {
        try {
            VIEWS = MethodHandles.lookup().findVarHandle(PageStats.class, "views", int.class);
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private volatile int views;

    void recordView() {
        VIEWS.getAndAdd(this, 1);
    }

    boolean resetIf(int expected) {
        return VIEWS.compareAndSet(this, expected, 0);
    }

    int views() {
        return views;
    }
}
PageStats stats = new PageStats();
IntStream.range(0, 10_000).parallel().forEach(i -> stats.recordView());
int counted = stats.views();                                   // 10000
boolean staleReset = stats.resetIf(9_999);                     // false
boolean reset = stats.resetIf(10_000);                         // true
int afterReset = stats.views();                                // 0

Ten thousand parallel increments give a count of 10,000, so getAndAdd() is atomic in the same way as Unsafe.getAndAddInt(). The method names map almost one to one.

sun.misc.UnsafeVarHandle
objectFieldOffset(field)MethodHandles.lookup().findVarHandle(Owner.class, “name”, type)
getIntVolatile(obj, offset)handle.getVolatile(obj)
putOrderedInt(obj, offset, v)handle.setRelease(obj, v)
compareAndSwapInt(obj, offset, e, v)handle.compareAndSet(obj, e, v)
getAndAddInt(obj, offset, d)handle.getAndAdd(obj, d)
arrayBaseOffset + arrayIndexScaleMethodHandles.arrayElementVarHandle(int[].class)
fullFence()VarHandle.fullFence()

For a single counter, AtomicInteger is simpler. The theory behind compareAndSet() is covered in compare-and-swap, and the visibility rules of the access modes come from the Java memory model.

Arrays work the same way with an element handle, which also replaces the manual address arithmetic with arrayBaseOffset().

VarHandle slots = MethodHandles.arrayElementVarHandle(int[].class);
int[] seats = new int[4];
boolean booked = slots.compareAndSet(seats, 2, 0, 1);           // true
boolean bookedTwice = slots.compareAndSet(seats, 2, 0, 1);      // false
int outside = (int) slots.getVolatile(seats, 4);                // ArrayIndexOutOfBoundsException

5. Replacing Off-Heap Memory With the FFM API

The FFM API in java.lang.foreign allocates memory outside the heap through an Arena, which decides when the memory is freed, and returns a MemorySegment, which knows its address and size. Every access is checked against the bounds of the segment and against the lifetime of the arena, so the errors that crash the JVM with Unsafe become exceptions.

A price feed keeps the latest bid and ask for 100,000 instruments in a fixed-size off-heap table, so the garbage collector does not scan the data. With Unsafe, the code allocated one block and computed addresses by hand. With FFM, the arena owns the block and closes it at the end of the try block.

static long lastAsk(int instruments, int instrument, long ask) {
    try (Arena arena = Arena.ofConfined()) {
        MemorySegment table = arena.allocate(ValueLayout.JAVA_LONG, instruments * 2L);
        table.setAtIndex(ValueLayout.JAVA_LONG, instrument * 2L + 1, ask);
        return table.getAtIndex(ValueLayout.JAVA_LONG, instrument * 2L + 1);
    }
}
long ask = lastAsk(100_000, 7, 10_125L);                       // 10125
long tableBytes = Arena.ofAuto().allocate(ValueLayout.JAVA_LONG, 200_000L).byteSize(); // 1600000
long outOfRange = lastAsk(100_000, 100_000, 1L);               // IndexOutOfBoundsException

The bounds check that throws IndexOutOfBoundsException in the last line would have been a silent write past the end of the block with Unsafe. Segment sizes and offsets are long values, so a segment can be larger than 2 GB, which was the reason for the “big array” trick with allocateMemory(). A segment used after its arena is closed throws IllegalStateException instead of reading freed memory.

Arena shortLived = Arena.ofConfined();
MemorySegment buffer = shortLived.allocate(16);
shortLived.close();
byte afterClose = buffer.get(ValueLayout.JAVA_BYTE, 0);         // IllegalStateException: Already closed
Unsafe callFFM replacement
allocateMemory(size)arena.allocate(size)
putLong(address, v) / getLong(address)segment.set(JAVA_LONG, offset, v) / segment.get(JAVA_LONG, offset)
copyMemory(src, dst, n)MemorySegment.copy(src, srcOffset, dst, dstOffset, n)
setMemory(address, n, value)segment.fill(value)
freeMemory(address)arena.close(), or garbage collection with Arena.ofAuto()
invokeCleaner(directBuffer)segment.asByteBuffer() on a segment from a closeable arena

The same API maps files into memory with FileChannel.map(mode, offset, size, arena), which returns a MemorySegment. That replaces both Unsafe address tricks and the 2 GB limit of MappedByteBuffer.

6. Old Unsafe Tricks and Their Safe Versions

Many blog posts from the Java 7 era show Unsafe tricks that are broken, unsafe or unnecessary on Java 25. Each one has a plain Java answer.

6.1. Creating an Object Without Calling the Constructor

The method allocateInstance() creates an object and skips every constructor and field initializer, so all fields keep their default values. It is not deprecated, and serialization and mocking libraries still use it, but in application code it produces objects that break their own rules.

static class Ticket {
    private final String event;
    Ticket(String event) {
        this.event = Objects.requireNonNull(event);
    }
    String event() {
        return event;
    }
}
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe u = (Unsafe) unsafeField.get(null);
Ticket blank = (Ticket) u.allocateInstance(Ticket.class);
String noEvent = blank.event();                                // null

The constructor rejects a null event, yet the object exists with one. The same trick creates a second instance of a singleton with a private constructor. An enum singleton blocks Constructor.newInstance() and serialization copies, but even an enum class gets a second, broken constant from allocateInstance() on Java 25, with null as its name.

6.2. Wiping a Password String

Old examples copy question marks over the memory of a String to erase a password. On current JDKs, the characters of a String sit in a byte[] that the object only references, so copying over the object writes into the wrong place and can corrupt the heap. Strings are immutable by design, and the safe way is to keep secrets in a char[] or byte[] and clear it with Arrays.fill().

char[] password = {'s', 'e', 'c', 'r', 'e', 't'};
Arrays.fill(password, '\0');
int nonZero = (int) new String(password).chars().filter(c -> c != 0).count(); // 0

6.3. Defining Classes at Runtime

The method Unsafe.defineClass() was removed in Java 11, and defineAnonymousClass() was removed in Java 17. The supported versions are MethodHandles.Lookup.defineClass() for a normal class in the same package, and Lookup.defineHiddenClass() for classes that frameworks generate and unload, as lambdas and proxies do.

6.4. Shallow Copies by Copying Memory

Copying the bytes of one object onto another skips the object header rules, the garbage collector barriers and the field layout, which changes between JDK versions and between garbage collectors. A copy constructor, a static factory or a record with a with-style method gives the same result, and the JIT compiler makes a field-by-field copy fast.

7. Finding Unsafe in Your Dependencies

Most applications never call Unsafe themselves, and the warning comes from a library. The tool jdeps lists every class that uses a JDK internal API, and the debug mode of the JVM option shows the call site with a stack trace.

jdeps --jdk-internals classes/LegacyCounter.class
java --sun-misc-unsafe-memory-access=debug -cp classes LegacyCounter
LegacyCounter.class -> jdk.unsupported
   LegacyCounter                                      -> sun.misc.Unsafe                                    JDK internal API (jdk.unsupported)

WARNING: sun.misc.Unsafe::objectFieldOffset called by LegacyCounter (file:/home/dev/app/classes/)
	at LegacyCounter.main(LegacyCounter.java:10)
WARNING: sun.misc.Unsafe::getAndAddInt called by LegacyCounter (file:/home/dev/app/classes/)
	at LegacyCounter.main(LegacyCounter.java:12)

For a JAR, we run jdeps –jdk-internals -cp ‘lib/*’ app.jar on the whole classpath. When the caller is a library, we upgrade it to a version that uses VarHandle and FFM, or report the call to its maintainers, as the warning itself asks.

8. sun.misc.Unsafe FAQs

After a JDK upgrade prints the Unsafe warning, teams want to know whether it breaks anything and how to remove it.

8.1. Is sun.misc.Unsafe removed in Java 25?

No. The class and its methods still work on Java 25, 26 and 27, and the memory-access methods print one warning on first use. Removal comes after a release where the default is deny, so the exact version is not fixed yet.

8.2. How do we get rid of the warning “A terminally deprecated method in sun.misc.Unsafe has been called”?

The real fix is to replace the call, or to upgrade the library that makes it, using the class named in the second warning line. As a short-term measure, –sun-misc-unsafe-memory-access=allow hides the warning on Java 24 to 27.

8.3. Is VarHandle as fast as Unsafe?

For the same access mode, yes in practice. The JIT compiler turns a call on a static final VarHandle into the same instructions as the Unsafe call, and performance on par with Unsafe was a stated goal of JEP 193. The handle must be stored in a static final field, otherwise the compiler cannot optimize it.

8.4. What is the difference between sun.misc.Unsafe and jdk.internal.misc.Unsafe?

The class sun.misc.Unsafe is the old public-by-accident API in jdk.unsupported, kept for compatibility. The class jdk.internal.misc.Unsafe is the JDK’s own implementation, in a package that the module java.base does not export, so application code cannot use it without –add-exports java.base/jdk.internal.misc=ALL-UNNAMED.

9. Conclusion

The class sun.misc.Unsafe gave libraries raw memory access and CAS before the JDK had safe APIs for them. On Java 25, almost all of its methods are deprecated for removal, the JVM warns on first use, and a future release makes the memory-access methods throw.

The migration path is clear. Field and array access move to VarHandle or the atomic classes, off-heap memory moves to Arena and MemorySegment, and the old tricks have plain Java versions. Running tests with –sun-misc-unsafe-memory-access=deny shows what still needs work. The Java examples page groups more small programs by task.

10. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Lokesh,
    First, thanks for a great post.
    In your example, Player p = (Player) unsafe.allocateInstance(Player.class); Where is the object created? On JAVA HEAP or non-heap direct memory?

    Are all the operations you have explained in the article non-heap allocations? I ask this because, when you use “new” key-word, its supposed to create an instance on the heap. If it does, then what is the actual advantage, because then, it doesnt by-pass the GC.

    Thanks
    Ashley

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.