05 October 2018

java内存模型

CPU和缓存一致性

计算机在执行程序的时候,每条指令都在cpu中执行,而执行的时候,需要和计算机物理内存交互,但是随着cpu的迅猛发展,而内存技术没有太大的变化,所以内存中读取和写入数据的速度远远低于cpu的执行速度,所以想到了在cpu和内存中间加入高速缓存

**

1.单线程(串行):cpu核心缓存只被一个线程访问

2.单核(单个领导人),多线程(并行):不同线程在访问相同的物理地址的时候,会映射到相同的缓存位置,即使发生线程切换,缓存不会失效,任何时刻只有一个线程在执行,不会出现缓存访问冲突

3.多核(多个领导人),多线程(并行):每核至少有一个L1缓存,每个线程访问某个共享内存,多个线程在不同的核心上执行,则每个核心都会在各自cache中保留一份共享内存的缓冲,因此存在缓存不一致的情况

解决缓存不一致

前面说的缓存不一致性问题,处理器优化的指令重排问题是硬件的不断升级导致的。为了解决这个问题,保证并发编程可以满足原子性,有序性,可见性,有一个重要的概念,那就是内存模型。

内存模型定义了共享内存系统中多线程读写内存操作的行为规范

java多线程内存模型

存在理由:屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。

java内存模型是一种符合计算机内存模型规范的模型,为的是保证并发编程可以满足原子性,有序性,可见性

java内存模型规定了所有变量都存储在主存中,每个线程有自己的工作内存,用到的变量是主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,不能直接写主存,不同线程之间无法直接访问对方工作内存中的变量

jvm内存模型运行数据区

企业微信截图_cedd0efa-b16a-48f5-acc2-17e7467846f0.png

企业微信截图_0cfe5e7e-09b4-445a-862f-27abeff05761.png

  • 程序计数器:当前线程所执行字节码的行号指示器,多线程通过轮流切换并分配处理器执行时间的方式来实现,每一个时刻,一个核心(单核,或者多核的其中一个核),只会执行一条线程指令,为了线程切换后能回到正确的执行位置,每个线程需要一个程序计数器

  • 虚拟机栈:用来对我们的操作数进行运算的过程中,临时中转内存的存放区域,每个方法被执行的时候都会在栈中创建一个栈帧内存区域用于存储局部变量,操作数栈,动态链接返回地址等信息,方法的调用到执行完成,对应一个栈帧在虚拟机栈中从入栈到出栈(比如main方法里面调用了一个compute方法,main方法先在虚拟机栈中创建一个main方法的栈针存储main方法对应的局部变量等信息,之后调用compute方法,继续在虚拟机栈中创建一个compute方法的栈针,等compute执行完成之后,compute方法对应的栈针先出栈,之后main方法结束,main方法对应的栈针再出栈,也就是对应着数据结构中栈的先进后出)

  • 虚拟机栈之栈针:局部变量表,操作数栈,动态链接,方法出口

​ javap -c Math.class > Math.txt 进行反编译

  • package com.example.myapplication.test;
      
    public class Math {
        public static int initData = 666;
      
        public Math() {
        }
      
        public int compute() {
            int a = 1;
            int b = 2;
            int c = (a + b) * 10;
            return c;
        }
      
        public static void main(String[] args) {
            Math math = new Math();
            math.compute();
            System.out.println("test");
        }
    }
    
  • Compiled from "Math.java"
    public class com.example.myapplication.test.Math {
      public static int initData;
      
      public com.example.myapplication.test.Math();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
      
      public int compute();
        Code:
           0: iconst_1
           1: istore_1
           2: iconst_2
           3: istore_2
           4: iload_1
           5: iload_2
           6: iadd
           7: bipush        10
           9: imul
          10: istore_3
          11: iload_3
          12: ireturn
      
      public static void main(java.lang.String[]);
        Code:
           0: new           #2                  // class com/example/myapplication/test/Math
           3: dup
           4: invokespecial #3                  // Method "<init>":()V
           7: astore_1
           8: aload_1
           9: invokevirtual #4                  // Method compute:()I
          12: pop
          13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
          16: ldc           #6                  // String test
          18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          21: return
      
      static {};
        Code:
           0: sipush        666
           3: putstatic     #8                  // Field initData:I
           6: return
    }
    

    根据jvm指令码解析

    iconst_1 将int类型常量1压入栈:暂时把1放到操作数栈中。
    istore_1 将int类型值存入局部变量1:在局部变量表中给a分配一块内存区域,同时把1这个值也放从操作数栈中拿出来,放到局部变量a的内存区域,完成赋值(此时操作数栈中已经没有1了)。
    4: iload_1:
    其中的4代表的是程序计数器的值,告诉线程正在执行哪行代码了,这个值由字节码执行引擎进行修改,每执行一行代码,程序计数器的值都会变动;
    iload_1 从局部变量1中装载int类型值,把局部变量a对应的值1,装载到操作数栈中。
    iadd:执行int类型的加法,从操作数栈中依次弹出需要执行的两个元素,执行a+b也就是1+2,把结果3重新压入到操作数栈中
    bipush 10: 将一个8位带符号整数压入栈,把10压到操作数栈中
    immul:执行乘法操作,从操作数栈中依次弹出需要执行的两个元素,执行3*10,把结果30重新压入到操作数栈中
    istore_3 :将int类型值存入局部变量3,就是c,在局部变量表给c分配区域,同时把30这个值从操作数栈中出栈,放到局部变量c的区域,完成赋值
    

    方法出口:存的就是一个行号指针,需要知道执行完compute方法之后,回到main方法之后在哪行继续执行

    动态链接:对象在new的时候会再对象头中存储这个对象对应的类元指针,执行compute方法(符号引用)的时候,会找到这个方法在方法区类元信息中的地址(直接引用),这个地址就会存在动态链接中

  • 方法区(元空间):常量,静态变量,类元信息(类有什么方法,变量,这些东西就叫这个类的类元信息)。就是类装载子系统把字节码文件Math.class加载到方法区,字节码执行引擎执行类的指令码

  • 本地方法栈:本地方法中也有一些局部变量的存储需要用到内存空间

