Make the "direct" fallback in FinalizableReferenceQueue avoid reflection entirely.

I want to call attention up front to the fact that _`Finalizer`_ still uses reflection (to invoke `finalizeReferent`). We might imagine an alternative class similar to `Finalizer` that we'd use in the same-class-loader case. That class could call `finalizeReferent` directly. Hopefully it wouldn't need to [worry about class unloading](https://github.com/google/guava/blob/de2b19178d2748fe09402c531ef75350af634667/guava/src/com/google/common/base/internal/Finalizer.java#L70-L71), since we might be running under an environment in which that's not a thing? But this would deserve more thought.

Anyway: The motivation here is that GraalVM needs help to see through reflection, just as Proguard, R8, etc. do. We haven't made much [effort to support Graal](https://github.com/google/guava/issues/3894), but this CL is an easy enough win. (Again, given that we _also_ use reflection in `Finalizer`, I wouldn't assume that `FinalizableReference`+GraalVM now "works." But it does now get through an initial step that I was testing against.)

This CL might also allow us to simplify our Proguard configuration. However, it's not necessarily that simple:
- Since we still _attempt_ reflection, we might want the class we reflect upon to still have its expected shape, even if we ultimately don't use it for one reason or another.
- We could consider configuring Proguard with `-assumevalues` so that it _doesn't_ attempt reflection. (Compare https://github.com/google/guava/commit/14050c34d13b64846875d1a7b3ca4a1003a1e5cf, which I later undid in https://github.com/google/guava/commit/75da92419a7d414bd3de23c89f2ddfd83754767d.)
  - _Technically_, we should keep in mind that it's possible to use Proguard for a _server_ app, so "Proguard" does not imply "Android," and we ideally would write rules with that in mind. (This also came up in https://github.com/google/guava/commit/75da92419a7d414bd3de23c89f2ddfd83754767d.) In practice, I'm not sure how much that would matter.

RELNOTES=n/a
PiperOrigin-RevId: 884581412
This commit is contained in:
cpovirk authored and Google Java Core Libraries committed 2026-03-16 12:26:59 -07:00
1 parent 199848a84f
commit 6fff6ef706
3 files changed
+92 -38

No files matched your search

@@ -187,12 +187,14 @@ public class FinalizableReferenceQueue implements Closeable {
private static final String FINALIZER_CLASS_NAME = "com.google.common.base.internal.Finalizer";
/** Reference to Finalizer.startFinalizer(). */
private static final Method startFinalizer;
private static final FinalizerStarter finalizerStarter;
static {
Class<?> finalizer =
loadFinalizer(new SystemLoader(), new DecoupledLoader(), new DirectLoader());
startFinalizer = getStartFinalizer(finalizer);
Class<?> finalizer = loadFinalizer(new SystemLoader(), new DecoupledLoader());
finalizerStarter =
finalizer != null
? new ReflectiveStarter(getStartFinalizer(finalizer))
: new DirectStarter();
}
/** The actual reference queue that our background thread will poll. */
@@ -210,10 +212,8 @@ public class FinalizableReferenceQueue implements Closeable {
frqRef = new PhantomReference<>(this, queue);
boolean threadStarted = false;
try {
startFinalizer.invoke(null, FinalizableReference.class, queue, frqRef);
finalizerStarter.startFinalizer(FinalizableReference.class, queue, frqRef);
threadStarted = true;
} catch (IllegalAccessException impossible) {
throw new AssertionError(impossible); // startFinalizer() is public
} catch (Throwable t) {
logger.log(
Level.INFO,
@@ -257,11 +257,11 @@ public class FinalizableReferenceQueue implements Closeable {
}
/**
* Iterates through the given loaders until it finds one that can load Finalizer.
* Iterates through the given loaders until it finds one that will load Finalizer.
*
* @return Finalizer.class
* @return a {@link Class} for {@link Finalizer} or {@code null} if none of the loaders load it
*/
private static Class<?> loadFinalizer(FinalizerLoader... loaders) {
private static @Nullable Class<?> loadFinalizer(FinalizerLoader... loaders) {
for (FinalizerLoader loader : loaders) {
Class<?> finalizer = loader.loadFinalizer();
if (finalizer != null) {
@@ -269,14 +269,15 @@ public class FinalizableReferenceQueue implements Closeable {
}
}
throw new AssertionError();
return null;
}
/** Loads Finalizer.class. */
interface FinalizerLoader {
/**
* Returns Finalizer.class or null if this loader shouldn't or can't load it.
* Returns a {@link Class} for {@link Finalizer} or {@code null} if this loader shouldn't or
* can't load it.
*
* @throws SecurityException if we don't have the appropriate privileges
*/
@@ -384,14 +385,38 @@ public class FinalizableReferenceQueue implements Closeable {
}
}
/**
* Returns the {@code Finalizer} class from the current class loader. If that class gets used,
* then we won't be able garbage collect this class loader, but at least the world doesn't end.
*/
private static final class DirectLoader implements FinalizerLoader {
private interface FinalizerStarter {
void startFinalizer(
Class<?> finalizableReferenceClass,
ReferenceQueue<Object> queue,
PhantomReference<Object> frqRef)
throws Throwable;
}
private static final class ReflectiveStarter implements FinalizerStarter {
final Method startFinalizer;
ReflectiveStarter(Method startFinalizer) {
this.startFinalizer = startFinalizer;
}
@Override
public Class<?> loadFinalizer() {
return Finalizer.class;
public void startFinalizer(
Class<?> finalizableReferenceClass,
ReferenceQueue<Object> queue,
PhantomReference<Object> frqRef)
throws Throwable {
startFinalizer.invoke(null, finalizableReferenceClass, queue, frqRef);
}
}
private static final class DirectStarter implements FinalizerStarter {
@Override
public void startFinalizer(
Class<?> finalizableReferenceClass,
ReferenceQueue<Object> queue,
PhantomReference<Object> frqRef) {
Finalizer.startFinalizer(finalizableReferenceClass, queue, frqRef);
}
}
@@ -187,12 +187,14 @@ public class FinalizableReferenceQueue implements Closeable {
private static final String FINALIZER_CLASS_NAME = "com.google.common.base.internal.Finalizer";
/** Reference to Finalizer.startFinalizer(). */
private static final Method startFinalizer;
private static final FinalizerStarter finalizerStarter;
static {
Class<?> finalizer =
loadFinalizer(new SystemLoader(), new DecoupledLoader(), new DirectLoader());
startFinalizer = getStartFinalizer(finalizer);
Class<?> finalizer = loadFinalizer(new SystemLoader(), new DecoupledLoader());
finalizerStarter =
finalizer != null
? new ReflectiveStarter(getStartFinalizer(finalizer))
: new DirectStarter();
}
/** The actual reference queue that our background thread will poll. */
@@ -210,10 +212,8 @@ public class FinalizableReferenceQueue implements Closeable {
frqRef = new PhantomReference<>(this, queue);
boolean threadStarted = false;
try {
startFinalizer.invoke(null, FinalizableReference.class, queue, frqRef);
finalizerStarter.startFinalizer(FinalizableReference.class, queue, frqRef);
threadStarted = true;
} catch (IllegalAccessException impossible) {
throw new AssertionError(impossible); // startFinalizer() is public
} catch (Throwable t) {
logger.log(
Level.INFO,
@@ -257,11 +257,11 @@ public class FinalizableReferenceQueue implements Closeable {
}
/**
* Iterates through the given loaders until it finds one that can load Finalizer.
* Iterates through the given loaders until it finds one that will load Finalizer.
*
* @return Finalizer.class
* @return a {@link Class} for {@link Finalizer} or {@code null} if none of the loaders load it
*/
private static Class<?> loadFinalizer(FinalizerLoader... loaders) {
private static @Nullable Class<?> loadFinalizer(FinalizerLoader... loaders) {
for (FinalizerLoader loader : loaders) {
Class<?> finalizer = loader.loadFinalizer();
if (finalizer != null) {
@@ -269,14 +269,15 @@ public class FinalizableReferenceQueue implements Closeable {
}
}
throw new AssertionError();
return null;
}
/** Loads Finalizer.class. */
interface FinalizerLoader {
/**
* Returns Finalizer.class or null if this loader shouldn't or can't load it.
* Returns a {@link Class} for {@link Finalizer} or {@code null} if this loader shouldn't or
* can't load it.
*
* @throws SecurityException if we don't have the appropriate privileges
*/
@@ -383,14 +384,38 @@ public class FinalizableReferenceQueue implements Closeable {
}
}
/**
* Returns the {@code Finalizer} class from the current class loader. If that class gets used,
* then we won't be able garbage collect this class loader, but at least the world doesn't end.
*/
private static final class DirectLoader implements FinalizerLoader {
private interface FinalizerStarter {
void startFinalizer(
Class<?> finalizableReferenceClass,
ReferenceQueue<Object> queue,
PhantomReference<Object> frqRef)
throws Throwable;
}
private static final class ReflectiveStarter implements FinalizerStarter {
final Method startFinalizer;
ReflectiveStarter(Method startFinalizer) {
this.startFinalizer = startFinalizer;
}
@Override
public Class<?> loadFinalizer() {
return Finalizer.class;
public void startFinalizer(
Class<?> finalizableReferenceClass,
ReferenceQueue<Object> queue,
PhantomReference<Object> frqRef)
throws Throwable {
startFinalizer.invoke(null, finalizableReferenceClass, queue, frqRef);
}
}
private static final class DirectStarter implements FinalizerStarter {
@Override
public void startFinalizer(
Class<?> finalizableReferenceClass,
ReferenceQueue<Object> queue,
PhantomReference<Object> frqRef) {
Finalizer.startFinalizer(finalizableReferenceClass, queue, frqRef);
}
}
+4
View File
@@ -18,6 +18,9 @@
# Proguard is intelligent enough to spot the use of reflection onto this, so we
# only need to keep the names, and allow it to be stripped out if
# FinalizableReferenceQueue is unused.
#
# (Under Android, we end up using Finalzier non-reflectively, so we could
# likely rework all this configuration.)
-keepclassmembernames class com.google.common.base.internal.Finalizer {
*** startFinalizer(...);
}
@@ -25,6 +28,7 @@
-keepclassmembers class com.google.common.base.internal.Finalizer {
*** startFinalizer(...);
}
-keepclassmembernames class com.google.common.base.FinalizableReference {
void finalizeReferent();
}