Java内存模型
Java 内存模型
Java内存模型缩写为JMM,全称是Java内存模型。Java内存模型指定了JVM应该如何使用计算机内存。广义地说,Java内存模型分为两部分:
JVM内存结构
JMM和螺纹规范
其中,JVM内存结构是底层实现,也是我们了解和认识JMM的基础。众所周知的运行时数据区的划分,如堆内存和堆栈内存,可以归类为JVM内存结构。
JVM 内存结构
我们先来看看JVM的整体内存概念图:
JVM内部使用的Java内存模型在逻辑上将内存划分为线程栈和堆内存。如下图所示:
JVM中,每个运行的线程都有自己的线程栈。线程堆栈包含当前执行的方法链/调用链中所有方法的状态信息。
所以线程栈也被称为“方法栈”或“调用栈”。当线程执行代码时,调用堆栈中的信息总是会改变。
调用链中正在执行的所有方法中的局部变量都存储在线程堆栈中。
每个线程只能访问自己的线程堆栈。
每个线程都不能访问(看不到)其他线程的局部变量。
即使两个线程正在执行相同的代码,每个线程都会在自己的线程堆栈中创建在相应代码中声明的局部变量。所以每个线程都有自己的局部变量副本。
所有本机类型的局部变量都存储在线程堆栈中,因此其他线程看不到它们。
一个线程可以将本机变量的值的副本传递给另一个线程,但是它不能共享本机局部变量本身。
堆包含所有用Java代码创建的对象,不管是哪个线程创建的。它还涵盖了封装类型(如字节、整数、长等)。).
无论您是创建一个对象并将其分配给局部变量,还是将其分配给另一个对象的成员变量,创建的对象都将保存到堆内存中。
下图说明了线程堆栈上的调用堆栈和局部变量,以及存储在堆内存中的对象:
如果它是本机数据类型的局部变量,它的所有内容都将保留在线程堆栈上。
如果是对象引用,堆栈中的局部变量槽保存对象的引用地址,而实际的对象内容存储在堆中。
对象的成员变量与对象本身一起存储在堆中,无论成员变量的类型是本机值还是对象引用。
类的静态变量存储在堆中,就像类定义一样。
总结一下:原始数据类型和对象引用地址都在栈上;对象、对象成员和类定义以及静态变量都在堆上。
堆内存也称为“共享堆”。堆中的所有对象都可以被所有线程访问,只要它们可以获得对象的引用地址。
如果一个线程可以访问一个对象,它也可以访问该对象的成员变量。
如果两个线程同时调用一个对象的同一个方法,那么它们都可以访问该对象的成员变量,但是每个线程的局部变量副本是独立的。
示意图如下:
总结一下:虽然每个线程使用的局部变量都在自己的栈上,但是每个人都可以共享栈上的对象,尤其是当不同线程访问同一个对象实例的基本类型成员变量时,每个线程都会得到一个变量的副本。
栈内存的结构
当每个线程启动时,JVM会在栈空间栈中分配相应的线程栈,例如1MB的空间(-Xss1m)。
线程栈也叫Java方法栈。如果使用JNI方法,则会分配一个单独的本机方法堆栈。
在线程执行过程中,通常有几种方法形成调用Stack Trace,例如A调用B,B调用C.每次执行一个方法,都会创建一个相应的堆栈框架。
栈帧是一个逻辑概念,具体大小基本上可以在一个方法写完之后确定。
例如,返回值需要一个存储空间。每个局部变量都需要一个对应的地址空间。此外,还有一个用于指令的操作数堆栈和一个类指针(一种标识该堆栈框架对应于哪个类并指向非堆中的类对象的方法)。
堆内存的结构
堆内存是所有线程共享的内存空间。理论上,每个人都可以访问它的内容。
但是,JVM的具体实现一般都有各种优化。例如,逻辑Java堆分为堆和非堆。这种划分是基于我们编写的Java代码基本上只能使用这部分Heap空间,内存分配和回收发生的主要区域也在这部分,所以有一种说法,这里的Heap也叫GC Heap。
气相色谱理论中有一个重要的概念,叫做世代。经过研究发现,程序中分配的对象要么在使用后被扔掉,要么可以
存活很久很久。
因此,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 的内存结构大致如此。
指令重排序
计算机按支持的指令大致可以分为两类:
- 精简指令集计算机(RISC), 代表是如今大家熟知的 ARM 芯片,功耗低,运算能力相对较弱。
- 复杂指令集计算机(CISC), 代表作是 Intel 的 X86 芯片系列,比如奔腾,酷睿,至强,以及 AMD 的 CPU。特点是性能强劲,功耗高。(实际上从奔腾 4 架构开始,对外是复杂指令集,内部实现则是精简指令集,所以主频才能大幅度提高)
不管哪一种指令集,CPU 的实现都是采用流水线的方式。如果 CPU 一条指令一条指令地执行,那么很多流水线实际上是闲置的。于是硬件设计人员就想出了一个好办法: “指令乱序”。 CPU 完全可以根据需要,通过内部调度把这些指令打乱了执行,充分利用流水线资源,只要最终结果是等价的,那么程序的正确性就没有问题。但这在如今多 CPU 内核的时代,随着复杂度的提升,并发执行的程序面临了很多问题。
内存屏障简介
前面提到了CPU会在合适的时机,按需要对将要进行的操作重新排序,但是有时候这个重排机会导致我们的代码跟预期不一致。
怎么办呢JMM 引入了内存屏障机制。
内存屏障(Memory Barrier)与内存栅栏(Memory Fence)是同一个概念,不同的叫法。
通过volatile标记,可以解决编译器层面的可见性与重排序问题。而内存屏障则解决了硬件层面的可见性与重排序问题。
先简单了解两个指令:
- Store:将处理器缓存的数据刷新到内存中。
- Load:将内存存储的数据拷贝到处理器的缓存中。
内存屏障可分为读屏障和写屏障,用于控制可见性。 常见的内存屏障包括:
LoadLoad StoreStore LoadStore StoreLoad
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作 |
StoreStore Barriers | Store1;StoreStore;Store2 | 该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作 |
LoadStore Barriers | Load1;LoadStore;Store2 | 确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作 |
StoreLoad Barriers | Store1;StoreLoad;Load2 | 该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令 |
这些屏障的主要目的,是用来短暂屏蔽 CPU 的指令重排序功能。 和 CPU 约定好,看见这些指令时,就要保证这个指令前后的相应操作不会被打乱。
- 比如看见LoadLoad, 那么屏障前面的 Load 指令就一定要先执行完,才能执行屏障后面的 Load 指令。
- 比如我要先把 a 值写到 A 字段中,然后再将 b 值写到 B 字段对应的内存地址。如果要严格保障这个顺序,那么就可以在这两个 Store 指令之间加入一个 StoreStore屏障。
- 遇到LoadStore屏障时, CPU 自废武功,短暂屏蔽掉指令重排序功能。
- StoreLoad屏障, 能确保屏障之前执行的所有 store 操作,都对其他处理器可见; 在屏障后面执行的 load 指令, 都能取得到最新的值。换句话说, 有效阻止屏障之前的 store 指令,与屏障之后的 load 指令乱序 、即使是多核心处理器,在执行这些操作时的顺序也是一致的。
代价最高的是 StoreLoad屏障, 它同时具有其他几类屏障的效果,可以用来代替另外三种内存屏障。
如何理解呢
就是只要有一个 CPU 内核收到这类指令,就会做一些操作,同时发出一条广播, 给某个内存地址打个标记,其他 CPU 内核与自己的缓存交互时,就知道这个缓存不是最新的,需要从主内存重新进行加载处理。
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/114067.html