## Application fundamentals ### C++, Java и Kotlin + JNI 1. **C++** C++ является объектно-ориентированным языком программирования, который был создан как преемник C. Первоначально он создавался как расширение C и со временем стал самостоятельным функциональным языком. В отличие от Java, он способствует низкоуровневому манипулированию памятью и является низкоуровневым языком программирования, который намного ближе к системным ресурсам. 2. **Java** Java - это язык программирования высокого уровня, разработанный для простого переноса и миграции. Код Java является переносимым и может работать на всех платформах, не требуя перекомпиляции. Он является одновременно объектно-ориентированным программированием и процедурным. Работает на любой Java Virtual Machine aka JVM. Как говорится, *compile once, run anywhere*. **Java Vs C++: Pros And Cons** (нужно перевести) Java vs C++ both boasts their own advantages and drawbacks. Let us check the main pros and cons of using each one of them. * Java can be executed through any platform as it is platform-independent. However, C++ is platform-dependent, fundamentally needing to be compiled for each platform. Java is portable and can be translated into bytecode. Bytecodes are flexible and can be run across any platform. * C++ has support for multiple types of inheritances. C++ uses virtual keywords and stable syntaxes in order to effectively use multiple and single inheritances. Java only supports single inheritances and can only simulate the effects of multiple inheritances. C++ also supports pointers while Java has just recently started supporting limited support for pointers. * Java boasts of default thread support while C++ needs external libraries to support thread classes. * C++ does not promote documentation while Java has default support for documentation comments for source code. * C++ has strong encapsulation, which ensures protection and provides a flexible model. Java possesses weak encapsulation and is not comparatively flexible. * C++ supports both method and operator overloading. Java can only allow method overloading. * Runtime errors are detected by the system in Java while it must be done manually in C++ by the programmer. Debugging is also a very time-consuming process in C++ while Java makes it much simpler. * C++ is much better for system-level programming or hardware manipulation due to allowing direct calls to native system libraries. Java is not great for system-level programming due to the requirement of Native Access and Native Interface to call on native libraries. * Java requires all functions and data to be inside classes and does not boast of any global scope. Meanwhile, C++ allows functions or data to existing outside classes while boasting of namespace and global scope. * C++ supports goto statements while Java does not support goto statements. * C++ also supports structures and unions that Java does not support. **Similarities Between Java Vs C++** Firstly, both these languages are object-oriented programming languages. Their syntax is similar and the ‘main’ function is the entry points for both C++ and Java. They support similar data types and promote using conditional statements, value assignments, arithmetic operators etc. Java and C++ are both great for building applications and both are truly powerful in their own terms. The basics of C/C++ or Java cover similar concepts and even though the languages are different, they can project or build similarly functioning applications. Both C++ and Java are backwards compatible with C++ being based on C and Java versions supporting older rollouts with plenty of resources or libraries. Both the languages are great at supporting and assisting other languages as well. Even though C++ is much more compatible, JVM is becoming the environment of choice when running other languages as well. C++ is used for building Operating Systems and browsers but Java is used for building applications for these systems such as Android. This is why these two languages are co-dependent and both must be referred to when planning to build successful applications or environments. **Differences Between Java Vs C++** We have covered the advantages of using C++ and Java are and their individual disadvantages are. The main difference between C/C++ and Java is how they are compiled. While programs in C++ like its predecessor are compiled into object codes, source codes in Java are bytecodes. C++ is completely a compiled language while Java is both compiled and interpreted. Now, let us check some more fundamental differences between C++ and Java. * C++ is a low-level procedural language while Java is a high-level programming language. * C++ requires manual memory management. In C++, memory needs to be allocated or deallocated separately through deletion/new operators. In Java, the system controls memory management and does not require the user to manually perform it. * C++ is procedural and does not maintain root hierarchy while Java maintains single root hierarchies. * Source codes and file names do not have any relationship in C++ while Java requires classes containing source codes to be the same as the file names. * C++ is compatible with other high-level languages while Java is not compatible with other languages without additional support. * Type semantics are consistent in C++ while Java does not provide consistency between primitive and object types. * Codes that will cause exceptions must be added to the try/catch block in Java as destructors are not supported. In C++, programmers can simply exclude blocks even if it will cause an exception. * Objects are managed manually in C++ while Java depends on automatic garbage collection for identifying and removing objects. * C++ is much more effective for controlling hardware resources and for accessing systems and databases. Java is a complex language and needs time to be able to access hardware resources effectively. * C++ and Java differences also lie in their intractability with native libraries. C++ is also capable of accessing every hardware resource while Java cannot do so. 3. **Kotlin** Kotlin является детищем JetBrains. Первоначально он был разработан для среды JVM и сочетает в себе функциональное и объектно-ориентированное программирование. Основые фичи Kotlin: * Совместимость с Java: Kotlin позволяет использовать существующие навыки и знания в области Java. Это означает, что разработчики могут продолжать писать приложения для Android, используя свой текущий язык программирования. * Null Safety: Kotlin устойчив к null, что предотвращает ошибки, которые обычно вызываются отсутствием типов или неправильно инициализированными параметрами. Важно убедиться, что вы не используете тип в качестве значения null по умолчанию. * Nullable Types: Kotlin не нуждается в null или nil. Это означает, что он предотвращает ошибки, вызванные отсутствием типов и параметров. Это также делает код более читаемым. * Свойства с автоматически созданными геттерами и сеттерами: Kotlin поставляется со свойствами, которые автоматически генерируют геттеры и сеттеры. Это означает, что разработчикам намного проще писать код и быстрее читать приложение для Android. * Краткий синтаксис: нет лишних токенов или ключевых слов, что помогает сделать код кратким и удобным для глаз разработчиков. 4. **JNI** JNI — это интерфейс, позволяющий из Java вызывать нативные функции. Например, метод С++, который что-нибудь делает. Допустим, мы пишем большую программу на простом и любимом Java или Kotlin, и нужно реализовать задачу коммивояжера для нашего клиента. Или мы пишем генетический алгоритм, который ищет что-то в большом объёме данных, и так уж вышло, что у нас есть замечательная реализация на С++. Можно написать фрагмент кода на C/С++, и при необходимости дёргать нативный метод и получать из него результат вычисления. ### Эволюция VM В Android вместо виртуальной машины Java использовалась собственная, куда более эффективная, виртуальная машина Dalvik. А также вместо байт-кода Java собственный, куда более эффективный, байт-код, который внутри APK-шек записыватся в файлах с расширением DEX. :::info DEX - формат файлов, который использовался Dalvik, аналогичный байт-коду JVM. Если в кратце, то он просто несколько поэффективней. Подробнее [тут](https://source.android.com/devices/tech/dalvik/dex-format). ::: 1. **Dalvik**: Работает медленно Вплоть до Android версии 4.4 KitKat приложения запускались через виртуальную машину Dalvik, которая работала по принципу JIT-компиляции. :::info JIT (Just in time) - генерирует машинный код в процессе испольнения. JIT компилирует горячие точки (hot stops) - небольшие части кода выполняющиеся чаще, и производительность приложения в основном зависит от скорости выполнения именно этих частей. Подробнее [тут](https://habr.com/ru/post/536288/). ::: Анализ приложений на Dalvik: | Плюсы | Минусы | | -------- | -------- | | Экономия оперативной памяти | Долго запускаются | | Занимают мало места | Работают медленно | | Скомпилированный код можно записать в кэш и брать оттуда | Потребляют больше ресурсов в runtime | 2. **ART**: Требует много ресурсов Полная смена с JIT на AOT. :::info AOT (Ahead of time) - генерирует машинный код в процессе установки. ::: Анализ приложений на Dalvik: | Плюсы | Минусы | | -------- | -------- | | Приложения начали работать без задержек | Сильно увеличился вес приложений | | Стали быстрее запускаться | Увеличилось время установки | Также, при каждом обновлении системы приходилось перекомпилировать все приложения. 3. **Profiling**: Начало оптимизации По статистике пользователи очень редко используют более 10-20% кода приложения. Значит, правильней будет заранее скомпилировать только ту часть, а для остального использовать JIT-компиляцию. Для этого в Android 7.0 Nougat Google представили технологию PGC — Profile guided compilation. То есть это компиляция, основанная на профилях использования приложения. По началу используется JIT-компиляция, и результат сохраняется в кеш. Но по ночам запускается "Демона" (это, если что, официальное название специальной службы), который анализирует кеши всех приложений, после чего создает профили с оптимизированным кодом. Анализ приложений на Profiling: | Плюсы | Минусы | | -------- | -------- | | Сильно ускорилось время установки приложений | Первое время приходилось терпеть тормоза (до Android 9.0 Pie) | | Ускорилось время обновления системы | | | Скомпилированный код стал занимать на 80% меньше места | | Этот минус Google решили в Android 9.0 Pie, где представили Облачные профили. Они просто собрали профили приложений со всех пользователей, проанализировали их и создали усредненный профиль для каждого приложения, который теперь стал автоматически скачиваться во время установки приложения из Google Play Store. Всё это позволило значительно повысить скорость первого запуска приложения, да и в целом, первые дни использования девайса. Однако оставалась одна очень важная проблема: огромное разнообразие устройств. 4. **AAB**: Баланс Система Android поддерживает 4 архитектуры, 6 разрешений графики и более 150 языков: ![](https://i.imgur.com/3EibDNG.jpg) Google придумал умную систему публикации - во время финальной сборки приложения они просто формируют бандл, то есть архив вообще со всеми необходимыми файлами под все девайсы. Делается это автоматически через Android Studio. И загружают этот архив в Google Play. А дальше, когда вы заходите в Google Play и скачиваете приложение, то Google Play сам собирает для вас идеальную APK-шку только с необходимым набором данных: подгружается только графика необходимого разрешения, библиотеки только под вашу архитектуру и только тот языковой пакет, который выбран у вас в системе. Плюсы такого решения: * Уменьшен размер скомпилированного кода * Уменьшен размер самих приложений (многие приложения похудели более чем на 30 процентов) * Для Google это позволяет экономить ежедневно 10 ПБ трафика ### Источники * [Как Android пришел к AAB? Что будет с APK?](https://habr.com/ru/company/droider/blog/568760/) ### Подробнее про DEX, Proguard, D8 и R8 ###### :rocket: Тык на [source](https://nphau.medium.com/android-journey-proguard-d8-r8-what-are-they-e8f2bfe079a7) ###### :rocket: Нужно прочитать: [source](https://proandroiddev.com/android-cpu-compilers-d8-r8-a3aa2bfbc109) ## Android Operating System Основой платформы Android является ядро ​​Linux, которое позволяет Android использовать определенные функции безопасности. Ядро Linux использовалось в миллионах чувствительных к безопасности систем с момента его создания в 1991 году, поэтому оно имеет долгую историю постоянного исследования, тестирования и улучшения тысячами разработчиков. Согласно документации Android, Android использует несколько ключевых функций безопасности Linux, в том числе: - Модель разрешений на основе пользователей - Изоляция процесса - Расширяемый механизм безопасного межпроцессного взаимодействия (IPC) - Возможность удаления ненужных и/или небезопасных частей ядра Другой ключевой особенностью Linux является его многопользовательская операционная система, которая позволяет нескольким пользователям получать доступ к независимым системным ресурсам, таким как память, ресурсы ЦП, ОЗУ и приложения. Изолируя пользовательские ресурсы друг от друга, они могут быть защищены друг от друга. :::info Когда пользователи добавляются на устройство, некоторые функции ограничиваются, когда другой пользователь находится на переднем плане. Поскольку данные приложений разделены пользователем, состояние этих приложений различается в зависимости от пользователя. Например, электронная почта, предназначенная для учетной записи пользователя, который в данный момент не находится в фокусе, не будет доступна, пока этот пользователь и учетная запись не будут активны на устройстве. ::: Платформа Android использует преимущества многопользовательской системы Linux с собственной песочницей приложений, также именуемая как ==Application Sandbox==, которая изолирует ресурсы приложений друг от друга и защищает приложения и систему от вредоносных приложений. ### Application Sandbox Платформа Android использует Linux модель разрешений на основе пользователей для изоляции ресурсов приложений. Этот процесс называется песочницей приложения. ![](https://i.imgur.com/z2gViZt.png) Целью песочницы является предотвращение взаимодействия вредоносных внешних программ с защищенными приложениями. Внутренние компоненты операционной системы также защищены механизмом песочницы. Уязвимости, открытые приложением, не могут быть использованы для получения доступа к внешней системе. Безопасная связь между приложениями обеспечивается пользовательской защитой Linux. В отличие от традиционных операционных систем, например. MacOS и Windows, Android используют концепцию идентификатора пользователя (UID) для управления контролем доступа приложения, а не контроля доступа системного пользователя. Приложению запрещается доступ к данным других приложений или системным функциям без необходимых разрешений. Приложение изолировано на уровне ядра, поэтому гарантируется, что приложение будет изолировано от остальной системы, независимо от конкретной среды разработки, используемых языков программирования или API. По умолчанию приложения имеют ограниченный доступ к операционной системе. Это гарантирует, что вредоносное приложение не сможет получить доступ к внешней системе изнутри. ### Verified Boot Процесс загрузки Android построен так, что он, с одной стороны, не позволяет злоумышленникам загружать на устройстве произвольную ОС, с другой стороны, может позволять пользователям устанавливать кастомизированные сборки Android (и другие системы). Прежде всего, Android-устройства, в отличие от «десктопных» компьютеров, обычно не позволяют пользователю (или злоумышленнику) произвести загрузку со внешнего носителя; вместо этого сразу запускается установленный на устройстве ==bootloader== (загрузчик). Bootloader — это относительно простая программа, в задачи которой (при загрузке в обыкновенном режиме) входят: - Инициализация и настройка Trusted Execution Environment (например, ARM TrustZone) - Нахождение разделов встроенной памяти, в которых хранятся образы ядра Linux и initramfs - Проверка их целостности и неприкосновенности (integrity) — в противном случае загрузка прерывается с сообщением об ошибке — путём верификации цифровой подписи производителя - Загрузка ядра и initramfs в память и передача управления ядру ### Flashing, unlocking, fastboot и recovery Кроме того, bootloader поддерживает дополнительную функциональность для обновления и переустановки системы. Во-первых, это возможность загрузить вместо основной системы (Android) специальную минимальную систему, называемую recovery. Версия recovery, устанавливаемая на большинство Android-устройств по умолчанию, очень минималистична и поддерживает только установку обновлений системы в автоматическом режиме, но многие энтузиасты Android устанавливают кастомную recovery. Это делается путём использования второй «фичи» bootloader'а, направленной на обновление и переустановку системы — поддержки перезаписи (flashing) содержимого и структуры разделов по командам с подсоединённого по кабелю компьютера. Для этого bootloader способен загружаться в ещё один специальный режим, который называют fastboot mode (или иногда просто bootloader mode), поскольку обычно для общения между компьютером и bootloader'ом в этом режиме используется протокол fastboot (и соответствующий ему инструмент fastboot из Android SDK со стороны компьютера). Некоторые реализации bootloader'а используют другие протоколы. В основном это касается устройств, выпускаемых компанией Samsung, где специальная реализация bootloader'a (Loke) общается с компьютером по собственному проприетарному протоколу (Odin). Для работы с Odin со стороны компьютера можно использовать либо реализацию от самих Samsung (которая тоже называется Odin), либо свободную реализацию под названием Heimdall. <img src="https://i.imgur.com/oYuBy5u.jpg" height="300"/> <img src="https://i.imgur.com/PqS43C3.png" height="300"/> Конкретные детали зависят от реализации bootloader'а (то есть различаются в зависимости от производителя устройства), но во многих случаях установка recovery и сборок Android, подписанных ключом производителя устройства, просто работает без дополнительных сложностей. Таким образом можно вручную обновлять систему; а вот установить более старую версию не получится: функция, известная как защита от отката, не позволит bootloader'у загрузить более старую версию Android, чем была загружена в прошлый раз, даже если она подписана ключом производителя, поскольку загрузка старых версий открывает дорогу к использованию опубликованных уязвимостей, которые исправлены в более новых версиях. Кроме того, многие устройства поддерживают разблокировку bootloader'а (unlocking the bootloader, также известную как OEM unlock) — отключение проверки bootloader'ом подписи системы и recovery, что позволяет устанавливать произвольные сборки того и другого (у части производителей это аннулирует гарантию). Именно так обычно устанавливаются такие популярные дистрибутивы Android, как [LineageOS](https://lineageos.org/) (бывший CyanogenMod), [Paranoid Android](https://paranoidandroid.co/), [AOKP](http://aokp.co/), [OmniROM](https://omnirom.org/) и другие. Поскольку разблокировка bootloader'а всё-таки позволяет загрузить на устройстве собственную версию системы, в целях безопасности при разблокировке все пользовательские данные (с раздела data) принудительно удаляются. Если систему переустанавливает сам пользователь, а не злоумышленник, после переустановки он может восстановить свои данные из бэкапа (например, из облачного бэкапа на серверах Google или из бэкапа на внешнем носителе), если злоумышленник — он получит работающую систему, но не сможет украсть данные владельца устройства. ### Шифрование диска Современные версии Android используют пофайловое шифрование данных (file-based encryption). Этот механизм основан на встроенной в ext4 поддержке шифрования, реализованной в ядре Linux (fscrypt), и позволяет системе зашифровывать различные части файловой системы различными ключами. По умолчанию система шифрует большинство данных пользователя, расположенных на разделе data, с помощью ключа, который создаётся на основе пароля пользователя и не сохраняется на диск (credential encrypted storage). :::info Это означает, что при загрузке система должна попросить пользователя ввести свой пароль, чтобы вычислить с его помощью ключ для расшифровки данных. Именно поэтому первый раз после включения устройства пользователя встречает требование ввести полный пароль или графический ключ, а не просто пройти аутентификацию, приложив палец к сканеру отпечатков. ::: В дополнение к credential encrypted storage в Android также используется device encrypted storage — шифрование ключом на основе данных, которые хранятся на устройстве (в том числе в Trusted Execution Environment). Файлы, зашифрованные таким образом, система может расшифровать до того, как пользователь введёт пароль. Это лежит в основе функции, известной как Direct Boot: система способна загружаться в некоторое работоспособное состояние и без ввода пароля; при этом приложения могут явно попросить систему сохранить (наименее приватную) часть своих данных в device encrypted storage, что позволяет им начинать выполнять свои базовые функции, не дожидаясь полной разблокировки устройства. :::info Например, Direct Boot позволяет будильнику срабатывать и до первого ввода пароля, что особенно полезно, если устройство непредвиденно перезагружается ночью из-за временного отключения питания или сбоя системы. ::: ### Root Так называемый root-доступ — это возможность выполнять код от имени «пользователя root» (UID 0, также известного как ==суперпользователь==). Напомню, что root — это специально выделенный Unix-пользователь, которому разрешён полный доступ ко всему в системе, и на которого не распространяются никакие ограничения прав. Как и большинство других современных операционных систем, Android спроектирован с расчётом на то, что ==обыкновенному пользователю ни для чего не требуется использовать root-доступ==. В отличие от более закрытых операционных систем, пользователи которых называют разрушение наложенных на них ограничений буквально «побегом из тюрьмы», в Android прямо «из коробки», без необходимости получать root-доступ и устанавливать специальные сторонние «твики», есть возможность. Зачем это может быть нужно? Конечно, root-доступ полезен для отладки и исследования работы системы. Кроме того, обладая root-доступом, можно неограниченно настраивать систему, изменяя её темы, поведение и многие другие аспекты. Можно принудительно подменять код и ресурсы приложений — например, можно удалить из приложения рекламу или разблокировать его платную или скрытую функциональность. Можно устанавливать, изменять и удалять произвольные файлы, в том числе в разделе system (хотя это почти наверняка плохая идея). *Чем больше сила, тем больше ответственность.* ![](https://i.imgur.com/wlUZBvN.png) Напомню, что пользователь почти всегда взаимодействует с устройством не напрямую, а через приложения. А это значит, что и root-доступ он будет использовать в основном через приложения, которые, можно надеяться, будут добросовестно пользоваться root-доступом для хороших целей. Но если на устройстве доступен root, это, в принципе, означает, что приложения могут воспользоваться им и для нехороших целей — навредить системе, украсть ценные данные, заразить систему вирусом, установить кейлоггер и т.п. В отличие от традиционной модели доверия программам в классическом Unix, Android рассчитан на то, что пользователь не может доверять сторонним приложениям — поэтому их и помещают в песочницу. Тем более нельзя доверять приложениям root-доступ, надеясь, что они будут использовать его только во благо. Фактически, root-доступ разрушает аккуратно выстроенную модель безопасности Android, снимая с приложений все ограничения и открывая им права на доступ ко всему в системе. С другой стороны, и приложения не могут доверять устройству, на котором подключен root-доступ, поскольку на таком устройстве в его работу имеют возможность непредусмотренными способами вмешиваться пользователь и остальные приложения. :::info Например, разработчики приложения, содержащего платную функциональность, естественно, не захотят, чтобы проверку совершения покупки можно было отключить. Многие приложения, работающие с особенно ценными данными — например, Google Pay (бывший Android Pay) — явно отказываются работать на устройствах с root-доступом, считая их недостаточно безопасными. ::: ### Процессы #### Модель процесса Процесс — это просто экземпляр выполняемой программы, включая текущие значения счетчика команд, регистров и переменных. Концептуально у каждого процесса есть свой, виртуальный, центральный процессор. Разумеется, на самом деле настоящий центральный процессор постоянно переключается между процессами, но чтобы понять систему, куда проще думать о наборе процессов, запу- щенных в (псевдо) параллельном режиме, чем пытаться отслеживать, как центральный процессор переключается между программами. Это постоянное переключение между процессами, называется **мультипрограммированием**, или **многозадачным режимом работы**. Разница между процессом и программой довольно тонкая, но весьма существенная. Здесь нам, наверное, поможет какая-нибудь аналогия. Представим себе программиста, решившего заняться кулинарией и испечь пирог на день рождения дочери. У него есть рецепт пирога, а на кухне есть все ингредиенты: мука, яйца, сахар, ванильный экстракт и т. д. В данной аналогии рецепт — это программа (то есть алгоритм, выраженный в некой удобной форме записи), программист — это центральный процессор, а ингредиенты пирога — это входные данные. Процесс — это действия, состоящие из чтения рецепта нашим кулинаром, выбора ингредиентов и выпечки пирога. Процесс — это своего рода действия. У него есть программа, входные и выходные данные и состояние. Один процессор может совместно использоваться несколькими процессами в соответствии с неким алгоритмом планирования, который используется для определения того, когда остановить один процесс и обслужить другой. В отличие от процесса программа может быть сохранена на диске и вообще ничего не делать. #### Создание процесса Существуют четыре основных события, приводящих к созданию процессов. 1. Инициализация системы. 2. Выполнение работающим процессом системного вызова, предназначенного для создания процесса. 3. Запрос пользователя на создание нового процесса. 4. Инициация пакетного задания. Фоновые процес- сы, предназначенные для обработки какой-либо активной деятельности, связанной, например, с электронной почтой, веб-страницами, новостями, выводом информации на печать и т. д., называются **демонами**. В UNIX существует только один системный вызов для создания нового процесса — fork. Этот вызов создает точную копию вызывающего процесса. После выполнения системного вызова fork два процесса, родительский и дочерний, имеют единый образ памяти, единые строки описания конфигурации и одни и те же открытые файлы. И больше ничего. Обычно после этого дочерний процесс изменяет образ памяти и запускает новую программу, выполняя системный вызов execve или ему подобный. В Windows все происходит иначе: одним вызовом функции Win32 CreateProcess создается процесс, и в него загружается нужная программа. В обеих системах, UNIX и Windows, после создания процесса родительский и дочер- ний процессы обладают своими собственными, отдельными адресными пространства- ми. Если какой-нибудь процесс изменяет слово в своем адресном пространстве, другим процессам эти изменения не видны. В UNIX первоначальное состояние адресного про- странства дочернего процесса является копией адресного пространства родительского процесса, но это абсолютно разные адресные пространства — у них нет общей памяти, доступной для записи данных. #### Завершение процесса Рано или поздно новые процессы будут завершены, обычно в силу следующих обстоятельств: - Обычного выхода (добровольно). - Выхода при возникновении ошибки (добровольно). - Возникновения фатальной ошибки (принудительно). - Уничтожения другим процессом (принудительно). #### Иерархия процессов Все процессы во всей системе UNIX принадлежат единому дереву, в корне которого находится процесс init. В отличие от этого в Windows не существует понятия иерархии процессов, и все процессы являются равнозначными. Единственным намеком на иерархию процессов можно считать присвоение родительскому процессу, создающему новый процесс, специального маркера (называемого **дескриптором**), который может им использоваться для управления дочерним процессом. Но он может свободно передавать этот маркер какому-нибудь другому процессу, нарушая тем самым иерархию. А в UNIX процессы не могут лишаться наследственной связи со своими дочерними процессами. #### Состояния процессов Три состояния, в которых может находиться процесс: - Выполняемый (в данный момент использующий центральный процессор). - Готовый (работоспособный, но временно приостановленный, чтобы дать возмож- ность выполнения другому процессу). - Заблокированный (неспособный выполняться, пока не возникнет какое-нибудь внешнее событие). #### Реализация процессов Для реализации модели процессов операционная система ведет таблицу (состоящую из массива структур), называемую **таблицей процессов**, в которой каждая запись соответствует какому-нибудь процессу. (Ряд авторов называют эти записи **блоками управления процессом**.) Эти записи содержат важную информацию о состоянии процесса, включая счетчик команд, указатель стека, распределение памяти, состояние открытых им файлов, его учетную и планировочную информацию и все остальное, касающееся процесса, что должно быть сохранено, когда процесс переключается из состояния выполнения в состояние готовности или блокировки, чтобы позже он мог возобновить выполнение, как будто никогда не останавливался. ### Потоки #### Применение потоков Мы уже сталкивались с подобными аргументами. Именно они использовались в поддержку создания процессов. Вместо того чтобы думать о прерываниях, таймерах и контекстных переключателях, мы можем думать о параллельных процессах. Но только теперь, рассматривая потоки, мы добавляем новый элемент: возможность использования параллельными процессами единого адресного пространства и всех имеющихся данных. Эта возможность играет весьма важную роль для тех приложений, которым не подходит использование нескольких процессов (с их раздельными адресными пространствами). Вторым аргументом в пользу потоков является легкость (то есть быстрота) их создания и ликвидации по сравнению с более «тяжеловесными» процессами. Во многих системах создание потоков осуществляется в 10–100 раз быстрее, чем создание процессов. Это свойство особенно пригодится, когда потребуется быстро и динамично изменять количество потоков. Третий аргумент в пользу потоков также касается производительности. Когда потоки работают в рамках одного центрального процессора, они не приносят никакого прироста производительности, но когда выполняются значительные вычисления, а также значительная часть времени тратится на ожидание ввода-вывода, наличие потоков позволяет этим действиям перекрываться по времени, ускоряя работу приложения. И наконец, потоки весьма полезны для систем, имеющих несколько центральных процессоров, где есть реальная возможность параллельных вычислений. Они дают возможность сохранить идею последовательных процессов, которые осуществляют блокирующие системные вызовы (например, для операций дискового ввода-вывода), но при этом позволяют все же добиться распараллеливания работы. Блокирующие системные вызовы упрощают программирование, а параллельная работа повышает производительность. #### Классическая модель потоков Использование объектов потоками. | Элементы, присущие каждому процессу | Элементы, присущие каждому потоку | | -------- | -------- | | Адресное пространство | Счетчик команд | | Глобальные переменные | Регистры | | Открытые файлы | Стек | | Дочерние процессы | Состояние | | Необработанные аварийные сигналы | | | Сигналы и обработчики сигналов | | | Учетная информация | | Подобно традиционному процессу (то есть процессу только с одним потоком), поток должен быть в одном из следующих состояний: выполняемый, заблокированный, готовый или завершенный. Выполняемый поток занимает центральный процессор и является активным в данный момент. В отличие от этого, заблокированный поток ожидает события, которое его разблокирует. Например, когда поток выполняет системный вызов для чтения с клавиатуры, он блокируется до тех пор, пока на ней не будет что-нибудь набрано. Поток может быть заблокирован в ожидании какого-то внешнего события или его разблокировки другим потоком. Готовый поток планируется к выполнению и будет выполнен, как только подойдет его очередь. Переходы между состояниями потока аналогичны переходам между состояниями процесса. Иногда потоки имеют иерархическую структуру, при которой у них устанавливаются взаимоотношения между родительскими и дочерними потоками, но чаще всего такие взаимоотношения отсутствуют и все потоки считаются равнозначными. Одной из распространенныхх процедур, вызываемой потоком, является thread_yield. Она позволяет потоку добровольно уступить центральный процессор для выполнения другого потока. Важность вызова такой процедуры обусловливается отсутствием прерывания по таймеру, которое есть у процессов и благодаря которому фактически задается режим многозадачности. Для потоков важно проявлять вежливость и время от времени добровольно уступать центральный процессор, чтобы дать возможность выполнения другим потокам. Другие вызываемые процедуры позволяют одному потоку ожидать, пока другой поток не завершит какую-нибудь работу, а этому потоку — оповестить о том, что он завершил определенную работу, и т. д. #### Реализация потоков в пользовательском пространстве ![](https://i.imgur.com/1CZbqdU.png) Первый способ — это поместить весь набор потоков в пользовательском пространстве. И об этом наборе ядру ничего не известно. Что касается ядра, оно управляет обычными, однопотоковыми процессами. Первое и самое очевидное преимущество состоит в том, что набор потоков на пользовательском уровне может быть реализован в операционной системе, которая не поддерживает потоки. Когда потоки управляются в пользовательском пространстве, каждому процессу необходимо иметь собственную **таблицу потоков**, чтобы отслеживать потоки, имеющиеся в этом процессе. Эта таблица является аналогом таблицы процессов, имеющейся в ядре, за исключением того, что в ней содержатся лишь свойства, принадлежащие каждому потоку, такие как счетчик команд потока, указатель стека, регистры, состояние и т. д. Но у потоков есть одно основное отличие от процессов. Когда поток на время останавливает свое выполнение, например когда он вызывает thread_yield, код процедуры thread_yield может самостоятельно сохранять информацию о потоке в таблице потоков. Более того, он может затем вызвать планировщик потоков, чтобы тот выбрал для выполнения другой поток. Процедура, которая сохраняет состояние потока, и планировщик — это всего лишь локальные процедуры, поэтому их вызов намного более эффективен, чем вызов ядра. Помимо всего прочего, не требуется перехват управления ядром, осуществляемый инструкцией trap, не требуется переключение контекста, кэш в памяти не нужно сбрасывать на диск и т. д. Благодаря этому планировщик потоков работает очень быстро. У потоков, реализованных на пользовательском уровне, есть и другие преимущества. Они позволяют каждому процессу иметь собственные настройки алгоритма плани- рования. Например, для некоторых приложений, которые имеют поток сборщика мусора, есть еще один плюс — им не следует беспокоиться о потоках, остановленных в неподходящий момент. Эти потоки также лучше масштабируются, поскольку потоки в памяти ядра безусловно требуют в ядре пространства для таблицы и стека, что при очень большом количестве потоков может вызвать затруднения. Но несмотря на лучшую производительность, у потоков, реализованных на пользовательском уровне, есть ряд существенных проблем. Первая из них — как реализовать блокирующие системные вызовы. Представьте, что поток считывает информацию с клавиатуры перед нажатием какой-нибудь клавиши. Мы не можем разрешить потоку осуществить настоящий системный вызов, поскольку это остановит выполнение всех потоков. Другой наиболее сильный аргумент против потоков, реализованных на пользователь- ском уровне, состоит в том, что программистам потоки обычно требуются именно в тех приложениях, где они часто блокируются, как, к примеру, в многопоточном веб-сервере. Эти потоки часто совершают системные вызовы. Как только для выполнения системно- го вызова ядро осуществит перехват управления, ему не составит особого труда занять- ся переключением потоков, если прежний поток заблокирован, а когда ядро займется решением этой задачи, отпадет необходимость постоянного обращения к системному вызову select, чтобы определить безопасность системного вызова read. Зачем вообще использовать потоки в тех приложениях, которые, по существу, полностью завязаны на скорость работы центрального процессора и редко используют блокировку? Никто не станет всерьез предлагать использование потоков при вычислении первых n простых чисел или при игре в шахматы, поскольку в данных случаях от них будет мало проку. #### Реализация потоков в ядре Здесь нет и таблицы процессов в каждом потоке. Вместо этого у ядра есть таблица потоков, в которой отслеживаются все потоки, имеющиеся в системе. Когда потоку необходимо создать новый или уничтожить существующий поток, он обращается к ядру, которое и создает или разрушает путем обновления таблицы потоков в ядре. Все вызовы, способные заблокировать поток, реализованы как системные, с более существенными затратами, чем вызов процедуры в системе поддержки исполнения программ. Поскольку создание и уничтожение потоков в ядре требует относительно более весомых затрат, некоторые системы с учетом складывающейся ситуации применяют более правильный подход и используют свои потоки повторно. Для потоков, реализованных на уровне ядра, не требуется никаких новых, неблокирующих системных вызовов. Более того, если один из выполняемых потоков столкнется с ошибкой обращения к отсутствующей странице, ядро может с легкостью проверить наличие у процесса любых других готовых к выполнению потоков и при наличии таковых запустить один из них на выполнение, пока будет длиться ожидание извлечения запрошенной страницы с диска. Главный недостаток этих потоков состоит в весьма существенных затратах времени на системный вызов, поэтому, если операции над по- токами (создание, удаление и т. п.) выполняются довольно часто, это влечет за собой более существенные издержки. #### Гибридная реализация попытках объединить преимущества создания потоков на уровне пользователя и на уровне ядра была исследована масса различных путей. Один из них заключается в использовании потоков на уровне ядра, а затем нескольких потоков на уровне пользователя в рамках некоторых или всех потоков на уровне ядра. При использовании такого подхода программист может определить, сколько потоков использовать на уровне ядра и на сколько потоков разделить каждый из них на уровне пользователя. Эта модель обладает максимальной гибкостью. ![](https://i.imgur.com/mWTr5qm.png) При таком подходе ядру известно только о потоках самого ядра, работу которых оно и планирует. У некоторых из этих потоков могут быть несколько потоков на пользовательском уровне, которые расходятся от их вершины. Создание, удаление и планирование выполнения этих потоков осуществляется точно так же, как и у пользовательских потоков, принадлежащих процессу, запущенному под управлением операционной системы, не способной на многопоточную работу. В этой модели каждый поток на уровне ядра обладает определенным набором потоков на уровне пользователя, которые используют его по очереди. #### Всплывающие потоки Возможен и совершенно иной подход, при котором поступление сообщения вынуждает систему создать новый поток для его обработки. Такой поток называется **всплывающим**. Основное преимущество всплывающих потоков заключается в том, что они создаются заново и не имеют прошлого — никаких регистров, стека и всего остального, что должно быть восстановлено. Каждый такой поток начинается с чистого листа, и каждый их них идентичен всем остальным. Это позволяет создавать такие потоки довольно быстро. Новый поток получает сообщение для последующей обработки. В результате использования всплывающих потоков задержку между поступлением и началом обработки сообщения можно свести к минимуму. ### Источники * Современные операционные системы | Бос Херберт, Таненбаум Эндрю * [Официальная документация Android](https://developer.android.com/docs) * [Как работает Android](https://habr.com/ru/company/solarsecurity/blog/427431/) * [The Layers of the Android Security Model](https://proandroiddev.com/the-layers-of-the-android-security-model-90f471015ae6) ## Android Startup В данной главе пройдемся по тому, как запускается система Android (над текстом надо поработать ага). ### Bootloader **Bootloader** и **Linux Kernel** - по сути это black boxes для джуна (как и для большинства разрабов), так что достаточно поверхностно понять что в них происходит. Bootloader запускается в момент запуска уствойства, и их реализация зачастую отличается от производителя и используемой архитекруты (например, LittleKernel, U-Boot, UEFI). Задача в целом очистить и подготовить железо, после чего обнаружить, разархивировать и загрузить в памать Linux Kernel (так как Android OS базируется на Linux). ### Linux Kernel Linux Kernel в свою очеред будет делать свои "операционно-системные дела", по типу настройки памяти. Для андроид разработчика из всего, что делает Linux Kernel интереснее всего **initramfs**. Во-первых, он отвественный за то, чтобы взять **ramdisk.img** (он содержит корневую файловую систему **RootFS**), и установить его в память устройства. Вторым шагом для initramfs будет найти бинарный файл init и запустить его. Данный init файл очень андроид-специфичен, который сильно кастомизирован под Android, он и запускает систему. Он ответственен за настройку и запуск основного функционала (создавать пустые директории, настраивать драйвера устройства и так далее), запуск множества демонов (например, installd - демон, запимающийся файловыми системныеми задачами, как установка пакетов; logd - демон ответственный за логи). :::info Внутри RootFS есть конфигурационные файлы **init.*name*.rc** (бывают разные), которые имеют внутри себя список команд инициализирующих устройство. Также, в них настраиваются демоны, чтобы запустить и следить за ними (если что перезапустить например). ::: Одним из запускаемых демонов является **zygote**. ### Zygote Zygote ответственен за то, чтобы становится основанием других приложений. Это процесс, который сам по себе не делает ничего интересного - он запускается, инициализирует ряд моментов, но основной задачей внутри системы является запускать другие приложения. Помимо этого, Zygote является первым процессом, внутри которого запускается виртуальная машина, и это нужно для эффективности запуска приложений. Zygote занимается тяжелыми задачами предзагрузки java классов, ресурсов фреймворков, общих библиотек, после чего просто ждет. И когда какое-то приложение должно запуститься, система отправляет запрос Zygote чтобы тот создал копию себя, и запуск приложения начиается в этой копии, что ускоряет процесс его запуска. Еще один плюс этого в том, что память после форка Zygote остается та же, если нет необходимости в нее делать записи (тогда делается копия для такого процесса). Но так как многое внутри является константами, так что большая часть памяти является общей, что ускоряет запуск. ### SystemServer Это первый реальный процесс, в котором имеется Java Runtime. Также, он запускается только один, и около 90% системных сервисов (например, LocationManagerService, PowerManagerService) инициализируются и создаются им. После, он передает ответственность по BOOT особому сервису **ActivityManagerService**. Три шага, которые здесь происходят: - Broadcast: action.PRE_BOOT_COMPLETED, не юзабелен, используется например при показе что система обновляется - Запуск Home Activity, захардкоженный интент - Broadcast: action.BOOT_COMPLETED, можно получить ActivityManagerService ответственный за весь функционал жизненных циклов приложений в системе, все проходит через него: ![](https://i.imgur.com/5VrP6bY.png) ### Источники * [Digging Into Android Startup ](https://youtu.be/5SQP0qfUDjI) ## IPC / Binder Framework ### Что такое Binder? Это компонент системы межпроцессного объектно-ориентированного взаимодействия для сервисов операционной системы, то есть это среда в системе, которая работает в Linux Kernel. ### Что такое IPC? Inter-process communication (IPC), или vежпроцессное взаимодействие, это фреймворк для передачи сигналов и данных между множеством процессов. ### Почему Binder? * Security - каждый процесс находится в песочнице, и работает под отдельным системным идентификатором * Stability - краш какого-либо процесса не отражается на остальных * Memory management - неиспользуемые процессы удаляются для освобождения ресурсов Почему бы не использовать только Intents и Content providers? Вы вполне можете пользоваться ими, но проблема в том, что это асинхронно и не очень ООПшно. Но на самом деле, "под капотом" все это в любом случае базируется на Binder. Есть еще штука как Messenger IPC, но по-прежнему асинхронна, хоть и с меньшей задержкой. ### Как все работает Тут нужно внимательно посмотреть на схему и пройтись по порядку операций, все дело выглядит примерно вот так: ![](https://i.imgur.com/bAA4oxi.png) Давайте немножко разберемся в терминологии: * **IBinder interface** - методы, которые Binder объекты должны реализовать. * **AIDL** - Android Interface Definition Language, используется для описания бизнес операции в IBinder Interface. * **Binder (Object)** -  generic(?) имплементация IBinder. Имеет Binder Token, который иденцифицирует объект среди процессов системы. * **Binder Service** - Binder Object c реализацией бизнес операций. * **Binder Client** - тот, кто хочет использовать Binder Servoce, например ваше приложение. * **Binder transaction** - процесс передачи и получения данных между Binder Client и Binder Service. * **Parcel** - юнит транзакционных данных, контейнер сообщения, передаваемый через IBinder. * **Marshalling** - процедура конвертирования структур данных высокоуровневых приложений в parcels с целью передачи их в транзакции Binder. Unmarshalling - обратный процесс marshalling. * **Proxy, Stub** - они автогенерируются и позволяют marshalling/unmarshalling. * **Context Manager** - позволяет находить Binder Services, в андроиде более известный как **Service Manager**. ### Источники * [Deep Dive into Android IPC/Binder Framework ](https://youtu.be/Jgampt1DOak) ## Java Core ### Creational design patterns #### Factory and abstract factory (provider model) ```kotlin class MyRecyclerViewAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { 0 -> HeaderViewHolder() 1 -> SeparatorViewHolder() else -> ContentViewHolder() } } } ``` ```kotlin abstract class CarPartsFactory { fun buildEngine(/* engine parts */): Engine fun buildChassis(/* chassis materials */): Chassis } class CarPartsFactoryProvider { inline fun <reified M : Manufacturer> createFactory(): CarPartsFactory { return when (M::class) { Manufacturer.Audi -> AudiPartsFactory() Manufacturer.Toyota -> ToyotaPartsFactory() } } } class AudiPartsFactory: CarPartsFactory() { override fun buildEngine(...): AudiEngine override fun buildChassis(...): AudiChassis } class ToyotaPartsFactory: CarPartsFactory() { override fun buildEngine(...): ToyotaEngine override fun buildChassis(...): ToyotaChassis } // Usage: val factoryProvider = CarPartsFactoryProvider() val partsFactory = factoryProvider.createFactory<Manufacturer.Toyota>() val engine = partsFactory.buildEngine() ``` #### Singleton ```javascript // Java implementation public class Singleton { private static Singleton instance = null; private Singleton() { // Initialization code } public static Singleton getInstance() { if (instance == null) { instance = Singleton() } return instance; } // Class implementation... } ``` ```kotlin // Kotlin implementation object Singleton { // class implementation } ``` #### Builder design pattern ```kotlin class Car(val engine: Engine, val turbine: Turbine?, val wheels: List<Wheel>, ...) { class Builder { private lateinit var engine: Engine private var turbine: Turbine? = null private val wheels: MutableList<Wheel> = mutableListOf() fun setEngine(engine: Engine): Builder { this.engine = engine return this } fun setTurbine(turbine: Turbine): Builder { this.turbine = turbine return this } fun addWheels(wheels: List<Wheel>): Builder { this.wheels.addAll(wheels) return this } fun build(): Car { require(engine.isInitialized) { "The car needs an engine" } require(wheels.size < 4) { "The car needs at least 4 wheels" } ``` ```kotlin val car = Car.Builder() .setEngine(...) .setTurbine(...) .addWheels(...) .build() ``` ```kotlin data class Car(val engine: Engine, val turbine: Turbine? = null, val wheels: List<Wheel>) { init { /* Validation logic */ } } ``` ### Structural design patterns #### Adapter design pattern ```kotlin class Car(...) { fun move() { /* Implementation */ } } interface WaterVehicle { fun swim() } class CarWaterAdapter(private val adaptee: Car): WaterVehicle { override fun swim() { // whatever is needed to make the car work on water // ... might be easier to just use a boat instead startSwimming(adaptee) } } // Usage val standardCar: Car = Car(...) val waterCar: WaterVehicle = CarWaterAdapter(standardCar) waterCar.swim() ``` #### Decorator design pattern ```kotlin class BasicCar { fun drive() { /* Move from A to B */ } } class OffroadCar : BasicCar { override fun drive() { initialiseDrivingMode() super.drive() } private fun initialiseDrivingMode() { /* Configure offroad driving mode */ } } ``` ### Behavioral design patterns #### Observer design pattern ```kotlin val observable: Flow<Int> = flow { while (true) { emit(Random.nextInt(0..1000)) delay(100) } } val observerJob = coroutineScope.launch { observable.collect { value -> println("Received value $value") } } ``` #### Strategy design pattern ```kotlin fun interface ValidationRule { fun validate(input: UserInput): Boolean } class EmailValidation: ValidationRule { override fun validate(input: UserInput) = validateEmail(input.emailAddress) } val emailValidation = EmailValidation() val dateValidation: ValidationRule = { userInput -> validateDate(userInput.date) } val userInput = getUserInput() val validationRules: List<ValidationRule> = listOf(emailValidation, dateValidation) val isValidInput = validationRules.all { rule -> rule.validate(userInput) ``` ### Источники * [Understanding design patterns in Kotlin ](https://blog.logrocket.com/understanding-kotlin-design-patterns/#:~:text=fits%20your%20needs.-,Types%20of%20Kotlin%20design%20patterns,and%20how%20we%20use%20them.) ### Подготовка к собеседованию #### ООП ##### Что такое ООП? ##### Чем отличается ООП от функционального программирования? #### Abstract vs Interface ##### В чем их разница? Абстрактный класс — это класс, у которого не реализован один или больше методов (некоторые языки требуют такие методы помечать специальными ключевыми словами). Интерфейс — это абстрактный класс, у которого ни один метод не реализован, все они публичные и нет переменных класса. Интерфейс нужен обычно когда описывается только интерфейс (тавтология). Например, один класс хочет дать другому возможность доступа к некоторым своим методам, но не хочет себя «раскрывать». Поэтому он просто реализует интерфейс. Абстрактный класс нужен, когда нужно семейство классов, у которых есть много общего. Конечно, можно применить и интерфейс, но тогда нужно будет писать много идентичного кода. Абстрактный класс — это «заготовка» класса: реализовано большинство методов (включая внутренние), кроме нескольких. Эти несколько нереализованных методов вполне могут быть внутренними методами класса, они лишь уточняют детали имплементации. Абстрактный класс — средство для повторного использования кода, средство, чтобы указать, какой метод обязан быть перекрыт для завершения написания класса. Интерфейс же — это своего рода контракт: интерфейсы используются в определениях чтобы указать, что объект, который будет использован на самом деле, должен реализовывать (для входных параметров) или будет гарантированно реализовывать (для выходных параметров) набор методов и (что намного важнее!) иметь определённую семантику. Интерфейс вполне может быть и пустым, тем не менее, имплементировать интерфейс означает поддерживать данную семантику. https://www.tutorialspoint.com/when-to-use-an-abstract-class-and-when-to-use-an-interface-in-java#:~:text=Abstract%20classes%20should%20be%20used,common%20functionality%20to%20unrelated%20classes. ##### Плюсы, минусы #### Коллекции ##### Асимптотикая hashmap, treemap ![](https://i.imgur.com/sbAbMG0.png) https://habr.com/ru/post/237043/ https://vertex-academy.com/tutorials/ru/klyuchevoe-slovo-synchronized-java/ #### Equals/hashcode ##### Что будет если переопределить? https://www.techiedelight.com/ru/why-override-equals-and-hashcode-methods-java/#:~:text=%D0%9F%D0%BE%D1%81%D0%BA%D0%BE%D0%BB%D1%8C%D0%BA%D1%83%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%20hashCode%20%D0%BF%D0%BE%20%D1%83%D0%BC%D0%BE%D0%BB%D1%87%D0%B0%D0%BD%D0%B8%D1%8E,hashCode()%20. #### Garbage collector ##### Что делает? ##### Как работает алгоритм очистки? ##### Что такое мусор? Два способа: - Reference counting - у каждого объекта счетчик ссылок. Когда он равен нулю, объект считается мусором. Проблема такого подхода в том, что могут быть цикличные ссылки у объектов друг на друга, в то время как они фактически мусор и не используются программой. - Tracing - объект считается не мусором, если до него можно добраться с корневых точек (GC Root: локальные переменные и параметры методов, java-потоки, статичные переменные, ссылки из JNI. ##### Что такое heap/stack? Делится на две части: - Heap - куча. Основной сегмент памяти, где содержатся все объекты и происходит сборка мусора. - Permanent Generation - содержит мета-данные классов. Сразу про Permanent Generation. Может менять размер во время выполнения, и это довольно дорогостоящая операция. Размер настраивается (-XX: PermSize - мин размер, -XX: MaxSize - макс размер). Часто мин = макс. Heap. Куча. Тут и работает GC. Делится на две области: - New (Yang) Generation - объекты, кот. тут считаются короткоживущими. - Old Generation (Tenured) - обекты считаются долгоживущими. Алгоритм GC исходит из того предположения, что большинство java-объектов живут недолго. Быстро становятся мусором. От них необходимо довольно оперативно избавляться. Что и происходит в New Generation. Там сбор мусора гораздо чаще, чем в Old Generation, где хранятся долгоживущие объекты. После создания объект попадает в New Generation и имеет шанс попасть в Old Generation по прошествии некоторого времени (циклов GC). Heap состоит из: - Eden - переводится как Едем (?). Сюда аллоцируются объекты. Если нет места запускается GC. - Survivor - точнее их два, S1 и S2, и они меняются ролями. Хранятся объекты, которые признаются живыми во время GC. Размер Heap настраивается. https://ziginsider.github.io/Garbage_Collector_Java/ ##### Какие есть поколения объектов внутри heap? https://habr.com/ru/company/otus/blog/553996/ ##### Как GC работает с циклическими зависимостями? Поиск мусора методом Tracing, а не Reference counting ##### Какие типы ссылок есть? https://habr.com/ru/post/169883/