Выполнение программного кода
Выполнение программного кода
Работа на низком уровне
А сейчас мы погрузимся в низкоуровневое исследование того, что происходит при выполнении кода.
К слову, я с этим и столкнулся поначалу - когда познал основы на разных языках, взяв за основу лишь всё то, что было в учебниках, курсах, лекциях и на моей практике, но столкнувшись со сложностями понимания тонкостей - если в самом начале пути ещё можно разбираться в том, что такое функции, переменные, классы, методы и прочее, то в технической плоскости можно утонуть.
Давайте же изучим сложный путь данных в программировании.
Так, представим, что у нас есть некая сущность - те самые данные. Они поначалу проектируются, в ходе чего определяется, какой же у них тип, смысл, логика, потом записываются, читаются и обрабатываются - этот путь происходит буквально со всей информацией - её ведь, по сути, нужно просто проектировать, а потом можно хранить, читать, записывать и обрабатывать, и вот и весь смысл.
Спроектировал - записал - сохранил - прочитал - обработал - очистил.
Схема выполнения
Но если погрузиться на уровень ближе к железу, как профессионалы, то увидим более сложную схему.
| № | Название этапа | Описание |
|---|---|---|
| 1 | Проектирование сущности | Чтобы что-то создать, нужно сначала понять, что это такое. На этапе проектирования определяется концепция сущности: её назначение, поведение, атрибуты, связи с другими объектами. Создаются диаграммы (например, UML), описывающие класс, его поля, методы, наследование, агрегацию. Это абстрактная модель, ещё не привязанная к коду. Определить нужно: свойства и их типы данных (строки, числа, и иные); поведение (методы); цель и задачи; связи с другими сущностями. |
| 2 | Определение шаблона (класса) | Класс как шаблон (чертёж) сущности реализуется в коде. Включает объявление имени, полей (атрибутов), методов, конструкторов, деструкторов, модификаторов доступа. Грубо говоря, мы воплощаем проект в код, превращая диаграмму сущностей в текстовую информацию — занимаемся написанием кода. Итоговый код оформляется как класс — шаблон построения сущности. Класс не занимает память — он лишь описывает структуру будущих экземпляров. |
| 3 | Компиляция / трансляция класса | При компиляции написанный класс преобразуется в байт-код (в языках с виртуальной машиной) или машинный код (в компилируемых языках). Исходный код на языке программирования преобразуется в машиночитаемый формат. Информация о структуре класса, сигнатурах методов, типах полей сохраняется в метаданных (например, таблица виртуальных методов, VTable). |
| 4 | Загрузка класса в среду выполнения | При запуске программы класс загружается в память среды выполнения (например, JVM, CLR). Происходит разрешение зависимостей, проверка доступности родительских классов и интерфейсов. Класс инициализируется: выполняются статические блоки, инициализируются статические поля. |
| 5 | Объявление переменной-ссылки | Объявляется переменная, которая будет указывать на экземпляр сущности. На основе типа данных определяется объём требуемой памяти. На этом этапе выделяется память под ссылку (указатель), но она ещё не указывает на объект. Например: MyClass obj; — ссылка obj создана, но объект не создан. |
| 6 | Выделение памяти под объект (куча) | При создании экземпляра (new MyClass()) среда выполнения запрашивает память в куче (heap). Выделяется блок памяти, достаточный для хранения всех полей объекта и служебной информации (указатель на VTable, lock-объект, хэш-код). |
| 7 | Инициализация объекта | Поля объекта инициализируются значениями по умолчанию (0, null, false), затем выполняется конструктор. Конструктор может вызывать цепочку инициализации родительских классов (через super() или base()), устанавливать начальные значения полей, выполнять проверки. |
| 8 | Присваивание ссылки | Ссылка (объявленная ранее переменная) получает адрес созданного объекта в куче. Теперь переменная указывает на конкретный экземпляр. Ссылка хранится в стеке (если локальная) или в куче (если поле другого объекта). |
| 9 | Запись ссылки в стек вызовов | Если объект создаётся внутри метода, ссылка помещается в стек вызовов (stack frame) текущего потока. Это позволяет быстро обращаться к объекту в рамках текущего контекста. Стек управляется процессором через указатель стека (ESP/RSP). |
| 10 | Обращение к полям и методам | При доступе к полям (obj.field) или вызове методов (obj.method()) процессор или виртуальная машина вычисляет смещение от базового адреса объекта в куче. Для методов происходит разрешение через таблицу виртуальных методов (VTable) при динамическом полиморфизме. |
| 11 | Чтение данных из памяти | Процессор по физическому адресу (полученному из виртуального через MMU) читает данные из ОЗУ. Данные могут быть сначала загружены в кэш L1/L2/L3 для ускорения доступа. Читаемые байты интерпретируются согласно типу (int, float, ссылка и т.д.). |
| 12 | Работа с данными на уровне битов | Все данные хранятся в виде битов (0 и 1). При операциях (арифметических, побитовых) процессор выполняет манипуляции с регистрами. Например, сложение инкрементирует биты, сдвиги (<<, >>) перемещают биты влево/вправо. Битовые маски используются для флагов. |
| 13 | Модификация данных (запись) | При изменении поля объекта (obj.value = 42) новое значение записывается по смещённому адресу в куче. Процессор использует шину данных для записи байтов в ОЗУ. Кэш-память может быть обновлена (write-back или write-through). |
| 14 | Кэширование данных | Часто используемые данные объекта могут кэшироваться на уровне CPU. Это ускоряет доступ, но требует синхронизации при многопоточности (например, через volatile или блокировки). Когерентность кэша поддерживается протоколами (MESI). |
| 15 | Фрагментация кучи | При частом выделении и освобождении объектов память в куче может фрагментироваться: остаются мелкие «дыры» между выделенными блоками. Наличие таких дыр снижает эффективность выделения памяти. Сборщик мусора может выполнять дефрагментацию (compact). |
| 16 | Вызов методов и переходы по адресам | При вызове метода адрес возврата, параметры и локальные переменные помещаются в новый фрейм стека. Процессор выполняет команду перехода (jump) на адрес метода. В случае виртуальных методов адрес определяется динамически через VTable. |
| 17 | Обработка данных (логика) | Внутри методов выполняется бизнес-логика: вычисления, ветвления, циклы. Процессор выполняет инструкции из машинного кода: загружает операнды, выполняет ALU-операции, записывает результаты. |
| 18 | Построение модели на основе данных | Объект может агрегировать или ссылаться на другие объекты, формируя сложную структуру данных (например, дерево, граф). Эти связи определялись на этапе проектирования. Ссылки между объектами создают граф достижимости, критичный для сборки мусора. |
| 19 | Использование объекта (взаимодействие) | Объект участвует в работе программы: передаётся как параметр, возвращается из метода, используется в коллекциях, сериализуется, участвует в событиях. Его состояние может изменяться многократно. |
| 20 | Поиск объекта (по ссылке или значению) | При поиске (например, в коллекции) выполняется обход ссылок или сравнение значений. Хэширование (hashCode) ускоряет поиск. При сравнении (equals) могут сравниваться поля объекта побитово или по ссылке. |
| 21 | Сериализация / передача данных | Объект может быть сериализован в поток байтов (например, JSON, бинарный формат). Поля преобразуются в последовательность битов, сохраняются порядок байтов (endianness), типы, структура. Это необходимо для хранения или передачи по сети. |
| 22 | Десериализация / восстановление | Из потока байтов воссоздаётся объект. Выделяется память, поля инициализируются из данных, восстанавливаются ссылки (при поддержке графов). Может вызываться специальный конструктор или метод восстановления. |
| 23 | Проверка достижимости (reachability) | Сборщик мусора (GC) определяет, доступен ли объект из корней (статические поля, локальные переменные в стеке, регистры). Объект считается живым, если существует путь по ссылкам от корня. |
| 24 | Пометка (mark phase) | На этапе сборки мусора GC проходит по всем достижимым объектам, помечая их как «живые». Это делается обходом графа ссылок (обычно в глубину или ширину). Пометка хранится в служебных битах объекта или отдельной структуре. |
| 25 | Сборка (sweep / compact) | Недостижимые объекты (не помеченные) освобождаются. При sweep — память помечается как свободная. При compact — живые объекты сдвигаются в одну область, устраняя фрагментацию. Указатели (ссылки) обновляются (ремаппинг). |
| 26 | Очистка ресурсов (деструктор / финализатор) | Перед уничтожением объекта может быть вызван финализатор (если определён). Он позволяет освободить неуправляемые ресурсы (файлы, сокеты, память вне кучи). Однако финализаторы ненадёжны и медленны — предпочтительнее использовать RAII или using/dispose. |
| 27 | Освобождение памяти в куче | Память, занимаемая объектом, возвращается в пул свободной памяти. Менеджер памяти обновляет метаданные (списки свободных блоков, битовые карты). |
| 28 | Освобождение ссылки в стеке | При выходе из метода фрейм стека удаляется. Ссылки, хранящиеся в нём, уничтожаются. Это может сделать объект недостижимым, если других ссылок на него нет. |
| 29 | Уничтожение данных (обнуление) | Освобождённая память может быть явно обнулена (в безопасных средах) или оставлена «как есть» (для производительности). Данные физически остаются до перезаписи, что может быть угрозой безопасности. |
| 30 | Анализ использования (профилирование) | Инструменты профилирования отслеживают создание, время жизни, размер, частоту сборки мусора для объектов. Это помогает находить утечки памяти, оптимизировать аллокации, выбирать оптимальные структуры данных. |
| 31 | Оптимизация компилятором / JIT | Современные среды (например, JVM, .NET) применяют JIT-компиляцию: часто вызываемые методы компилируются в нативный код. Применяются оптимизации: inlining, escape analysis (чтобы выделить объект на стеке вместо кучи), устранение синхронизации. |
| 32 | Управление жизненным циклом (ручное / автоматическое) | В некоторых языках (C++, Rust) управление памятью ручное или определяется владением. В других (Java, C#, Python) — автоматическое через GC. Выбор влияет на производительность, предсказуемость и сложность. |
Важно понимать, что вышеприведенный алгоритм выполнения кода является скорее инженерной памяткой, применимой для объектно-ориентированного программирования. И как раз проектирование сущности и определение класса являются высокоуровневой работой, а всё остальное - низкоуровневой.
Представляете теперь, какова работа на современных языках (первые два пункта) и какова была работа на первых устройствах? Технически, мы уже выполняем не все 32 пункта, а лишь 2, причем серьезность никуда не ушла - мы должны проектировать и писать эти 2 пункта так, чтобы учитывать все последующие 30. Какие-то ньюансы за нас заботливо рассчитывает компьютер, а что-то продумывать придётся нам.
Схематично:
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). В языках с автоматическим управлением памятью, таких как JavaScript или Python, многие потенциально опасные операции перехватываются средой выполнения. Например, обращение к несуществующему элементу… В конечном счёте, вся программа — это сеть взаимодействующих функций. Каждая решает свою маленькую задачу, но вместе они создают сложное поведение. Отображение веб-страницы, обработка платежа, расчёт… В программировании механизм циклов работает точно так же. Он состоит из двух ключевых частей — Тело цикла — это набор инструкций, которые нужно повторять, Условие цикла — это правило, которое… Условные операторы — это мост между статичным кодом и динамическим миром, в котором он работает. Они позволяют программе думать, анализируя текущую ситуацию и выбирая наилучший ответ. Стек — это упорядоченная область памяти с принципом последним пришёл — первым ушёл (LIFO). Он используется для хранения локальных переменных, параметров функций и адресов возврата. Когда функция… № Этап Описание ----------------- 1 Вызов метода (синтаксис) Программист пишет obj.method(arg). На уровне языка это вызов метода на экземпляре. Компилятор или интерпретатор проверяет сигнатуру, типы… Шестнадцатеричная система — это мост между человеком и машиной. Она сохраняет точность двоичного представления, но делает его доступным для восприятия. Префикс 0x — это знак, указывающий на переход… Стек - это сегмент памяти, работающий по принципу LIFO (Last In, First Out). Он используется для хранения локальных переменных, параметров функций, адресов возврата и сохранённых регистров. Регистры — это высокоскоростные ячейки памяти, расположенные непосредственно внутри центрального процессора. Они служат временным хранилищем для данных, адресов и управляющей информации, с которой… Но процессоры не работают с отдельными битами. Они оперируют блоками фиксированной длины, которые называются машинными словами. Это базовая единица данных, с которой центральный процессор… Куча представляет собой область динамической памяти, размер которой не известен на этапе компиляции. Программа запрашивает блоки памяти в куче во время выполнения и сама отвечает за их освобождение. Байт-код представляет собой промежуточное представление программного кода, предназначенное для выполнения на виртуальной машине. Он занимает положение между исходным кодом, написанным человеком на…Неопределённое поведение в программах
Внутреннее устройство функций
Реализация циклов на уровне системы
Как работают условные операторы
Жизненный цикл переменных
Процесс выполнения исходного кода
Шестнадцатеричная система счисления в программировании
Архитектура современных процессоров
Регистры процессора и их роль
Машинное слово
Расположение данных в памяти и директивы компилятора
Исполнение байт-кода виртуальными машинами