Написанный вами код сам по себе является формой использования памяти. Каждый класс, метод и строковая константа в вашем приложении должны быть загружены в оперативную память при выполнении. Чем больше кодовая база вашего приложения, тем больше памяти оно будет потреблять просто для своего существования.
Файловая память и страничная подкачка
Android загружает исполняемый код из ваших .apk файлов (например .oat или .so ) с помощью mmap . Это означает, что код привязан к файлу .
Важно отметить, что Android использует страничную адресацию по требованию . При запуске приложения ядро не загружает весь APK-файл в оперативную память немедленно. Вместо этого оно только отображает файл в виртуальное адресное пространство процесса. Когда приложение выполняется и процессор переходит к новой функции, возникает ошибка страничной адресации. Ядро приостанавливает поток, считывает эту конкретную страницу кода размером 4 КБ из памяти в физическую оперативную память и возобновляет выполнение.

Это означает, что код, который вы упаковываете , но никогда не выполняете, не использует физическую память для самих страниц кода. Однако неиспользуемые библиотеки всё равно увеличивают общий размер APK и могут значительно увеличить объём памяти, используемой внутренними метаданными системы (такими как индексы DEX и дескрипторы классов), которые необходимо прочитать, чтобы вообще узнать о существовании кода. Кроме того, многие библиотеки содержат статические инициализаторы или затрагиваются фреймворками внедрения зависимостей во время запуска приложения, что приводит к их выгрузке в оперативную память в любом случае.
Удаление страниц и замедление загрузки
Поскольку память, хранящая данные в файлах, всегда может быть повторно считана из хранилища, ядро считает эти страницы «чистыми». Когда система испытывает нехватку памяти, ядро вытесняет (удаляет) эти чистые страницы кода из ОЗУ, чтобы освободить место для других элементов.
Если вашему приложению позже потребуется снова выполнить этот код, процессор выдаст ошибку, и ядру придётся заново считывать страницу из памяти. Чем больше кода в вашем приложении, тем более уязвимым оно становится к его удалению. Когда пользователь возвращается к вашему перегруженному приложению после использования других приложений, он будет сталкиваться со случайными рывками и замедлениями, поскольку процессор постоянно зависает, ожидая, пока код будет загружен обратно из памяти.
Стоимость ошибки страничного доступа: Хотя она сильно зависит от скорости хранилища устройства (UFS или eMMC) и состояния ядра, серьезная ошибка страничного доступа (чтение 4 КБ из хранилища) может стоить от 0,5 до 5 мс . Если ваш путь запуска затрагивает 500 различных страниц неоптимизированного кода, вы легко можете добавить несколько сотен миллисекунд чистой задержки ввода-вывода ко времени запуска вашего приложения.
Изучение размера кода с помощью Compiler Explorer
Чтобы понять, как ваш код на Java или Kotlin преобразуется в машинный код (и, следовательно, в байты памяти), вы можете использовать Compiler Explorer .
Поддержка Android встроена непосредственно в Godbolt. Это позволяет увидеть, как различные части инструментария Android (D8, R8 и dex2oat) преобразуют ваш исходный код.
Как использовать Compiler Explorer на Android
- Перейдите на сайт godbolt.org .
- Выберите Android Java или Android Kotlin из выпадающего списка языков (вверху слева).
- В раскрывающемся списке компилятора (в правом верхнем углу панели кода) вы можете выбрать один из следующих инструментов:
-
d8: Отображает байт-код Dalvik (.dex). Это наиболее близкое к вашему исходному коду представление, и его легче читать. -
r8: Показывает, как оптимизатор R8 уменьшает и оптимизирует ваш байт-код. -
dex2oat: Отображает окончательный машинный код ARM64 , который фактически выполняется на устройстве. Здесь можно увидеть реальное влияние на память (4 байта на инструкцию).dex2oatможет быть ориентирован на различные архитектуры набора команд (ISA), но ARM64 является наиболее распространенным для мобильных телефонов.
-
- Подсветка исходного кода и выходных данных : при наведении курсора на строку кода будут выделены соответствующие инструкции байт-кода или машинного кода, что позволит легко отслеживать влияние конкретных операторов.
- Конвейер оптимизации : В окне дизассемблирования можно нажать «Добавить новый...» -> «Конвейер оптимизации» . Это позволяет увидеть внутренние шаги, выполняемые компилятором. Вы можете проверить, как преобразуется внутреннее представление (IR) на каждом этапе (например, между шагами «Встраивание (до)» и «Встраивание (после)»), прежде чем оно будет преобразовано в окончательный машинный код ARM64.

