Java内存区域和内存模型

写在最前

Java 内存区域和内存模型是不一样的东西。

内存区域:JVM 运行时将数据分区域存储,强调对内存空间的划分。

内存模型(Java Memory Model,简称 JMM ):定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。

内存区域

下图是 JDK 1.8 之前的 JVM 运行时数据区域分布图:

image-20220108134140352

下图是 JDK 1.8 之后的 JVM 运行时数据区域分布图:

image-20220108132735115

通过 JDK 1.8 之前与 JDK 1.8 之后的 JVM 运行时数据区域分布图对比,我们可以发现区别就是 1.8有一个元空间替代方法区。下文元空间章节介绍了为何替换方法区

下面我们针对 JDK 1.8 之后的 JVM 内存分布图介绍每个区域的它们是干什么的。

本地方法栈

Native Method Stacks:是为虚拟机使用到的 Native 方法服务,可以认为是通过 JNI (Java Native Interface) 直接调用本地 C/C++ 库,不受 JVM 控制。

我们常用获取当前时间毫秒就是 Native 本地方法,方法被 native 关键字修饰。

package java.lang;

public final class System {
    public static native long currentTimeMillis();
}

其实就是为了解决一些 Java 本身做不到,但是 C/C++ 可以,通过 JNI 扩展 Java 的使用,融合不同的编程语言。

程序计数器

Program Counter Register:一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。由于 JVM 可以并发执行线程,所以会为每个线程分配一个程序计数器,与线程的生命周期相同。因此会存在线程之间的切换,而这个时候就程序计数器会记录下当前程序执行到的位置,以便在其他线程执行完毕后,恢复现场继续执行。

如果线程正在执行的是 Java 方法,这个计数器记录的是正在执行虚拟机字节码指令的地址;如果正在执行的是 Native 方法,计数器的值则为空(undefined)

此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

Java虚拟机栈

Java Virtual Machine Stacks:与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。

虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表操作栈动态链接方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

栈是一个先入后出(FILO-First In Last Out)的有序列表。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。

image-20220108165202245

局部变量表

局部变量表是存放方法参数局部变量的区域。局部变量没有准备阶段,必须显式初始化。全局变量是放在堆的,有两次赋值的阶段,一次在类加载的准备阶段,赋予系统初始值;另外一次在类加载的初始化阶段,赋予代码定义的初始值。

操作栈

操作栈是个初始状态为空的桶式结构栈(先入后出)。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。

动态链接

每个栈帧都包含一个指向运行时常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。当前方法中如果需要调用其他方法的时候,能够从运行时常量池中找到对应的符号引用,然后将符号引用转换为直接引用,然后就能直接调用对应方法。

不是所有方法调用都需要动态链接的,有一部分符号引用会在类加载解析阶段将符号引用转换为直接引用,这部分操作称之为: 静态解析,就是编译期间就能确定调用的版本,包括: 调用静态方法, 调用实例的私有构造器, 私有方法,父类方法。

方法返回地址

方法执行时有两种退出情况:

  1. 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
  2. 异常退出。

无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧。

我们经常说的 GC 调优/JVM 调优,99%指的都是调堆!Java 栈、本地方法栈、程序计数器这些一般不会产生垃圾。

Heap:Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。

元空间

JDK 1.8就把方法区改用元空间了。类的元信息被存储在元空间中,元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大。

方法区

Method Area:与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

方法区与元空间的变迁

下图是 JDK 1.6、JDK 1.7 到 JDK 1.8 方法区的大致变迁过程:

image-20220108181728163

JDK 1.8 中 HotSpot JVM 移出 永久代(PermGen),开始时使用元空间(Metaspace)。使用元空间取代永久代的实现的主要原因如下:

  1. 避免OOM异常,字符串存在永久代中,容易出现性能问题和内存溢出;
  2. 永久代设置空间大小是很难确定,太小容易出现永久代溢出,太大则容易导致老年代溢出;
  3. 永久代进行调优非常困难;
  4. 将 HotSpot 与 JRockit 合二为一;

内存模型

内存模型是为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。

内存模型解决并发问题主要采用两种方式:限制处理器优化使用内存屏障

Java 内存模型(JMM)控制 Java 线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见。

计算机高速缓存和缓存一致性

计算机在高速的 CPU 和相对低速的存储设备之间使用高速缓存,作为内存和处理器之间的缓冲。当程序在运行过程中,会将运算需要的数据从主存(计算机的物理内存)复制一份到 CPU 的高速缓存当中,那么 CPU 进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

在多处理器的系统中(或者单处理器多核的系统),每个处理器内核都有自己的高速缓存,它们有共享同一主内存(Main Memory)。当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到,再从二级缓存中查找,如果还是没有就从三级缓存(不是所有 CPU 都有三级缓存)或内存中查找。

image-20220108212123524

在多核 CPU 中,每个核在自己的缓存中,关于同一个数据的缓存内容可能不一致。为此,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,来维护缓存的一致性。

JVM 主内存与工作内存

Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

这里的工作内存是 JMM 的一个抽象概念,其存储了该线程以读 / 写共享变量的副本

image-20220108220654439

重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  1. 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序:由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

image-20220108221516167

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

Java 编译器禁止处理器重排序是通过在生成指令序列的适当位置会插入内存屏障(重排序时不能把后面的指令重排序到内存屏障之前的位置)指令来实现的。

happens-before

Java 内存模型提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

Java内存模型的实现

在Java中提供了一系列和并发处理相关的关键字,比如 volatilesynchronizedfinalJUC 包等。其实这些就是 Java 内存模型封装了底层的实现后提供给程序员使用的一些关键字。

原子性

为了保证原子性,提供了两个高级的字节码指令 monitorentermonitorexit,这两个字节码,在Java中对应的关键字就是 synchronized

我们对 synchronized 关键字都很熟悉,你们可以把下面的代码编译成 class 文件,用 javap -v SyncViewByteCode.class 查看字节码,就可以找到 monitorentermonitorexit 字节码指令。

public class SyncViewByteCode {
  public synchronized void buy() {
    System.out.println("buy porsche");
  }
}

字节码,部分结果如下:

 public com.dolphin.thread.locks.SyncViewByteCode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/dolphin/thread/locks/SyncViewByteCode;

  public void test2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
      Exception table:
可见性

Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。

Java 中的 volatile 关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用 volatile 来保证多线程操作时变量的可见性。

除了 volatile,Java中的 synchronizedfinal 两个关键字也可以实现可见性。只不过实现方式不同。

有序性

在 Java 中,可以使用 synchronizedvolatile 来保证多线程之间操作的有序性。实现方式有所区别:

  • volatile:关键字会禁止指令重排。
  • synchronized:关键字保证同一时刻只允许一条线程操作。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Strive_MY

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值