Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runtime code implementing lazy val should not use sun.misc.Unsafe on Java 9+ (JEP-471) #9013

Open
smarter opened this issue May 20, 2020 · 10 comments
Labels
area:transform compat:java:jep:stable Issues corresponding to a JEP which got already delivered as a stable feature in a JDK release compat:java:jep Issues corresponding to a JEP (JDK Enhancement Proposal) compat:java itype:enhancement

Comments

@smarter
Copy link
Member

smarter commented May 20, 2020

Currently, any usage of a lazy val requires getitng an instance of sun.misc.Unsafe at runtime: https://github.com/lampepfl/dotty/blob/43e4bfa5598e9cdbebb1dc56ed25a319d5aa8fbe/library/src/dotty/runtime/LazyVals.scala#L7 which is problematic for various reasons (e.g. usage of a security manager, using Graal Native (#13985)).

On Java 8, there's no good alternative, but on Java 9+ we should be able to replace that using VarHandle.

We should be able to use the same trick used in scala.runtime.Statics to check once at runtime if VarHandle is available to stay compatible with Java 8: https://github.com/scala/scala/blob/a8a726118d06c90b5506f907b1524457c0d401a7/src/library/scala/runtime/Statics.java#L158-L173 (EDIT: actually I don't think this is good enough: using VarHandle would require adding static fields in any class that has a lazy val, so we need to know at compile-time the version of Java we support (via -release/-Xtarget)

carlosedp added a commit to carlosedp/zio-scalajs-stack that referenced this issue Oct 14, 2022
carlosedp added a commit to carlosedp/zio-scalajs-stack that referenced this issue Oct 14, 2022
carlosedp added a commit to carlosedp/zio-scalajs-stack that referenced this issue Oct 14, 2022
carlosedp added a commit to carlosedp/zio-scalajs-stack that referenced this issue Oct 14, 2022
carlosedp added a commit to carlosedp/zio-scalajs-stack that referenced this issue Oct 14, 2022
@stewSquared
Copy link

stewSquared commented Jun 5, 2023

Here's some prototype target code @smarter paired with me to write:

/*
 * Decompiled with CFR 0.152.
 *
 * Could not load the following classes:
 *  scala.runtime.LazyVals$
 */
import scala.runtime.LazyVals$;
import java.lang.invoke.VarHandle;
import java.lang.invoke.MethodHandles;

/*
 * Illegal identifiers - consider using --renameillegalidents true
 */
public class Decompiled {
    // public static final long OFFSET$0 = LazyVals$.MODULE$.getOffsetStatic(LazyVal.package.Foo.1.class.getDeclaredField("bitmap$1"));
    // get a varhandle instead of an offset
    private static final VarHandle handle;

