【基础篇-堆栈】传值?传引用?(一)

    xiaoxiao2021-03-25  122

    写在前头

    最近,在一个java开发群看到几位有着2-3年工作经验的前辈因为传值和传引用吵得不可开交,细看了一下之前的聊天记录,发现问题是因为对java数据储存方式的混淆不清造成的,本想着这样一个基础且偏概念性的问题于我而言根本不是问题,可就在今天下班的归途中,回想起他们争论的核心点,猛地发现自己脑子里也是一片模糊……

    我们一直提倡使用已经造好的轮子来提高开发效率,可是当我们浮躁的连螺丝都忘记上的时候,那结果只有一个——车毁人亡。

    可怕的不是忘记,而是你不知道自己已经忘记!

    本篇将会基于java数据存储、堆栈、传值、传引用做一较为完整的分析整理,希望能够帮助到那些对这些概念依然混淆不清的朋友。

    由于本人水平极其有限,文章肯定会有很多勘误和疏漏,希望各位前辈能够予以指正,万分感谢!

    --------------------------------------------------------------------------------分割线--------------------------------------------------------------------------------------------

    前情提要

    java中的数据分为两类:基本数据类型、引用数据类型。其中,基本数据类型共有8种(一说9种(部分java文献中将viod也当作基本数据类型的一种)),引用数据类型不表(String将作为特殊引用数据类型在后续篇章中单独分析)。

    数据如何储存?

    在java中,数据会根据自身的特性以各不相同的生命周期储存在以下五个地方:

    (1)寄存器。

    (2)栈。

    (3)堆。

    (4)常量储存。

    (5)非RAM储存。

    我们重点来看栈和堆。

    栈:

    基本数据类型的数据和对象引用(注意:这里是对象引用,而非对象)都是在栈中直接采用栈内指针的上下移动以值的形式存储起来的。

    我们可以形象的把栈想象为类似于大小一致的储物格的序列,如图所示。一个格子(内存块)代表1bit大小,而指针就像一个管理者一样,来控制这些储物格是否需要存放或者取出东西。假设我们的应用程序现在没有任何上述类型的数据(变量),那么指针就应如图所示位于栈顶。

    来,让我们来声明一个byte类型名为number的变量并给其赋值22。这个时候,栈内的指针会迅速的向下移动8个格子,为刚才声明的这个byte类型的变量分配出1字节(8bit)的内存空间,并且将22以二进制值的形式来填充进这些格子,如图所示:

    以此类推,我们便可得知其他数据类型是如何在内存中存储的。

    注意:boolean类型并未明确指定所占内存空间,你只需要理解它代表true or false就可以了;所有的对象引用所占的内存大小也是相同的。

    我们知道,在java中,基本数据类型和对象引用所占的内存空间都是固定且非常小的(c和c++会因为硬件架构的差异而导致相同类型的变量所占空间不同),所以,栈中的指针能够快速的确定这些数据所占用的空间的大小并予以分配,这样做,高效却缺乏灵活性,并且,栈内所有数据项需要在编译时确定生命周期和数据合法性,举个栗子:

    public class ValueTest { public static void main(String[] args) { byte number; System.out.println(number); } }

    我们创建一个ValueTest类,在该类main方法中声明一个byte类型的变量number,然后直接打印number,这个时候,如果你是用的txt文本文档进行编写和编译的话,cmd命令台会给出你这样的错误提示:

    如果你使用的是其他idea工具,在编译之前,idea就会给你指出你的错误之处:

    错误信息为:变量未初始化。

    在实际编码过程中,我们都知道所有的变量(常量)都应该声明并初始化之后才能够正常在程序中使用,这也正印证了前面所说的:栈中的所有数据项必须在编译时确认生命周期和合法性

    堆:

    java中所有的对象都存储于堆中。

    堆和栈的最大区别是:编译器无需知道堆内对象确切的生命周期,也无需知道对象何时被创建,并且,堆内的内存分配完全采取动态分配的方式,即直到程序运行创建对象的new对象语句时,才会在堆内根据当前对象所需的空间大小来分配内存。这样做具有极高的灵活性,但是却会因为内存动态分配的缘故导致堆内数据的储存和清理耗时更多(c和c++是可以在栈内创建对象的),这也就是java诞生之初一直为人所诟病的效率问题的原因所在。

    在说对象在堆内如何储存之前,让我们先来厘清对象引用和对象之间的关系和区别。

    同样,举个栗子(我们在这里使用数组来更好的解释堆内内存的动态分配机制):

    首先,声明一个byte[]类型的变量obj,然后new一个byte[]类型长度为1的对象,将new出来的对象指向(赋值给)变量obj。虽然我们平时都已经习惯将obj称为对象,但我们应该清楚,obj只是储存在栈中的一个对象引用,而非对象本身。为了验证这一说法,你可以声明一个类似的引用数据类型变量(String类型除外)并予以赋值,执行该对象引用的toString()方法得到一个字符串,然后将得到的字符串打印出来,你会看到一串神秘字符,这串字符所代表的含义就是obj这个对象引用所表示的对象在堆中的地址。(对象引用是储存在栈中的一个固定大小的地址,这个固定大小在不同平台、不同虚拟机下都不尽相同,但这并不影响java的跨平台性)。

    那么,刚才new出来的对象到底在堆中是如何存储的呢?

    我们刚刚new出了包含有一个byte元素的byte[]类型的数组(java已经对该元素默认初始化),也就意味着,这个对象在堆中只占了一个byte类型所占空间大小,如下图所示。看到这里,有的人可能会问,这个对象在堆中储存和在栈中储存有什么区别?都只占了8bit,为什么要搞的这么复杂?还有所谓的动态分配体现在哪里?别急,栗子,接着:

    public class ObjectTest { public void createByte(int count){ byte[] obj = new byte[count]; } } 如上所示,我们编写一个创建byte数组的方法,形参为int类型的变量count,在方法内部,我们需要声明名为obj,并new一个长度为count的byte[]类型的数组指向obj。现在,你能确切的告诉我,这个count的值吗?事实是,只有当我们的程序通过编译,正式运行,调用createByte这个方法并传入相应的参数之后,java才能够知道,obj所代表的这个对象具体有几个元素,要占用多大的内存空间。

    我们假设count值为2,并且,在对象创建完成之后,我们对obj数组中的第0个元素进行赋值操作,结果如下图:

    很明显,在堆内,java为obj所代表的对象分配了2字节大小的储存空间,与之前相比,两个obj对象虽然数据类型相同,但所占存储空间却不相同,原因就是java为这两个对象按照自身需求动态的分配了内存大小(我们可以将其理解为动态绑定)。

    所以,这就是为什么栈的效率高却存储最简单最小的基本数据,而堆却要承担java中一切对象的存储任务。

    好了,《【基础篇-堆栈】传值?传引用?(一)》就到这里了,本篇着重分析数据是如何在堆、栈中存储,为什么有的数据存储在栈中,而有的数据存储在堆中。而关于堆栈中数据的生命周期,我会在后续篇章中分析。

    by The_Ashes

    转载请注明原文地址: https://ju.6miu.com/read-7498.html

    最新回复(0)