【问题标题】:Static references are cleared--does Android unload classes at runtime if unused?静态引用被清除——如果未使用,Android 是否会在运行时卸载类?
【发布时间】:2011-02-24 13:09:00
【问题描述】:

我有一个关于类加载/垃圾收集如何在 Android 中工作的问题。我们已经多次偶然发现这个问题,据我所知,Android 在这里的行为与普通 JVM 不同。

问题是这样的:我们目前正在尝试减少应用程序中的单例类,转而使用单个根工厂单例,其唯一目的是管理其他管理器类。如果你愿意的话,一个顶级经理。这使我们无需选择完整的 DI 解决方案即可轻松替换测试中的实现,因为所有活动和服务共享对该根工厂的相同引用。

它是这样的:

public class RootFactory {

    private static volatile RootFactory instance;

    @SuppressWarnings("unused")
    private Context context; // I'd like to keep this for now

    private volatile LanguageSupport languageSupport;
    private volatile Preferences preferences;
    private volatile LoginManager loginManager;
    private volatile TaskManager taskManager;
    private volatile PositionProvider positionManager;
    private volatile SimpleDataStorage simpleDataStorage;

    public static RootFactory initialize(Context context) {
        instance = new RootFactory(context);
        return instance;
    }

    private RootFactory(Context context) {
        this.context = context;
    }

    public static RootFactory getInstance() {
        return instance;
    }

    public LanguageSupport getLanguageSupport() {
        return languageSupport;
    }

    public void setLanguageSupport(LanguageSupport languageSupport) {
        this.languageSupport = languageSupport;
    }

    // ...
}

initializeApplication.onCreate 中被调用一次,即任何活动或服务启动之前。现在,问题来了:getInstance 方法有时会返回为null——即使是在同一个线程上调用!听起来这不是能见度问题。相反,类级别的静态单例引用保留似乎实际上已被垃圾收集器清除。也许我在这里草率下结论,但这可能是因为 Android 垃圾收集器或类加载机制实际上可以在内存不足时 unload 类,在这种情况下,对单例实例的唯一引用将消失离开?我对 Java 的内存模型并不是很深入,但我认为这不应该发生,否则这种实现单例的常用方法在任何 JVM 上都不起作用,对吧?

知道为什么会这样吗?

PS:可以通过在单个应用程序实例上保留“全局”引用来解决此问题。事实证明,当人们必须在应用程序的整个生命周期中始终保持对象时,这是可靠的。

更新

显然我在这里使用 volatile 引起了一些混乱。我的目的是确保静态引用的当前状态对所有访问它的线程始终可见。我必须这样做,因为我从多个线程中编写和读取该引用:在一个普通的应用程序中只在主应用程序线程中运行,但在一个仪器测试运行中,对象被替换为模拟,我从检测线程并在 UI 线程上读取它。我也可以同步对getInstance 的调用,但这更昂贵,因为它需要声明对象锁。有关此问题的更详细讨论,请参阅 What is an efficient way to implement a singleton pattern in Java?

【问题讨论】:

  • 我在从 Instrumentation 运行时也看到过这种行为。我有兴趣看到答案。但感谢有关volatile 的提示;我去看看。
  • "initialize 在 Application.onCreate 中被调用一次,即在任何 Activity 或 Service 启动之前" --> 对此的任何引用?
  • 一个有用的步骤是将日志记录添加到initialize()getInstance(),并确认在前者之前永远不会调用后者。此外,将日志记录添加到更新 instance 的任何内容中。垃圾收集器不会 NULL 字段输出,即使 Android 确实卸载了类,它也只会在同一 ClassLoader 加载的 all 类可以被丢弃的情况下这样做。请注意,当应用程序被系统杀死并重新启动时,所有静态字段本质上都是“重置”。
  • 你有没有弄明白这个?仅供参考:Effective Java 2nd Edition 第 262 页第 66 条中介绍了静态 volatile 的示例。如前所述,需要 volatile 以确保实例的最新值对所有线程都是可见的。不过,使用枚举或其他同步技术可能是值得的。就目前而言,没有什么能阻止客户端违反假定的单例不变量,因为每次调用初始化都会创建另一个实例。

标签: java android garbage-collection singleton classloader


【解决方案1】:

您 (@Matthias) 和 Mark Murphy (@CommonsWare) 的说法都是正确的,但主旨似乎丢失了。 (volatile的使用是正确的,类没有卸载。)

问题的关键是从哪里调用initialize

这是我认为正在发生的事情:

  • 您正在从 Activity * 调用初始化
  • Android 需要更多内存,杀死整个Process
  • Android重启Application和顶部Activity
  • 您调用 getInstance 将返回 null,因为未调用 initialize

如果我错了,请纠正我。


更新
我的假设——initialize 是从Activity * 调用的——在这种情况下似乎是错误的。但是,我将保留这个答案,因为这种情况是常见的错误来源。

【讨论】:

  • 重新启动应用程序实例时,应调用 onCreate,即使它会膨胀 Activity 而不是启动默认 Activity(这是许多错误的来源。在“启动画面”Activity 中初始化而不是应用程序实例)。有没有人弄清楚这一点?我尽量减少静力学,但我们使用它们。听说过关于卸载类的谣言,但我倾向于同意@CommonsWare。当 VM 打盹时,类会被卸载。以前没有。
  • 有趣 - 我没有意识到 Android 只在进程终止和重启时重新创建了最顶层的 Activity。
