Java虚拟机的堆、栈、堆栈如何去理解?

我只片面的知道堆和堆栈是一种存储结构,栈是一种数据结构
关注者
1463
被浏览
54672

19 个回答

泻药。踌躇很久要不要答…月经问题其实颇难回答得一了百了。
还是照例搬运一下JVM规范吧…

堆是堆(heap),栈是栈(stack),堆栈是栈。嗯我很不喜欢“堆栈”这种叫法,容易让新人掉坑里。

关于栈:为什么函数调用要用栈实现? - RednaxelaFX 的回答
这段代码有没有正确释放堆栈空间? - RednaxelaFX 的回答

JVM规范让每个Java线程拥有自己的独立的JVM栈,也就是Java方法的调用栈。Chapter 2. The Structure of the Java Virtual Machine
Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (§2.6). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.
同时JVM规范为了允许native代码可以调用Java代码,以及允许Java代码调用native方法,还规定每个Java线程拥有自己的独立的native方法栈。Chapter 2. The Structure of the Java Virtual Machine
An implementation of the Java Virtual Machine may use conventional stacks, colloquially called "C stacks," to support native methods (methods written in a language other than the Java programming language). Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine's instruction set in a language such as C. Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created.

这俩都是JVM规范所规定的概念上的东西,并不是说具体的JVM实现真的要给每个Java线程开两个独立的栈。以Oracle JDK / OpenJDK的HotSpot VM为例,它使用所谓的“mixed stack”——在同一个调用栈里存放Java方法的栈帧与native方法的栈帧,所以每个Java线程其实只有一个调用栈,融合了JVM规范的JVM栈与native方法栈这俩概念。

JVM里的“堆”(heap)特指用于存放Java对象的内存区域。所以根据这个定义,Java对象全部都在堆上。Chapter 2. The Structure of the Java Virtual Machine
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.

The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor's system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous.

要注意,这个“堆”并不是数据结构意义上的堆(Heap (data structure),一种有序的树),而是动态内存分配意义上的堆——用于管理动态生命周期的内存区域。

JVM的堆被同一个JVM实例中的所有Java线程共享。它通常由某种自动内存管理机制所管理,这种机制通常叫做“垃圾回收”(garbage collection,GC)。JVM规范并不强制要求JVM实现采用哪种GC算法。

顺带放个传送门:垃圾回收机制中,引用计数法是如何维护所有对象引用的? - RednaxelaFX 的回答(请不要被问题的标题误导)

至于上面这些概念如何映射到实际的JVM的内存,还可以跳另一个传送门:老师说字符串常量和静态变量放在data segment中,问一下这里的data segment和常量池是一回事吗? - RednaxelaFX 的回答
这要从操作系统里进程的内存结构说起了。。。
下图是linux 中一个进程的虚拟内存分布:


图中0号地址在最下边,越往上内存地址越大。

以32位地址操作系统为例,一个进程可拥有的虚拟内存地址范围为0-2^32。分为两部分,一部分留给kernel使用(kernel virtual memory),剩下的是进程本身使用, 即图中的process virtual memory。

普通Java 程序使用的就是process virtual memory.

上图中最顶端的一部分内存叫做user stack. 这就是题目问的 stack. 中间有个 runtime heap。就是题目中的heap. 他们的名字和数据结构里的stack 和 heap 几乎每啥关系。

注意在上图中,stack 是向下生长的; heap是向上生长的。

当程序进行函数调用时,每个函数都在stack上有一个 call frame。

比如对于以下程序,
public void foo(){
  //do something...
  println("haha"); // <<<=== 在这儿设置breakpoint 1
}

public void bar(){
  foo();
}

main(){
  bar();
  println("hahaha"); // <<<=== 在这儿设置 breakpoint 2
}

当程序运行到breakponit1时,user stack 里会有三个frame

|
| main 函数的 frame
-------------------
|
| bar 函数的 frame
-------------------<<<=== %ebp
|
| foo 函数的 frame
------------------- <<<===%esp

其中 esp 和 ebp 都是寄存器。 esp 指向stack 的顶(因为stack 向下生长,esp会向下走); ebp 指向当前frame的边界。
当程序继续执行到brekapoing 2的时候stack 大概是这样的:


|
-------------------<<<=== %ebp
|
| main 函数的 frame
------------------- <<<===%esp


也就是说当一个函数执行结束后,它对应的call frame就被销毁了。(其实就是esp 和 ebp分别以东,但是内存地址中的数据只有在下一次写的时候才被覆盖。)

说了这么多,终于该说什么东西放在stack 上什么东西放在heap 上了。

最直白的解释:
public void foo(){
  int i = 0; // <= i 的值存在stack上,foo()的call frame 里。
  Object obj = new Object(); // object 对象本身存在heap 里, foo()的call frame 里存该对象的地址。
}



===========
图片引自CMU15-213的课件
cs.cmu.edu/~213/