Descripción general de la administración de memoria

Android Runtime (ART) y la máquina virtual Dalvik usan las funciones de paginación y mapeo de memoria (mmapping) para administrar la memoria. Esto significa que cualquier memoria que modifique una app, ya sea asignando objetos nuevos o tocando páginas con mapeo de memoria, permanece en la memoria RAM y no se puede transferir a un almacenamiento auxiliar. La única forma de liberar memoria de una app es liberar referencias a objetos contenidos en la app para que la memoria esté disponible para el recolector de elementos no utilizados. Hay una excepción: cualquier archivo que se mapee sin modificación, como el código, se puede quitar de la RAM si el sistema desea usar esa memoria en otro lugar.

En esta página, se explica cómo administra Android los procesos de las apps y la asignación de memoria. Si deseas obtener más información para administrar la memoria de manera más eficiente en tu app, consulta Cómo administrar la memoria de tu app.

Recolección de elementos no utilizados

Un entorno de memoria administrada, como ART o la máquina virtual Dalvik, realiza un seguimiento de cada asignación de memoria. Una vez que determina que el programa ya no usa una porción de memoria, la libera de nuevo al montón sin ninguna intervención del programador. El mecanismo para reclamar memoria no utilizada dentro de un entorno de memoria administrada se conoce como recolección de elementos no utilizados. La recolección de elementos no utilizados tiene dos objetivos: encontrar objetos de datos en un programa al que no se podrá acceder en el futuro y reclamar los recursos que esos objetos usan.

El montón de memoria de Android es generacional, lo que significa que realiza un seguimiento de diferentes depósitos de asignaciones según la duración y el tamaño esperados del objeto que se está asignando. Por ejemplo, los objetos asignados recientemente pertenecen a la generación Young. Cuando un objeto permanece activo el tiempo suficiente, se puede promover a una generación anterior, seguido de una generación permanente.

Cada generación de montón tiene su propio límite máximo para la cantidad de memoria que los objetos pueden ocupar. Cada vez que una generación comienza a llenarse, el sistema ejecuta un evento de recolección de elementos no utilizados a fin de liberar memoria. La duración de la recolección de elementos no utilizados depende de qué generación de objetos se está recolectando y cuántos objetos activos hay en cada generación.

Aunque la recolección de elementos no utilizados puede ser bastante rápida, puede afectar el rendimiento de tu app. Por lo general, no es posible controlar cuándo ocurre un evento de recolección de elementos no utilizados desde tu código. El sistema tiene un conjunto de criterios para determinar cuándo realizar esa recolección. Cuando se cumplen los criterios, el sistema deja de ejecutar el proceso y comienza la recolección de elementos no utilizados. Si la recolección se realiza en medio de un ciclo de procesamiento intensivo, como una animación o durante la reproducción de música, puede aumentar el tiempo de procesamiento. Este aumento puede impulsar la ejecución de código en tu app más allá del umbral recomendado de 16 ms para una representación de fotogramas eficiente y uniforme.

Además, tu flujo de código puede realizar tipos de trabajo que obliguen a que los eventos de recolección de elementos no utilizados ocurran con más frecuencia o que duren más de lo normal. Por ejemplo, si asignas varios objetos en la parte más interna de un bucle for durante cada fotograma de una animación alfa, quizá contamines el montón de memoria con muchos objetos. En esa circunstancia, el recolector de elementos no utilizados ejecuta múltiples eventos de recolección y puede disminuir el rendimiento de tu app.

Para obtener información más general sobre la recolección de elementos no utilizados, consulta Recolección de elementos no utilizados.

Cómo compartir memoria

