Skip to content

Commit ad07813

Browse files
authored
chore: Replace sun.misc.Unsafe memory access (#32802)
* Optimized string access without sun.misc.Unsafe for JDK 26+ * Replace unsafe memory access with VarHandle for all supported Java versions
1 parent 996ebd0 commit ad07813

File tree

25 files changed

+354
-210
lines changed

25 files changed

+354
-210
lines changed

akka-actor-tests/src/test/scala/akka/util/AsciiStringCopySpec.scala

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,40 @@
33
*/
44

55
package akka.util
6-
import java.nio.charset.StandardCharsets
76

87
import org.scalatest.matchers.should.Matchers
98
import org.scalatest.wordspec.AnyWordSpec
109

10+
import java.lang.reflect.InaccessibleObjectException
11+
import java.nio.charset.StandardCharsets
12+
1113
class AsciiStringCopySpec extends AnyWordSpec with Matchers {
1214

1315
"The copyUSAsciiStrToBytes optimization" must {
1416

15-
"select working algorithm" in {
16-
if (Unsafe.isIsJavaVersion9Plus) {
17+
if (JavaVersion.majorVersion <= 25) {
18+
"select working algorithm when stdlib internals accessible through sun.misc.Unsafe" in {
19+
Unsafe.testUSAsciiStrToBytesAlgorithm0("abc") should ===(true)
20+
// this is known to fail with JDK 11 on ARM32 (Raspberry Pi),
21+
// and therefore algorithm 0 is selected on that architecture
22+
Unsafe.testUSAsciiStrToBytesAlgorithm1("abc") should ===(true)
23+
Unsafe.testUSAsciiStrToBytesAlgorithm2("abc") should ===(false)
24+
}
25+
} else if (canAccessStringInternals) {
26+
"select working algorithm when stdlib internals accessible through VarHandle" in {
27+
// Note: requires --add-opens=java.base/java.lang=ALL-UNNAMED on JDK 26 and later
1728
Unsafe.testUSAsciiStrToBytesAlgorithm0("abc") should ===(true)
1829
// this is known to fail with JDK 11 on ARM32 (Raspberry Pi),
1930
// and therefore algorithm 0 is selected on that architecture
2031
Unsafe.testUSAsciiStrToBytesAlgorithm1("abc") should ===(true)
2132
Unsafe.testUSAsciiStrToBytesAlgorithm2("abc") should ===(false)
22-
} else {
33+
}
34+
} else {
35+
"select working algorithm when stdlib internals not accessible" in {
36+
// if the JVM does not allow us stdlib internals access, public API is the way to go
2337
Unsafe.testUSAsciiStrToBytesAlgorithm0("abc") should ===(true)
2438
Unsafe.testUSAsciiStrToBytesAlgorithm1("abc") should ===(false)
25-
Unsafe.testUSAsciiStrToBytesAlgorithm2("abc") should ===(true)
39+
Unsafe.testUSAsciiStrToBytesAlgorithm2("abc") should ===(false)
2640
}
2741
}
2842

@@ -42,4 +56,13 @@ class AsciiStringCopySpec extends AnyWordSpec with Matchers {
4256

4357
}
4458

59+
private def canAccessStringInternals: Boolean = {
60+
try {
61+
classOf[String].getDeclaredField("value").setAccessible(true)
62+
true
63+
} catch {
64+
case _: InaccessibleObjectException => false
65+
}
66+
}
67+
4568
}

akka-actor/src/main/java/akka/actor/AbstractActorRef.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,26 @@
44

55
package akka.actor;
66

7-
import akka.util.Unsafe;
7+
import akka.annotation.InternalApi;
8+
import java.lang.invoke.MethodHandles;
9+
import java.lang.invoke.VarHandle;
810

11+
/** INTERNAL API */
12+
@InternalApi
913
final class AbstractActorRef {
10-
static final long cellOffset;
11-
static final long lookupOffset;
14+
15+
static final VarHandle cellHandle;
16+
static final VarHandle lookupHandle;
1217

1318
static {
1419
try {
15-
cellOffset =
16-
Unsafe.instance.objectFieldOffset(
20+
MethodHandles.Lookup lookup =
21+
MethodHandles.privateLookupIn(RepointableActorRef.class, MethodHandles.lookup());
22+
cellHandle =
23+
lookup.unreflectVarHandle(
1724
RepointableActorRef.class.getDeclaredField("_cellDoNotCallMeDirectly"));
18-
lookupOffset =
19-
Unsafe.instance.objectFieldOffset(
25+
lookupHandle =
26+
lookup.unreflectVarHandle(
2027
RepointableActorRef.class.getDeclaredField("_lookupDoNotCallMeDirectly"));
2128
} catch (Throwable t) {
2229
throw new ExceptionInInitializerError(t);

akka-actor/src/main/java/akka/actor/dungeon/AbstractActorCell.java

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,49 @@
55
package akka.actor.dungeon;
66

77
import akka.actor.ActorCell;
8-
import akka.util.Unsafe;
8+
import akka.annotation.InternalApi;
9+
import java.lang.invoke.MethodHandles;
10+
import java.lang.invoke.VarHandle;
11+
import java.lang.reflect.Field;
912

13+
/** INTERNAL API */
14+
@InternalApi
1015
final class AbstractActorCell {
11-
static final long mailboxOffset;
12-
static final long childrenOffset;
13-
static final long nextNameOffset;
14-
static final long functionRefsOffset;
16+
static final VarHandle mailboxHandle;
17+
static final VarHandle childrenHandle;
18+
private static final VarHandle nextNameHandle;
19+
static final VarHandle functionRefsHandle;
1520

1621
static {
1722
try {
18-
mailboxOffset =
19-
Unsafe.instance.objectFieldOffset(
20-
ActorCell.class.getDeclaredField(
21-
"akka$actor$dungeon$Dispatch$$_mailboxDoNotCallMeDirectly"));
22-
childrenOffset =
23-
Unsafe.instance.objectFieldOffset(
24-
ActorCell.class.getDeclaredField(
25-
"akka$actor$dungeon$Children$$_childrenRefsDoNotCallMeDirectly"));
26-
nextNameOffset =
27-
Unsafe.instance.objectFieldOffset(
28-
ActorCell.class.getDeclaredField(
29-
"akka$actor$dungeon$Children$$_nextNameDoNotCallMeDirectly"));
30-
functionRefsOffset =
31-
Unsafe.instance.objectFieldOffset(
32-
ActorCell.class.getDeclaredField(
33-
"akka$actor$dungeon$Children$$_functionRefsDoNotCallMeDirectly"));
23+
MethodHandles.Lookup lookup =
24+
MethodHandles.privateLookupIn(ActorCell.class, MethodHandles.lookup());
25+
Field mailboxField =
26+
ActorCell.class.getDeclaredField(
27+
"akka$actor$dungeon$Dispatch$$_mailboxDoNotCallMeDirectly");
28+
mailboxHandle = lookup.unreflectVarHandle(mailboxField);
29+
30+
Field childrenField =
31+
ActorCell.class.getDeclaredField(
32+
"akka$actor$dungeon$Children$$_childrenRefsDoNotCallMeDirectly");
33+
childrenHandle = lookup.unreflectVarHandle(childrenField);
34+
35+
Field nextNameField =
36+
ActorCell.class.getDeclaredField(
37+
"akka$actor$dungeon$Children$$_nextNameDoNotCallMeDirectly");
38+
nextNameHandle = lookup.unreflectVarHandle(nextNameField);
39+
40+
Field functionRefsField =
41+
ActorCell.class.getDeclaredField(
42+
"akka$actor$dungeon$Children$$_functionRefsDoNotCallMeDirectly");
43+
functionRefsHandle = lookup.unreflectVarHandle(functionRefsField);
3444
} catch (Throwable t) {
3545
throw new ExceptionInInitializerError(t);
3646
}
3747
}
48+
49+
// Note: manual forwarder to be certain we avoid boxing the long
50+
static long getAndAddNextName(Children children) {
51+
return (long) nextNameHandle.getAndAdd(children, 1L);
52+
}
3853
}

akka-actor/src/main/java/akka/dispatch/AbstractBoundedNodeQueue.java

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
package akka.dispatch;
66

7-
import akka.util.Unsafe;
7+
import java.lang.invoke.MethodHandles;
8+
import java.lang.invoke.VarHandle;
9+
import java.lang.reflect.Field;
810

911
/**
1012
* Lock-free bounded non-blocking multiple-producer single-consumer queue based on the works of:
@@ -36,29 +38,29 @@ protected AbstractBoundedNodeQueue(final int capacity) {
3638
}
3739

3840
private void setEnq(Node<T> n) {
39-
Unsafe.instance.putObjectVolatile(this, enqOffset, n);
41+
enqHandle.setVolatile(this, n);
4042
}
4143

4244
@SuppressWarnings("unchecked")
4345
private Node<T> getEnq() {
44-
return (Node<T>)Unsafe.instance.getObjectVolatile(this, enqOffset);
46+
return (Node<T>) enqHandle.getVolatile(this);
4547
}
4648

4749
private boolean casEnq(Node<T> old, Node<T> nju) {
48-
return Unsafe.instance.compareAndSwapObject(this, enqOffset, old, nju);
50+
return enqHandle.compareAndSet(this, old, nju);
4951
}
5052

5153
private void setDeq(Node<T> n) {
52-
Unsafe.instance.putObjectVolatile(this, deqOffset, n);
54+
deqHandle.setVolatile(this, n);
5355
}
5456

5557
@SuppressWarnings("unchecked")
5658
private Node<T> getDeq() {
57-
return (Node<T>)Unsafe.instance.getObjectVolatile(this, deqOffset);
59+
return (Node<T>)deqHandle.getVolatile(this);
5860
}
5961

6062
private boolean casDeq(Node<T> old, Node<T> nju) {
61-
return Unsafe.instance.compareAndSwapObject(this, deqOffset, old, nju);
63+
return deqHandle.compareAndSet(this, old, nju);
6264
}
6365

6466
protected final Node<T> peekNode() {
@@ -174,12 +176,17 @@ public final Node<T> pollNode() {
174176
}
175177
}
176178

177-
private final static long enqOffset, deqOffset;
179+
private final static VarHandle enqHandle;
180+
private final static VarHandle deqHandle;
178181

179182
static {
180183
try {
181-
enqOffset = Unsafe.instance.objectFieldOffset(AbstractBoundedNodeQueue.class.getDeclaredField("_enqDoNotCallMeDirectly"));
182-
deqOffset = Unsafe.instance.objectFieldOffset(AbstractBoundedNodeQueue.class.getDeclaredField("_deqDoNotCallMeDirectly"));
184+
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(AbstractBoundedNodeQueue.class, MethodHandles.lookup());
185+
Field enqField = AbstractBoundedNodeQueue.class.getDeclaredField("_enqDoNotCallMeDirectly");
186+
enqHandle = lookup.unreflectVarHandle(enqField);
187+
188+
Field deqField = AbstractBoundedNodeQueue.class.getDeclaredField("_deqDoNotCallMeDirectly");
189+
deqHandle = lookup.unreflectVarHandle(deqField);
183190
} catch(Throwable t){
184191
throw new ExceptionInInitializerError(t);
185192
}
@@ -193,18 +200,20 @@ public static class Node<T> {
193200

194201
@SuppressWarnings("unchecked")
195202
public final Node<T> next() {
196-
return (Node<T>)Unsafe.instance.getObjectVolatile(this, nextOffset);
203+
return (Node<T>) nextHandle.getVolatile(this);
197204
}
198205

199206
protected final void setNext(final Node<T> newNext) {
200-
Unsafe.instance.putOrderedObject(this, nextOffset, newNext);
207+
nextHandle.setRelease(this, newNext);
201208
}
202209

203-
private final static long nextOffset;
210+
private final static VarHandle nextHandle;
204211

205212
static {
206213
try {
207-
nextOffset = Unsafe.instance.objectFieldOffset(Node.class.getDeclaredField("_nextDoNotCallMeDirectly"));
214+
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Node.class, MethodHandles.lookup());
215+
Field nextField = Node.class.getDeclaredField("_nextDoNotCallMeDirectly");
216+
nextHandle = lookup.unreflectVarHandle(nextField);
208217
} catch(Throwable t){
209218
throw new ExceptionInInitializerError(t);
210219
}

akka-actor/src/main/java/akka/dispatch/AbstractMailbox.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,41 @@
44

55
package akka.dispatch;
66

7-
import akka.util.Unsafe;
7+
import akka.annotation.InternalApi;
88

9+
import java.lang.invoke.MethodHandles;
10+
import java.lang.invoke.VarHandle;
11+
12+
/**
13+
* INTERNAL API
14+
*/
15+
@InternalApi
916
final class AbstractMailbox {
10-
final static long mailboxStatusOffset;
11-
final static long systemMessageOffset;
17+
private final static VarHandle mailboxStatusHandle;
18+
final static VarHandle systemMessageHandle;
1219

1320
static {
1421
try {
15-
mailboxStatusOffset = Unsafe.instance.objectFieldOffset(Mailbox.class.getDeclaredField("_statusDoNotCallMeDirectly"));
16-
systemMessageOffset = Unsafe.instance.objectFieldOffset(Mailbox.class.getDeclaredField("_systemQueueDoNotCallMeDirectly"));
22+
MethodHandles.Lookup lookup =
23+
MethodHandles.privateLookupIn(Mailbox.class, MethodHandles.lookup());
24+
mailboxStatusHandle = lookup.unreflectVarHandle(Mailbox.class.getDeclaredField("_statusDoNotCallMeDirectly"));
25+
systemMessageHandle = lookup.unreflectVarHandle(Mailbox.class.getDeclaredField("_systemQueueDoNotCallMeDirectly"));
1726
} catch(Throwable t){
1827
throw new ExceptionInInitializerError(t);
1928
}
2029
}
30+
31+
32+
// Note: manual forwarder to be certain we avoid boxing the int
33+
static int volatileGetMailboxStatus(Mailbox mailbox) {
34+
return (int)mailboxStatusHandle.getVolatile(mailbox);
35+
}
36+
37+
static boolean compareAndSetMailboxStatus(Mailbox mailbox, int oldStatus, int newStatus) {
38+
return mailboxStatusHandle.compareAndSet(mailbox, oldStatus, newStatus);
39+
}
40+
41+
static void volatileSetMailboxStatus(Mailbox mailbox, int newStatus) {
42+
AbstractMailbox.mailboxStatusHandle.setVolatile(mailbox, newStatus);
43+
}
2144
}

akka-actor/src/main/java/akka/dispatch/AbstractMessageDispatcher.java

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,40 @@
44

55
package akka.dispatch;
66

7-
import akka.util.Unsafe;
7+
8+
import java.lang.invoke.MethodHandles;
9+
import java.lang.invoke.VarHandle;
810

911
abstract class AbstractMessageDispatcher {
10-
final static long shutdownScheduleOffset;
11-
final static long inhabitantsOffset;
12+
private final static VarHandle shutdownScheduleHandle;
13+
private final static VarHandle inhabitantsHandle;
1214

1315
static {
1416
try {
15-
shutdownScheduleOffset = Unsafe.instance.objectFieldOffset(MessageDispatcher.class.getDeclaredField("_shutdownScheduleDoNotCallMeDirectly"));
16-
inhabitantsOffset = Unsafe.instance.objectFieldOffset(MessageDispatcher.class.getDeclaredField("_inhabitantsDoNotCallMeDirectly"));
17+
MethodHandles.Lookup lookup =
18+
MethodHandles.privateLookupIn(MessageDispatcher.class, MethodHandles.lookup());
19+
shutdownScheduleHandle = lookup.unreflectVarHandle(MessageDispatcher.class.getDeclaredField("_shutdownScheduleDoNotCallMeDirectly"));
20+
inhabitantsHandle = lookup.unreflectVarHandle(MessageDispatcher.class.getDeclaredField("_inhabitantsDoNotCallMeDirectly"));
1721
} catch(Throwable t){
1822
throw new ExceptionInInitializerError(t);
1923
}
2024
}
25+
26+
// Note: manual forwarders to be certain we avoid boxing the ints/longs
27+
long volatileGetInhabitants() {
28+
return (long) inhabitantsHandle.getVolatile(this);
29+
}
30+
31+
long getAndAddInhabitants(long add) {
32+
return (long) inhabitantsHandle.getAndAdd(this, add);
33+
}
34+
35+
int volatileGetShutdownSchedule() {
36+
return (int) shutdownScheduleHandle.getVolatile(this);
37+
}
38+
39+
boolean compareAndSetShutdownSchedule(int expect, int update) {
40+
return shutdownScheduleHandle.compareAndSet(this, expect, update);
41+
}
42+
2143
}

0 commit comments

Comments
 (0)