# JVM中的堆和栈

为了让程序可以以最佳的状态运行,java把内存分为了堆和栈。每当我们声明新的对象和变量,调用新的方法,声明字符串或执行更多此类相似的操作,JVM都会从堆或栈中为这些操作分配空间。

本文将讨论这两个内存模型。我将会列举堆和栈的几个关键的不同,以及阐述它们是如何存储在物理内存中的,还有它们提供给我们的一些特性,何时去使用它们。

# java中的栈

java中的栈被用于静态内存分配和执行线程。 它包含函数中的原始类型值,和函数中的引用,这些引用指向的对象存储在堆中。

访问栈中内存需要按照后进先出的顺序。每当调用一个新方法时,栈顶就会创建一个新的区块,这个区块包含了和这个函数有关的一些特定的值,例如原始类型变量和对象的引用。

该方法执行完毕后,将刷新相应的堆帧,数据回流到所调用方法中,该区块被清空,为下一个要执行的函数腾出空间。

# 栈的关键特性

除了上文讨论的内容,栈还有如下特性:

  • 随着新方法的调用和返回,栈的大小会自动增长和收缩。
  • 栈中的变量仅在创建这些变量的方法运行时存在。
  • 方法执行完毕后栈内空间会自动分配和释放。
  • 如果栈内存已满,java会丢出java.lang.StackOverFlowError异常。
  • 相比与访问堆内存,访问栈内存更快。
  • 栈内存是线程安全的,因为每个线程只操作自己的栈。

# java中的堆

java中的堆空间被用于java对象和JRE类在运行时的动态内存分配。 新的对象总是在堆中创建,并且该对象的引用被存储在栈中。

这些对象具有全局访问属性,可以从程序的任何位置被访问。

在之前的JVM实现中堆中会被细分为世代以便于GC回收内存,但是新的ZGC已经不通过世代来回收内存,每次GC都会标记整个堆空间。故有关世代的内容本文不再阐述。

# 堆的关键特性

除了上文提到的内容,堆还有如下特性:

  • 如果堆已满,会抛出java.lang.OutOfMemoryError异常
  • 对该堆内存的访问会比栈内存慢
  • 与栈相比,堆空间不会自动释放。它需要GC去清空无用的对象,以保持内存使用的效率
  • 与栈不同,堆不是线程安全的,并且需要被正确的同步代码保护

# 代码示例

基于上文的知识点,来分析一段简单的kotlin代码吧!

class Person(pid: Int = 0,name: String? = null)

fun main() {
    val id = 23
    val pName = "Jon"
    var p: Person? = null
    p = Person(id, pName)
}
  1. 进入main() 方法后,栈会创建空间来存放原始类型和对象的引用。
  • 整数的原始类型值id 会被直接存到栈内。
  • Person 类型的变量p 的引用也会被存于栈空间,该引用会指向堆中的实际对象
  1. main() 中调用带参构造函数Person(Int,String?) 将会在栈顶分配内存。用于存储:
  • this 对象的引用
  • 原始类型值id
  • 参数personNameString? 类型的变量的引用,将会指向堆内存中的字符串池中的实际字符串
  1. 此默认构造函数将进一步调用隐式的setName() 方法,栈顶也会为这个方法分配内存,然后以上文提到的方式再一次存储变量。
  2. 对于新创建的Person 类型的对象p ,所有的实例变量将存到堆内存中。

整个分配如下图所示:

Stack-Memory-vs-Heap-Space-in-Java

# 总结

参数
应用场景 栈在局部使用,在线程执行期间一次只访问一个栈 整个程序在运行时都会使用堆空间
容量大小 根据os不同,栈有大小限制,通常比堆低 堆没有大小限制
存储 只存储原始类型变量和堆中对象的引用 存储所有新创建的对象
生命周期 栈只存在于当前方法运行的时候 只要程序在运行,堆空间就存在
分配效率 相较于堆分配较快 相较与栈分配较慢
分配/释放 当方法被调用时和返回时,内存自动分配和释放 当新对象被创建时,分配堆空间,当对象不再被引用时,GC释放空间