Как осуществляется размещение переменных в памяти компьютера

Обновлено: 03.07.2024

Главная задача компьютерной системы – выполнять программы. Программы вместе с данными, к которым они имеют доступ , в процессе выполнения должны (по крайней мере частично) находиться в оперативной памяти . Операционной системе приходится решать задачу распределения памяти между пользовательскими процессами и компонентами ОС. Эта деятельность называется управлением памятью. Таким образом, память ( storage , memory ) является важнейшим ресурсом, требующим тщательного управления. В недавнем прошлом память была самым дорогим ресурсом.

Часть ОС, которая отвечает за управление памятью , называется менеджером памяти.

Физическая организация памяти компьютера

Запоминающие устройства компьютера разделяют, как минимум, на два уровня: основную (главную, оперативную , физическую ) и вторичную (внешнюю) память.

Основная память представляет собой упорядоченный массив однобайтовых ячеек, каждая из которых имеет свой уникальный адрес (номер). Процессор извлекает команду из основной памяти , декодирует и выполняет ее. Для выполнения команды могут потребоваться обращения еще к нескольким ячейкам основной памяти . Обычно основная память изготавливается с применением полупроводниковых технологий и теряет свое содержимое при отключении питания.

Вторичную память (это главным образом диски) также можно рассматривать как одномерное линейное адресное пространство , состоящее из последовательности байтов. В отличие от оперативной памяти , она является энергонезависимой, имеет существенно большую емкость и используется в качестве расширения основной памяти .

Эту схему можно дополнить еще несколькими промежуточными уровнями, как показано на рис. 8.1. Разновидности памяти могут быть объединены в иерархию по убыванию времени доступа, возрастанию цены и увеличению емкости.

Многоуровневую схему используют следующим образом. Информация, которая находится в памяти верхнего уровня, обычно хранится также на уровнях с большими номерами. Если процессор не обнаруживает нужную информацию на i-м уровне, он начинает искать ее на следующих уровнях. Когда нужная информация найдена, она переносится в более быстрые уровни.

Локальность

Оказывается, при таком способе организации по мере снижения скорости доступа к уровню памяти снижается также и частота обращений к нему.

Ключевую роль здесь играет свойство реальных программ, в течение ограниченного отрезка времени способных работать с небольшим набором адресов памяти. Это эмпирически наблюдаемое свойство известно как принцип локальности или локализации обращений.

Свойство локальности (соседние в пространстве и времени объекты характеризуются похожими свойствами) присуще не только функционированию ОС, но и природе вообще. В случае ОС свойство локальности объяснимо, если учесть, как пишутся программы и как хранятся данные, то есть обычно в течение какого-то отрезка времени ограниченный фрагмент кода работает с ограниченным набором данных. Эту часть кода и данных удается разместить в памяти с быстрым доступом. В результате реальное время доступа к памяти определяется временем доступа к верхним уровням, что и обусловливает эффективность использования иерархической схемы. Надо сказать, что описываемая организация вычислительной системы во многом имитирует деятельность человеческого мозга при переработке информации. Действительно, решая конкретную проблему, человек работает с небольшим объемом информации, храня не относящиеся к делу сведения в своей памяти или во внешней памяти (например, в книгах).

Кэш процессора обычно является частью аппаратуры, поэтому менеджер памяти ОС занимается распределением информации главным образом в основной и внешней памяти компьютера. В некоторых схемах потоки между оперативной и внешней памятью регулируются программистом (см. например, далее оверлейные структуры ), однако это связано с затратами времени программиста, так что подобную деятельность стараются возложить на ОС.

Адреса в основной памяти , характеризующие реальное расположение данных в физической памяти , называются физическими адресами. Набор физических адресов, с которым работает программа, называют физическим адресным пространством .

Логическая память

Аппаратная организация памяти в виде линейного набора ячеек не соответствует представлениям программиста о том, как организовано хранение программ и данных. Большинство программ представляет собой набор модулей, созданных независимо друг от друга. Иногда все модули, входящие в состав процесса, располагаются в памяти один за другим, образуя линейное пространство адресов. Однако чаще модули помещаются в разные области памяти и используются по-разному.

