Стек и куча
Структура памяти в Java достаточно сложна, но на начальном этапе обучения изучим две ее области: стек (stack) и куча (heap).
1. Куча
Java Heap (куча) используется Java Runtime для выделения памяти под объекты и JRE классы. Создание нового объекта также происходит в куче. Здесь работает сборщик мусора: освобождает память путем удаления объектов, на которые нет каких-либо ссылок.
Любой объект, созданный в куче, имеет глобальный доступ и на него могут ссылаться из любой части приложения.
2. Стек
Что такое стек? Стековая память в Java работает по схеме LIFO (Последний-зашел-Первый-вышел). Всякий раз, когда вызывается метод, в памяти стека создается новый блок, который содержит примитивы и ссылки на другие объекты в методе. Как только метод заканчивает работу, блок также перестает использоваться, тем самым предоставляя доступ для следующего метода.
Размер стека намного меньше объема памяти в куче.
Всякий раз, когда создается объект, он всегда хранится в куче, а в памяти стека содержится ссылка на него. Память стека содержит только локальные переменные примитивных типов и ссылки на объекты в куче.
Следующая картинка отображает разницу в создании примитивной и ссылочной переменной. Если создается примитивная локальная переменная int a = 10 , то ее значение хранится в стеке. При создании же ссылочной переменной Test a = new Test() , сам объект создается в куче, а переменная Test a — в стеке. Значение переменной Test a — это ссылка на объект в куче.
- Процедурное и объектно-ориентированное программирование
- Принципы ООП
- Классы и объекты
- Конструктор
- Ключевое слово this
- Перегрузка
- Передача объектов в методы
- Java varargs
- Рекурсия
- Сборщик мусора и метод finalize
- Наследование
- Ключевое слово super
- Модификаторы доступа
- Геттеры и сеттеры
- Переопределение методов
- Абстрактные классы и методы
- Ключевое слово final
- Задания
Основы памяти в Java: Куча и Стек
Узнайте, как устроена память в Java: разбор работы Java-стека и кучи, особенностей управления памятью и роли сборщика мусора в данном процессе.
2 авг. 2023 · 7 минуты на чтение
Мы всё активнее применяем современные языки программирования, обеспечивающие нам возможность писать минимум кода для решения проблем. Возьмём за пример Java: он был разработан с целью максимального упрощения жизни программистов. Вам не приходится заботиться о памяти — ваши мысли целиком направлены на решение бизнес-задач. Однако это облегчение имеет свою цену.
В прошлой статье, мы обсудили общее устройство памяти в компьютере. А в этой статье мы сосредоточимся на том, как язык Java работает с памятью. Это включает в себя обзор стека и кучи в контексте Java, а также их отличия от общего представления. Мы также немного обсудим роль сборщика мусора в управлении памятью.
В данной статье не разбирается JMM. Углубиться в эту тему вы можете ознакомившись с дополнительными материалами в конце статьи.
Спонсор поста
Устройство памяти в программировании
Переходим к анализу ключевых концепций программирования, связанных с управлением памятью. Сначала обсудим стек и кучу — два фундаментальных механизма, лежащих в основе управления памятью. Затем рассмотрим их взаимодействие и особенности работы в процессе выполнения функций и методов.
Стек и куча представляют собой области памяти, используемые программами для хранения данных во время выполнения, но они используются по-разному и для разных целей.
Стек (Stack)
Стек — это область памяти, где функции хранят свои переменные и информацию для выполнения. Представьте стек как физическую стопку подносов в ресторане: вы можете добавить поднос сверху (push) или взять верхний поднос (pop). Аналогично, когда функция вызывается, ее локальные переменные и информация о вызове кладутся на стек сверху, и забираются сверху (уничтожаются), когда функция завершает работу.
Стек обеспечивает быстрый доступ к данным и автоматическое управление памятью, но имеет ограниченный размер. Если ваша программа использует больше стековой памяти, чем доступно, программа может завершиться с ошибкой переполнения стека.
Куча (Heap)
Куча — это область памяти, где данные могут быть размещены динамически во время выполнения программы. В отличие от стека, где данные удаляются автоматически после выхода из функции, данные в куче остаются, пока их явно не удалить.
Это делает кучу идеальным местом для хранения данных, которые должны пережить вызов функции, или для работы с большими объемами данных. Однако работа с кучей требует аккуратного управления: если вы не удаляете объекты, когда они больше не нужны, может произойти утечка памяти, что в конечном итоге может привести к исчерпанию доступной памяти.
Для удобства визуализируем стек и кучу. Серые объекты потеряли свою ссылку из стека, их необходимо удалить, чтобы освободить память для новых объектов.

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

