Java 如何在 RAM 中存储原始类型? [复制]
Posted
技术标签:
【中文标题】Java 如何在 RAM 中存储原始类型? [复制]【英文标题】:How does Java store primitive types in RAM? [duplicate] 【发布时间】:2014-01-16 22:27:45 【问题描述】:这与原语是否进入堆栈或堆无关,而是关于它们在实际物理 RAM 中的保存位置。
举个简单的例子:
int a = 5;
我知道 5 被存储到一个内存块中。
我感兴趣的领域是变量“a”存储在哪里?
相关子问题: 'a' 与包含原始值 5 的内存块相关联时会发生什么情况?是否创建了另一个内存块来保存“a”?但这看起来好像 a 是指向对象的指针,但它是这里涉及的原始类型。
【问题讨论】:
***.com/questions/3646632/… ***.com/questions/3698078/… 你问的是方法中的局部变量,还是类的字段? Java 语言没有指定。这取决于 JVM 的实现。 说它们是 RAM 内的分组是不准确的。内存是的,但是可以在 RAM 上压缩内存,或者在物理磁盘上交换空间,具体取决于操作系统如何管理分配给 Java 进程的内存。 【参考方案1】:阐述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
将 X + 0 用于第一个 int, X + 4 用于第二个 int, X + 8 用于第三个。
编译器可以在编译时计算这个值,并且编译器可以依赖堆栈指针寄存器的值(x86 系统上的 ESP),因此它写出的汇编代码会执行类似“在地址 ESP 中存储 0”的操作+ 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 之间的界限,但我希望它有助于说明硬件的实际工作原理。
现在,这仅涵盖“堆栈”。堆栈存在于程序中的每个线程,并捕获在该线程上运行的每个函数之间的函数调用链的状态。但是,一个程序可以有多个线程,因此每个线程都有自己独立的堆栈。
当两个函数调用想要处理同一块内存时会发生什么,无论它们在哪个线程上或它们在堆栈中的什么位置?
这就是堆的用武之地。通常(但不总是)一个程序只有一个堆。堆之所以称为堆,是因为它只是一个很大的内存堆。
要使用堆中的内存,您必须调用分配例程 - 找到未使用空间并将其提供给您的例程,以及让您返回已分配但不再使用的空间的例程。内存分配器从操作系统获取大内存页,然后将单独的小比特分配给任何需要它的东西。它跟踪操作系统给它的东西,以及它给程序其余部分的东西。当程序请求堆内存时,它会寻找它可用的满足需要的最小内存块,将该块标记为已分配,并将其交还给程序的其余部分。如果它没有更多的空闲块,它可以向操作系统请求更多的内存页并从那里分配(直到某个限制)。
在像 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 有自己的机器模型需要担心,它的即时编译器的工作就是担心将其转换为您的实际 CPU)。
我简单地提到了参数是如何在函数之间传递的,比如“参数 A 存储在 ESP - 8,参数 B 存储在 ESP - 12”等。这通常称为“调用约定”,以及其中不止几个。在 x86-32 上,寄存器是稀疏的,因此许多调用约定传递堆栈上的所有参数。这有一些权衡,特别是访问这些参数可能意味着访问 ram(尽管缓存可能会减轻这种情况)。 x86-64 有更多的命名寄存器,这意味着最常见的调用约定在寄存器中传递前几个参数,这可能会提高速度。此外,由于 Java JIT 是唯一为整个过程(本地调用除外)生成机器代码的人,因此它可以选择使用它想要的任何约定来传递参数。
我提到了当你在某个函数中声明一个变量时,该变量的内存来自堆栈 - 这并不总是正确的,它真的取决于环境运行时的突发奇想来决定从哪里获取它记忆来自。在 C#/DotNet 的情况下,如果变量用作闭包的一部分,则该变量的内存可能来自堆 - 这称为“堆提升”。大多数语言通过创建隐藏类来处理闭包。所以经常发生的情况是闭包中涉及的方法本地成员被重写为某个隐藏类的成员,当调用该方法时,而是在堆上分配该类的新实例并将其地址存储在堆栈中;现在所有对该原始局部变量的引用都通过该堆引用发生。
【讨论】:
如果我能为伟大而漫长的回应多加分,我会的。我仍在吸收整件事,但这是一个很好的回应。【参考方案2】:我想我明白了,您并不是要问数据是存储在堆中还是堆栈中!我们对此有同样的困惑!
您提出的问题与编程语言以及操作系统如何处理进程和变量高度相关。
这很有趣,因为当我在大学学习 C 和 C++ 时,我遇到了和你一样的问题。看了一些ASM
GCC
编写的代码,对这个有点理解,大家讨论一下,有什么问题欢迎评论,让我了解更多。
在我看来,不会存储变量名和存储变量值,因为在ASM
代码中,除了cache name
简称variable name
之外,并没有真正的variable name
,所有所谓的变量都只是来自stack
或heap
的off set
。
我认为这是对我学习的提示,因为ASM
以这种方式处理变量名,其他语言可能有相同的策略。
他们只是将off set
存储为保存数据的真实位置。
举个例子,假设变量名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,然后执行打印命令。
重新思考
写完之后,觉得别人很难想象,有什么问题或者错误可以讨论。
【讨论】:
是的,这很难吸收,但你的观点很有意义。让我也看看另一个答案。 4年左右思考这些问题很孤独! ^_^以上是关于Java 如何在 RAM 中存储原始类型? [复制]的主要内容,如果未能解决你的问题,请参考以下文章