Java 内存模型
Java 内存模型规定了 JVM 应该如何使用计算机内存(RAM)。 广义来讲, Java 内存模型分为两个部分:
- JVM 内存结构
- JMM 与线程规范
其中,JVM 内存结构是底层实现,也是我们理解和认识 JMM 的基础。 大家熟知的堆内存、栈内存等运行时数据区的划分就可以归为 JVM 内存结构。
JVM 内部使用的 Java 内存模型, 在逻辑上将内存划分为 线程栈
(thread stacks)和堆内存
(heap)两个部分。 如下图所示:
JVM 中,每个正在运行的线程,都有自己的线程栈。 线程栈包含了当前正在执行的方法链/调用链上的所有方法的状态信息。
所以线程栈又被称为“方法栈
”或“调用栈
”(call stack)。线程在执行代码时,调用栈中的信息会一直在变化。
线程栈里面保存了调用链上正在执行的所有方法中的局部变量。
- 每个线程都只能访问自己的线程栈。
- 每个线程都不能访问(看不见)其他线程的局部变量。
即使两个线程正在执行完全相同的代码,但每个线程都会在自己的线程栈内创建对应代码中声明的局部变量。 所以每个线程都有一份自己的局部变量副本。
- 所有原生类型的局部变量都存储在线程栈中,因此对其他线程是不可见的。
- 线程可以将一个原生变量值的副本传给另一个线程,但不能共享原生局部变量本身。
- 堆内存中包含了 Java 代码中创建的所有对象,不管是哪个线程创建的。 其中也涵盖了包装类型(例如
Byte
,Integer
,Long
等)。 - 不管是创建一个对象并将其赋值给局部变量, 还是赋值给另一个对象的成员变量, 创建的对象都会被保存到堆内存中。
下图演示了线程栈上的调用栈和局部变量,以及存储在堆内存中的对象:
- 如果是原生数据类型的局部变量,那么它的内容就全部保留在线程栈上。
- 如果是对象引用,则栈中的局部变量槽位中保存着对象的引用地址,而实际的对象内容保存在堆中。
- 对象的成员变量与对象本身一起存储在堆上, 不管成员变量的类型是原生数值,还是对象引用。
- 类的静态变量则和类定义一样都保存在堆中。
总结一下:原始数据类型和对象引用地址在栈上;对象、对象成员与类定义、静态变量在堆上。
堆内存又称为“共享堆
”,堆中的所有对象,可以被所有线程访问, 只要他们能拿到对象的引用地址。
- 如果一个线程可以访问某个对象时,也就可以访问该对象的成员变量。
- 如果两个线程同时调用某个对象的同一方法,则它们都可以访问到这个对象的成员变量,但每个线程的局部变量副本是独立的。
示意图如下所示:
总结一下:虽然各个线程自己使用的局部变量都在自己的栈上,但是大家可以共享堆上的对象,特别地各个不同线程访问同一个对象实例的基础类型的成员变量,会给每个线程一个变量的副本。
先看看栈内存(Stack)的大体结构:
每启动一个线程,JVM 就会在栈空间栈分配对应的线程栈, 比如 1MB 的空间(-Xss1m
)。
线程栈也叫做 Java 方法栈。 如果使用了 JNI 方法,则会分配一个单独的本地方法栈(Native Stack)。
线程执行过程中,一般会有多个方法组成调用栈(Stack Trace), 比如 A 调用 B,B 调用 C……每执行到一个方法,就会创建对应的栈帧(Frame)。
栈帧是一个逻辑上的概念,具体的大小在一个方法编写完成后基本上就能确定。
比如 返回值
需要有一个空间存放吧,每个局部变量
都需要对应的地址空间,此外还有给指令使用的 操作数栈
,以及 class 指针(标识这个栈帧对应的是哪个类的方法, 指向非堆里面的 Class 对象)。
Java 程序除了栈内存之外,最主要的内存区域就是堆内存了。
堆内存是所有线程共用的内存空间,理论上大家都可以访问里面的内容。
但 JVM 的具体实现一般会有各种优化。比如将逻辑上的 Java 堆,划分为堆(Heap)
和非堆(Non-Heap)
两个部分.。这种划分的依据在于,我们编写的 Java 代码,基本上只能使用 Heap 这部分空间,发生内存分配和回收的主要区域也在这部分,所以有一种说法,这里的 Heap 也叫 GC 管理的堆(GC Heap)。
GC 理论中有一个重要的思想,叫做分代。 经过研究发现,程序中分配的对象,要么用过就扔,要么就能存活很久很久。
因此,JVM 将 Heap 内存分为年轻代(Young generation)和老年代(Old generation, 也叫 Tenured)两部分。
年轻代还划分为 3 个内存池,新生代(Eden space)和存活区(Survivor space), 在大部分 GC 算法中有 2 个存活区(S0, S1),在我们可以观察到的任何时刻,S0 和 S1 总有一个是空的, 但一般较小,也不浪费多少空间。
具体实现对新生代还有优化,那就是 TLAB(Thread Local Allocation Buffer), 给每个线程先划定一小片空间,你创建的对象先在这里分配,满了再换。这能极大降低并发资源锁定的开销。
Non-Heap 本质上还是 Heap,只是一般不归 GC 管理,里面划分为 3 个内存池。
- Metaspace, 以前叫持久代(永久代, Permanent generation), Java8 换了个名字叫 Metaspace. Java8 将方法区移动到了 Meta 区里面,而方法又是class的一部分和 CCS 交叉了?
- CCS, Compressed Class Space, 存放 class 信息的,和 Metaspace 有交叉。
- Code Cache, 存放 JIT 编译器编译后的本地机器代码。
JVM 的内存结构大致如此。 掌握了这些基础知识,我们再来看看 JMM。