Основы памяти в Java: Куча и Стек

Узнайте, как устроена память в Java: разбор работы Java-стека и кучи, особенностей управления памятью и роли сборщика мусора в данном процессе.

· 7 минуты на чтение

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

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

⚠️
В данной статье не разбирается JMM. Углубиться в эту тему вы можете ознакомившись с дополнительными материалами в конце статьи.
Спонсор поста

Устройство памяти в программировании

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

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

Стек (Stack)

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

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

Куча (Heap)

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

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

Для удобства визуализируем стек и кучу. Серые объекты потеряли свою ссылку из стека, их необходимо удалить, чтобы освободить память для новых объектов.

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

Стек

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

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

0:00
/
Так выглядят фреймы стека в режиме дебага в Idea

Особенности работы

  1. LIFO (Last-In, First-Out): Стек работает по принципу "последним пришел - первым ушел". Это означает, что последняя функция, которая была вызвана, будет первой, которая вернет управление обратно.
  2. Автоматическое управление памятью: Как только функция завершает свою работу, все локальные переменные автоматически удаляются. Это облегчает управление памятью, поскольку нет необходимости явно освобождать память.
  3. Ограниченный размер: Размер стека обычно ограничен. Если программа пытается использовать больше стековой памяти, чем доступно, это вызовет ошибку переполнения стека.
  4. Быстрый доступ: Доступ к данным в стеке обычно быстрее, чем к данным в куче, поскольку стек локализован в памяти и данные из стека могут быть загружены в кэш процессора для быстрого доступа.

Куча

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

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

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

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

Особенности

  1. Динамическое распределение памяти: Куча позволяет программе динамически запросить точное количество памяти, которое ей нужно, и использовать эту память до тех пор, пока она не решит, что больше ее не нужно.
  2. Долговечность данных: Поскольку память не освобождается автоматически, как в стеке, данные в куче могут существовать до тех пор, пока не будут освобождены, что позволяет им пережить вызовы функций и даже полное выполнение программы.
  3. Управление памятью: Работа с кучей требует аккуратного управления памятью. Утечки памяти могут стать проблемой, если программист не освобождает память, когда она больше не нужна.
  4. Медленный доступ: В сравнении со стеком, доступ к данным в куче может быть медленнее из-за отсутствия локализации и больших размеров данных.

В следующем разделе мы углубимся в специфику устройства памяти в Java и рассмотрим особенности работы с памятью в этом языке программирования.

Рандомный блок

Cтек и куча в Java

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

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

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

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

Сборщик мусора

Сборщик мусора (Garbage Collector, GC) - это процесс в виртуальной машине Java (JVM), который автоматически освобождает память, выделенную для объектов, которые больше не используются. Это происходит следующим образом:

  1. Маркировка: Сборщик мусора начинает "маркировать" все объекты в куче, которые больше не доступны из корня вашего приложения. "Корни" вашего приложения обычно включают в себя стек вызова и глобальные ссылки. Если на объект в куче не существует активной ссылки из этих "корней", этот объект считается недоступным.
  2. Удаление: После того, как все недоступные объекты были помечены, сборщик мусора освобождает память, которую они занимали.

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

Пример с кодом

Разберем процесс работы с памятью в Java на простом примере

public class Main {
    public static void main(String[] args) {
        Person person1 = new Person("John");
        Person person2 = new Person("Jane");

        System.out.println("Name of person1: " + person1.getName());
        System.out.println("Name of person2: " + person2.getName());

        swapNames(person1, person2);

        System.out.println("After swapping:");
        System.out.println("Name of person1: " + person1.getName());
        System.out.println("Name of person2: " + person2.getName());
    }

    public static void swapNames(Person p1, Person p2) {
        String temp = p1.getName();
        p1.setName(p2.getName());
        p2.setName(temp);
    }
}

В этом примере мы создаем два объекта Person в методе main(). Эти объекты создаются в куче, и ссылки на них (person1 и person2) хранятся в стеке.

Когда мы вызываем метод swapNames(), происходит следующее:

  1. Ссылки p1 и p2 создаются в стеке. Они указывают на те же объекты Person, на которые указывают person1 и person2. Это потому, что в Java все объекты передаются по ссылке.
  2. Внутри метода swapNames(), мы создаем переменную temp, которая хранится в стеке и содержит значение имени первого объекта Person.
  3. Затем мы меняем имя p1 на имя p2, а затем имя p2 на temp. Это изменяет объекты в куче.

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

Соответственно, в нашем случае, память для переменной temp выделяется сразу при вызове метода swapNames(), даже если фактическое присвоение значения переменной temp происходит позже, на этапе выполнения строки String temp = p1.getName();.

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

Влияние многопоточности в Java на память

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

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

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

Синхронизация в Java служит для контроля доступа нескольких потоков к общим ресурсам. Синхронизация может быть реализована с использованием ключевого слова synchronized или специальных классов, таких как ReentrantLock или Semaphore.

Сборщик мусора в Java работает в многопоточной среде и способен обрабатывать объекты из всех потоков. Однако следует быть внимательным с долгоживущими объектами и ресурсами, которые могут заблокировать сборщик мусора.

Заключение

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

В случае с Java, управление памятью становится еще более интересным благодаря сборщику мусора, который автоматически освобождает неиспользуемую память, и разделению памяти на Heap Space и Stack Space. Эти особенности делают язык Java удобным для работы, но также требуют осознанного использования памяти для предотвращения проблем с производительностью.

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

Дополнительные материалы

Struchkov Mark
Struchkov Mark
Задавайте вопросы, если что-то осталось не понятным👇