Java基础:堆内存和栈内存的深入探究

448 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情

一、堆和栈的数据结构定义:

栈是一种运算受限的线性表,其限制是指只仅允许在表的一端进行插入和删除操作,这一端被称为栈顶(Top),相对地,把另一端称为栈底(Bottom)。这种受限的运算使栈拥有“先进后出”的特性。栈是一种连续的内存地址。

1665042257835.png

堆是一种常用的树形结构,是一种特殊的完全二叉树,当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆。堆的这一特性称之为堆序性。堆是一种不连续的内存地址。

1665042302402.png

二、堆内存和占内存的区别

堆内存定义

只有一个堆,是线程共享的,存放所有的对象实例(基本数据类型没有类,也就没有对象)和数组。

堆内存优缺点

可以动态地分配内存大小。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

栈内存定义

每个线程都包含一个栈,包含基本数据类型的对象和自定义对象的引用。

栈内存优缺点

存取速度比堆要快,仅次于寄存器,另外,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

三、堆内存和占内存的存储数据类型探索

Java中的数据类型有两种

1.基本数据类型

第一种是基本类型, 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。

这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。

值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。

如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于中。

另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义

        int a = 3;
        int b = 3;
        System.out.println(a==b); //true

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。

接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。字面值的引用与其不同之处在于,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。

如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

        String str1 = "abc";
        String str2 = "abc";
        System.out.println(str1==str2); //true

2.包装类数据

第二种是包装类数据,如Integer, String, Double等,这些是将相应的基本数据类型(int,double)包装起来的类。这些类数据全部存在于堆内存中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

每个JVM的线程都有自己的私有的栈空间,随线程创建而创建,java的stack存放的是frames ,java的stack和c的不同,只是存放本地变量,返回值和调用方法,不允许直接push和pop frames ,因为frames 可能是由heap分配的,所以java的stack分配的内存不需要是连续的。java的heap是所有线程共享的,堆存放所有 runtime data ,里面是所有的对象实例和数组,heap是JVM启动时创建。

只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

3.不用类数据存放内存的探究

注意:数据类型包装类的值不可修改。不仅仅是String类的值不可修改,所有的数据类型包装类都不能更改其内部的值。

        String str1 = “abc”;
        String str2 = new String(“abc”);
        System.out.println(str1==str2); //false

String是个特殊的包装类,可以用String str = new String(“abc”);的形式来创建,也可以用String str = “abc”;

        Integer a = new Integer(1);
        int b = 1;
        System.out.println(a==b); //true

注意:Integer是int的封装类,当Integer与int进行==比较时,Integer就会拆箱成一个int类型,相当于我把堆内存的里面的对象拿出来,放在栈里面。所以还是相当于两个int类型进行比较,并不是说new的内存不放在堆里面了。

Integer包装类型在和基本数据类型比较时,jvm会自动把包装数据类型拆箱为基本数据类型int

Integer和Integer就不会有拆箱过程了。以下可以看出Integer的常量b放在栈,a作为对象的实例化放在堆。

        Integer a = new Integer(1);
        Integer b =1;
        System.out.println(a==b); //false

而且new的数据都是放在堆里面的,堆内存的数据不公用。

        Integer a = new Integer(1);
        Integer b =new Integer(1);
        System.out.println(a==b); //false

而栈内数据公用

        Integer a = 127;
        Integer b =127;
        System.out.println(a==b); // true

但是要注意Integer这个包装类没有用new的不合法使用,是JVM给你自动转换缓存的这里相当于int a = 127;int b =127;但是这个自动转换有限制的只有在-128到127之间的数才会进行缓存

超了的话人家不管的,如下

        Integer a = 128;
        Integer b =128;
        System.out.println(a==b); //false

这里的a、b都是new Integer(128)出来的变量,所以它们不等。

有几个特殊的地方需要重点关注

1.== 和 equals()

== 是比较内存地址的, equals()是比较值得

        Integer a = new Integer(127);
        Integer b =127;

        System.out.println(a==b); //false
        System.out.println(a.equals(b)); //true

2.String是个特殊的包装类,不是基本数据类型

3.Integer是int的封装类,当Integer与int进行==比较时,Integer就会拆箱成一个int类型。而Integer和Integer比较则不会。

4.Integer 等包装类使用 Integer a=1;这种不用new的不合法使用在-128到127之间相当于int 在栈内存,在这区间之外相当于Integer在堆内存。

个人总结

1.堆满,栈快

2.堆唯一,栈多个。

3.堆存放所有的对象实例和数组。

基本数据类型不是类,也就没有对象,但有其对应类

4.栈存放基本数据类型的对象和自定义对象的引用及其数据。