面试记录之 Java 为什么区分了堆和栈

我们都知道堆和栈,但是不知道为什么有这些。本文从原理讲述一下

目录

一、什么是堆,什么是栈

1.1 堆

1.2 栈

1.3 总结

二、堆会存什么

2.1 Java 类实例对象

2.2 数组对象

2.3 对象实例的成员变量

2.4 静态变量

2.5 常量池:

2.5.1 有趣的常量池

三、栈会存什么

3.1 有趣的栈

四、总结


简单点说,Java 划分了堆和栈是为了更好的使用内存,提高内存使用效率。

一、什么是堆,什么是栈

        Java的堆(Heap)和栈(Stack)都是内存中的两种不同的存储区域,用于存储程序执行过程中的数据。

1.1 堆

        堆则是一种基于树的数据结构,由于对象和数组的大小不固定,因此不能将它们存储在栈内存中,否则会导致栈内存不够用,因此堆是存储对象及其实例变量的地方。堆的大小不固定,由虚拟机自动管理,当堆空间不足时,就会抛出OutOfMemoryError异常。堆内存的特点是动态分配和释放,因此对象和数组的大小可以动态调整,堆内存的使用效率也比较高。同时,由于堆内存是不连续的,因此访问速度相对较慢。

        堆是一种树型数据结构,主要是因为它需要支持高效的动态内存分配和回收。在堆中,内存空间是按照树形结构组织的,其中每个节点都代表一个内存块,包括该内存块的大小和状态(已分配或未分配)。

        堆的树形结构使得它可以支持高效的内存分配和回收操作。当需要分配一块内存时,堆会从根节点开始遍历树,寻找一块足够大的未分配内存块,将其分配给程序。同样地,当需要释放某块内存时,堆会从该内存块的位置开始,递归地合并相邻的未分配内存块,形成一个更大的未分配内存块,最终恢复该内存块到未分配状态,以便后续程序继续使用。

        由于堆是一种动态数据结构,内存的分配和回收是频繁进行的操作。而树形结构具有较好的扩展性和灵活性,因此堆被设计成树型结构,以支持高效的内存分配和回收。

1.2 栈

       栈是一种先进后出的数据结构存储的是基本类型和对象的引用(即指向对象在堆中存储位置的指针),每个线程都拥有自己的栈空间,栈的大小固定且较小,由虚拟机自动管理,当栈空间不足时,就会抛出StackOverflowError异常。当一个方法被调用时,它会在栈内存中创建一个栈帧,该栈帧存储了方法的参数、局部变量和方法返回值等信息。当方法执行完成后,该栈帧将被弹出并从栈内存中移除。由于栈内存的数据存储方式是连续的,因此访问速度比较快,同时由于栈帧的动态创建和销毁,栈内存的管理比较容易。

        栈是一种线性数据结构,只能在栈顶进行插入和删除操作。因为在栈中,元素的插入和删除都只会发生在栈顶,因此使用数组结构可以方便地实现这种操作。数组可以直接访问任意位置上的元素,因此在栈顶插入和删除元素时,只需要在数组的末尾进行插入和删除即可,这样可以保证操作的时间复杂度为O(1)。同时,使用数组结构还可以避免因为需要频繁地进行内存分配和释放导致的时间和空间开销,提高了栈的效率。因此,栈通常是使用数组结构来实现的。

1.3 总结

        所有的线程共享同一个堆空间,其中的对象可以被所有线程访问,而栈空间则是独立的,每个线程拥有自己的栈空间。

  1. 当Java程序首先被javac编译成字节码后,通过类加载器加载到内存中,执行引擎会读取这些字节码指令并执行它们。
  2. 当程序启动时,JVM创建一个主线程,并为该线程创建一个虚拟机栈、本地方法栈和程序计数器等私有区域。
  3. 当方法被调用时,执行引擎会创建一个新的栈帧,包含该方法的信息,并将其推入虚拟机栈顶。如果需要创建对象,则在堆中分配内存并指向栈中该对象的引用。静态变量和常量则存储在方法区。
  4. 当执行完成后,虚拟机栈顶的栈帧将被弹出。垃圾回收会在程序执行期间自动运行,回收不再使用的内存。

二、堆会存什么

2.1 Java 类实例对象

        当使用 `new` 关键字创建一个对象时,该对象存储在堆中。

例如:`MyClass obj = new MyClass();` 中的 `obj` 对象就存储在堆中。

2.2 数组对象

        Java 中的数组也是对象,数组对象也存储在堆中。

例如:`int[] arr = new int[5];` 中的 `arr` 数组对象就存储在堆中。

2.3 对象实例的成员变量

        当一个 Java 对象中包含其他对象类型的成员变量时,这些成员变量也存储在堆中。

例如: `MyClass` 类中,`value` 对象就存储在堆中。

public class MyClass {
    private String value;
    // ...
}

2.4 静态变量

        Java 中的静态变量存储在方法区,但是它们的初始值在类加载时会被存储在堆中。

例如: `MyClass` 类中的 `count` 静态变量的初始值就存储在堆中。

