Subprocesos y memoria

Cada vez que tu aplicación crea un subproceso nuevo, el sistema operativo debe asignar memoria para admitirlo. Si bien los subprocesos se consideran "livianos" en comparación con los procesos completos, su costo de memoria no es cero. La fuga de subprocesos es una forma muy común de fuga de memoria en Android.

El costo de un subproceso

Cuando se crea un subproceso en Android, dos componentes principales consumen memoria:

  1. La pila nativa: La biblioteca pthread asigna una pila para que el subproceso almacene variables locales y marcos de llamadas. En Android, suele ser de 1 MB de forma predeterminada.
  2. Estructuras internas de JVM/kernel: ART y el kernel de Linux deben asignar estructuras de datos (como objetos pthread_internal_t y java.lang.Thread) para hacer un seguimiento del subproceso y administrarlo.

Memoria de pila y mmap

Es importante comprender que Android asigna la pila nativa de 1 MB con mmap con las marcas MAP_ANONYMOUS | MAP_NORESERVE.

Esto significa que el SO reserva 1 MB de espacio de direcciones de memoria virtual, pero no asigna 1 MB de RAM física de inmediato. La memoria física solo se asigna (se pagina) a medida que el subproceso ejecuta código y envía datos a la pila. Un subproceso que no hace nada consumirá muy poca RAM física para su pila.

Ejercicio práctico: Subprocesos con fugas

Usaremos la aplicación de ejemplo MemoryLab para observar lo que sucede cuando una aplicación crea cientos de subprocesos inactivos.

1. Lanzamiento y modelo de referencia

Inicia la app y toma una lectura de memoria de referencia:

adb shell am start -W -n com.android.memorylab/.MainActivity
adb shell dumpsys meminfo -s com.android.memorylab

Observa las filas Stack y Native Heap en el resultado. Es probable que sean pequeños.

2. Crea 100 hilos

Presiona el botón Create 100 Threads. La aplicación generará 100 subprocesos nuevos. Para garantizar que la memoria de pila se pagine en la RAM física (para que podamos verla en nuestras herramientas), cada subproceso asigna y escribe en un pequeño array de 10 KB en su pila antes de comenzar a esperar.

Vuelve a ejecutar el comando meminfo mientras los subprocesos estén activos:

adb shell dumpsys meminfo -s com.android.memorylab

Los resultados:

Compara el resultado nuevo con el de referencia. Notarás aumentos significativos mientras los subprocesos estén activos:

  • Stack: Aumentó un megabyte o más. Esto representa las páginas físicas asignadas para las 100 pilas de subprocesos (cada una de las cuales toca al menos 10 KB).
  • Native Heap/Private Other: Aumenta debido a las estructuras internas de pthread que asigna el sistema para cada subproceso.

Para realizar una limpieza, presiona el botón Destroy All Threads.

3. Cómo ver los subprocesos en Perfetto

Perfetto es excelente para hacer un seguimiento de los ciclos de vida y los recuentos de subprocesos a lo largo del tiempo.

Captura de pantalla de la IU de Perfetto que muestra un segmento de contador con la cantidad de subprocesos activos, y los subprocesos que se inician y salen en la app de memorylab

PerfettoSQL para el análisis de subprocesos

Puedes consultar el registro para contar exactamente cuántos subprocesos generó la aplicación:

SELECT p.name AS process_name, COUNT(t.id) as thread_count
FROM thread t JOIN process p USING (upid)
WHERE p.name = 'com.android.memorylab' AND t.name LIKE 'LeakedThread-%';

Se puede usar una consulta como esta para generar un segmento de depuración con un contador, como se muestra en la captura de pantalla.


← Código de la app | ↑ Arriba | Localidad →