Устройство памяти компьютера

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

План урока

  1. Память — важнейший ресурс
  2. Жизненный цикл объектов и переменных
  3. «Область видимости» переменных

Зачем программисту знать, как устроена память?

Вспомним нашу метафору с дорогой. Программа — дорога, которую мы построили для обработчика-машинки Ruby.

Иллюстрация метафоры с дорогой

До этого момента мы сосредотачивались только на результате: «Доехать до места назначения», нас не волновало, сколько топлива мы потратим, сколько покрышек сотрём и как износится наш двигатель.

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

Оптимальное использование памяти и процессора, в конце концов, влияет на качество вашей программы, на её скорость, а значит и на удовольствие её пользователей. Если ваша программа работает медленно, её в конце концов выкинут и будут пользоваться другими.

Как компьютер выделяет память под программы?

Вы, наверное, не раз слышали что-то типа «У Сани восемь гигов оперативы, а у Лёши всего два». Вот эта вот «оператива» или оперативная память — устройство внутри вашего компьютера, которое выполняет функцию оперативного хранения данных во время работы вашего компьютера. Объём оперативной памяти может быть разным у разных компьютеров и от него зависит, сколько информации компьютер может «держать в уме».

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

В любой момент вы можете нажать Ctrl+Alt+Delete (в Windows) и запустить «Диспетчер задач» (в Mac OS X программа называется «Activity Monitor» — Монитор активности), чтобы посмотреть, какие программы занимают в данный момент память вашего компьютера:

Диспетчер задач Windows

Если программ слишком много или они занимают слишком много памяти, то память процессора кончается. Тогда часть данных начинает сохраняться на специальный раздел жёсткого диска компьютера, который называется swap (от английского слова «менять, обменивать») и восстанавливаться оттуда при необходимости. Этот процесс (чтение/запись с жёсткого диска) очень медленный (в несколько раз медленнее, чем чтение/запись в оперативную память), поэтому говорят, что компьютер «залез в своп» — начал тормозить из-за нехватки оперативной памяти.

Чем программа занимает память?

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

Как-то так наши переменные занимают место в памяти компьютера

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

Иллюстрация занимаемой программой памяти

Давайте для примера напишем простенькую программу. Создайте в RubyMine новый проект winnie_the_pooh и создайте в нём файл pooh.rb:

# Винни Пух лег спать и пытается заснуть...
puts "Winnie the Pooh is trying to fall asleep..."

# Чтобы упростить засыпание, он считает горшочки с медом
honeypots = []  # объявим пустой массив, куда будут собираться все горшочки

# Надо насчитать по меньшей мере пять миллионов горшочков!
# Эта конструкция создаст цикл, который повторится 5 млн. раз и на каждом повторении
# внутри цикла будет переменная i — с номером текущей итерации цикла.
5000_000.times do |i|
  honeypots << "Honeypot ##{i}" # добавляем в массив строки 'Горшочек 1', 'Горшочек 2' и т. д.
end

puts "Now check your memory!"
# теперь программа ждет ввода из консоли, но вместо ввода мы пойдем и посмотрим
# в диспетчере задач — сколько памяти съела наша программа
gets

Запустите программу и пока она ждёт от нас ввода данных, откройте диспетчер задач (Ctrl+Alt+Delete в Windows или запустите Activity Monitor в Mac OS X) и посмотрите, сколько памяти занимает наша программа:

Acitity Monitor в Mac OS X

Диспетчер задач в Windows 7

Программа "Медведь" на Windows 8

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

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

Жизненный цикл объекта

Когда конкретно объекты создаются и начинают занимать место в памяти, и когда они умирают?

Когда мы вызываем у класса конструктор new и записываем результат в какую-то переменную, объект создаётся.

a = MyClass.new

Теперь у нас есть переменная a и объект класса MyClass, на который она указывает.

Обратите внимание, на один объект может указывать несколько переменных:

b = a

Теперь у нас есть ещё и переменная b, которая также указывает на всё тот же объект класса MyClass.

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

a = nil
b = nil

Теперь обе переменные a и b указывают на nil (можно считать, ни на что не указывают), а объект класса MyClass в памяти всё равно остался. Если подумать, то выяснится, что он для нас потерян. Мы никак не сможем к нему обратиться, т.к. на него нет ни одного доступного нам указателя (переменной).

В компилируемых языка типа C программист должен сам следить за тем, чтобы таких мусорных объектов в памяти не оставалось. Если вы программист на C, нельзя просто так перекидывать ярлычки.

Руби же (а также Java или python) обладают встроенным «сборщиком мусора»: это избавляет вас от необходимости постоянно думать о мусорных объектах: вы просто зануляете переменные и объекты рано или поздно сами будут очищены из памяти.