Схема управления памятью, поддерживающая этот взгляд пользователя на то, как хранятся программы и данные, называется сегментацией. Сегмент – область памяти определенного назначения, внутри которой поддерживается линейная адресация. Сегменты содержат процедуры, массивы, стек или скалярные величины , но обычно не содержат информацию смешанного типа.

По-видимому, вначале сегменты памяти появились в связи с необходимостью обобществления процессами фрагментов программного кода (текстовый редактор, тригонометрические библиотеки и т. д.), без чего каждый процесс должен был хранить в своем адресном пространстве дублирующую информацию. Эти отдельные участки памяти, хранящие информацию, которую система отображает в память нескольких процессов, получили название сегментов . Память, таким образом, перестала быть линейной и превратилась в двумерную. Адрес состоит из двух компонентов: номер сегмента , смещение внутри сегмента . Далее оказалось удобным размещать в разных сегментах различные компоненты процесса (код программы, данные, стек и т. д.). Попутно выяснилось, что можно контролировать характер работы с конкретным сегментом , приписав ему атрибуты, например права доступа или типы операций, которые разрешается производить с данными, хранящимися в сегменте .


Рис. 8.2. Расположение сегментов процессов в памяти компьютера

Некоторые сегменты , описывающие адресное пространство процесса, показаны на рис. 8.2. Более подробная информация о типах сегментов имеется в лекции 10.

Большинство современных ОС поддерживают сегментную организацию памяти. В некоторых архитектурах (Intel, например) сегментация поддерживается оборудованием.

Адреса, к которым обращается процесс, таким образом, отличаются от адресов, реально существующих в оперативной памяти . В каждом конкретном случае используемые программой адреса могут быть представлены различными способами. Например, адреса в исходных текстах обычно символические. Компилятор связывает эти символические адреса с перемещаемыми адресами (такими, как n байт от начала модуля). Подобный адрес, сгенерированный программой, обычно называют логическим (в системах с виртуальной памятью он часто называется виртуальным) адресом. Совокупность всех логических адресов называется логическим (виртуальным) адресным пространством .

Связывание адресов

Итак логические и физические адресные пространства ни по организации, ни по размеру не соответствуют друг другу. Максимальный размер логического адресного пространства обычно определяется разрядностью процессора (например, 2 32 ) и в современных системах значительно превышает размер физического адресного пространства . Следовательно, процессор и ОС должны быть способны отобразить ссылки в коде программы в реальные физические адреса, соответствующие текущему расположению программы в основной памяти . Такое отображение адресов называют трансляцией (привязкой) адреса или связыванием адресов (см. рис. 8.3).

Связывание логического адреса, порожденного оператором программы, с физическим должно быть осуществлено до начала выполнения оператора или в момент его выполнения. Таким образом, привязка инструкций и данных к памяти в принципе может быть сделана на следующих шагах [Silberschatz, 2002].

Все компиляторы языка программирования генерируют код, который во время выполнения хранится в оперативной памяти. Размещение данных в памяти, чтобы память была зарезервирована в exe-модуле, осуществляется по формуле:

Определение Объявление = инициализация;

Время жизни переменных – это время, в течение которого переменная хранится в определенной области памяти. Время жизни переменных полностью определяется программистом. С понятием время жизни тесно связано понятие видимости (действия) переменных, которое бывает:

  • глобальное, когда в любой момент во время работы с переменной всегда ассоциирована область памяти и обратиться к ней можно из любой точки программы;
  • статическое, когда во время работы с переменной ассоциирована область памяти, но обратиться к ней можно только из определенных точек программы;
  • локальное, когда при каждом входе в блок операторов < >для хранения переменной выделяется область памяти, а при выходе память освобождается и переменная теряет свое значение.

