举一个简单的例子:
int a = 5;
我知道5会被存储到内存块中。
我感兴趣的领域是变量'a'存储在哪里?
相关子问题:在'a'与包含原始值5的内存块关联的位置会发生什么?是否有另一个内存块来保存'a'?但这似乎是一个指向对象的指针,但这是一个原始类型。
答案 0 :(得分:19)
阐述Do Java primitives go on the Stack or the Heap? -
假设你有一个函数foo()
:
void foo() {
int a = 5;
system.out.println(a);
}
然后,当编译器编译该函数时,它会创建字节码指令,只要调用该函数,就会在堆栈上留下4个字节的空间。名字' a'只对您有用 - 对于编译器,它只是为它创建一个点,记住该点的位置,以及它想要使用' a'的值的任何地方。它改为插入对它为该值保留的内存位置的引用。
如果您不确定堆栈是如何工作的,它的工作方式如下:每个程序至少有一个线程,每个线程只有一个堆栈。堆栈是一个连续的内存块(如果需要也可以增长)。最初堆栈为空,直到调用程序中的第一个函数。然后,当你的函数被调用时,你的函数为堆栈本身,所有局部变量,返回类型等分配空间。
当你的函数main
调用另一个函数foo
时,这里有一个可能发生的例子(这里有一些简化的白色谎言):
main
想要将参数传递给foo
。它会将这些值推送到堆栈顶部,以便foo
确切知道它们的放置位置(main
和foo
将以一致的方式传递参数。)< / LI>
main
在foo
完成后推送程序执行应返回的地址。这会增加堆栈指针。main
来电foo
。foo
启动时,它会看到堆栈当前位于地址X foo
想在堆栈上分配3个int
变量,因此它需要12个字节。foo
将使用X + 0作为第一个int,将X + 4作为第二个int,将X + 8作为第三个int。
main
在调用foo
之前在堆栈上推送的参数也可以由foo
通过计算堆栈指针的一些偏移来访问。
foo
知道它需要多少参数(比如3)所以它知道,比方说,X - 8是第一个,X - 12是第二个,X - 16是第三个。 foo
在堆栈上有空间来完成它的工作,它就这样完成了main
调用foo
之前,main
在递增堆栈指针之前在堆栈上写了它的返回地址。foo
查找要返回的地址 - 假设地址存储在ESP - 4
- foo
查看堆栈上的那个位置,找到那里的返回地址,然后跳转到返回地址。main
中的其余代码继续运行,我们已经完成了全程往返。请注意,每次调用函数时,它都可以使用当前堆栈指针指向的内存及其后的所有内容执行任何操作。每次函数在堆栈上为自己腾出空间时,它会在调用其他函数之前递增堆栈指针,以确保每个人都知道他们可以在哪里使用堆栈。
我知道这个解释稍微模糊了x86和java之间的界限,但我希望它有助于说明硬件的实际工作方式。
现在,这只涵盖了堆栈&#39;。堆栈存在于程序中的每个线程,并捕获在该线程上运行的每个函数之间的函数调用链的状态。但是,程序可以有多个线程,因此每个线程都有自己独立的堆栈。
当两个函数调用想要处理同一块内存时会发生什么,无论它们处于什么线程或它们在堆栈中的什么位置?
这是堆进入的地方。通常(但不总是)一个程序只有一个堆。堆被称为堆,因为它只是一大堆内存。
要在堆中使用内存,您必须调用分配例程 - 找到未使用空间并将其提供给您的例程,以及允许您返回已分配但不再使用的空间的例程。内存分配器从操作系统获取大页面的内存,然后将各个小位分发给任何需要的内存。它记录了操作系统给它的内容,以及它为程序的其余部分提供的内容。当程序要求堆内存时,它会查找满足需要的最小内存块,将该块标记为已分配,并将其交还给程序的其余部分。如果它没有任何更多的空闲块,它可以向操作系统询问更多页面的内存并分配到那里(直到某个限制)。
在像C这样的语言中,我提到的那些内存分配例程通常被称为malloc()
以询问内存,而free()
则返回它。
现在我们知道内存是在堆或堆栈上分配的,当我在类中创建私有变量时会发生什么?
public class Test {
private int balance;
...
}
那记忆来自哪里?答案是堆。您有一些代码可以创建新的Test
对象 - Test myTest = new Test()
。调用java new
运算符会导致在堆上分配新的Test
实例。您的变量myTest
将地址存储到该分配中。 balance
只是偏离该地址的一些偏差 - 实际上可能是0。
最底层的答案都只是......会计。
...
我谈到的白色谎言?让我们来解决其中的一些问题。
Java首先是一个计算机模型 - 当您将程序编译为字节码时,您正在编译成一个完全虚构的计算机体系结构,它没有任何其他常见的寄存器或汇编指令CPU-Java和.Net以及其他一些使用基于堆栈的处理器虚拟机,而不是基于寄存器的机器(如x86处理器)。原因是基于堆栈的处理器更易于推理,因此更容易构建操作该代码的工具,这对于构建将代码编译为实际运行在公共处理器上的代码的工具尤其重要。
给定线程的堆栈指针通常从某个非常高的地址开始,然后至少在大多数x86计算机上向下增长,而不是向上。也就是说,既然是机器细节,那么实际上并不担心Java的问题(Java有自己的机器模型需要担心,它的Just In Time编译器和#39 ;担心将其转换为实际CPU的工作。)
我简要地提到了如何在函数之间传递参数,说&#34;参数A等东西存储在ESP-8中,参数B存储在ESP-12&#34;这通常称为&#34;调用约定&#34;,并且它们不止一些。在x86-32上,寄存器是稀疏的,因此很多调用约定都会传递堆栈上的所有参数。这有一些权衡,特别是访问这些参数可能意味着访问ram(尽管缓存可能会缓解这种情况)。 x86-64有更多命名寄存器,这意味着最常见的调用约定会传递寄存器中的前几个参数,这可能会提高速度。此外,由于Java JIT是唯一为整个过程生成机器代码的人(除了本机调用),它可以选择使用它想要的任何约定传递参数。
我曾经提到过当你在某个函数中声明一个变量时,该变量的内存来自堆栈 - 这并不总是正确的,而且它真正取决于环境的运行时决定从哪里获取内存。在C#/ DotNet的情况下,如果变量用作闭包的一部分,则该变量的内存可能来自堆 - 这称为&#34;堆升级&#34;。大多数语言通过创建隐藏类来处理闭包。所以经常发生的是,闭包中涉及的方法本地成员被重写为某些隐藏类的成员,并且当调用该方法时,而是在堆上分配该类的新实例并将其地址存储在堆栈中;现在所有对原始局部变量的引用都是通过该堆引用而发生的。
答案 1 :(得分:3)
我认为我明白你并不是要问数据是存储在堆还是堆栈中!我们对此有同样的难题!
您提出的问题与编程语言以及操作系统如何处理流程和变量密切相关。
这非常有趣,因为当我在大学学习C和C ++时,我遇到了和你一样的问题。在阅读ASM
编译的一些GCC
代码之后,我对此有一点了解,让我们讨论一下,如果有任何问题,请评论它并让我了解更多相关信息。
在我看来,变量名称不会被存储,变量值也会被存储,因为在ASM
代码中,除了variable name
之外没有真正的cache name
,所有所谓的变量只是来自off set
或stack
的{{1}}
我认为这是对我学习的暗示,因为heap
以这种方式处理变量名,其他语言可能有相同的策略。
他们只是存储ASM
用于保存数据的真实位置
让我们举一个例子,比如变量名off set
放在地址a
中,这个@1000
的类型是整数,因此在内存地址
a
其中@ 1000是存储真实数据的addr type value
@1000 int 5
。
正如您所看到的那样,数据被放入真实的off set
中
在我对过程的理解中,所有变量将在过程开始时被“变量”的“地址”替换,这意味着CPU只处理已在内存中分配的“地址”。
让我们再次审查这个程序:你已经定义了
off set
编译后,程序转换成另一种格式(全部由我的想象力):
int a=5; print(a);
在实际执行的过程中,我认为内存将是这样的:
stack:0-4 int 5
print stack:0-4
由于进程的内存是由CPU分配的,@2000 4 5 //allocate 4 byte from @2000, and put 5 into it
print @2000 4 //read 4 byte from @2000, then print
是此变量名的@2000
,这意味着off set
将仅被内存地址替换,然后将读取数据5从该地址开始,然后执行print命令。