Базовый CS с нуля
Среда выполнения (runtime)
Ты пишешь функцию на TypeScript. Вызываешь её. Она работает. Ты создаёшь массив. Массив появляется. Ты выбрасываешь ошибку. Она распространяется вверх по цепочке вызовов. Ты никогда не думал о том, по какому адресу в памяти находится массив, что происходит с ним, когда он больше не нужен, или как ошибка знает, к какой функции вернуться.
Обо всём этом заботится нечто, что всегда работает рядом с твоей программой, незаметно. Это нечто называется средой выполнения (или runtime-системой, или runtime- средой).
Среда выполнения — это не твой код. Ты её не писал. Она написана разработчиками языка и поставляется как часть установки языка. Но она работает всё время, пока работает твоя программа, выполняя важную работу, от которой зависит твой код. Понять среду выполнения — значит понять, что реально находится под поверхностью каждой программы, которую ты когда- либо запускал, включая программы, которые пишешь сам.
После этого урока ты сможешь определить, что такое runtime-система, назвать четыре главных сервиса, которые предоставляют среды выполнения (управление памятью, механизм стека вызовов, стандартная библиотека и сам интерпретатор или виртуальная машина), объяснить, что такое сборка мусора и зачем она нужна, и описать, как твой код стоит поверх среды выполнения.
Что такое среда выполнения. Runtime-система (часто просто среда выполнения или runtime) — это код и механизмы, которые реализация языка устанавливает рядом с твоей скомпилированной или интерпретируемой программой для обеспечения сервисов, которые CPU и ОС сами по себе не предоставляют.
Когда ты устанавливаешь Python, Node.js или Java Development Kit, ты не просто устанавливаешь компилятор или интерпретатор — ты устанавливаешь среду выполнения. Когда твоя программа запускается, среда выполнения запускается тоже, часто до выполнения первой строки твоего кода. Среда выполнения остаётся активной на протяжении всего времени жизни процесса и выполняет важные сервисы, которые твой код делегирует ей молча.
Можно думать об этом так: CPU обеспечивает сырое выполнение (выборка, декодирование, исполнение). ОС обеспечивает процессы, файлы и доступ к железу. Среда выполнения располагается между языком и ОС, предоставляя высокоуровневые сервисы, которые обещает семантика языка: автоматическое управление памятью, безопасные соглашения о вызовах, встроенные структуры данных и так далее.
Даже компилируемые языки имеют среды выполнения. Программа на C, скомпилированная в
двоичный файл, всё равно компонуется со стандартной библиотекой C (libc), которая
предоставляет printf, malloc, free, memcpy и сотни других функций. Runtime C также
обрабатывает запуск программы (настройка стека, вызов main) и выход из программы (сброс
буферов, вызов зарегистрированных функций очистки). В C среда выполнения тоньше, чем в
Python, но она всегда присутствует.
Сервис 1: управление памятью и сборка мусора. Каждый раз, когда твоя программа создаёт новый объект, новый массив или новую строку, ей нужен кусок памяти для его хранения. На голой машине получение памяти означает вызов OS-сервиса для получения блока адресов и самостоятельное отслеживание этих адресов. Большинство языков высокого уровня скрывают это: среда выполнения управляет областью памяти, называемой кучей (heap), и выдаёт её части, когда твой код создаёт новые значения.
В таких языках как C ты управляешь памятью вручную: вызываешь malloc для выделения блока
и free для его освобождения, когда закончил. Если забудешь освободить — эта память будет
занята на протяжении всей жизни процесса — утечка памяти. Если освободишь память, которую
всё ещё используешь — получишь сбой или уязвимость безопасности — использование после
освобождения.
Большинство современных языков (Java, Python, JavaScript, Go, C#) избегают этого, предоставляя автоматическое управление памятью через компонент под названием сборщик мусора (GC). Сборщик мусора — это часть среды выполнения, которая периодически сканирует кучу для нахождения всех объектов, которые твоя программа больше не может достичь (объектов без оставшихся ссылок из каких-либо живых переменных). Затем он автоматически освобождает эти объекты. Твой код просто создаёт объекты; GC убирает за тобой.
GC не бесплатен. Он периодически запускает свой машинный код, приостанавливая или работая параллельно с твоей программой. Это добавляет задержку и потребляет время CPU, что является одной из причин, по которой тщательно оптимизированный код на C или Rust может превосходить Python или JavaScript при CPU-интенсивных вычислениях.
Сервис 2: механизм стека вызовов. Каждый вызов функции должен отслеживать локальные переменные и место для возврата после завершения функции. Стек вызовов — это область памяти, где хранится эта информация. Каждый вызов функции помещает кадр стека на стек вызовов; кадр содержит локальные переменные функции, адрес возврата (куда возобновить выполнение после возврата функции) и некоторые служебные данные. Когда функция возвращает управление, её кадр снимается со стека.
Механизм стека вызовов — правила того, как кадры располагаются в памяти, как аргументы передаются между функциями, как сохраняется и восстанавливается адрес возврата — является частью ответственности среды выполнения. Для большинства компилируемых языков это определяется соглашением о вызовах платформы (стандарт, согласованный ОС и компилятором). Для интерпретируемых языков и виртуальных машин среда выполнения управляет виртуальным стеком вызовов в программном обеспечении.
Ты полагаешься на стек вызовов каждый раз, когда вызываешь функцию, даже если никогда не думаешь об этом. Когда ошибка времени выполнения сообщает «stack overflow» (потому что ты написал бесконечную рекурсию), это означает, что стек вызовов вырос настолько глубоко, что исчерпал память, выделенную для него средой выполнения.
Сервис 3: стандартная библиотека. Стандартная библиотека — это коллекция функций
и структур данных, поставляемая вместе с языком и доступная без установки каких-либо
дополнительных пакетов. Примеры: len(), range(), open() в Python и модуль json.
Array.prototype.map(), Math.sqrt() и Date в JavaScript. java.util.ArrayList и
java.io.File в Java.
Стандартная библиотека является частью среды выполнения. Когда твоя программа на Python
вызывает len(my_list), она вызывает функцию, код которой находится в двоичном файле
среды выполнения Python, а не в твоей программе. Среда выполнения поставляет код; твой
код вызывает его.
Стандартные библиотеки обычно включают: структуры данных (списки, словари, множества), строковые операции, математические функции, файловый и сетевой ввод-вывод, работу с датами и временем, примитивы параллелизма и утилиты форматирования/разбора. Качество и полнота стандартной библиотеки языка — важный фактор продуктивности программиста. Философия Python «batteries included» (богатая стандартная библиотека) — одна из причин его популярности для скриптинга и работы с данными.
Сервис 4: интерпретатор или виртуальная машина. Если язык интерпретируется или компилируется в байт-код, среда выполнения также включает движок, который реально запускает твой код. Для Python это интерпретатор CPython. Для Java и Kotlin — JVM. Для C# и F# — .NET Common Language Runtime (CLR). Для JavaScript в браузере или Node.js — движки V8, SpiderMonkey или JavaScriptCore.
Эти движки сами являются скомпилированными двоичными файлами — программами, написанными на C или C++ и заранее скомпилированными для хост-платформы. Они запускают твой код, интерпретируя его или компилируя в нативный код (через JIT). Движок является частью среды выполнения в той же мере, что и сборщик мусора и стандартная библиотека.
Когда ты запускаешь node server.js, ты запускаешь двоичный файл Node.js (который включает
V8, сборщик мусора, стандартную библиотеку Node.js и механизм цикла событий). Всё это —
среда выполнения. Твой файл server.js — это пользовательский код поверх неё.
Почему это работает
Почему ОС не может просто предоставить все эти сервисы? ОС предоставляет среду процессов общего назначения — сырую память, файловые дескрипторы, сетевые сокеты, потоки. Она не знает и не заботится о системе типов твоего языка, твоей модели объектов или о том, как твой язык определяет область видимости и замыкания. Среда выполнения является мостом между обобщённым интерфейсом ОС и конкретной семантикой твоего языка. Среда выполнения Java знает о Java-объектах, Java-типах и Java-модели памяти. ОС знает только о страницах памяти и системных вызовах. Среда выполнения — это адаптер, который превращает сырые сервисы ОС в высокоуровневые обещания языка.
Граничные случаи
Языки почти без среды выполнения. C и Rust часто называют «системными языками», потому
что они имеют очень тонкие среды выполнения. Двоичный файл Rust, скомпилированный с
#![no_std], практически не имеет среды выполнения вообще — нет GC, нет стандартной
библиотеки, нет механизма обработки исключений. Программист управляет памятью вручную (в
Rust — через правила владения, обеспечиваемые компилятором, а не runtime GC). Это делает
такие программы подходящими для микроконтроллеров и ядер ОС, где нет базовой ОС или среды
выполнения, на которую можно положиться, но ответственность возвращается к программисту.
Прослеживание сервисов среды выполнения, задействованных в одной строке TypeScript.
Рассмотрим эту строку в программе Node.js:
const users = JSON.parse(fs.readFileSync("users.json", "utf8"));Эта одна строка одновременно задействует как минимум четыре сервиса среды выполнения:
-
Стандартная библиотека —
fs.readFileSync: Твой код вызывает функцию стандартной библиотеки Node.jsreadFileSync. Среда выполнения делает системный вызов ОС для открытия файла, чтения его байтов в буфер памяти и возврата буфера. Ты никогда не писал системный вызов ОС; это сделала среда выполнения. -
Стандартная библиотека —
JSON.parse: Твой код вызывает встроенный метод JavaScriptJSON.parse. Среда выполнения запускает код разбора JSON (который находится в C++-исходнике V8, а не в твоём TypeScript). Во время разбора на куче выделяются промежуточные объекты. -
Куча / сборщик мусора: Разобранный результат — объект с полями, возможно вложенными массивами — выделяется на куче средой выполнения. Ты никогда не вызывал
malloc. Среда выполнения нашла блок памяти кучи и поместила туда объект. Когдаusersвыйдет из области видимости, GC в конечном итоге освободит эту память. -
Механизм стека вызовов:
readFileSyncиJSON.parse— оба вызовы функций. Среда выполнения помещала и снимала кадры стека для каждого, управляя адресами возврата и локальными переменными. Среда выполнения также обрабатывает случай, когда внутриreadFileSyncвыбрасывается исключение (файл не найден), разматывая стек вызовов для распространения ошибки к ближайшему блоку try/catch твоего кода.
Ничего из этого не присутствует в твоей одной строке TypeScript. Всё это делает среда выполнения.
Сборщик мусора автоматически освобождает память, которую программа больше не может достичь. В языках без GC (например, C) кто отвечает за освобождение памяти? Введи 1 (программист, вручную) или 2 (ОС, автоматически).
Стек вызовов хранит кадры стека, по одному на каждый активный вызов функции. Каждый кадр содержит локальные переменные функции и адрес возврата. Что сообщает CPU адрес возврата? Введи 1 (куда продолжить после возврата функции) или 2 (имя функции в виде строки).
Когда ты вызываешь 'JSON.parse(text)' в JavaScript, какой программный компонент содержит реальный код разбора JSON? Введи 1 (твой JavaScript-файл) или 2 (среда выполнения JavaScript — V8 или аналог).
Программа запускает «бесконечную рекурсию» — каждый вызов немедленно вызывает себя снова. Какой сервис среды выполнения даёт сбой и вызывает крах? Введи 1 (сборщик мусора) или 2 (стек вызовов).
Даже скомпилированная программа на C имеет среду выполнения. Runtime C выполняет две задачи при запуске до вызова main(). Введи 1, если 'настройка стека и инициализация процесса' является одной из этих задач, или 0, если нет.
Что такое среда выполнения и как твой код соотносится с ней?
Runtime-система (среда выполнения) — это код и механизмы, которые реализация языка предоставляет и запускает рядом с твоей программой. Она обеспечивает четыре главных сервиса: (1) управление памятью — выделение пространства кучи для новых объектов и, в большинстве современных языков, сборщик мусора, который автоматически освобождает память, на которую твой код больше не ссылается; (2) механизм стека вызовов — кадры стека, адреса возврата и соглашения о вызовах, которые обеспечивают корректную работу вызовов и возвратов функций; (3) стандартная библиотека — коллекция встроенных функций и структур данных (ввод-вывод, коллекции, математика, строки), поставляемая с языком; и (4) сам интерпретатор или виртуальная машина для интерпретируемых языков или языков с байт-кодом. Твой код стоит поверх всех этих слоёв. Даже компилируемые языки имеют среды выполнения (runtime C обрабатывает запуск, стандартную библиотеку и настройку стека). Чем богаче среда выполнения, тем больше работы она делает за тебя — ценой некоторого времени CPU и памяти для собственных нужд среды выполнения.