Для управления статическим размещением переменных в памяти, их временем жизни и областью видимости язык программирования C++ предоставляет понятие классов памяти и ряд ключевых слов модификаторов типа:

  1. extern используется для определения глобальных переменных. Память для переменных extern распределяется постоянно. Такая переменная глобальна для всех функций и доступна в любой точке программы. Значение переменной всегда сохраняется.
  2. static используется для определения статических переменных. Статические определения используются в случае если необходимо сохранять предыдущее значение при повторном входе в блок операторов < >. Такая переменная инициализируется единственный раз, когда программа встречает ее объявление.
  3. auto используются для определения автоматических переменных. Переменные определенные внутри функции или блока операторов < >по умолчанию являются автоматическими. При входе в блок < >программа автоматически располагает переменную в сегменте стека. При выходе из блока память освобождается, а значения теряются.
  4. register используются для определения регистровых переменных. Переменные с классом памяти register, ассоциируются со скоростными регистрами памяти процессора. Но это не всегда возможно, поэтому часто компилятор преобразует такие переменные к классу auto.

Переменные extern и static явно не инициализированные программистом устанавливаются системой в нуль. Переменные auto и register не инициализируются и могут содержать "мусор".

Глобальные переменные создаются путем размещения их объявлений за пределами определений любых классов или функций. Глобальные переменные сохраняют свои значения в течение всего выполнения программы. На глобальные переменные и функции может ссылаться любая функция, которая расположена в исходном файле после их объявления или описания.

Идентификаторы, объявленные внутри блока, имеют область действия блока. Область действия блока начинается от объявления идентификатора и заканчивается завершающей правой фигурной скобкой блока, в котором идентификатор объявляется. Локальные переменные функции имеют область действия блока, как и параметры функции, которые также являются ее локальными переменными.

Если блоки вложены, и идентификатор во внешнем блоке имеет такое же имя, как идентификатор во внутреннем блоке, идентификатор внешнего блока «скрыт» до момента завершения работы внутреннего блока. Это означает, что пока выполняется внутренний блок, он «видит» значение своего собственного локального идентификатора, а не значение идентификатора с тем же именем в охватывающем блоке.

Программа демонстрирует области действия глобальных переменных, автоматических локальных переменных и статических локальных переменных.

Константные переменные

Ключевое слово сonst означает что переменная инициализируется один раз после объявления и ее значение не модифицируемое:

Мы продолжаем рассматривать операционные системы со всеми их проблемами и техническими подробностями. В этой статье поговорим о динамических переменных и менеджере памяти. В прошлых статьях мы уже успели узнать, что каждая запущенная программа формируется в четкое описание, называемое процессом . В состав этого описания входят участки памяти для хранения исполняемых машинных инструкций, глобальных переменных, а также участок памяти, выделенный под стек . В логическом адресном пространстве они соседствуют друг с другом, однако в физической памяти это не обязательно так. Каждый из сегментов памяти разбит на страницы, которые могут быть разбросаны по физической памяти в соответствии с пожеланиями операционной системы. Такой состав сегментов памяти не учитывает одну ситуацию. Не всегда можно заранее знать какой объем данных необходимо разместить в памяти для обработки.

Зачем нужны динамические переменные?

Очень большая доля данных поступает на обработку уже в тот момент, когда приложение обработки уже запущено и работает. Объемы данных, которые поступят в будущем неизвестны, поэтому память для размещения этих объемов выделяется в процессе работы. Иными словами выделяется динамически. Например, при обработке цифровых снимков их размер может быть заранее неизвестен.

Есть снимки большого размера, есть малые. Это становится совершенно не важно с появлением еще одного очень значимого механизма операционной системы, называемого управление памятью . Это самое управление занимается тем, что выделяет память по запросу приложения. Если бы процесс был один, то думать было бы нечего. Просто можно было отдать все что есть, но в многозадачной операционной системе так поступать нельзя. Такой ценный ресурс как память может понадобиться любому процессу в абсолютно любой момент времени. Такой важный вопрос как распределение памяти между различными процессами находится в ведении операционной системы.

Как получить дополнительное место для хранения данных?

Итак, приложение, нуждающееся в дополнительном месте для размещения новых данных отправляет запрос операционной системе. Библиотека языка Си имеет для этого функцию malloc() .

