杰瑞科技汇

Java heap和stack有何区别?

核心概念:一句话概括

  • :是线程私有的,生命周期与线程相同,它像一个“数据结构栈”(后进先出 LIFO),存储的是方法调用基本数据类型变量,速度快,但空间小。
  • :是所有线程共享的一块大内存区域,它存储的是对象实例数组,速度相对较慢,但空间大,是垃圾回收的主要区域。

深入理解:栈

栈是什么?

栈是 JVM 中为每个线程创建的私有的内存区域,你可以把它想象成一叠盘子,你最后放上去的盘子,会最先被拿走(这就是后进先出,LIFO - Last-In, First-Out)。

Java heap和stack有何区别?-图1
(图片来源网络,侵删)

栈里存什么?

栈主要存储两样东西:

a) 栈帧 每当一个方法被调用时,JVM 会为该方法创建一个“栈帧”(Stack Frame),并将其压入栈中,当一个方法执行完毕,对应的栈帧就会从栈中弹出。 一个栈帧包含了:

  • 局部变量表:存储方法中定义的基本数据类型int, double, boolean 等)和对象的引用Object reference),注意,它只存引用,不存对象本身。
  • 操作数栈:像一个临时的计算区域,用于执行字节码指令时的数据计算和传递。
  • 动态链接:指向运行时常量池的引用,用于解析方法调用。
  • 方法返回地址:方法执行完成后,程序应该从哪里继续执行。

b) 基本数据类型变量 你在方法里写 int a = 10;,这个变量 a 和它的值 10 就直接存储在栈帧的局部变量表中。

栈的特点

  • 线程私有:每个线程都有自己独立的栈,线程之间无法访问对方的栈数据,因此是线程安全的。
  • 生命周期:与线程的生命周期绑定,线程创建时,栈被创建;线程结束时,栈被销毁。
  • 速度快:内存分配和回收速度非常快,因为它只是简单的压栈和出栈操作,类似于数据结构中的栈。
  • 空间较小:栈的内存大小是固定的(可以通过 -Xss 参数设置),通常只有几兆,如果线程请求的栈深度过大(无限递归),就会抛出 StackOverflowError
  • 数据共享性差:数据只在当前线程的方法调用链中可见。

深入理解:堆

堆是什么?

堆是 JVM 中最大的一块内存区域,是所有线程共享的,它就像一个大型的“对象仓库”,所有的对象实例和数组几乎都是在堆上分配内存的。

Java heap和stack有何区别?-图2
(图片来源网络,侵删)

堆里存什么?

  • 对象实例:通过 new 关键字创建的所有对象都存储在堆中。new Person()
  • 数组:数组也是对象,所以也存储在堆中。new int[10]
  • 成员变量:对象中的非静态成员变量也随对象一起存储在堆中。

堆的特点

  • 所有线程共享:堆中的内存可以被所有线程访问,当多个线程同时访问同一个堆中的对象时,需要考虑同步问题(线程不安全)。
  • 生命周期:堆的生命周期与 JVM 的生命周期相同,只要 JVM 还在运行,堆就存在。
  • 速度较慢:堆的内存分配和回收比栈要慢得多,因为堆是动态分配的,需要寻找合适的内存空间,并且有复杂的垃圾回收机制在工作。
  • 空间较大:堆是 JVM 内存中最大的一块区域,可以通过 -Xms(初始大小)和 -Xmx(最大大小)参数来调整。
  • 垃圾回收的主要区域:堆是垃圾收集器管理的主要区域,当一个对象不再被任何引用指向时,GC 就会回收它所占用的内存。

堆的内部结构(现代 JVM 中的优化)

为了提高内存分配和回收的效率,现代 JVM 的堆内部并不是一整块,而是分成了几个不同的区域,最常见的是分代模型:

  • 新生代
    • Eden 区:新创建的对象首先在这里分配。
    • Survivor 区 (S0, S1):经过一次 GC 后仍然存活的对象会被移到这里,两个 Survivor 区交替使用。
    • 特点:大部分对象在新生代创建后很快就不再使用,GC 非常频繁,速度很快。
  • 老年代
    • 特点:在新生代中存活了足够多次(达到“年龄”阈值)的对象会被“晋升”到老年代,老年代的 GC(如 Full GC)频率较低,但耗时较长。
  • 元空间
    • 在 JDK 1.8 及之后,永久代元空间取代,元空间并不在虚拟机中,而是直接使用本地内存。
    • 作用:存储类的元数据信息(如类名、字段、方法信息、常量池等)。
    • 优点:避免了永久代可能出现的 OutOfMemoryError,因为本地内存不受 JVM 最大堆大小的限制。

