java - 如何在没有引用的情况下找到类的单例对象?

标签 java design-patterns reflection jvm unsafe

让我们考虑以下示例:

import java.util.Map;
import java.util.Optional;

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));
        }
    }

    // We save our singleton object to this filed
    // to protect from being garbage collected
    private volatile UserService userService = null;

    private UserService getUserService() {
        /// TODO we need to implement it
        /// Obviously, we cannot return Scratch#userService field
        return null;
    }

    public void doJobs() {
        // I need to get `userService` here, without explicitly passing it
        // to execute some service method

        UserService userService = getUserService();
        if (userService != null) {
            userService.findUserById("userId");
        }
    }

    public void startApplication() {
        userService = new UserService();

        doJobs();
    }

    public static void main(String[] args) {
        Scratch program = new Scratch();
        program.startApplication();
    }
}

所以。我们有简单的 java 应用程序,没有任何框架,比如 spring。我需要在 doJobs() 方法中找到 UserService 对象,而不是显式传递它。很明显,这是求职面试问题。

有以下任务前提条件:

  • UserService 不是 spring bean 或类似的东西。这与 DI
  • 无关
  • 您不能显式地将 UserService 对象传递给 doJobs() 方法
  • 您不能将 UserService 对象设置为某些静态/全局变量/接口(interface)/方法。
  • 您不能使用 javaagents。
  • 你知道,当前类加载器中只有一个 UserService 对象。
  • 如果愿意,您可以使用任何反射(包括库)
  • 你不能创建新的对象,你应该使用现有的
  • 您不能将 Scratch#userService 字段用于任何目的。引入它是为了防止 gc。

因此,一般来说,我们需要以某种方式获取所有对象的列表并使用有关类名的知识找到所需的对象。

我没有在求职面试中解决这个任务。你能帮我做吗?

最佳答案

也许您错过了一些您没有意识到其重要性的问题细节。例如,以下示例适用于 HotSpot/OpenJDK 和派生的 JRE:

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

关键的方面是,拥有一个重要的finalize() 方法会导致创建一个特殊的引用,允许在不存在对该对象的其他引用时执行终结。上面的代码遍历了这些特殊的引用。

这也暗示了为什么不存在其他解决方案(不阅读该领域)。如果该字段包含对对象的唯一引用,则只有此引用才能区分实际的现有对象和例如对象。一 block 内存恰好包含相同的位模式。或垃圾,即过去碰巧是对象的内存块,但现在与从未包含对象的内存没有区别。

垃圾收集器不关心未使用的内存,无论它是否包含过去的对象,它们遍历 Activity 引用以确定可访问的对象。因此,即使您找到了一种方法来窥视内部结构以在发现现有 UserService 实例时搭载垃圾收集器,您也只是间接读取字段 Scratch.userService,因为这就是垃圾收集器要做的事情,以发现该对象的存在。

唯一的异常(exception)是终结,因为当没有其他引用存在时,它会有效地复活对象以调用 finalize() 方法,这需要特殊引用,上面的代码利用了。这个额外的引用是在构造 UserService 实例时创建的,这也是为什么积极使用终结会降低内存管理效率的原因之一,How does Java GC call finalize() method? 也是如此。和 why allocation phase can be increased if we override finalize method?


也就是说,我们必须澄清另一点:

在此特定场景中,userService 字段不会阻止垃圾回收

这可能与直觉相矛盾,但正如 Can java finalize an object when it is still in scope? 中所阐述的那样,让一个对象被局部变量引用并不能阻止垃圾收集本身。如果该变量随后未被使用,引用的对象可能会被垃圾回收,但语言规范甚至明确允许代码优化以降低可达性,这可能会导致类似 this 的问题。 , that , 或 yet another .

在示例中,Scratch 实例仅由局部变量引用,并且在写入对 userService 字段的引用后,完全未使用,即使没有运行时优化。甚至要求字段不被读取,换句话说,未使用。所以原则上,Scratch 实例有资格进行垃圾回收。请注意,由于 Scratch 实例的本地特性,volatile 修饰符没有任何意义。即使对象不是纯粹的本地对象,没有任何读取也会使它变得毫无意义,尽管这很难被优化器识别。因此,由于 Scratch 实例符合垃圾回收条件,因此仅由可回收对象引用的 UserService 实例也是如此。

上面的例子仍然有效,因为它运行的时间不够长,无法进行运行时代码优化或垃圾回收。但重要的是要了解,即使有字段,也不能保证对象会持久存在于内存中,因此,必须有一种方法可以在堆内存中找到它的假设通常是错误的。

关于java - 如何在没有引用的情况下找到类的单例对象?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63854380/

相关文章:

java - 需要访问存储库的测试方法

java - 使用 org.json.JSONObject 解析 JSON 中的转义字符;

java - 如何在 Java 中实现 API/SPI 模式?

reflection - 如何用threejs实现逼真反射

java - 使用 Java 拆分 URL 格式的字符串

object - 两个对象重叠的模式

.net - 什么被认为是多个相似类的良好设计

c# - 从静态方法获取派生类型

c# - Use Reflection to Build a Class(构建一个动态的FileHelper类)

java - `gradle run` 没有 gradle