Параметром является необходимое количество байт и возвращает она адрес начала выделенного участка. Участок памяти выделяется из специального места. Называется оно куча . Звучит не очень, но так уж получилось. Абсолютно ничего не мешает выделить место и для второго снимка, лишь бы хватило места.

При этом в куче из незанятого пространства выделяется область, предназначенная для хранения еще одной порции данных. Адрес начала вновь выделенного участка попадает в объявленный указатель. Имея указатели на выделенные участки, можно как помещать туда информацию, так и считывать ее оттуда. Логично предположить, что эти участки выделены внутри адресного пространства процесса, иначе бы так просто обращаться к ним было бы невозможно.

Где расположена «куча»?

Место под кучу выделяется в адресном пространстве процесса. Незанятое пространство между глобальными данными и стеком используется для размещения там динамически выделяемых участков памяти по запросу самого процесса.

Красным цветом на схеме выделена та самая куча. В данном примере показано, как для данных выделяется место в куче, а указатели на выделенные участки памяти являются данными стекового фрейма функции.

Это и хорошо и плохо. Смотря какая квалификация у программиста. После завершения функции, как мы уже знаем , стековые переменные могут быть перезаписаны переменными другой функции и это лишь вопрос времени и вероятности. Но давайте о проблемах чуть позже. Правильным действием тут является отправка запроса на освобождение выделенной ранее памяти. Освобождением занимается функция free() с параметром указатель на ранее выделенную область. Сделаем все правильно чтоб немного рассмотреть работу менеджера памяти.

Технические подробности

Манипуляции с регистрами

Первым делом при запуске процесса необходимо выделить хотя бы немного места под хранения динамических переменных. Делается это путем занесения в служебные регистры необходимых данных.

Как мы ранее выяснили, часть регистров заняты выполнением своих особенных задач. Адресные регистры ( PAR0 - PAR15 ) учебного процессора занимаются размещением в физической памяти страниц из логического адресного пространства процесса. Для определения полного физического адреса ячейки памяти необходимо младшие 12 бит логического адреса сложить с содержимым адресного регистра , сдвинутого на 8 бит влево.

Старшие 4 бита логического адреса определяют номер страницы памяти и соответственно номер адресного регистра с которым необходимо проводить сложение. Задачей менеджера является во-первых, выделить новые страницы под кучу. А это информация кроме как где она будет физически находиться, еще и атрибуты этих страниц памяти. В атрибутах есть информация о размере страниц памяти и правах на чтение и запись. Эти атрибуты позволяют выявить аварийную ситуацию, при некорректной работе программы. Аварийная ситуация это сигнал, позволяющий операционной системе удалить процесс нарушитель из очереди задач и сообщить об этом пользователю.

Обращение к системному менеджеру памяти

Теперь рассмотрим как запрос прикладного приложения пользователя добирается до менеджера памяти. Системный менеджер памяти представляет собой функции ядра операционной системы. Работа с памятью в условиях многозадачности это весьма интеллектуальная задача и ее нельзя доверять программисту с непонятным уровнем квалификации. Поэтому все что остается прикладному программисту это сделать простой запрос. Указать сколько байт потребовалось и куда положить адрес выделенного участка памяти. Функция malloc() является частью стандартной библиотеки, поэтому выполняется в пространстве пользователя. В свою очередь, эта функция обращается к прикладному интерфейсу операционной системы, где представлена другая функция, обладающая целым набором параметров ( mmap ).

Этим набором можно указать кроме необходимого количества байт пространства еще и атрибуты участка памяти, касающиеся чтения и записи и при желании другую необходимую информацию. Эта API функция, в свою очередь, вызывает программное прерывание, сопровождающееся переключением контекста пользователя в контекст ядра операционной системы. В контексте ядра вызывается функция менеджера памяти, которая в куче процесса занимается поиском свободного пространства необходимого размера, необходимыми отметками о том, что память занята и возвратом адреса начала выделенного участка.

Как мы ранее выяснили , переключение контекста и довольно длинная цепочка вызовов приводят к существенным задержкам, так что лучше снижать число обращений за памятью.