Синхронизация в Java служит для контроля доступа нескольких потоков к общим ресурсам. Синхронизация может быть реализована с использованием ключевого слова synchronized или специальных классов, таких как ReentrantLock или Semaphore .
Сборщик мусора в Java работает в многопоточной среде и способен обрабатывать объекты из всех потоков. Однако следует быть внимательным с долгоживущими объектами и ресурсами, которые могут заблокировать сборщик мусора.
Заключение
В контексте программирования, стек и куча играют различные, но важные роли. Стек служит для хранения информации о функциях и их вызовах, в то время как куча используется для динамического выделения памяти.
В случае с Java, управление памятью становится еще более интересным благодаря сборщику мусора, который автоматически освобождает неиспользуемую память, и разделению памяти на Heap Space и Stack Space. Эти особенности делают язык Java удобным для работы, но также требуют осознанного использования памяти для предотвращения проблем с производительностью.
В заключение, управление памятью — это сложная, но неотъемлемая часть работы с компьютерами и программным обеспечением. Понимание основ и принципов управления памятью может помочь разработчикам создавать более эффективные и надежные приложения.
Дополнительные материалы
- Habr: Глубокое погружение в Java Memory Model
- Habr: Откуда растут ноги у Java Memory Model
Как управлять памятью в Java
Узнайте об управлении памятью в Java: сборке мусора, разделении памяти на области и ключевых словах для оптимизации приложений!
Алексей Кодов
Автор статьи
9 июня 2023 в 16:31
Управление памятью является важной частью работы Java-разработчика, так как позволяет оптимизировать производительность и стабильность приложения. В этой статье мы рассмотрим основные аспекты управления памятью в Java, такие как сборка мусора, разделение памяти на области и использование ключевых слов для работы с памятью.
Сборка мусора (Garbage Collection)
Сборщик мусора в Java автоматически удаляет неиспользуемые объекты из памяти, освобождая ресурсы для других частей приложения. Это процесс, который исполнительная система Java или JVM (Java Virtual Machine) выполняет в фоновом режиме, чтобы освободить память от объектов, которые больше не используются программой.
Пример использования сборки мусора:
public class GarbageCollectionExample < public static void main(String[] args) < String str = new String("Hello, world!"); str = null; // Делаем ссылку на объект null, что позволяет сборщику мусора удалить его из памяти System.gc(); // Вызываем сборку мусора вручную (хотя она и так произойдет автоматически) >>
Разделение памяти на области
В Java память разделена на две основные области: кучу (heap) и стек (stack). Куча используется для хранения объектов, созданных с помощью оператора new , а стек используется для хранения примитивных типов данных и ссылок на объекты.
Пример разделения памяти на кучу и стек:
public class MemoryExample < public static void main(String[] args) < int x = 5; // Переменная x хранится в стеке Integer y = new Integer(5); // Объект Integer y хранится в куче, а ссылка на него - в стеке >>
Использование ключевых слов для работы с памятью
В Java существуют ключевые слова, которые помогают управлять памятью, такие как static , final и transient .
- static : статические переменные и методы принадлежат классу, а не объекту, и хранятся в разделе памяти, называемом PermGen (Permanent Generation). Они существуют в единственном экземпляре и доступны для всех объектов данного класса.
- final : константные переменные, которые не могут быть изменены после инициализации. Они могут помочь оптимизировать использование памяти и упростить код.
- transient : переменные, которые не участвуют в процессе сериализации (преобразования объекта в последовательность байтов). Это может быть полезно, если вам не нужно сохранять некоторые данные между сеансами работы с приложением.
public class KeywordsExample < public static int staticVar = 0; // Статическая переменная public final int finalVar = 1; // Константная переменная public transient int transientVar = 2; // Переменная, которая не сериализуется >
Java-разработчик: новая работа через 11 месяцев
Получится, даже если у вас нет опыта в IT

Заключение
Управление памятью в Java включает в себя ряд важных аспектов, таких как сборка мусора, разделение памяти на области и использование ключевых слов для работы с памятью. Освоение этих аспектов поможет вам оптимизировать производительность и стабильность вашего приложения. Не забывайте практиковаться и изучать новые материалы, чтобы стать успешным Java-разработчиком!
Память в JVM
Как ты уже знаешь, JVM запускает Java-программы внутри себя. Как и любая виртуальная машина, она имеет собственную систему организации памяти.
Схема организации внутренней памяти указывает на принцип работы вашего Java-приложения. Таким образом можно определить узкие места в работе приложений и алгоритмов. Давай разберемся, как она устроена.

Важно! Модель Java в первоначальном виде была недостаточно хороша, поэтому она была пересмотрена в Java 1.5. Эта версия используется по сей день (Java 14+).
Стек потока
Модель памяти в Java, используемая внутри JVM, делит память на стеки потоков (thread stacks) и кучу (heap). Посмотрим на Java-модель памяти, логично разделенную на блоки:

