Las referencias estáticas se borran: ¿Android descarga las clases en tiempo de ejecución si no se utilizan?

Tengo una pregunta específica sobre cómo funciona la colección / recolección de basura en Android. Hemos tropezado con este problema varias veces, y por lo que puedo decir, Android se comporta de forma diferente aquí en una JVM normal.

El problema es el siguiente: actualmente estamos tratando de reducir las clases de singleton en la aplicación a favor de un único singleton de fábrica raíz, cuyo único propósito es administrar otras clases de administrador. Un gerente de alto nivel si quieres. Esto nos facilita la sustitución de implementaciones en pruebas sin optar por una solución DI completa, ya que todas las actividades y servicios comparten la misma referencia a esa fábrica raíz.

Así es como se ve:

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; } // ... } 

initialize se llama una vez, en Application.onCreate , es decir, antes de iniciar cualquier actividad o servicio. Ahora, aquí está el problema: el método getInstance veces vuelve como null , ¡incluso cuando se invoca en el mismo hilo! Parece que no es un problema de visibilidad; en su lugar, la retención estática de referencia singleton en el nivel de clase parece haber sido borrada por el recolector de basura. Tal vez estoy sacando conclusiones aquí, pero ¿podría ser porque el recolector de basura de Android o el mecanismo de carga de clases realmente puede descargar clases cuando la memoria escasea, en cuyo caso la única referencia a la instancia de singleton desaparecerá? No estoy muy metido en el modelo de memoria de Java, pero supongo que no debería suceder, de lo contrario, esta forma común de implementar singletons no funcionaría en ninguna JVM ¿verdad?

¿Alguna idea de por qué esto está sucediendo exactamente?

PD: uno puede evitar esto manteniendo referencias “globales” en la instancia de aplicación única en su lugar. Eso ha demostrado ser confiable cuando uno debe mantenerse en el objeto durante toda la vida útil de una aplicación.

ACTUALIZAR

Aparentemente mi uso de volátiles aquí causó cierta confusión. Mi intención era asegurar que el estado actual de la referencia estática sea siempre visible para todos los subprocesos que accedan a él. Debo hacer eso porque estoy escribiendo y leyendo esa referencia de más de un hilo: en una aplicación ordinaria, solo en el hilo principal de la aplicación, pero en una ejecución de prueba de instrumentación, donde los objetos son reemplazados por simulacros, lo escribo desde el hilo de instrumentación y léalo en el hilo de UI. Podría haber sincronizado la llamada a getInstance , pero eso es más costoso ya que requiere reclamar un locking de objeto. Consulte ¿Cuál es una forma eficiente de implementar un patrón singleton en Java? para una discusión más detallada sobre esto.

Nunca en mi vida he visto a un miembro de datos estáticos declarado volatile . Ni siquiera estoy seguro de lo que eso significa.

Los miembros de datos estáticos existirán hasta que finalice el proceso o hasta que se deshaga de ellos (por ejemplo, null la referencia estática). El proceso puede finalizar una vez que el usuario haya cerrado proactivamente todas las actividades y servicios (por ejemplo, el botón ATRÁS) y su código (por ejemplo, stopService() ). El proceso puede finalizar incluso con componentes en vivo si Android tiene una RAM desesperadamente corta, pero esto es bastante inusual. El proceso puede finalizar con un servicio en vivo si Android piensa que su servicio ha estado en segundo plano demasiado tiempo, aunque puede reiniciar ese servicio dependiendo de su valor de retorno desde onStartCommand() .

Las clases no se descargan, punto, corto del proceso terminado.

Para abordar el otro punto de @sergui, las actividades pueden destruirse, con el estado de instancia almacenado (aunque en RAM, no en “almacenamiento fijo”), para liberar RAM. Android tenderá a hacer esto antes de finalizar procesos activos, aunque si destruye la última actividad de un proceso y no hay servicios en ejecución, ese proceso será un candidato principal para la terminación.

Lo único significativamente extraño sobre su implementación es su uso de volatile .

Tanto usted (@Matthias) como Mark Murphy (@CommonsWare) tienen razón en lo que usted dice, pero la esencia parece perdida. (El uso de volatile es correcto y las clases no se descargan).

El quid de la pregunta es desde donde se llama a initialize .

Esto es lo que creo que está sucediendo:

  • Estás llamando inicializar desde una Activity
  • Android necesita más memoria, mata todo el Process
  • Android reinicia la Application y la Activity principal
  • Llama a getInstance que devolverá null , ya que no se initialize

Corrígeme si estoy equivocado.

Las referencias estáticas se borran cada vez que el sistema lo desea y su aplicación no es de nivel superior (el usuario no la está ejecutando explícitamente). Cada vez que minimice su aplicación y el sistema operativo desee más memoria, la eliminará o la serializará en un almacenamiento fijo para usarla posteriormente, pero en ambos casos, las variables estáticas se borran. Además, cada vez que su aplicación obtiene un error Force Close , también se borran todas las estadísticas. En mi experiencia, vi que siempre es mejor usar variables en el objeto Aplicación que las variables estáticas.

He visto un comportamiento extraño similar con mi propio código que implica la desaparición de las variables estáticas (no creo que este problema tenga algo que ver con la palabra clave volátil). En particular, esto ha surgido cuando he inicializado un marco de trabajo de registro (por ejemplo, Crashlytics, log4j) y luego, después de un período de actividad, parece que no está inicializado. La investigación ha demostrado que esto sucede después de que el sistema operativo llama a onSaveInstanceState(Bundle b) .

El Classloader contiene las variables estáticas que se encuentran dentro del proceso de su aplicación. De acuerdo con google:

Una característica inusual y fundamental de Android es que la vida útil de un proceso de aplicación no está directamente controlada por la propia aplicación. En cambio, el sistema lo determina mediante una combinación de las partes de la aplicación que el sistema sabe que se están ejecutando, la importancia de estas cosas para el usuario y la cantidad total de memoria disponible en el sistema.

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

Lo que eso significa para un desarrollador es que no puede esperar que las variables estáticas permanezcan inicializadas indefinidamente. Necesita confiar en un mecanismo diferente para la persistencia.

Una solución que he utilizado para mantener mi marco de trabajo de inicio de sesión inicializado es que todas mis actividades extiendan una clase base en la que onCreate y verifique la inicialización y vuelva a inicializar si es necesario.

Creo que la solución oficial es utilizar la callback onSaveInstanceState(Bundle b) para conservar todo lo que su actividad necesita más tarde, y luego reiniciar en onCreate(Bundle b) cuando b != null .

Google lo explica mejor:

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