JVM好好学二(Java内存模型和运行时数据区)

JVM好好学二(Java内存模型和运行时数据区)

大纲

  • 1、JAVA内存运行区域划分

  • 2、JAVA内存模型

1、JAVA内存运行区域划分

1.1 运行时数据区

        通常,Java代码通过编译器编译成字节码文件,然后会通过JVM的类加载器加载,会执行方法区内的代码,其中JVM在执行过程中用来存储数据的区域我们就叫做运行时数据区(Runtime Data Area),也就是常说的JVM内存。

    

1.2 方法区

        方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(类的名称、方法信息、字段信息)、final信息、静态变量、编译器即时编译的代码等

        方法区可以选择是否执行垃圾回收机制,一般方法区上执行垃圾回收机制是很少的,所以在HotSpot中方法区又被称为永久代。

1.3 虚拟机栈

        Java虚拟机栈(Java Virtual Machine Stacks) 是线程私有的,它的生命周期与线程相同

每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

1.4 本地方法栈

        和虚拟机栈类似,区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的

1.5 堆

        堆是所有线程共享的一块内存区域,在虚拟机启动时就会创建,并且也是GC(垃圾回收)的主要区域。原则上说,所有的对象和数组都会在堆上分配内存。

        异常:如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常

1.6 程序计数器

        线程私有,占用比较小的内存,用于指示当前线程所执行的字节码执行到了第几行,可以理解为当前线程的行号指示器。字节码解释器在工作的时候,会通过改变这个计数器的值来取下一条语句指令

        存储内容:当前需要执行的虚拟机字节码指令地址,如果该方法是一个native方法,则值为undfine。由于只是存储当前指令地址,存储的数据所占空间的大小不会随程序的执行而发生改变。

2、JAVA内存模型

        内存模型(Java Memory Model,简称 JMM )是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式,用于屏蔽掉各种硬件和操作系统的内存访问差异,其中JMM规范了Java虚拟机和操作系统的内存是如何协同工作的,如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。

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

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

        计算机在高速的 CPU 和相对低速的存储设备之间使用高速缓存,作为内存和处理器之间的缓冲。将运算需要使用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中。

        在多处理器的系统中(或者单处理器多核的系统),每个处理器内核都有自己的高速缓存,它们有共享同一主内存(Main Memory)。

        当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。为此,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,来维护缓存的一致性。

2.2 JVM主内存和工作内存

        Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。

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

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

        就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存

        不同线程之间无法直接访问对方工作内存中的变量,线程间的通信一般有两种方式进行,一是通过消息传递,二是共享内存。Java 线程间的通信采用的是共享内存方式,线程、主内存和工作内存的交互关系如下图所示:

        这里所讲的主内存、工作内存与 Java 内存区域中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域

2.3 重排序

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

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

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

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

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

2.4 happens-before规则

        从 JDK5 开始,java 内存模型提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。

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

        这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

        如果 A happens-before B,那么 Java 内存模型将向程序员保证—— A 操作的结果将对 B 可见,且 A 的执行顺序排在 B 之前。

重要的 happens-before 规则如下:

  • (1)程序顺序规则:

        一个线程中的每个操作,happens- before 于该线程中的任意后续操作。

  • (2)监视器锁规则

        对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。

  • (3)volatile 变量规则

        对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。

  • (4)传递性

        如果 A happens- before B,且 B happens- before C,那么 A happens- before C。

下图是 happens-before 与 JMM 的关系

2.5 volatile关键字

volatile 可以说是 JVM 提供的最轻量级的同步机制,当一个变量定义为volatile之后,它将具备两种特性:

(1)保证此变量对所有线程的可见性。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。

        注意,volatile 虽然保证了可见性,但是 Java 里面的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不安全的。而 synchronized 关键字则是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得线程安全的。

(2)禁止指令重排序优化。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

end
  • 作者:旭仔(联系作者)
  • 发表时间:2024-03-21 23:37
  • 版权声明:自由转载-非商用-非衍生-保持署名
  • 转载声明:如果是转载栈主转载的文章,请附上原文链接
  • 公众号转载:请在文末添加作者公众号二维码(公众号二维码见右边,欢迎关注)
  • 评论