Con el objetivo de adaptarse a todo lo que necesita en la memoria RAM, Android intenta compartir páginas de memoria RAM en los procesos. Puede hacerlo de las siguientes maneras:

  • Se bifurca cada proceso de la app desde un proceso existente llamado Zygote. El proceso Zygote comienza cuando el sistema se inicia y carga los recursos y el código del framework común (como los temas de actividad). Para iniciar un nuevo proceso de la app, el sistema bifurca el proceso Zygote y, luego, carga y ejecuta el código de la app en el nuevo proceso. Este enfoque permite que se compartan en todos los procesos de la app la mayoría de las páginas de memoria RAM asignadas para el código del framework y los recursos.
  • La mayoría de los datos estáticos se mapean en un proceso. Esta técnica permite compartir los datos entre procesos y también permite transferirlos a un almacenamiento auxiliar cuando sea necesario. Los ejemplos de datos estáticos incluyen el código Dalvik (colocándolo en un archivo .odex previamente vinculado para mapeo de memoria directo); los recursos de app (diseñando la tabla de recursos como una estructura a la que se pueda mapear memoria y alineando las entradas ZIP del APK); y los elementos tradicionales del proyecto, como el código nativo en archivos .so.
  • En muchos lugares, Android comparte la misma memoria RAM dinámica en todos los procesos que utilizan regiones de memoria compartida asignadas explícitamente (ya sea con ashmem o gralloc). Por ejemplo, las superficies de las ventanas usan memoria compartida entre la app y el compositor de pantalla, y los búferes del cursor usan memoria compartida entre el proveedor de contenido y el cliente.

Debido al uso extensivo de la memoria compartida, determinar la cantidad de memoria que usa la app requiere cuidado. Las técnicas para determinar correctamente el uso de la memoria de tu app se analizan en Cómo investigar el uso de tu memoria RAM.

Cómo asignar y reclamar memoria de la app

El montón de Dalvik está restringido a un solo rango de memoria virtual para cada proceso de la app. Define el tamaño lógico de pila, que se puede ampliar según sea necesario, pero solo hasta un límite que el sistema define para cada app.

El tamaño lógico de pila no es el mismo que la cantidad de memoria física que usa la pila. Al inspeccionar la pila de tu app, Android calcula un tamaño llamado Tamaño del conjunto proporcional (PSS), que incluye las páginas no sincronizadas y limpias que se comparten con otros procesos, pero solo en proporción a la cantidad de apps que comparten esa memoria RAM. Este total (PSS) es lo que el sistema considera tu espacio en memoria. Si deseas obtener más información sobre el PSS, consulta la guía Cómo investigar el uso de tu memoria RAM.

El montón de Dalvik no compacta el tamaño lógico del montón, lo que significa que Android no desfragmenta el montón para reducir el espacio. Android solo puede reducir el tamaño lógico del montón cuando hay espacio no utilizado al final del montón. Sin embargo, el sistema puede reducir la memoria física que utiliza el montón. Después de la recolección de elementos no utilizados, Dalvik recorre el montón y encuentra páginas no utilizadas; luego, devuelve esas páginas al kernel usando madvise. Por lo tanto, las asignaciones y desasignaciones vinculadas de grandes fragmentos deberían dar como resultado la recuperación de toda (o casi toda) la memoria física utilizada. Sin embargo, recuperar memoria de pequeñas asignaciones puede ser mucho menos eficiente porque la página utilizada para una pequeña asignación todavía se puede compartir con otro elemento que aún no se liberó.

Cómo restringir la memoria de la app

A fin de mantener un entorno funcional multitarea, Android establece un límite estricto para el tamaño de pila de cada app. El límite exacto del tamaño de pila varía entre dispositivos según la cantidad de memoria RAM que el dispositivo tenga disponible en general. Si tu app alcanzó la capacidad máxima de pila y trata de asignar más memoria, puede recibir un OutOfMemoryError.

En algunos casos, tal vez te convenga consultar el sistema a fin de determinar exactamente cuánto espacio de pila tienes disponible en el dispositivo en el momento (por ejemplo, para determinar cuántos datos es seguro guardar en una memoria caché). Puedes consultar esta cifra en el sistema llamando a getMemoryClass(). Este método muestra un número entero que indica la cantidad de megabytes disponibles en la pila de tu app.

