跳到主要内容

Java内存模型

Java内存模型

Java内存模型(Java Memory Model, JMM)定义了Java程序中线程如何交互和访问共享数据,旨在保证在并发编程中程序的正确性和一致性。JMM主要关注的是以下几个方面:

内存可见性

内存可见性问题涉及一个线程修改了一个变量的值后,另一个线程能否及时看到这个变化。在JMM中,所有变量都存储在主内存中,每个线程有自己的工作内存(或线程栈),线程对变量的操作都在工作内存中进行,然后同步到主内存。

顺序一致性

JMM规定了在不违反正确性的前提下,编译器和处理器可以对指令进行重排序(即改变代码执行的顺序)。但是,对于不同线程的执行结果来说,程序应该看起来像是按照某种顺序执行的,这个顺序由happens-before关系定义。

Happens-Before关系

Happens-before是JMM中的一个重要概念,用来定义操作之间的顺序性。Happens-before的规则如下:

  • 程序次序规则: 在一个线程内,按照程序的控制流顺序,先后发生的每个操作,前一个操作happens-before后一个操作。
  • 监视器锁规则: 对一个锁的解锁happens-before随后对这个锁的加锁。
  • volatile变量规则: 对一个volatile变量的写操作happens-before后续对这个volatile变量的读操作。
  • 线程启动规则: 对一个线程的start()方法的调用happens-before该线程中的任意操作。
  • 线程终止规则: 线程中的任意操作happens-before其他线程检测到该线程已经终止。

volatile变量

volatile关键字保证了变量的可见性,即一个线程对volatile变量的写操作对其他线程立即可见。此外,它还禁止了对该变量的指令重排序,保证了有序性。

原子性、可见性、有序性

  • 原子性: 指一系列操作要么全部执行,要么全部不执行。JMM保证了基本数据类型的读取和写入的原子性。
  • 可见性: 指一个线程对共享变量的修改,能及时被其他线程看到。volatilesynchronizedfinal等关键字可以提供不同程度的可见性保证。
  • 有序性: 通过happens-before规则来确保。JMM允许编译器和处理器对指令进行优化和重排序,但这种优化不能破坏程序的正确性。

Java内存区域

Java的内存区域在JVM中主要分为几个重要的部分:堆(Heap)、方法区(Method Area)、虚拟机栈(Java Virtual Machine Stack)、本地方法栈(Native Method Stack)和程序计数器(Program Counter Register)。

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存 (非运行时数据区的一部分)

程序计数器

程序计数器(Program Counter Register): 程序计数器是一个小内存空间,指向下一条需要执行的指令地址。它帮助CPU了解下一步需要执行什么操作。

  • 跟踪指令执行: 程序计数器保存当前线程的下一条需要执行的指令地址。这在多线程环境中特别重要,因为每个线程在切换时需要知道它上次执行到哪一条指令,以便继续执行。
  • 辅助分支、循环控制: 在执行跳转指令、分支或循环时,程序计数器会更新其内容,以指向新的一条指令地址。
  • 线程恢复与调度: 在上下文切换时,程序计数器帮助线程恢复到正确的执行位置,从而维持线程的正确性和独立性。

Java虚拟机栈

虚拟机栈(Java Virtual Machine Stack): 每个线程在执行时都有一个独立的虚拟机栈,栈帧存储了局部变量表、操作数栈、动态链接、方法出口等信息。虚拟机栈主要用于管理Java方法的调用和执行过程。

  • 栈帧管理: 每个方法调用时,都会创建一个栈帧(Stack Frame),栈帧包含了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。栈帧在方法调用时入栈,方法执行结束后出栈。
  • 局部变量表: 局部变量表存储了方法执行过程中的所有局部变量,包括基本数据类型和对象引用。这些变量表是线程私有的,不会被其他线程访问。
  • 操作数栈: 操作数栈用于临时存储方法执行过程中产生的中间结果和操作数。Java虚拟机指令集是基于栈的机器,很多操作需要通过操作数栈进行。
  • 动态链接: 栈帧包含指向运行时常量池中方法引用的指针,用于支持方法调用过程中的动态链接。
  • 方法返回地址: 当方法执行完成后,虚拟机栈会根据方法返回地址恢复上一个方法的执行状态。

本地方法栈

本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈类似,只不过它为本地方法服务。它主要用于Native方法调用时的堆栈操作。

  • 本地方法调用支持: 本地方法栈用于支持通过Java Native Interface(JNI)调用的本地方法。这些方法通常用C或C++等其他编程语言编写。
  • 栈帧管理: 和Java虚拟机栈一样,本地方法栈在调用本地方法时创建栈帧,存储本地方法的局部变量、操作数等信息。
  • 错误管理: 如果本地方法栈的栈深度超过了规定的深度,JVM会抛出StackOverflowError。同样,如果JVM在扩展栈时无法获得足够的内存,则会抛出OutOfMemoryError

堆(Heap): 堆是JVM中最大的一块内存区域,用于存储对象实例。所有的对象在堆中分配内存,这也是垃圾回收机制重点管理的区域。

  • 对象存储: 所有对象实例和数组都在堆中分配内存。这意味着不论是在方法中创建的临时对象,还是全局对象,都在堆中管理。
  • 自动内存管理: 堆的内存管理由垃圾回收机制负责。当对象不再被引用时,垃圾回收器会自动释放这些对象所占用的内存。
  • 分代收集: 为了提高垃圾回收的效率,堆通常分为不同的区域,如新生代(Young Generation)和老年代(Old Generation)。新生代进一步分为Eden区和两个Survivor区。新对象一般在Eden区分配,存活较久的对象会被移动到老年代。

方法区

方法区(Method Area): 方法区是堆的一部分,用于存储已被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。

  • 类加载信息: 方法区存储了JVM加载的每一个类的信息,包括类的名称、访问修饰符、父类名、实现的接口、字段、方法等。
  • 常量池: 方法区包含运行时常量池(Runtime Constant Pool),这是一个在类或接口被加载后创建的数据结构。常量池包含编译期生成的各种字面量和符号引用,如字符串字面量、方法和字段引用等。
  • 静态变量: 所有类的静态变量在方法区中分配存储空间,静态变量在类被加载时分配内存,并在类卸载时被回收。
  • 即时编译后的代码: JIT编译器在运行时将一些热点代码编译为机器码,以提高程序的执行效率。编译后的机器码也存储在方法区中。