阐述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
将以一致的方式传递参数)。 main
推送完成foo
后程序执行应返回到的地址。这将增加堆栈指针。 main
调用foo
。 当foo
启动时,它看到堆栈当前位于地址X foo
希望在堆栈上分配3个int
变量,因此需要12个字节。 foo
将对第一个int使用X + 0,对于第二个int使用X + 4,对于第三个int使用X + 8。
编译器可以在编译时进行计算,并且编译器可以依赖堆栈指针寄存器(x86系统上的ESP)的值,因此它写出的汇编代码的作用类似于“将0存储在地址ESP + 0中” ”,“将1存储到地址ESP + 4中”等 通过调用堆栈指针的一些偏移量,main
也可以访问foo
在调用foo
之前将其压入堆栈的参数。
foo
知道需要多少个参数(例如3),因此它知道例如X-8是第一个,X-12是第二个,而X-16是第三个。 因此,现在foo
在堆栈上有足够的空间来完成其工作,它就这样做了,并完成了在称为main
的foo
之前,main
在递增堆栈指针之前将其返回地址写在堆栈上。 foo
查找要返回的地址-假设该地址存储在ESP - 4
中-foo
查找堆栈上的该点,在此处找到返回地址,然后跳转到返回地址。 现在main
中的其余代码继续运行,我们进行了一次完整的往返。 请注意,每次调用函数时,它都可以对当前堆栈指针指向的内存及其后的所有内容执行所需的任何操作。每次函数在堆栈上为其自身腾出空间时,它将在调用其他函数之前递增堆栈指针,以确保每个人都知道自己可以在哪里使用堆栈。
我知道这个解释使x86和java之间的界线有些模糊,但我希望它有助于说明硬件的实际工作原理。
现在,这仅涉及“堆栈”。程序中每个线程都存在该堆栈,并捕获该线程上运行的每个函数之间的函数调用链的状态。但是,一个程序可以有多个线程,因此每个线程都有自己的独立堆栈。
当两个函数调用要处理同一块内存时,无论它们在哪个线程上或在堆栈中的什么位置,该怎么办?
这就是堆的所在。通常(但并非总是),一个程序只有一个堆。堆之所以称为堆,是因为它只是一个很大的内存堆。
要使用堆中的内存,您必须调用分配例程-查找未使用空间并将其分配给您的例程,以及使您返回分配的但不再使用的空间的例程。内存分配器从操作系统获取大内存页,然后将各个小部分分发给任何需要的内存。它会跟踪OS给它的东西,以及它给程序其余部分的东西。当程序请求堆内存时,它会寻找满足需要的最小可用内存块,将该内存块标记为已分配,然后将其交还给程序的其余部分。如果没有更多的空闲块,则可以向操作系统请求更多的内存页面,并在其中分配内存(直至达到一定限制)。
在像C这样的语言中,我提到的那些内存分配例程通常被称为
malloc()
来请求内存,而被称为
free()
来返回内存。
另一方面,Java没有像C那样具有显式的内存管理,而是具有垃圾回收器-您分配所需的任何内存,然后在完成后就停止使用它。 Java运行时环境将跟踪已分配的内存,并将扫描程序以查找是否不再使用所有分配,并将自动取消分配这些块。
因此,既然我们知道内存是分配在堆或堆栈上的,那么当我在类中创建私有(private)变量时会发生什么?
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编译器的工作就是担心将其转换为实际的CPU)。 我简要地提到了如何在函数之间传递参数,说诸如“参数A存储在ESP-8,参数B存储在ESP-12”之类的东西。这通常称为“调用约定”,除了他们很少。在x86-32上,寄存器是稀疏的,因此许多调用约定将所有参数传递给堆栈。这有一些权衡,特别是访问那些参数可能意味着要访问ram(尽管缓存可能会减轻这种情况)。 x86-64具有更多的命名寄存器,这意味着最常用的调用约定会传递寄存器中的前几个参数,从而可以提高速度。此外,由于Java JIT是唯一为整个过程生成机器代码的人( native 调用除外),因此它可以选择使用所需的任何约定来传递参数。 我提到了当您在某个函数中声明一个变量时,该变量的内存来自堆栈-并非总是如此,这完全取决于环境运行时的想法,以决定从何处获取该内存。在C#/DotNet的情况下,如果将该变量用作闭包的一部分,则该变量的内存可能来自堆-这称为“堆提升”。大多数语言通过创建隐藏类来处理闭包。因此,经常发生的情况是,将闭包中涉及的方法本地成员重写为某个隐藏类的成员,并且在调用该方法时,而是在堆上分配该类的新实例并将其地址存储在堆栈中;现在,所有对该原始本地变量的引用都通过该堆引用发生。