Вы здесь:

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