    static {
        try {
            MethodHandles.Lookup l = MethodHandles.lookup();
            handle = l.findVarHandle(Decompiled.class, "bitmap$1", long.class);
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public long bitmap$1;
    public int value$lzy1;

    static final long LAZY_VAL_MASK = 3;
    static final long BITS_PER_LAZY_VAL = 2;


    public long casHelper(long current, long newState, int ord) {
        long mask = ~(LAZY_VAL_MASK << ord * BITS_PER_LAZY_VAL);
        return (current & mask) | (newState << (ord * BITS_PER_LAZY_VAL));
    }

    private boolean CAS(Object t, long current, long newState, int ord) {
        return handle.compareAndSet(t, current, casHelper(current, newState, ord));
    }

    public static void setFlag(Object t, int v, int ord) {
        boolean retry = true;
        while (retry) {
            long cur = handle.getVolatile(t);
            if (LazyVals$.MODULE$.STATE(cur, ord) == 1)
                retry = !CAS(t, cur, v, ord);
            else {
                // cur == 2, somebody is waiting on monitor
                if (CAS(t, cur, v, ord)) {
                    Class<?> clazz = LazyVals$.class;
                    var privateMethod = clazz.getDeclaredMethod("getMonitor", Object.class, int.class);
                    privateMethod.setAccessible(true);

                    Object monitor = privateMethod.invokew(LazyVals$.MODULE$, t, ord);
                    synchronized (monitor) {
                        monitor.notifyAll();
                    }
                    retry = false;
                }
            }
        }
    }

    public static void wait4Notification(Object t, long cur, int ord) {
        boolean retry = true;
        while (retry) {
            long current = handle.getVolatile(t);
            int state = LazyVals$.MODULE$.STATE(current, ord);
            if (state == 1)
                CAS(t, current, 2, ord);
            else if (state == 2) {
                Class<?> clazz = LazyVals$.class;
                var privateMethod = clazz.getDeclaredMethod("getMonitor", Object.class, int.class);
                privateMethod.setAccessible(true);

                Object monitor = privateMethod.invokew(LazyVals$.MODULE$, t, ord);
                synchronized (monitor) {
                    if (LazyVal$.MODULE$.STATE(handle.getVolatile(t), ord) == 2) // make sure notification did not happen yet.
                        monitor.wait();
                }
            } else {
                retry = false;
            }
        }
    }

    public int value() {
        long l;
        long l2;
        while ((l2 = LazyVals$.MODULE$.STATE(l = handle.getVolatile(this), 0)) != 3L) {
            if (l2 == 0L) {
                if (!handle.compareAndSet(this, l, casHelper(l, 1, 0))) continue;
                try {
                    int n;
                    this.value$lzy1 = n = 13;
                    handle.
                    setFlag((Object)this, 3, 0);
                    return n;
                }
                catch (Throwable throwable) {
                    setFlag((Object)this, 0, 0);
                    throw throwable;
                }
            }
            wait4Notification((Object)this, l, 0);
        }

        return this.value$lzy1;
    }
}

@reid-spencer
Copy link

reid-spencer commented Mar 14, 2024

This occurs for me in another context: using jlink to minimize the JVM foot print for a command line tool. My tool only uses the java.base module, so I want to eliminate 600MB of JDK 21 down to a 95MB JDK to deliver with my application. Since my use of threading exceeds the threading capabilities of Scala Native, I can't use that. The same problem occurs with GraalVM (see #13985). So, I'm blocked from minimizing my tool's footprint. :(

This occurs with Scala 3.3.3.

@reid-spencer
Copy link

Also: in response to this from the issue description:

actually I don't think this is good enough: using VarHandle would require adding static fields in any class that has a lazy val, so we need to know at compile-time the version of Java we support (via -release/-Xtarget)

I'd be happy if Scala declared a minimum JVM version to the version that supports adding static fields in any class. Didn't the JVM just add its own bytecode manipulation API? I'm okay with JDK 21 as the minimum for Scala, and I'm sure others are not.

@alexklibisz
Copy link

Noting that also comes up when using the new given keyword. implicit val and implicit def do not invoke LazyVals and Unsafe. given does invoke LazyVals and Unsafe.

@alexklibisz
Copy link

OOC, why is this not a problem with lazy vals in 2.12 and 2.13? Is there something fundamentally different about lazy vals in 3.x?

@SethTisue
Copy link
Member

@alexklibisz yes, see #15296 (and linked history stretching at least as far back as #6979)

@Gedochao
Copy link
Contributor

Note: JEP-471 deprecates memory access methods in sun.misc.Unsafe for removal.
https://openjdk.org/jeps/471
(thanks to @smarter for raising this)

@Gedochao Gedochao changed the title Runtime code implementing lazy val should not use sun.misc.Unsafe on Java 9+ Runtime code implementing lazy val should not use sun.misc.Unsafe on Java 9+ (JEP-471) Jul 11, 2024
@Gedochao Gedochao added compat:java:jep Issues corresponding to a JEP (JDK Enhancement Proposal) compat:java:jep:stable Issues corresponding to a JEP which got already delivered as a stable feature in a JDK release labels Jul 11, 2024
@bishabosha
Copy link
Member

so I guess you can use some static methodhandles that resolve to the right thing at runtime?

@konsoletyper
Copy link

BTW, Kotlin uses AtomicReferenceFieldUpdater. Oracle did some work to improve its performance.

And for me personally most important that this ABI depends on the idea that object is located somewhere in flat memory with fields available by some offsets. For some JVM implementations this not necessarily true. So it would be a good step forward if at least at ABI level there was something more high-level, at least similar to interface provided by AtomicReferenceFieldUpdater. I think OpenJDK is smart enough to optimize any overhead here.

@He-Pin
Copy link
Contributor

He-Pin commented Jan 21, 2025

AtomicReferenceFieldUpdater is nice, but not sure why AtomicReference uses the VarHandle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:transform compat:java:jep:stable Issues corresponding to a JEP which got already delivered as a stable feature in a JDK release compat:java:jep Issues corresponding to a JEP (JDK Enhancement Proposal) compat:java itype:enhancement
Projects
None yet
Development

No branches or pull requests

9 participants