Все потоки, работающие в JVM, имеют свой стек. Стек в свою очередь держит информацию о том, какие методы вызвал поток. Я буду называть это “стеком вызовов”. Стек вызовов возобновляется, как только поток выполняет свой код.
Стек потока содержит в себе все локальные переменные, требующиеся для выполнения методов из стека потока. Поток может получить доступ только к своему стеку. Локальные переменные не видны остальным потокам, только потоку, создавшему их. В ситуации, когда два потока выполняют один и тот же код, они оба создают свои локальные переменные. Таким образом, каждый поток имеет свою версию каждой локальной переменной.
Все локальные переменные примитивных типов ( boolean , byte , short , char , int , long , float , double ) полностью хранятся в стеке потоков и не видны другим потокам. Один поток может передать копию примитивной переменной другому потоку, но не может совместно использовать примитивную локальную переменную.
Куча (heap)
Куча содержит все объекты, созданные в вашем приложении, независимо от того, какой поток создал объект. К этому относятся и обертки примитивных типов (например, Byte , Integer , Long и так далее). Неважно, был ли объект создан и присвоен локальной переменной или создан как переменная-член другого объекта, он хранится в куче.
Ниже диаграмма, которая иллюстрирует стек вызовов и локальные переменные (они хранятся в стеках), а также объекты (они хранятся в куче):

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

Два потока имеют набор локальных переменных. Local Variable 2 указывает на общий объект в куче ( Object 3 ). Каждый из потоков имеет свою копию локальной переменной со своей ссылкой. Их ссылки являются локальными переменными и поэтому хранятся в стеках потоков. Тем не менее, две разные ссылки указывают на один и тот же объект в куче.
Обрати внимание, что общий Object 3 имеет ссылки на Object 2 и Object 4 как переменные-члены (показано стрелками). Через эти ссылки два потока могут получить доступ к Object 2 и Object 4.
На диаграмме также показана локальная переменная ( Local variable 1 из methodTwo ). Каждая ее копия содержит разные ссылки, которые указывают на два разных объекта ( Object 1 и Object 5 ), а не на один и тот же. Теоретически оба потока могут обращаться как к Object 1 , так и к Object 5 , если они имеют ссылки на оба этих объекта. Но на диаграмме выше каждый поток имеет ссылку только на один из двух объектов.
Пример взаимодействия с объектами
Давай посмотрим, как мы можем продемонстрировать работу в коде:
public class MySomeRunnable implements Runnable() < public void run() < one(); >public void one() < int localOne = 1; Shared localTwo = Shared.instance; //… делаем что-то с локальными переменными two(); >public void two() < Integer localOne = 2; //… делаем что-то с локальными переменными >>
public class Shared < // храним инстанс на наш объект в переменной public static final Shared instance = new Shared(); // переменные-члены, указывающие на два объекта в куче public Integer object2 = new Integer(22); public Integer object4 = new Integer(44); >
Метод run() вызывает метод one() , а one() в свою очередь вызывает two() .
Метод one() объявляет примитивную локальную переменную ( localOne ) типа int и локальную переменную ( localTwo ), которая является ссылкой на объект.
Каждый поток, выполняющий метод one() , создаст свою собственную копию localOne и localTwo в своем стеке. Переменные localOne будут полностью отделены друг от друга, находясь в стеке каждого потока. Один поток не может видеть, какие изменения вносит другой поток в свою копию localOne .
Каждый поток, выполняющий метод one() , также создает свою собственную копию localTwo . Однако две разные копии localTwo в конечном итоге указывают на один и тот же объект в куче. Дело в том, что localTwo указывает на объект, на который ссылается статическая переменная instance . Существует только одна копия статической переменной, и эта копия хранится в куче.
Таким образом, обе копии localTwo в конечном итоге указывают на один и тот же экземпляр Shared . Экземпляр Shared также хранится в куче. Он соответствует Object 3 на диаграмме выше.
Обрати внимание, что класс Shared также содержит две переменные-члены. Сами переменные-члены хранятся в куче вместе с объектом. Две переменные-члены указывают на два других объекта Integer . Эти целочисленные объекты соответствуют Object 2 и Object 4 на диаграмме.
Также обрати внимание, что метод two() создает локальную переменную с именем localOne . Эта локальная переменная является ссылкой на объект типа Integer . Метод устанавливает ссылку localOne для указания на новый экземпляр Integer . Ссылка будет храниться в своей копии localOne для каждого потока. Два экземпляра Integer будут сохранены в куче и, поскольку метод создает новый объект Integer при каждом выполнении, два потока, выполняющие этот метод, будут создавать отдельные экземпляры Integer . Они соответствуют Object 1 и Object 5 на диаграмме выше.
Обрати также внимание на две переменные-члены в классе Shared типа Integer , который является примитивным типом. Поскольку эти переменные являются переменными-членами, они все еще хранятся в куче вместе с объектом. В стеке потоков хранятся только локальные переменные.