Почему это важно для памяти
Каждая инструкция, которую вы видите в выводе dex2oat , ориентированная на архитектуру ARM64, занимает 4 байта в исполняемом файле вашего приложения ( .odex или .oat ).
Попробуйте ввести код, использующий различные языковые возможности, и изучите вывод компилятора:
- Доступ к массиву против итераторов списков :
- Простой цикл по массиву
int[]может скомпилироваться примерно в 10 инструкций (около 40 байт). - Цикл foreach по
Listнеявно используетIterator. Это может привести к 30-40 инструкциям (~160 байт) из-за дополнительных вызовов методов (hasNext(),next()) и выделения памяти для самого объекта итератора. - Оптимизация R8 : При определенных условиях (например, когда доказано, что
ListявляетсяArrayList) оптимизатор R8 может преобразовать цикл foreach обратно в простой индексированный цикл, устраняя накладные расходы на итератор и уменьшая как размер кода, так и потребление памяти во время выполнения.
- Простой цикл по массиву
- Вызовы виртуальных методов : включают загрузку класса объекта, поиск метода в
vtableметодов, а затем переход к следующему шагу. Обычно это занимает 4-5 инструкций (~20 байт). - Прямые/статические вызовы : часто преобразуются в одну инструкцию
bl(Branch with Link) (4 байта). - Лямбда-выражения в Kotlin : могут генерировать целые анонимные классы и дополнительные методы-мосты, добавляя сотни байтов кода и метаданных к простому функциональному блоку.
С помощью Compiler Explorer вы можете увидеть, как сложные языковые возможности (например, лямбда-выражения Kotlin, API потоков или активное использование обобщений) влияют на итоговый размер скомпилированного приложения, и как оптимизаторы, такие как R8, в некоторых случаях могут компенсировать затраты на языковые абстракции. Этот инструмент поможет вам принимать обоснованные решения при проектировании и реализации приложения.
В целом, чем сложнее код вашего приложения, тем больше памяти используется. И наоборот, более простой код — или код, упрощенный R8 — приводит к меньшему объему инструкций ЦП и байтов в памяти и ОЗУ.
Измерение влияния кода с помощью meminfo и showmap
Для просмотра объема памяти, потребляемой кодом вашего приложения, можно использовать стандартные инструменты Android для работы с памятью.
dumpsys meminfo
При выполнении команды adb shell dumpsys meminfo <package> ` категория «Код» в разделе «Сводка приложения» предоставляет общий обзор памяти, связанной с кодом:
App Summary
Pss(KB)
------
Java Heap: 3244
Native Heap: 5412
Code: 24512 # <--- Sum of .so, .dex, .oat, .art, etc.
showmap
Для более детального просмотра используйте showmap . Она отображает области в конкретных файлах, отображаемые в память.
adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"
Вы увидите записи, относящиеся к скомпилированному коду вашего приложения:
size RSS PSS clean dirty clean dirty swap swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
12288 8192 8192 8192 0 0 0 0 0 /data/app/.../base.odex
Мертвый код и R8
Поскольку каждый выполняемый метод занимает память, наличие «раздутого» приложения с ненужными инициализациями или неиспользуемыми библиотеками может серьезно повлиять на производительность при запуске и базовое использование памяти.
Именно поэтому такие инструменты, как R8 (ProGuard), имеют решающее значение. R8 анализирует байт-код вашего приложения и удаляет все классы или методы, которые никогда не вызываются («удаление мертвого кода»).
Практические упражнения: цена вздутия живота
Чтобы продемонстрировать влияние размера кода, рассмотрим эксперимент по сравнению двух сборок приложения, содержащих 300 сгенерированных классов (каждый с 500 методами):
- CodeBloat (Unoptimized) : Стандартная, неоптимизированная сборка, содержащая все сгенерированные классы и уникальные строки.
- CodeBloatOptimized : Тот же исходный код, но скомпилированный с включенным уменьшением размера в соответствии с R8.
1. Предварительная компиляция (AOT)
Для максимального использования памяти, поддерживаемой файлами, мы будем использовать инструмент cmd package compile для предварительной компиляции приложений в файлы .oat (AOT).
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized
Обратите внимание, что это синтетический пример. Как правило, приложения используют режим компиляции speed-profile (подробнее см. ниже).
2. Запустите и сравните.
Чтобы добиться по-настоящему холодного запуска, при котором система должна считывать код из памяти, мы будем удалять кэш страниц ядра перед запуском каждого приложения. Для этого требуется root-доступ.
Запустите неоптимизированное приложение:
adb shell am force-stop com.android.codebloat
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5 # Wait for the background thread to load classes
adb shell dumpsys meminfo -s com.android.codebloat
Теперь проделайте то же самое для оптимизированного приложения:
adb shell am force-stop com.android.codebloat.optimized
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat.optimized/com.android.codebloat.MainActivity
sleep 5
adb shell dumpsys meminfo -s com.android.codebloat.optimized
Результаты
Если вы посмотрите на строку «Код» в разделе App Summary , вы увидите огромную разницу:
- Неоптимизированный
Code: ~30 000 КБ (30 МБ) - Оптимизированный
Code: ~2000 КБ (2 МБ)
Поскольку R8 определил, что 500 методов внутри этих классов на самом деле никогда не делали ничего полезного (метод doSomething() только вызывает method0() , а результаты игнорируются), он удалил почти весь искусственно сгенерированный код из финального APK.
3. Посмотрите, как это выглядит в Перфетто.
Влияние раздувания кода отчетливо видно на начальном этапе загрузки приложения. В частности, обратите внимание на фрагмент bindApplication в основном потоке и вложенные фрагменты, начинающиеся с madvising , которые указывают на подготовку системы к загрузке файлов из APK-файла и его скомпилированного кода ( .odex ).
При интерактивном холодном запуске система будет использовать функции mmap() и madvise() для загрузки кода и других данных из этих файлов, необходимых для работы приложения. Значение после "size=" в срезах madvising указывает, сколько данных необходимо загрузить. Эта предварительная загрузка кода приложения выполняется для ускорения его запуска.
Из сравнения видно, что объем кода приложения, который необходимо было загрузить из хранилища в оперативную память, был значительно больше в случае раздутого приложения, что приводило к увеличению времени выполнения и, как следствие, к замедлению запуска приложения. Кроме того, трассировка запуска раздутого приложения показывает фрагменты для загрузки вторичных DEX-файлов ( classes2.dex , classes3.dex ), в которые раздутое приложение было вынуждено "перетекать", поскольку оно не помещалось в один DEX-файл.
Для сравнения (холодный запуск на Pixel 10a)
| Метрика | Неоптимизированный код (раздувание кода) | Оптимизировано (оптимизировано для предотвращения раздувания кода) |
|---|---|---|
base.odex madvise size | ~7,9 МБ (2,0 мс) | ~16 КБ (0,003 мс) |
base.apk madvise размер | ~2,4 МБ (2,4 мс) | ~4 КБ (0,001 мс) |
classes2.dex madvise size | ~7,3 МБ (8,6 мс) | Н/Д |
classes3.dex madvise size | ~7,3 МБ (8,0 мс) | Н/Д |
Общая продолжительность madvising | ~21 мс | ~0,004 мс |
Неоптимизированная производительность загрузки приложения

Оптимизирована скорость загрузки приложения.

Влияние избыточного кода варьируется в зависимости от размера приложения, характеристик устройства пользователя и системной нагрузки.
PerfettoSQL для анализа загрузки
Для извлечения этих метрик из трассировок можно использовать следующие запросы.
1. Продолжительность запуска приложения
Здесь отображается время от момента запуска Activity до момента отрисовки первого кадра приложения.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';
См.: Понимание различных состояний запуска приложения
Длительность запуска приложения зависит от множества факторов, помимо тех, которые рассмотрены в этом руководстве!
2. Выведите информацию о размерах и продолжительности madvising
Этот запрос фокусируется на той части, madvising , которую мы рассмотрели выше.
INCLUDE PERFETTO MODULE slices.with_context;
SELECT
name,
dur/1e6 AS dur_ms
FROM thread_slice
WHERE process_name LIKE 'com.android.codebloat%'
AND name LIKE 'madvising %';
3. Разбивка состояний основного потока (общая продолжительность каждого состояния)
Этот запрос показывает, сколько времени основной поток приложения провел в различных состояниях.
SELECT
p.name AS process_name,
state,
sum(dur)/1e6 AS total_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
AND t.is_main_thread = 1
GROUP BY p.name, state;
Вы можете уточнить запрос, чтобы он учитывал только состояния основного потока во время запуска приложения.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT
p.name AS process_name,
ts.state,
-- Calculate only the duration that falls within the startup window
SUM(
MAX(0,
MIN(ts.ts + ts.dur, s.ts + s.dur) - MAX(ts.ts, s.ts)
)
) / 1e6 AS startup_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
-- Join on the package name to align thread states with the correct startup
JOIN android_startups s ON s.package = p.name
WHERE p.name LIKE 'com.android.codebloat%'
AND t.is_main_thread = 1
-- Only select thread states that overlap with the startup interval
AND ts.ts + ts.dur > s.ts
AND ts.ts < s.ts + s.dur
GROUP BY 1, 2
ORDER BY startup_dur_ms DESC;
Это может выявить некоторые интересные проблемы, например:
- Высокое время выполнения (R), но не "Запущено" : это указывает на задержку запуска приложения из-за конкуренции за ресурсы ЦП, то есть основной поток приложения не мог работать, поскольку другие потоки (возможно, из других приложений) занимали ресурсы ЦП.
- Большое время, проведенное в режиме прерываемого сна (D) : это обычно указывает на медленный ввод-вывод или нехватку памяти, которые замедляют запуск приложения.
- Большое время, проведенное в режиме ожидания (S) : это означает, что основной поток ожидал завершения работы другими потоками. Иногда это указывает на конфликт блокировок в процессе запуска приложения (т.е. основной поток был заблокирован на эксклюзивном ресурсе, который был занят другим потоком в приложении).
4. Максимальный объем памяти, поддерживаемой файлами (RSS-файлы)
Этот показатель хорошо коррелирует с объемом кода и данных, загружаемых приложением при запуске. Более «раздутое» приложение достигнет здесь более высокого значения, что вызовет нагрузку на память системы. Такая нагрузка, в свою очередь, может задержать запуск приложения, поскольку система будет испытывать трудности с удовлетворением запросов на выделение памяти или перенаправлять процессорное время с запуска приложения на освобождение памяти от других процессов для удовлетворения непосредственных потребностей запускаемого приложения.
SELECT
p.name AS process_name,
max(c.value)/1024.0/1024.0 AS max_rss_file_mb
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
AND t.name = 'mem.rss.file'
GROUP BY p.name;
Режимы компиляции ART и память
Среда выполнения Android (ART) может компилировать код вашего приложения в одном из нескольких различных режимов, также известных как фильтры компилятора . Выбранный фильтр компилятора напрямую влияет на объем памяти, используемый вашим приложением.
-
verify: ART выполняет только проверку байт-кода. Компиляция AOT не выполняется. Код выполняется через интерпретатор или компилируется во время выполнения JIT- компилятором.- Влияние на память : Наименьший размер на диске. Использование памяти для нативного кода перемещается в
JIT Cache(анонимная «грязная» память).
- Влияние на память : Наименьший размер на диске. Использование памяти для нативного кода перемещается в
-
speed: ART выполняет полную AOT-компиляцию всех методов.- Влияние на память : Максимальный размер файла
.odex. Максимально эффективно использует память, защищенную файлом (чистую память) .
- Влияние на память : Максимальный размер файла
-
speed-profile: ART компилирует только те методы, которые были помечены как "горячие" в JIT-профиле .- Влияние на память : Сбалансированный подход. Компилируется AOT только наиболее важный код.
Наиболее распространенный фильтр — speed-profile , используемый при установке пользовательских приложений. Он настраивается в системных свойствах pm.dexopt.install и pm.dexopt.bg-dexopt и обычно устанавливается в build/make/target/product/runtime_libart.mk .
Некоторые системные приложения используют speed компиляцию, а также компиляцию во время сборки образа системы. verify обычно используется только в сценариях разработки.
| Вариант использования | Типичный фильтр компилятора |
|---|---|
| Разработка | verify |
| Образ системы | speed |
| Пользовательские приложения | speed-profile |
Практическое упражнение: режимы компиляции и память.
Мы можем использовать приложение CodeBloat , чтобы увидеть, как эти фильтры влияют на использование памяти. Для воспроизведения этих измерений:
- Принудительно перекомпилируйте приложение в целевой режим.
- Принудительно остановите и перезапустите приложение.
- Дождитесь, пока фоновый поток завершит обработку запросов к классам (следите за логами в logcat или подождите 5 секунд).
- Выполните команду
adb shell dumpsys meminfo com.android.codebloat.`
Режим: verify (без AOT)
adb shell cmd package compile -m verify -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat
В режиме verify в сводке приложения отображается следующее: * Код PSS : ~8000 КБ * Dalvik Other (JIT) : ~25000 КБ
Поскольку код не компилируется AOT, среда выполнения должна выполнять JIT-компиляцию "горячих" методов в JIT-кэш , что отображается как "грязная" анонимная память ( Dalvik Other ).
Режим: speed (полный AOT)
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat
В режиме speed результаты резко меняются: * Код PSS : ~24 000 КБ * Dalvik Other (JIT) : ~5 000 КБ
Теперь код приложения отображается из файла .odex как чистая память, поддерживаемая файлом . Это снижает нагрузку на JIT-кэш и делает память доступной для вытеснения при повышенной нагрузке, а не «застрявшей» в качестве «грязной» оперативной памяти.
Режим: speed-profile (селективный AOT)
Современные приложения могут включать в себя базовый профиль baseline.prof . ART использует его для выборочной компиляции только того кода, который необходим для быстрой и эффективной с точки зрения использования памяти загрузки.
В этом упражнении мы создадим базовый профиль для перечисления классов, запускаемых приложением. Однако в реальности компилятор может также получать профили из внешних источников, таких как магазин приложений («облачные профили»), которые могут предоставлять созданные пользователями JIT-профили для приложений независимо от того, включил ли разработчик также созданный им базовый профиль.
Создание и использование профилей на устройстве.
Чтобы увидеть влияние speed-profile , вы можете создать собственный профиль на устройстве:
Сброс и запуск :
adb shell am force-stop com.android.codebloatВзаимодействие : Запустите приложение и позвольте ему выполнить последовательность запуска.
Профиль дампа :
adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)(Это заставляет приложение записывать свой текущий профиль на диск).
Установить профиль :
adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \ /data/misc/profiles/ref/com.android.codebloat/primary.profКомпиляция :
adb shell cmd package compile -m speed-profile -f com.android.codebloat
При повторном запуске вы увидите баланс: значение Code PSS будет ниже speed (например, ~16 000 КБ), поскольку были скомпилированы только методы "горячего" запуска, а остальные будут обрабатываться интерпретатором или JIT-компилятором только в случае их фактического использования.
Видеть:
Подробный анализ скомпилированного кода
Если вы хотите точно увидеть, какие инструкции генерирует ART, обратитесь к art/DISASSEMBLY_GUIDE.md .
В нем содержится подробная инструкция по использованию:
-
oatdump: Для просмотра инструкций ARM64 внутри существующего файла.odex. -
dex2oat: Для имитации компиляции с подробными флагами отладки.
Упражнение: встраивание кода
Одна из причин неожиданного увеличения размера скомпилированного кода — встраивание методов . Компилятор может решить скопировать тело небольшого, часто вызываемого метода непосредственно в вызывающие его функции.
В нашем приложении CodeBloat метод doSomething() в каждом сгенерированном классе просто вызывает method0() . При компиляции в режиме speed оптимизирующий компилятор ART, скорее всего, встроит method0() в doSomething() .
Упражнение: Проверьте это с помощью oatdump на вашем устройстве:
# 1. Find the path to the application's APK and compiled .odex file
adb shell pm path com.android.codebloat
# Output: package:/data/app/~~.../base.apk
adb shell "dumpsys package com.android.codebloat | grep 'location is' | head -n 1"
# Example output: [location is /data/app/~~.../oat/arm64/base.odex]
# 2. Run oatdump (substituting the correct path to base.odex)
adb shell oatdump --oat-file=/data/app/~~.../oat/arm64/base.odex \
--class-filter=com.android.codebloat.GeneratedClass0
Найдите метод doSomething в выводе. Если он был встроен, вы увидите инструкции по загрузке длинной строковой константы непосредственно внутри doSomething , а не инструкцию bl , нацеленную на method0 .
Визуализация оптимизации (CFG)
Чтобы точно определить, когда компилятор принял решение встроить метод, можно построить граф потока управления (CFG) . Он показывает состояние кода на каждом этапе конвейера оптимизации, с каждым преобразованием промежуточного представления (IR) компилятора, пока код не будет приведен к целевой архитектуре набора команд (например, ARM64).
Запустите
dex2oatс флагами дампа : используйте флаг--verbose-methods, чтобы ограничить вывод определенными методами; в противном случае файл.cfgдля большого приложения может вырасти до нескольких гигабайт.# Substitution of actual paths required: adb shell dex2oat64 --dex-file=/data/app/~~.../base.apk \ --oat-file=/data/local/tmp/dump.odex \ --compiler-filter=speed \ --dump-cfg=/data/local/tmp/codebloat.cfg \ --verbose-methods=doSomethingЗагрузка и просмотр : Загрузите файл
.cfgна свою рабочую станцию и откройте его с помощью IR Hydra .Найдите встроенный модуль : в IR Hydra загрузите артефакты компиляции и найдите
doSomething. Сравните представление до и после прохода встроенного модуля . Вы увидите, как граф расширяется по мере слияния инструкций изmethod0с вызывающим модулем.
В качестве альтернативы, используйте инструмент Opt Pipeline в Compiler Explorer (как описано в разделе выше) и введите аналогичный код, чтобы увидеть аналогичное преобразование, выполняемое на этапе встраивания кода .
Упражнение: нестабильные поля и барьеры памяти
В приложении MemoryLab поле mGarbageSink помечено как volatile . Это гарантирует, что компилятор не будет оптимизировать выделение памяти для мусора.
public volatile byte[] mGarbageSink;
В дизассемблированном коде ARM64 вы увидите, что каждая операция записи в это поле сопровождается барьером памяти ( dmb ish ) или использованием инструкций загрузки-захвата/сохранения-освобождения ( ldar / stlr ). Это обеспечивает видимость потоков, но добавляет несколько дополнительных инструкций к каждому обращению, немного увеличивая размер кода по сравнению с обычным полем.
Упражнение: Найдите в дизассемблированном коде обращения к полям и связанные с ними барьеры доступа к памяти.
Упражнение: неявные проверки приостановки
Если вы разберете цикл, например, тот, что используется в generateAllocationChurn , вы заметите любопытную инструкцию в конце тела цикла:
ldr x21, [x21]
Это неявная проверка приостановки . ART использует её, чтобы позволить сборщику мусора безопасно приостанавливать потоки. Регистр x21 обычно указывает сам на себя. Когда сборщику мусора необходимо приостановить поток, он «отравляет» это место в памяти. В следующий раз, когда поток выполнит этот ldr , он вызовет ошибку, которую среда выполнения перехватит и использует для перевода потока в приостановленное состояние.
Этот шаблон повторяется в каждом цикле и в начале каждого метода, что вносит свой вклад в общий размер кода вашего приложения.
Упражнение: Найдите все неявные проверки приостановки в дизассемблированном коде метода и попытайтесь сопоставить их с исходным кодом.