Итого жизненный цикл объекта (вкратце) таков:

  1. Объект создан с какими-то ссылками на него
  2. В процессе работы программы ссылки на него исчезли
  3. Объект удаляется из памяти сборщиком мусора

Жизненный цикл объекта

Чтобы освободить память в нашей программе с горшочками, достаточно в любой момент написать

honeypots = nil

и дождаться, пока сборщик мусора удалит ненужные горшочки.

Область видимости переменной

Переменная — это указатель на область памяти, где находится какой-то объект.

a = MyClass.new

Она возникает как только объявлена и даже может изначально ни на что не указывать

b = nil

Жизненный цикл переменной сильно зависит от её области видимости (по англ. scope). Грубо говоря, область видимости переменной можно отнести к одному из двух типов «локальная» или «глобальная». Переменные с локальной областью видимости называются «локальными», переменные с глобальной областью видимости называются «глобальными».

Глобальная область видимости (глобальные переменные)

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

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

Просто чтоб вы знали, как создать глобальную переменную напишем:

$a = 1 # Объявили глобальную переменную $a

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

Локальная область видимости (локальные переменные)

Каждый метод создаёт внутри себя уникальную область видимости. Любая переменная, объявленная между def method и end будет видна только внутри этого метода method. Именно поэтому такие переменные называются локальными.

puts a # будет ошибка, такой переменной нет

def method
  a = 'local variable' # объявили локальную переменную a
  puts a # внутри метода можно пользоваться переменной
end

method # выведет в консоль строку 'local variable' 

puts a # так снова будет ошибка, такой переменной нет

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

«Классовая» область видимости (область видимости полей класса)

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

Class MyClass
  def initialize
    @a = 1
  end

  def puts
    puts @a
  end
end

Напомним, что поля класса называются «instance variables» и ими также можно и нужно пользоваться для того, чтобы ваши переменные были там, где им положено. Там, где вы их планируете использовать.

Жизненный цикл переменной

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

Жизненный цикл переменной

Если же на объекты сохраняются ссылки, например, когда метод передаёт ссылку на такой объект с помощью конструкции return — они остаются, т.к. они ещё доступны программисту. Сборщик мусора тогда их не трогает.

Пока существует класс, существуют и объекты, на которые указывают его поля, хотя иногда они бывают недоступны программисту напрямую.

Область видимости ruby в методе

Давайте посмотрим как удаляется объект, когда переменная вышла из области видимости. Выделим в нашей программе pooh.rb подсчёт горшочков в отдельный метод count_honeypots:

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

# Метод, внутри которого идет подсчет горшочков с медом
def count_honeypots
  honeypots = []

  5000_000.times do |i|
    honeypots << "Honeypot ##{i}"
  end

  # ОБРАТИТЕ внимание — переменная honeypots объявлена и используется только
  # внутри этого метода, она НЕ является параметром метода, НЕ используется
  # в качестве возвращаемого значения.
  #
  # Это значит, что "область видимости" переменной honeypots ограничена методом
  # count_honeypots.
end

# Винни Пух лег спать и пытается заснуть...
puts "Winnie the Pooh is trying to fall asleep..."

# вызвали метод, внутри которого посчитали 5 млн. горшочков
count_honeypots

# задержка, чтобы успеть увидеть изменение памяти в диспетчере задач
sleep 3

# сказали Руби специальным методом "запусти сборку ненужных объектов"
GC.start

# задержка, чтобы успеть увидеть изменение памяти в диспетчере задач
sleep 3

puts "Now check your memory!"

# теперь программа ждет ввода из консоли, но вместо ввода мы пойдем и посмотрим
# в диспетчере задач — сколько памяти съела наша программа
gets

# После вызова метода  count_honeypots все объекты этого метода
# "снаружи" этого метода не видны и больше не нужны (метод то уже выполнен)
#
# Интерпретатор Руби понимает это и постарается почистить память от уже не нужных
# горшочков как только памяти в системе начнет не хватать.
# Мы вызвали метод GC.start, чтобы подтолкнуть Руби сделать это сразу.
#
# А в языке C например, программисту нужно было бы самому чистить память
# в подобных случаях. Пользуясь специальными методами.
#
# ПС: в зависимости от версии Руби и операционной системы — память может убираться не сразу,
# а через какое–то время. Или убираться только небольшой процент памяти.
#
# Когда запускаете программу, подождите немного и проследите в
# диспетчере задач (монитор активности или activity monitor в Mac OS) как меняется
# потребление памяти процессом ruby.

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

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

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