Java如何在RAM中存储原始类型?


这不是关于基元是否进入堆栈或堆,而是关于它们在实际物理RAM中保存的位置。


举一个简单的例子:

int a = 5; 

我知道5会被存储到内存块中。

我感兴趣的领域是变量’a’存储在哪里?

相关子问题: ‘a’与包含原始值5的内存块关联的位置在哪里? 是否有另一个内存块来保存’a’? 但这似乎是一个指向对象的指针,但这是一个原始类型。

要阐述Do Java原语是在堆栈还是堆上? –

假设你有一个函数foo()

 void foo() { int a = 5; system.out.println(a); } 

然后,当编译器编译该函数时,它将创建字节码指令,只要调用该函数,就会在堆栈上留下4个字节的空间。 名称’a’仅对您有用 – 对于编译器,它只是为它创建一个点,记住该点的位置,以及它想要使用’a’值的任何地方它改为插入对内存位置的引用它保留了该值。

如果您不确定堆栈是如何工作的,它的工作原理如下:每个程序至少有一个线程,每个线程只有一个堆栈。 堆栈是一个连续的内存块(如果需要也可以增长)。 最初堆栈为空,直到调用程序中的第一个函数。 然后,当你的函数被调用时,你的函数为堆栈本身,所有局部变量,返回类型等分配空间。

当你的函数main调用另一个函数foo ,这里有一个可能发生的例子(这里有几个简化的白色谎言):

  • main想要将参数传递给foo 。 它将这些值推送到堆栈的顶部,使得foo将确切地知道它们将被放置的位置( mainfoo将以一致的方式传递参数)。
  • mainfoo完成后推送程序执行应该返回的地址。 这会增加堆栈指针。
  • foo
  • foo启动时,它会看到堆栈当前位于地址X.
  • foo想在堆栈上分配3个int变量,因此需要12个字节。
  • foo将使用X + 0作为第一个int,X + 4作为第二个int,X + 8作为第三个int。
    • 编译器可以在编译时计算它,并且编译器可以依赖堆栈指针寄存器的值(x86系统上的ESP),因此它写出的汇编代码会执行诸如“在地址ESP + 0中存储0”之类的东西。 ,“将1存入地址ESP + 4”等。
  • 调用foo之前main在堆栈上推送的参数也可以通过计算堆栈指针的一些偏移量来访问foo
    • foo知道它需要多少参数(比如3)所以它知道,比方说,X – 8是第一个,X – 12是第二个,X – 16是第三个。
  • 所以现在foo在堆栈上有空间来完成它的工作,它就这样完成了
  • main调用foo之前, main在递增堆栈指针之前在堆栈上写了它的返回地址。
  • foo查找要返回的地址 – 比如地址存储在ESP - 4foo查看堆栈上的那个位置,找到那里的返回地址,然后跳转到返回地址。
  • 现在main的其余代码继续运行,我们已经完成了一次完整的往返。

请注意,每次调用函数时,它都可以使用当前堆栈指针指向的内存及其后的所有内容执行任何操作。 每次函数在堆栈上为自己腾出空间时,它会在调用其他函数之前递增堆栈指针,以确保每个人都知道他们可以在哪里使用堆栈。

我知道这个解释模糊了x86和java之间的界限,但我希望它有助于说明硬件的实际工作方式。

现在,这只涵盖’堆栈’。 堆栈存在于程序中的每个线程,并捕获在该线程上运行的每个函数之间的函数调用链的状态。 但是,程序可以有多个线程,因此每个线程都有自己独立的堆栈。

当两个函数调用想要处理同一块内存时会发生什么,无论它们在哪个线程上或它们在堆栈中的什么位置?

这是堆进入的地方。通常(但不总是)一个程序只有一个堆。 堆被称为堆,因为它只是一大堆内存。

要在堆中使用内存,您必须调用分配例程 – 找到未使用空间并将其提供给您的例程,以及允许您返回已分配但不再使用的空间的例程。 内存分配器从操作系统获取大页面内存,然后将各个小位分发给任何需要它的内容。 它记录了操作系统给它的内容,并从中了解了它给程序其余部分的内容。 当程序请求堆内存时,它会查找满足需要的最小内存块,将该块标记为已分配,并将其交还给程序的其余部分。 如果它没有任何更多的空闲块,它可以向操作系统询问更多页面的内存并分配到那里(直到某个限制)。

在像C这样的语言中,我提到的那些内存分配例程通常称为malloc()来请求内存,而free()返回它。

另一方面,Java没有像C那样的显式内存管理,而是它有一个垃圾收集器 – 你分配你想要的任何内存,然后当你完成后,你就停止使用它。 Java运行时环境将跟踪您分配的内存,并将扫描您的程序以确定您是否不再使用所有分配,并将自动解除分配这些块。

现在我们知道内存是在堆或堆栈上分配的,当我在类中创建私有变量时会发生什么?

 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是唯一为整个过程生成机器代码的人(除了本机调用),它可以选择使用它想要的任何约定来传递参数。

  • 我曾经提到过,当你在某个函数中声明一个变量时,该变量的内存来自堆栈 – 这并不总是正确的,并且它确实取决于环境运行时的奇思妙想来决定从哪里获取内存。 在C#/ DotNet的情况下,如果变量用作闭包的一部分,该变量的内存可能来自堆 – 这称为“堆升级”。 大多数语言通过创建隐藏类来处理闭包。 所以经常发生的事情是,闭包中涉及的方法本地成员被重写为某些隐藏类的成员,并且当调用该方法时,而是在堆上分配该类的新实例并将其地址存储在堆栈中; 现在所有对原始局部变量的引用都是通过该堆引用发生的。

我想我明白你并不是要问数据是存储在堆还是堆栈中! 我们对此有同样的难题!

您提出的问题与编程语言以及操作系统如何处理流程和变量密切相关。

这非常有趣,因为当我在大学学习C和C ++时,我遇到了和你一样的问题。 在阅读了由GCC编写的一些ASM代码之后,我对此有点了解,让我们讨论一下,如果有任何问题,请评论它并让我了解更多相关信息。

在我看来,变量名将不会被存储并且变量值被存储在中,因为在ASM代码中,除了cache name之外没有真正的variable name ,所有所谓的变量只是来自stackheapoff set
我认为这是对我学习的暗示,因为ASM以这种方式处理变量名,其他语言可能有相同的策略。
它们只是存储用于保存数据的真实位置。
让我们举一个例子,假设变量名称a放在地址@1000并且这个a的类型是整数,因此在内存地址中

 addr type value @1000 int 5 

@ 1000是存储真实数据的off set

正如您所看到的那样,数据被置于真正的off set中。
在我对过程的理解中,所有变量将在进程开始时被“变量”的“地址”替换,这意味着CPU只处理已在内存中分配的“地址”。
让我们再次审查这个程序:你已经定义了
int a=5; print(a);
编译后,程序转换成另一种格式(全部由我的想象力):

 stack:0-4 int 5 print stack:0-4 

而在真正执行的进程的情况下,我认为内存将是这样的:

 @2000 4 5 //allocate 4 byte from @2000, and put 5 into it print @2000 4 //read 4 byte from @2000, then print 

由于进程的内存由CPU分配,因此@2000是此变量名的off set ,这意味着name将仅被内存地址替换,然后将从该地址读取数据5,然后执行print命令。

反思

写完之后,我觉得很难被其他人想象,如果有任何问题或错误,我们可以讨论它。