代码示例:直观感受

让我们通过一段代码,看看 intObject 和它们的引用分别在哪里。

public class MemoryDemo {
    public static void main(String[] args) {
        // 1. main 方法被调用,一个栈帧被压入 main 线程的栈中。
        // 2. a 是基本数据类型,变量 a 和值 10 直接存储在 main 方法的栈帧(局部变量表)中。
        int a = 10; 
        // 3. myObject 是一个引用变量,它本身存储在 main 方法的栈帧(局部变量表)中。
        //    它指向的对象(new Object())是存储在堆内存中的。
        Object myObject = new Object();
        // 4. 调用 method1,会为 method1 创建一个新的栈帧,并压入栈顶。
        method1(a, myObject);
    }
    public static void method1(int paramA, Object paramObject) {
        // 5. paramA 是基本数据类型,是 main 中 a 的一个副本,存储在 method1 的栈帧中。
        //    修改 paramA 不会影响 main 中的 a。
        paramA = 20;
        // 6. paramObject 是一个引用变量,是 main 中 myObject 的一个副本(指向同一个堆对象)。
        //    它存储在 method1 的栈帧中,通过 paramObject 修改堆中对象的内容,
        //    main 中的 myObject 也能看到变化,因为它们指向同一个对象。
        paramObject = new Object(); // 这里只是让 paramObject 指向了一个新对象,不影响 main 中的 myObject
    }
}

内存分布图解:

+-------------------------+  <-- JVM 启动
|        Heap (堆)        |  <-- 所有线程共享
|-------------------------|
|  new Object() (myObject指向) |
|  new Object() (paramObject新指向) |
+-------------------------+
|         Stack (栈)      |  <-- main 线程私有
|-------------------------|  <-- 栈顶 (最新)
|  method1 栈帧           |
|  - paramA: 20           |  <-- 基本类型值的副本
|  - paramObject: 引用地址 |  <-- 引用变量的副本
|-------------------------|
|  main 栈帧              |
|  - a: 10                |  <-- 基本类型值
|  - myObject: 引用地址    |  <-- 引用变量
+-------------------------+

核心区别与总结

特性
目的 存储方法调用和局部变量 存储对象实例和数组
共享性 线程私有,线程安全 所有线程共享,非线程安全
生命周期 与线程相同 与 JVM 相同
速度 (压栈/出栈) (动态分配, GC)
大小 小 (固定, -Xss) 大 (可调整, -Xms, -Xmx)
异常 StackOverflowError (栈溢出) OutOfMemoryError (OOM)
基本数据类型、对象引用、方法调用 对象实例、数组、成员变量
管理方式 编译器自动管理,无需 GC 垃圾回收器 自动管理

常见面试题

Q1: String str = new String("hello"); 这行代码在内存中做了什么?

Java heap和stack有何区别?-图3
(图片来源网络,侵删)

A:

  1. 中,字符串常量池中检查是否存在 "hello" 这个字符串,如果不存在,则创建一个 "hello" 对象放入池中。
  2. 非池区域(普通堆内存)中,再创建一个新的 String 对象,并用池中的 "hello" 来初始化它。
  3. 中,为 str 这个局部变量分配空间,并将指向堆中那个新创建的 String 对象的引用赋值给 str

栈中一个引用,堆中两个对象(一个在池,一个不在池),这就是 new String() 和直接赋值的区别。

Q2: 什么时候会发生 StackOverflowError?什么时候会发生 OutOfMemoryError

A:

  • StackOverflowError

    • 原因:线程的栈深度超过了其分配的最大深度。
    • 场景:最常见的场景是无限或过深的递归调用,因为每次递归调用都会在栈上压入一个新的栈帧,当递归无法终止时,栈最终会溢出。
    • 例子
      public void infiniteRecursion() {
          infiniteRecursion(); // 无限递归
      }
  • OutOfMemoryError

    • 原因:JVM 没有足够的内存来为新的对象分配空间,并且垃圾回收器也无法释放出足够的内存。
    • 场景
      1. 堆内存溢出:创建了大量对象,且这些对象都存活(被引用),导致堆空间被耗尽,这是最常见的原因。
      2. 方法区/元空间溢出:加载了过多的类,导致元空间被占满。
      3. 本地内存溢出:使用了 NIO 直接分配了本地内存(DirectByteBuffer),超出了物理内存限制。

希望这个详细的解释能帮助你彻底理解 Java 的堆和栈!

分享:
扫描分享到社交APP
上一篇
下一篇