Stack – Каждый поток имеет свой стек, который создается в тоже время, когда создается поток. Стек (локальная или рабочая память потока) содержит фреймы, которые создаются при каждом вызове метода и хранят локальные переменные и промежуточные результаты, возвращают значения для методов и выбрасывают исключения, если это необходимо. Задается параметром Xss (default 1Mb, примеры: -Xss1m, -Xss1024k).
Хранит Примитивы, static, вызовы функций и ссылки на Heap, значение для нового потока (new Thread()). Фрейм разрушается, когда вызов метода завершается, неважно является это завершение успешным или с исключением. Ниже приведен пример кода:
1 2 3 4 5 |
public class Memory { public static void main(String[] args) { main(args); } } |
Его результатом будет исключение StackOverflowError, потому что этот код бесконечно вызывает сам себя, соответственно память в стеке заканчивается. Существуют возможности увеличить размер стека, для этого необходимо при запуске добавить аргумент для виртуальной машины –Xss1024k. Это установит размер стека равным 1 мегабайту. Программа выше выдает 444 строки для -Xss=128k, в то время как 256k дает ~ 1025 строк.
Heap – создается в момент запуска виртуальной машины, это область памяти в которой хранятся все созданные в процессе работы программы ССЫЛОЧНЫЕ типы данных. Он существует только один и разделяется между всеми потоками, существующими в программе. Для очистки от более неиспользуемых объектов (те объекты, на которые никто больше не ссылается) используется сборщик мусора (garbage collector). Аргумент для виртуальной машины -Xmx1024k (Xms - начальное значение, Xmx - макс. значение. пример: Xmx2048m). Ошибка OutOfMemory.
Volatile – говорит потоку что переменная может меняться другими потоками, и информирует поток о необходимости обращаться к последней версии (heap), а не к хешированной копии (stack) и своевременно распространять изменения. Например, когда мы в многопоточном приложении используем паттерн Синглтон в котором применяем синхронизацию synchronized... (stack -> heap) и хотим чтобы синхронизация осуществлялась только один раз при инициализации объекта, а не каждый раз, когда мы вызываем getInstance(), тогда модификатор volatile используем для объектной ссылки :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Singleton { private static volatile Singleton instance; // ! может меняться другими потоками private Singleton(){ } public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { // ! При выходе из synchronized if (instance == null) // ! синхронизироать stack -> heap instance = new Singleton(); // ! } // ! } return instance; } } |
(Внимание на static, volatile, synchronized)
Еще одно объяснение: https://timmson.github.io/java-interview/009-concurrency.html. С точки зрения Java все переменные (за исключением локальных переменных, объявленных внутри метода) хранятся в главной памяти, которая доступна всем потокам. Кроме этого, каждый поток имеет локальную(рабочую) память, где он хранит копии переменных, с которыми он работает, и при выполнении программы поток работает только с этими копиями.
При входе в synchronized метод или блок поток обновляет содержимое локальной памяти, а при выходе из synchronized метода или блока поток записывает изменения, сделанные в локальной памяти, в главную.
Еще ключи запуска java интересные в этой теме:
-verbose:gc - регистрирует, запуски сборщика мусора и сколько времени они занимают.
-XX:+PrintGCDetails - включает в себя данные из -verbose:gc, но также добавляет информацию о размере нового поколения и более точных временных параметрах.
-XX:-PrintGCTimeStamps - печатать метки времени при сборке мусора.
Ссылки:
https://www.baeldung.com/java-stack-heap
https://java-ru-blog.blogspot.com/2019/12/jvm-options.html
jvm_model.pdf