【解决方案2】:

我一生中从未见过声明为volatile 的静态数据成员。我什至不确定这意味着什么。

静态数据成员将一直存在,直到进程终止或删除它们(例如,null 出静态引用)。一旦用户(例如,BACK 按钮)和您的代码(例如,stopService())主动关闭所有活动和服务,该过程可能会终止。如果 Android 的 RAM 严重不足,即使使用实时组件,该过程也可能会终止,但这很不寻常。如果 Android 认为您的服务在后台运行的时间过长,则该进程可能会被实时服务终止,但它可能会根据您从 onStartCommand() 的返回值重新启动该服务。

类没有被卸载,期间,没有进程被终止。

为了解决@sergui 的其他观点,活动可能会被销毁,并存储实例状态(尽管在 RAM 中,而不是“固定存储”中),以释放 RAM。 Android 倾向于在终止活动进程之前执行此操作,但如果它破坏了进程的最后一个活动并且没有正在运行的服务,则该进程将成为终止的主要候选者。

您的实现唯一明显奇怪的是您使用了volatile

【讨论】:

  • 我们从不使静态引用为空。据我记得,volatile 会导致线程之间共享的内存刷新。因此,在此处使用 volatile 可确保该引用始终对所有访问它的线程可见。如果不使用 volatile,则检测线程可能会写入该引用,但 UI 线程可能仍将其视为 null(因为内存未同步)。不知道你觉得奇怪的是什么。你能详细说明一下吗?
  • 另外,我对您的“我一生中从未见过声明为 volatile 的静态数据成员”的评论感到有些困惑,所以我仔细检查了这里是否真的有问题,但是有一个看看那个:en.wikipedia.org/wiki/Singleton_pattern(“传统解决方案”)。我认为 volatile 在这里是有意义的,并不是问题的原因。
  • @Matthias:我知道volatile 是什么意思。我以前从未见过在静态数据成员上使用过volatile,并且您的维基百科参考并未指出volatile 静态与volatile 非静态是否有不同的特征。似乎现在大多数人都使用synchronizedjava.util.concurrent 来使事情变得线程安全。这并不意味着您对volatile 的使用是错误的。但是,这是我在您的实现中看到的一件不寻常的事情,因此它可能会成为您的困难的根源。
  • 感谢您的链接。如果 fadden 在那个问题中所说的是正确的,那么我仍然在我离开的地方。同样,我不认为它与并发和线程同步有任何关系,因为它发生在同一个线程上,即 UI 线程。因此,如果我设置了一个静态成员,但一秒钟后它就消失了,还有什么可能导致这种情况?
  • (我已经更新了我的问题,解释了我为什么使用 volatile,因为这似乎引起了一些混乱)
【解决方案3】:

只要系统感觉像静态引用并且您的应用程序不是顶级的(用户没有显式运行它),就会清除静态引用。每当您的应用程序被最小化并且操作系统需要更多内存时,它都会杀死您的应用程序或将其序列化到固定存储以供以后使用,但在这两种情况下,静态变量都会被删除。 此外,每当您的应用程序收到 Force Close 错误时,所有静态数据也会被删除。根据我的经验,我发现在 Application 对象中使用变量总是比使用静态变量更好。

【讨论】:

  • “只要系统感觉像静态引用并且你的应用程序不是顶级的(用户没有显式运行它),就会清除静态引用。” - 错误的。 “或将其序列化到固定存储以供以后使用”--false。
  • @CommonsWare:你能解释一下为什么当应用程序最小化时静态变量会被删除吗? (点击图标会将应用程序带到它的最后状态,但没有静态)
  • @sergui:静态变量不会“在应用程序最小化时被删除”。有关更多信息,请参阅我对这个问题的回答。
【解决方案4】:

我在自己的代码中看到了类似的奇怪行为,涉及消失的静态变量(我认为这个问题与 volatile 关键字没有任何关系)。特别是当我初始化一个日志框架(例如 Crashlytics、log4j)时,会出现这种情况,然后经过一段时间的活动,它似乎没有初始化。调查表明,这发生在操作系统调用 onSaveInstanceState(Bundle b) 之后。

您的静态变量由包含在您的应用进程中的类加载器保存。根据谷歌:

Android 的一个不同寻常的基本功能是应用程序 进程的生命周期不受应用程序直接控制 本身。而是由系统通过组合来确定 在系统知道正在运行的应用程序部分中,如何 这些东西对用户很重要,以及总内存有多少 在系统中可用。

http://developer.android.com/guide/topics/processes/process-lifecycle.html

这对开发人员来说意味着您不能期望静态变量无限期地保持初始化状态。您需要依赖不同的持久性机制。

我用来保持日志记录框架初始化的一种解决方法是让我的所有活动扩展一个基类,在该基类中我覆盖onCreate 并检查初始化并在必要时重新初始化。

我认为官方的解决方案是使用onSaveInstanceState(Bundle b)回调来持久化您的Activity以后需要的任何东西,然后在b != null时重新初始化onCreate(Bundle b)

谷歌解释得最好:

http://developer.android.com/training/basics/activity-lifecycle/recreating.html

【讨论】:

    猜你喜欢
    • 2010-10-11
    • 2016-11-30
    • 2023-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-19
    • 1970-01-01
    • 2021-04-06
    相关资源
    最近更新 更多