Менеджер памяти прикладного приложения

Теперь о том, как снижается число обращений к операционной системе с целью выделения памяти из кучи. Здесь все просто, один раз оптовой партией берем большой кусок памяти и долго потом не обращаемся к операционной системе.

Просто и одновременно сложно. В этом большом фрагменте теперь нам самим прийдется искать свободные участки необходимого размера и вести учет занятых и незанятых участков памяти. Такие функции выполняют роль менеджера памяти, но уже не в пространстве операционной системы , а в пространстве пользовательского приложения. Это снизит число обращений к операционной системе. Они нужны будут только для того, чтобы еще увеличить размер выделенной области если та что была уже закончилась. Необходимо отметить, что поиск фрагментов нужного размера, возвращение этих фрагментов обратно и весь связанный с этим учет все равно довольно трудоемкая работа, требующая большое количество процессорного времени.

Проблемы менеджеров памяти

Утечка памяти

Настало время поднять вопрос о самых распространенных проблемах менеджеров памяти. Менеджеры работают сами по себе, программисты сами по себе. Согласованности зачастую нет никакой. Если программист выделил себе место под данные, а потом забыл вернуть, то область памяти так и останется висеть в списке занятых, хоть и не осталось в программе ни одного места, нуждающегося в существовании этого участка памяти. Эта проблема называется утечкой памяти.

Программа забирает память у операционной системы и не возвращает. Если это происходит циклически и часто, то в итоге программа забирает всю свободную память, что приводит к нарушению нормальной работы операционной системы и чаще всего к необходимости ее перезагрузки. В этом примере функция берет в пользование 10 байт, после окончания функции стековая переменная с адресом выделенного участка исчезает. Подвешенный участок памяти остается. Менеджер не тратит время процессора на поиски таких проблем, память утекает. Десятилетиями лучшие умы человечества пытаются решить эту проблему и найдены по крайней мере два направления.

Решения проблем утечки

Первое это как ранее уже озвучено, ведение учета занятых областей памяти и поиск ссылок на такие области. Если ссылки еще есть, то значит кому-то это нужно и занятое пространство не освобождается. Но если никто уже не ссылается, то занятое пространство возвращается в кучу обратно как свободный участок. Такой механизм называется сборкой мусора . Она происходит циклически по расписанию или по запросу приложения. Из популярных языков, имеющих в своем вооружении сборщик мусора можно выделить Java , CSharp , JavaScript , Go .

Сборщик мусора выявляет, что никто больше не владеет выделенным участком памяти и он будет возвращен в кучу как незанятый. Сборка мусора это довольно затратный процесс, снижающий быстродействие программы.

Одним из новых языков программирования, сменившим традиционный подход к управлению памяти является Rust .

В нем основу работы с динамически выделяемой памятью составляет такое понятие как область видимости. Сам факт выхода стековой переменной из области видимости при окончании функции должен быть сигналом к немедленному возврату выделенной памяти.

Необходимо отметить, что существуют гораздо более сложные случаи, требующие некоторого напряжения ума программиста. Напряжения только потому, что компилятор языка Rust не позволяет программисту собрать приложение пока существуют хоть сколько нибудь вероятно опасные строки исходного кода. Опасные это в смысле могущие привести к некорректному результату. Еще такая безопасность касается многопоточной работы, поэтому всяких тонкостей, которые видит компилятор в исходном коде может быть довольно большое количество. В общем, язык этот не так прост, но считается очень производительным и безопасным одновременно. Давайте перейдем к следующей проблеме.

Фрагментация памяти

А другая проблема менеджеров это фрагментация памяти . Такое явление, при котором появляются неиспользуемые фрагменты памяти в общем блоке. При активной работе менеджера их набирается изрядное количество. При освобождении какого-то участка памяти он помечается как незанятый. Но его специфический размер зачастую не подходит для занятия другими блоками данных.