Cambio de apps

Cuando los usuarios cambian de una app a otra, Android mantiene las que no están en primer plano, es decir, que no son visibles para el usuario o que ejecutan un servicio en primer plano, como la reproducción de música, en la memoria caché. Por ejemplo, cuando un usuario inicia por primera vez una app, se crea un proceso para ella; pero, cuando el usuario sale de la app, ese proceso no se cierra. El sistema mantiene el proceso en caché. Si, luego, el usuario vuelve a la app, el sistema reutiliza el proceso, lo que hace que se cambie de app con más rapidez.

Si tu app tiene un proceso en caché y retiene recursos que no necesita en el momento, entonces, puede afectar el rendimiento general del sistema (incluso cuando el usuario no la usa). A medida que el sistema se queda sin recursos, como la memoria, cierra los procesos en la memoria caché. El sistema también considera los procesos que retienen la mayor cantidad de memoria y puede cerrarlos para liberar espacio en la memoria RAM.

Nota: Cuanto menor sea el consumo de memoria de la app en caché, mayor será la probabilidad de que no se fuerce su cierre y se reanude rápidamente. Sin embargo, según los requisitos instantáneos del sistema, es posible que los procesos en caché se cierren en cualquier momento independientemente de su uso de recursos.

Si deseas obtener más información sobre la forma en que los procesos se almacenan en caché mientras no se ejecutan en primer plano y cómo Android decide cuáles se pueden cerrar, consulta la guía Procesos y subprocesos.

Prueba de esfuerzo de memoria

Si bien los problemas de esfuerzo de memoria son menos comunes en los dispositivos de gama alta, pueden causar problemas a los usuarios de dispositivos con poca RAM, como los que ejecutan Android (edición Go). Es importante intentar reproducir este entorno con sobrecarga de memoria a fin de poder escribir pruebas de instrumentación para verificar el comportamiento de la app y mejorar la experiencia de los usuarios de dispositivos con poca memoria.

Prueba de aplicaciones de esfuerzo

La prueba de aplicaciones de esfuerzo (stressapptest) es una prueba de la interfaz de memoria que ayuda a crear situaciones realistas de carga alta a fin de probar varias limitaciones de memoria y hardware para tu app. Gracias a la capacidad de definir limitaciones de tiempo y memoria, esto te permite escribir instrumentación para verificar situaciones reales de memoria. Por ejemplo, usa el siguiente conjunto de comandos para enviar la biblioteca estática al sistema de archivos de datos, hacer que sea ejecutable y ejecutar una prueba de esfuerzo durante 20 segundos de 990 MB:
    adb push stressapptest /data/local/tmp/
    adb shell chmod 777 /data/local/tmp/stressapptest
    adb shell /data/local/tmp/stressapptest -s 20 -M 990

  

Consulta la documentación de stressapptest para obtener más información sobre cómo instalar la herramienta, los argumentos comunes y la información sobre el manejo de errores.

Observaciones sobre la prueba de estrés

Las herramientas como stressapptest se pueden usar para solicitar asignaciones de memoria más grandes que la disponible de forma gratuita. Este tipo de solicitudes puede generar varias alertas, que debes tener en cuenta desde el lado del desarrollador. A continuación, se indican tres alertas principales que pueden generarse si hay poca memoria disponible:
  • SIGABRT: Esta es una falla fatal y nativa de tu proceso, que se genera debido a las solicitudes de asignaciones de memoria mayores que la disponible cuando el sistema ya está bajo presión de memoria.
  • SIGQUIT: Se produce un volcado de memoria principal y finaliza el proceso cuando lo detecta la prueba de instrumentación.
  • TRIM_MEMORY_EVENTS: Estas devoluciones de llamada están disponibles en Android 4.1 (API nivel 16) y versiones posteriores, y proporcionan alertas de memoria detalladas para tu proceso.