模块关系

  • 堆指向方法区:对象的头指针中存储这个对象的类元信息
  • 方法区指向堆:Math类中有一个 public static User user=new User(),user是静态变量在方法区,但是他new出来的对象在堆里面(静态变量对应的值是一个对象类型的话就会这样)

java内存模型底层的原子操作

  • read(读取)从主内存中读取数据

  • load(载入)将主内存读取到的数据写入工作内存

  • use(使用)从工作内存读取数据来计算

  • assign(赋值)将计算好的值重新赋值都工作内存中

  • store(存储)将工作内存主数据写入主内存

  • write(写入)将store过去的的变量值赋值给主内存中的变量

  • lock(锁定)将主内存变量加锁,标识为线程独占状态

  • unlock(解锁)将主内存变量解锁,解锁后其他线程可以锁定该变量

多线程访问共享变量,看看java的线程内存模型

企业微信截图_99c139fd-06d4-40fc-b347-b52d758b2f48.png

不安全的底层实现

企业微信截图_00971351-d478-44ec-b2bf-e216486e58d9.png

安全的实现 [把数据从工作内存写回主内存,必须经过总线]

企业微信截图_559fd773-9edd-4fe6-8ade-1b8853b499be.png

注:cpu和主内存是两个硬件,通过总线传输数据,就是台式机电脑里面的排线

  • 数据总线加锁(性能低)[lock,unlock在read的时候就开始上锁,把请求变成串行了]

    cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其他cpu没法去读和写这个数据,直到这个cpu使用完数据释放锁之后其他cpu才能读取该数据

  • MESI缓存一致性协议

    多个cpu从主内存读取同一个数据都各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制(理解成监听)可以感知到数据的变化从而将自己缓存里的数据失效

volatile底层实现

  • [可见性]通过汇编lock前缀之类,他会锁定这块内存区域的缓存(缓存行锁定),并写回主内存

    IA-32对lock指令的解释

    • 会将当前处理器的缓行的数据立即写回到系统内存
    • 这个写回内存的操作会引起在其他CPU里缓存了该地址内存地址的数据无效(MESI协议)
  • [有序性] 带lock前缀的指令会在前后做一个内存屏障,有了内存屏障,就不会把屏障前面的代码挪到后面去

happens-before规则和as-if-serial语义

  • JSR-133使用happens-before的概念来阐述操作之间的内存可见性。
  • as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不会改变。

什么区域会发生OOM呢?

从 Java 代码的运行过程来看,有三个区域会发生 OOM,它们分别是:Metaspace、Java 虚拟机栈、堆内存。

  • 源文件 HelloWorld.java 将会被编译成可执行文件 HelloWorld.class
  • 类加载加载可执行文件到 Metaspace,Metaspace 保存类的基本信息,如果加载太多就会 OOM
  • Java是多线程的,运行代码的时候会启动一个线程。main()是Java程序的入口,首先会启动一个 main 线程。每个线程都有 Java 虚拟机栈,每执行一个方法都会有一个栈帧入栈,栈帧中包含参数、局部变量、返回值地址等信息。如果代码层次太深,不断有方法入栈却没有出栈,Java虚拟机栈就会 OOM。
  • 栈中的局部变量如果是一个对象,那就会在初始化的时候在堆中创建对象。堆中创建的对象过多就会触发 GC,GC 的速度赶不上新建对象的速度也会发生 OOM。


blog comments powered by Disqus