public class MyClass {
    private static int count = 0;
    // ...
}

2.5 常量池:

        常量池就比较特殊了。下面给出一些有趣的点

2.5.1 有趣的常量池

        如果字符串是使用 String 类的 literal 创建的,则字符串对象会被存储在常量池中。例如,以下两个字符串都是使用 literal 创建的,因此它们将存储在常量池中:

String str1 = "hello";
String str2 = "hello";

        str1 = "hello" 和 str2 = "hello" 都是字面值形式创建的字符串对象,因此它们会被存储在字符串常量池中。由于它们的值相同,因此它们实际上引用的是同一个字符串常量对象。所以,str1 和 str2 实际上引用的是同一个对象。       


         对于其他方式创建的字符串对象,例如使用 new 运算符或者 String 类的构造函数创建的,字符串对象将存储在堆中。在这种情况下,字符串对象的引用 str3 存储在栈中,而字符串实际的内容存储在堆中。str3 和 str 1就不是一个对象了。例如:

String str3 = new String("hello");

        在这段代码中,字符串 "hello" 会先在常量池中查找是否存在,如果不存在则在常量池中创建一个新的字符串对象,然后再创建一个新的字符串对象,将常量池中的字符串对象的引用传递给这个新的字符串对象。因为使用了 new 关键字,所以这个新的字符串对象会在堆内存中分配空间。所以在这段代码中,"hello" 字符串在常量池中创建了一个对象,并在堆内存中创建了一个新的对象。

三、栈会存什么

        在 Java 中,每个线程都有自己的栈。栈是一种先进后出(Last-In-First-Out,LIFO)的数据结构,一般使用数组或链表实现。当调用一个方法时,会将该方法的参数、局部变量和返回地址等信息压入栈顶,当方法返回时,这些信息从栈中弹出。

好的,下面以Java代码为例,简单介绍一下栈中可能存储的内容。

public class StackExample {
    public static void main(String[] args) {
        int a = 1;
        String b = "hello";
        int[] arr = {1, 2, 3};
        System.out.println(a + b + arr[0]);
    }
}

1. `args`参数数组的引用,它指向一个字符串数组,该字符串数组包含了程序运行时传递给main方法的参数。
2. `a`变量,它是一个基本数据类型的整数,存储在栈中。
3. `b`变量,它是一个字符串对象的引用,存储在栈中,指向堆中的一个字符串对象。
4. `arr`变量,它是一个整数类型的数组对象的引用,存储在栈中,指向堆中的一个整数数组对象。
5. `main`方法的调用栈帧,包含了方法的参数、局部变量、操作数栈等信息。

因为每次调用方法都会有新的栈帧,所以栈也有些面试点

3.1 有趣的栈

public static void main(String[] args) {
    MyClass obj = new MyClass();
    obj.setValue(5);
    System.out.println(obj.getValue()); // 输出5

    foo(obj);
    System.out.println(obj.getValue()); // 输出10
}

public static void foo(MyClass obj) {
    //obj = new MyClass();
    obj.setValue(10);
}

        在这个代码中,我们定义了一个MyClass类,它包含一个整数属性value以及对该属性的访问方法。在main方法中,我们创建了一个MyClass对象obj并将其value属性设置为5。然后我们调用了一个名为foo的方法,将obj作为参数传递给该方法。在foo方法内部,我们将obj所指向的对象的value属性设置为10。在main方法中,我们再次输出objvalue属性,此时输出的结果为10。


public static void main(String[] args) {
    MyClass obj = new MyClass();
    obj.setValue(5);
    System.out.println(obj.getValue()); // 输出5

    foo(obj);
    System.out.println(obj.getValue()); // 输出5
}

public static void foo(MyClass obj) {
    obj = new MyClass();
    obj.setValue(10);
}

        在这个代码中,我们定义了一个 MyClass 类,它包含一个整数属性 value 以及对该属性的访问方法。在 main 方法中,我们创建了一个 MyClass 对象 obj 并将其 value 属性设置为5。然后我们调用了一个名为 foo 的方法,将 obj 作为参数传递给该方法。在 foo 方法内部,我们将 obj 赋予了新的引用,然后给新的引用 obj 赋值。这个时候其实已经是 2 个对象了。在 foo 被调用完成,他的栈帧被销毁,所以原对象没有变化,输出 5。

四、总结

        面试的时候一定要记得栈和堆的概念和基本结构

存放线程私有数据 存放共享数据
数组或链表实现 树型结构
访问速度相对较快 访问速度相对较慢

        另外也要注意,在Java中,无论是基本数据类型还是自定义对象,参数传递都是值传递。这意味着当我们将一个自定义对象作为参数传递给一个方法时,实际上传递的是一个引用的副本。当方法内部修改该引用指向的对象的属性时,会影响到原始引用所指向的对象。但是当方法内部将引用指向一个新的对象时,不会影响到原始引用所指向的对象。 

猜你喜欢

转载自blog.csdn.net/qq_37761711/article/details/130569458