Так происходит непрерывный процесс накопления неиспользуемых фрагментов. Это конечно же приводит к запросу у операционной системы еще одного большого блока данных. Казалось бы, можно взять и переупаковать занятые участки таким образом, чтобы убрать промежутки, но это только на словах звучит просто. Во первых, перемещения областей памяти это затратно по времени, да еще и указатели на области памяти не должны быть изменены. Никто не даст гарантию того, что большое число копий указателя еще не было скопировано в разные части программы. Перемещение фрагментов должно приводить и к смене содержимого указателей. А это уже совсем другая история и вероятно игра не стоит свеч. Вот в этот самый момент становится понятной фраза о том, что

любая программа стремиться занять всю доступную память.

Пишите качественные программы, думайте о том, что происходит внутри и тем самым мы приостановим обрушение лавины посредственности в наш и так не самый совершенный мир.


У каждого компьютера есть оперативная память . Что же это такое, какими свойствами обладает и, самое главное, какая нам от этого польза?

Каждая программа (в том числе и программы , написанные на Java) перед выполнением загружается в оперативную память . В оперативной памяти находится код программы (который исполняется процессором) и данные программы (которые в память помещает сама программа).

Что же такое оперативная память и на что она похожа?


Процессор умеет исполнять команды из загруженной в память программы. Почти все команды процессора — это что-то типа: взять данные из некоторых ячеек → сделать с ними что-то → результат поместить в другие ячейки

Объединяя сотни простых команд, мы получаем сложные и полезные команды.

Когда в коде программы объявляется переменная, ей выделяется кусочек ещё не использованной памяти . Обычно это несколько байт. При объявлении переменной обязательно нужно указать тип информации, которую программа будет хранить в ней: числа, текст, или другие данные. Ведь не зная тип информации, не ясно, какого размера блок памяти нужно выделить под переменную.

На заре компьютерной отрасли программы работали просто с номерами ячеек памяти, но потом для удобства программистов ячейкам стали давать имена. Уникальное имя переменной — это в первую очередь для удобства программистов: программа во время работы отлично справилась бы и с номерами.

2. Переменные в памяти

Всего в Java есть 4 типа данных для хранения целых чисел. Это byte , short , int и long .

Тип Размер, байт Происхождение имени
byte 1 Byte , т.к. занимает один байт памяти
short 2 Сокращение от Short Integer
int 4 Сокращение от Integer
long 8 Сокращение от Long Integer

Также в Java есть 2 вещественных типа — float и double:

Тип Размер, байт Происхождение имени
float 4 Сокращение от Floating Point Number
double 8 Сокращение от Double Float

Каждый раз, когда выполнение программы доходит до команды создания переменной, ей выделяется небольшая область памяти (размер зависит от типа переменной).

Адресом переменной считается адрес первой ячейки выделенного под нее блока памяти.

Java-программам запрещено напрямую обращаться к памяти. Вся работа с памятью происходит только через Java-машину.

3. Тип String в памяти

Тип String может хранить большие объемы данных, поэтому это не просто тип данных, а полноценный класс.

Сами данные типа String (текст) помещаются в специальный объект, под который выделяется память, а уже адрес этого объекта помещается в переменную, под которую тоже выделяется память.


Переменная a типа int занимает 4 байта и хранит значение 1 .

Переменная b типа int занимает 4 байта и хранит значение 10,555 . Запятая - это не дробная часть числа, а разделение разрядов. Дробная часть отделяется точкой

Переменная d типа double занимает 8 байт и хранит значение 13.001 .

Переменная str типа String занимает 4 байта и хранит значение G13 — адрес первой ячейки объекта, содержащего текст.

Объект типа String (содержащий текст) хранится отдельным блоком памяти. Адрес его первой ячейки хранится в переменной str .

4. Почему в программировании всё нумеруют с нуля

Люди очень часто удивляются, почему в программировании почти везде принято считать с нуля. Дело в том, что есть очень много ситуаций, когда считать с нуля удобнее (хотя есть и ситуации, когда удобнее считать с 1 ).

Когда мы думаем об относительном адресе внутри какого-либо блока данных, всегда получаем нумерацию с нуля. Это и есть первая и самая распространенная причина счета с нуля .

Читайте также: