Maybe you missed some question details whose importance you did not realize. E.g., the following example works with HotSpot/OpenJDK and derived JREs:
import java.lang.ref.Reference;
import java.lang.reflect.*;
import java.util.*;
class Scratch {
public static class UserService {
private final Map<String, String> users = Map.of(
"user1", "Max", "user2", "Ivan", "user3", "Leo");
public Optional<String> findUserById(String userId) {
return Optional.ofNullable(users.get(userId));
}
@Override
protected void finalize() throws Throwable {
System.out.println("everything went wrong");
}
}
private volatile UserService userService; // never read
private UserService getUserService() {
try {
Class<?> c = Class.forName("java.lang.ref.Finalizer");
Field[] f={c.getDeclaredField("unfinalized"), c.getDeclaredField("next")};
AccessibleObject.setAccessible(f, true);
Reference r = (Reference)f[0].get(null);
while(r != null) {
Object o = r.get();
if(o instanceof UserService) return (UserService)o;
r = (Reference)f[1].get(r);
}
} catch(ReflectiveOperationException ex) {}
throw new IllegalStateException("was never guaranteed to work anyway");
}
public void doJobs() {
UserService userService = getUserService();
System.out.println(userService);
userService.findUserById("userId");
}
public void startApplication() {
userService = new UserService();
doJobs();
}
public static void main(String[] args) {
Scratch program = new Scratch();
program.startApplication();
}
}
Scratch$UserService@9807454
The crucial aspect is that having a nontrivial finalize()
method causes the creation of a special reference that allows to perform the finalization when no other reference to the object exists. The code above traverses these special references.
This does also provide a hint why no other solution (without reading the field) can exist. If the field contains the only reference to the object, only this reference makes the difference between an actual, existing object and, e.g. a chunk of memory that just happens to contain the same bitpattern by chance. Or garbage, i.e. a chunk of memory that happened to be an object in the past, but now is not different to memory that never contained an object.
Garbage collectors do not care about the unused memory, whether it contained objects in the past or not, they traverse the live references to determine the reachable objects. So even if you found a way to peek into the internals to piggyback on the garbage collector when it discovers the existing UserService
instance, you just read the field Scratch.userService
indirectly, as that’s what the garbage collector will do, to discover the existence of that object.
The only exception is finalization, as it will effectively resurrect the object to invoke the finalize()
method when no other reference to it exists, which requires the special reference, the code above exploited. This additional reference has been created when the UserService
instance was constructed, which is one of the reasons why actively using finalization makes the memory management less effecient, so also How does Java GC call finalize() method? and why allocation phase can be increased if we override finalize method?
That said, we have to clarify another point:
In this particular scenario, the field userService
does not prevent garbage collection.
This may contradict intuition, but as elaborated in Can java finalize an object when it is still in scope?, having an object referenced by a local variable does not prevent garbage collection per se. If the variable is not subsequently used, the referenced object may get garbage collected, but the language specification even explicitly allows code optimization to reduce the reachability, which may lead to issues like this, that, or yet another.
In the example, the Scratch
instance is only referenced by local variables and, after writing the reference to the userService
field, entirely unused, even without runtime optimizations. It’s even a requirement that the field is not read, in other words, unused. So in principle, the Scratch
instance is eligible to garbage collection. Note that the due to the local nature of the Scratch
instance, the volatile
modifier has no meaning. Even if the object was not purely local, the absence of any read made it meaningless, though this is hard to recognize by optimizers. So, since the Scratch
instance is eligible to garbage collection, the UserService
instance only referenced by the collectible object is too.
The above example still works because it doesn’t run long enough to make runtime code optimizations or garbage collection happen. But it’s important to understand that there is no guaranty that the object persists in memory, even with the field, so the assumption that there must be a way to find it in heap memory, is wrong in general.