本文转自:浅析java内存模型--JMM(Java Memory Model) - 路易小七 - 博客园,尊重作者,转载请注明出处~

JVM虚拟机内存结构 和 JAVA内存模型 是两个不同的概念

JVM虚拟机内存结构:java virtual machine 讲的是整个Java 虚拟机 的 内部结构划分,以及各个部分之间的交互和作用。包括堆区,栈区,方法区等。

JAVA内存模型(JMM):Java Memory Model,指的在java程序运行过程中 java线程的并发关系。计算机有主内存,每个java线程有自己的工作内存,java线程的工作内存是计算机主内存的拷贝。处理多线程间数据争夺和加锁的机制

注意区分这两个概念的定义。

问:在并发编程中,多个线程之间采取什么机制进行通信(信息交换),什么机制进行数据的同步?

答:在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的

线程之间通过共享程序公共的状态,通过读-写内存中公共状态的方式来进行隐式的通信。同步指的是程序在控制多个线程之间执行程序的相对顺序的机制,在共享内存模型中,同步是显式的,程序员必须显式指定某个方法/代码块需要在多线程之间互斥执行。

JVM的基本结构:JVM由三个主要的子系统构成

  • 类加载子系统
  • 运行时数据区(内存结构)
  • 执行引擎

一、类加载子系统(ClassLoader)

  • 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine(执行引擎)决定。
  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常最池信息,可能还包括字符串字而量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

类加载过程

1、加载-Loading

  1. 通过一个类的全限定名获取定义此类的二进制字节流(以物理磁盘为例)
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java. lang.Class对象,作为方法区这个类的各种数据的访问入口

2、链接-Linking

(1)验证(Verify)

  • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。(虚拟机要求:例如字节码文件以cafebabe开头等)
  • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。(验证出错会报VerifyError错误)

(2)准备(Prepare)

  • 为类变量分配内存并且设置该类变量的默认初始值。
  • 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
  • 这里不会为实例变量分配初始化,类变量会在方法区中,而实例变量是会随着对象一起分配到Java堆中。

(3)解析(Resolve)

  • 将常量池内的符号引用转换为直接引用的过程。
  • 事实上,解析操作往往会伴随着JVM在执行完*初始化*之后再执行。
  • 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_ Class_ info、 CONSTANT_ Fieldref_ info、 CONSTANT Methodref_ info等。

3、初始化-Initialization

  • 初始化阶段就是执行类构造器方法< clinit > ()的过程。(注意:这个方法和类中定义的构造方法不一样)
  • 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句(也就是static修饰的)合并而来。Clinit构造器会把显示初始化和构造代码块初始化合并在一起构成构造器方法,如果没有类变量(静态变量)的赋值动作或者是静态代码块语句那么就不会生成这个clinit方法了。还有一点:< init > ()这个就是默认的构造方法。

二、JVM虚拟机内存结构(运行时数据区)

在说Java内存模型之前,我们先说一下JVM的内存结构,也就是运行时的数据区域:

Java虚拟机在执行Java程序的过程中,会把它管理的内存划分为几个不同的数据区域,这些区域都有各自的用途、创建时间、销毁时间。

Java运行时数据区分为下面几个内存区域:

1、PC寄存器/程序计数器:

严格来说是一个数据结构,可以把它看作是当前线程执行的字节码的行号指示器,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。

线程独有的,比如如下字节码内容,在每个字节码`前面都有一个数字(行号),我们可以认为它就是程序计数器存储的内容

2、Java栈 Java Stack(重点)

Java栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量、操作栈和方法返回值等信息

每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用。

当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。

注意:由于Java栈是与线程对应起来的,Java栈数据不是线程共有的,所以不需要关心其数据一致性,也不会存在同步锁的问题。

Java中方法参数传递方式是按值传递

  1. 如果参数是基本类型,传递的是基本类型的字面量值的拷贝
  2. 如果参数是引用类型,传递的是该参数所引用的对象在堆中地址值的拷贝

这两句已经很清楚的解释了Java是如何传递方法参数的,即传递值的拷贝

我的理解为在调用方法的栈帧中,拷贝一份值(不论是基本类型还是地址)到局部变量表,在方法的执行过程由操作数栈处理,然后将其推出赋值给方法栈帧的局部变量中,最后返回。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。在Hot Spot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。

栈帧(重点)

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的虚拟机栈的栈元素。

栈帧存储了方法的局部变量表操作数栈动态连接方法返回地址等信息。

注意:在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了。因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

1、局部变量表(Local Variable Table)

局部变量表是一组 变量值存储空间 ,用于存放方法参数 和 方法内部定义的局部变量

我觉得可以想成Slot数组....JVMS7:“any parameters are passed in consecutive local variables starting from local variable 0”

局部变量表的容量以变量槽(Slot)为最小单位,是一片逻辑连续的内存空间。

虚拟机没有明确指明一个Slot的内存空间大小。但是 boolean、byte、char、short、int、float、reference、returnAddress类型的数据都可以用32位空间或更小的内存来存放。这些类型占用一个Slot。Java中的long和double类型是64位,占用两个Slot。(只有double和long是jvms里明确规定的64位数据类型)

虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。

2、操作数栈(Operand Stack)

虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据到CPU,执行运算,然后把结果压回操作数栈

操作数栈和局部变量表一样,在编译时期就已经确定了该方法所需要分配的局部变量表的最大容量。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。

例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的。

基于栈的设计模式

基于栈的设计模式则是将数据存放在栈中,在需要使用的时候将栈顶的数据出栈,并执行相应的操作。

举例来说,在JVM中 执行 a = b + c 的字节码执行过程中操作数栈以及局部变量表的变化如下图所示。

局部变量表中存储着a、b、c 三个局部变量,首先将b和c分别入栈

将栈顶的两个数出栈执行加法操作,并将结果保存至栈顶,之后将栈顶的数出栈赋值给a

比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:

begin
iload_0    // push the int in local variable 0 onto the stack
iload_1    // push the int in local variable 1 onto the stack
iadd       // pop two ints, add them, push result
istore_2   // pop int, store into local variable 2
end  

在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。下图详细表述了这个过程中局部变量和操作数栈的状态变化,图中没有使用的局部变量区和操作数栈区域以空白表示。

3、动态链接(Dynamic Linking)

符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。

每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。

一个方法调用另一个方法,或者一个类使用另一个类的成员变量时,总得知道被调用者的名字吧?(你可以不认识它本身,但调用它就需要知道他的名字)。符号引用就相当于名字,这些被调用者的名字就存放在Java字节码文件里。

名字是知道了,但是Java真正运行起来的时候,真的能靠这个名字(符号引用)就能找到相应的类和方法吗?

需要解析成相应的直接引用,利用直接引用来准确地找到。

举个例子:就相当于我在0X0300H这个地址存入了一个数526,为了方便编程,我把这个给这个地址起了个别名叫A, 以后我编程的时候(运行之前)可以用别名A来暗示访问这个空间的数据,但其实程序运行起来后,实质上还是去寻找0X0300H这片空间来获取526这个数据的。

这样的符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。

4、方法返回地址(Return Address)

当一个方法开始执行后,只有2种方式可以退出这个方法 :

  • 方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
  • 异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。

无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。

一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。

而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

3、堆 Heap:

堆是JVM所管理的内存中国最大的一块,是被所有Java线程锁共享的,不是线程安全的,在JVM启动时创建。堆是存储Java对象的地方,这一点Java虚拟机规范中描述是:所有的对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域,从内存回收的角度来看,由于现在GC基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。

4、方法区Method Area:

方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。方法区是被Java线程锁共享的,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。方法区也是堆中的一部分,就是我们通常所说的Java堆中的永久区 Permanet Generation,大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

5、常量池Constant Pool:

常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。

一般分为两类:字面量和引用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。

JVM常量池分为三种:

  1. 静态常量池
  2. 运行时常量池
  3. 字符串常量池

(1)静态常量池

  静态常量池是class文件的一部分。在class文件结构中有一个叫做constant_pool的部分,这就是静态常量池。静态常量池主要存放两大常量:字面量符号引用

  字面量分为文本字符串(例如 String s = "abc"; 其中的"abc"就是文本字符串)与final修饰的变量(包括静态变量、实例变量、局部变量)。

  符号引用包含了类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

  constant_pool中有一个数据类型叫做CONSTANT_Utf8_info,这就是存储UTF-8编码的字符串的数据类型,该类型的结构如下:

struct CONSTANT_UTF8_INFO {u1 tag;u2 length;u1 bytes[length];
}

其中length的类型是u2,u2是16位无符号整型,所以理论上bytes最大长度是65535,但是由于javac在编译时只允许长度小于65535的字符串,所以如果使用字面量定义字符串,其最大长度是65534(运行时产生的字符串最大长度是-1>>>1)。

  ps:静态常量池的数据类型里面有个叫做CONSTANT_String的类型,一般对他的描述是“存放字符串类型的字面量”,其实它的数据结构里面包含了一个CONSTANT_Utf8的指针,所以CONSTANT_Utf8才是真正存放字符串的数据结构。

(2)运行时常量池

  运行时常量池是方法区的一部分,是全局共享的。我们知道,jvm在执行某个类的时候,必须经过加载、连接(验证,准备,解析)、初始化,在第一步的加载阶段,虚拟机需要完成下面3件事情:

  • 通过一个类的“全限定名”来获取此类的二进制字节流
  • 将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构
  • 在内存中生成一个类代表这类的java.lang.Class对象,作为方法区这个类的各种数据访问的入口

  上面第二条中提到的静态存储结构就是包含了class文件中的静态常量池,所以静态常量池中的数据最终会进入到运行时常量池中,但是运行时常量池的数据还会来自运行时产生的。

  运行时常量池的作用是存储class文件常量池中的符号信息。运行时常量池 中保存着一些 class 文件中描述的符号引用,同时在类加载的“解析阶段”还会将这些符号引用所翻译出来的直接引用(直接指向实例对象的指针)存储在 运行时常量池 中。

  运行时常量池相对于 class 常量池一大特征就是其具有动态性,Java 规范并不要求常量只能在运行时才产生,也就是说运行时常量池中的内容并不全部来自 class 常量池,class 常量池并非运行时常量池的唯一数据输入口;在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的较多的是String.intern()。

  ps:方法区位于永久代中,在JDK8版本,永久代被移除,新增了一个元空间,运行时常量池也被移动到了元空间中。元空间的作用与方法去类似,其与永久代最大区别在于前者不使用虚拟机内存,而是使用本地内存。

(3)字符串常量池

  字符串常量池是JVM维护的一个字符串实例的引用的HashTable,在JDK7以前,字符串常量池位于永久代中,JDK7将字符串常量池移到了堆中,以下我们详细介绍JDK6,我们在最后介绍JDK7与JDK6的差别。在上面的运行时数据区看不到字符串常量池,所以我们换个更详细的图。

这是将JVM内存按照堆-非堆的维度划分,让我们放大非堆的结构

可以看到,在非堆中的永久代除了方法区之外还有一个Interned Strings,这就是字符串常量池,值得注意的是,字符串常量池不会存储字符串对象本身,存储的是字符串的引用

以下是openjdk源码中字符串常量池HashTableEntry结构的一部分:

/hotspot/src/share/vm/utilities/hashtable.hpp

class BasicHashtableEntry : public CHeapObj {friend class VMStructs;
private:unsigned int         _hash;           // 32-bit hash for itemBasicHashtableEntry* _next;} 

其中_hash就是字符串的地址,字符串对象本身是存储在永久代中。

  在上面说到,JVM在加载一个类时会将该类的静态常量池的数据加载到运行时常量池,其实这是创建了CONSTANT_UTF8,并没有创建CONSTANT_STRING,只有当该字符串被引用到时,才会被创建。

在类加载过程中,有一个叫做解析(resolve)的步骤,在JVM规范中明确指出,这个阶段可以是lazy的,CONSTANT_STRING就是lazy resolve的。

  ps:CONSTANT_UTF8和CONSTANT_STRING是JVM使用的对象不是JAVA对象,JAVA程序只认识java.lang.String。

 String进入到字符串常量池的过程

public class TestClass {public static void main(String[] args) {String s = "123";String s1 = new String("1");String s2 = s1 + new String("23");String s3 = s2.intern();final String s4 = "1";String s5 = s4 + "23";System.out.println(s==s3);System.out.println(s==s5);}} 

  当这个类编译完成后,在静态常量池中会创建一个保存有字符串 "123"CONSTANT_UTF8_INFO 和一个保存有前者的 indexCONSTANT_STRING_INFO

  当程序启动时,JVM会加载该类,将静态常量池中的 CONSTANT_UTF8_INFO 加载到运行时常量池。

  当执行到第三行代码时,发现字符串 "123" 被引用,实例化 CONSTANT_STRING_INFO,并且会在永久代创建字符串对象然后将字符串的引用保存到字符串常量池中。

  当执行到第五行代码时,会在堆中创建三个String对象,这是因为对于String的+运算符,如果参与运算的对象含有变量则会变成使用 StringBuilder.appen 来构建字符串,如果参与运算的对象都是常量的话,则在编译期间会直接将常量合并为一个字符串。

  当执行到第六行代码时,intern() 方法会先判断字符串常量池中是否有字符串 "123",如果有则返回引用,没有则在永久代创建该字符串,并且会在字符串常量池中保存该字符串的引用,然后返回引用。

  当执行到第八行代码时,JVM在字符串常量池中找到了字符串 "123",返回其引用。

  所以最终结果是true true,同理我们可以分析文章开头那一段代码。

  综上,对于字面量来说,CONSTANT_UTF8 是存储字符串内容的数据结构,CONSTANT_STRING 则持有前者的引用,当字面量被引用时 CONSTANT_STRING 才会被创建,并且同时字符串常量池会保存字符串的引用。对于 intern()方法来说,如果常量池没有该字符串,则会在永久代创建该字符串并返回引用,否则,直接返回常量池中字符串的引用。

6、本地方法栈Native Method Stack:

本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。所谓本地方法就是一个 java 调用非 java 代码的接口,该方法并非 Java 实现的,可能由 C 或 Python等其他语言实现的(比如调用操作系统的东西), Java 通过 JNI 来调用本地方法, 而本地方法是以库文件的形式存放的(在 WINDOWS 平台上是 DLL 文件形式,在 UNIX 机器上是 SO 文件形式)。通过调用本地的库文件的内部方法,使 JAVA 可以实现和本地机器的紧密联系,调用系统级的各接口方法,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

当调用 Java 方法时,虚拟机会创建一个栈桢并压入 Java 栈,而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简单地动态连接并直接调用指定的本地方法

了解 java调用本地方法:java调用本地方法--jni简介_w1992wishes的博客-CSDN博客_java调用本地方法

三、JAVA内存模型JMM(主内存和工作内存)

Java内存模型 的主要目标是定义程序中各个变量的 访问规则

即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节(结合上文栈帧的理解来看待)

此处的变量与Java编程里面的变量有所不同步,它包含了实例字段、静态字段和构成数组对象的元素,但不包含局部变量和方法参数,因为后者是线程私有的,不会共享,当然不存在数据竞争问题(如果局部变量是一个reference引用类型,它引用的对象在Java堆中可被各个线程共享,但是reference引用本身在Java栈的局部变量表中,是线程私有的)。为了获得较高的执行效能,Java内存模型并没有限制执行引起使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

  JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

线程1和线程2要想进行数据的交换一般要经历下面的步骤:

  1.线程1把工作内存1中的更新过的共享变量刷新到主内存中去。

  2.线程2到主内存中去读取线程1刷新过的共享变量,然后copy一份到工作内存2中去。

JVM 和 JMM 之间的关系

jmm中的主内存、工作内存与jvm中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低层次上说,主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。

1、并发编程的特征

Java内存模型是围绕着并发编程中原子性、可见性、有序性这三个特征来建立的,那我们依次看一下这三个特征:

(1)原子性

原子性(Atomicity):一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。

基本类型数据的访问大都是原子操作,long 和double类型的变量是64位,但是在32位JVM中,32位的JVM会将64位数据的读写操作分为2次32位的读写操作来进行,这就导致了long、double类型的变量在32位虚拟机中是非原子操作,数据有可能会被破坏,也就意味着多个线程在并发访问的时候是线程非安全的。

下面我们来演示这个32位JVM下,对64位long类型的数据的访问的问题:

public class NotAtomicity {//静态变量tpublic  static long t = 0;//静态变量t的get方法public  static long getT() {return t;}//静态变量t的set方法public  static void setT(long t) {NotAtomicity.t = t;}//改变变量t的线程public static class ChangeT implements Runnable{private long to;public ChangeT(long to) {this.to = to;}public void run() {//不断的将long变量设值到 t中while (true) {NotAtomicity.setT(to);//将当前线程的执行时间片段让出去,以便由线程调度机制重新决定哪个线程可以执行Thread.yield();}}}//读取变量t的线程,若读取的值和设置的值不一致,说明变量t的数据被破坏了,即线程不安全public static class ReadT implements Runnable{public void run() {//不断的读取NotAtomicity的t的值while (true) {long tmp = NotAtomicity.getT();//比较是否是自己设值的其中一个if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) {//程序若执行到这里,说明long类型变量t,其数据已经被破坏了System.out.println(tmp);}将当前线程的执行时间片段让出去,以便由线程调度机制重新决定哪个线程可以执行Thread.yield();}}}public static void main(String[] args) {new Thread(new ChangeT(100L)).start();new Thread(new ChangeT(200L)).start();new Thread(new ChangeT(-300L)).start();new Thread(new ChangeT(-400L)).start();new Thread(new ReadT()).start();}
}

我们创建了4个线程来对long类型的变量t进行赋值,赋值分别为100,200,-300,-400,有一个线程负责读取变量t,如果正常的话,读取到的t的值应该是我们赋值中的一个,但是在32的JVM中,事情会出乎预料。如果程序正常的话,我们控制台不会有任何的输出,可实际上,程序一运行,控制台就输出了下面的信息:

-4294967096
4294966896
-4294967096
-4294967096
4294966896
之所以会出现上面的情况,是因为在32位JVM中,64位的long数据的读和写都不是原子操作,即不具有原子性,并发的时候相互干扰了。

  32位的JVM中,要想保证对long、double类型数据的操作的原子性,可以对访问该数据的方法进行同步,就像下面的:

public class Atomicity {//静态变量tpublic  static long t = 0;//静态变量t的get方法,同步方法public synchronized static long getT() {return t;}//静态变量t的set方法,同步方法public synchronized static void setT(long t) {Atomicity.t = t;}//改变变量t的线程public static class ChangeT implements Runnable{private long to;public ChangeT(long to) {this.to = to;}public void run() {//不断的将long变量设值到 t中while (true) {Atomicity.setT(to);//将当前线程的执行时间片段让出去,以便由线程调度机制重新决定哪个线程可以执行Thread.yield();}}}//读取变量t的线程,若读取的值和设置的值不一致,说明变量t的数据被破坏了,即线程不安全public static class ReadT implements Runnable{public void run() {//不断的读取NotAtomicity的t的值while (true) {long tmp = Atomicity.getT();//比较是否是自己设值的其中一个if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) {//程序若执行到这里,说明long类型变量t,其数据已经被破坏了System.out.println(tmp);}将当前线程的执行时间片段让出去,以便由线程调度机制重新决定哪个线程可以执行Thread.yield();}}}public static void main(String[] args) {new Thread(new ChangeT(100L)).start();new Thread(new ChangeT(200L)).start();new Thread(new ChangeT(-300L)).start();new Thread(new ChangeT(-400L)).start();new Thread(new ReadT()).start();}
}

这样做的话,可以保证对64位数据操作的原子性。

(2)可见性

可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量这种修改(变化)。

Java内存模型是通过将在工作内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的。

无论是普通变量还是volatile变量都是如此,区别在于:volatile的特殊规则保证了volatile变量值修改后的新值立刻同步到主内存,每次使用volatile变量前立即从主内存中刷新,因此volatile保证了多线程之间的操作变量的可见性,而普通变量则不能保证这一点。

除了volatile关键字能实现可见性之外,还有synchronized,Lock,final也是可以的。

使用synchronized关键字,在同步方法/同步块开始时(Monitor Enter),使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在同步方法/同步块结束时(Monitor Exit),会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。

使用Lock接口的最常用的实现ReentrantLock(重入锁)来实现可见性:当我们在方法的开始位置执行lock.lock()方法,这和synchronized开始位置(Monitor Enter)有相同的语义,即使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在方法的最后finally块里执行lock.unlock()方法,和synchronized结束位置(Monitor Exit)有相同的语义,即会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。

final关键字的可见性是指:被final修饰的变量,在构造函数数一旦初始化完成,并且在构造函数中并没有把“this”的引用传递出去(“this”引用逃逸是很危险的,其他的线程很可能通过该引用访问到只“初始化一半”的对象),那么其他线程就可以看到final变量的值。

(3)有序性

有序性:对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。

Java提供了两个关键字volatile和synchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现,

在单线程程序中,不会发生“指令重排”和“工作内存和主内存同步延迟”现象,只在多线程程序中出现。

happens-before原则:

  Java内存模型中定义的两项操作之间的次序关系,如果说操作A先行发生于操作B,操作A产生的影响能被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。

  下面是Java内存模型下一些”天然的“happens-before关系,这些happens-before关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。

  a.程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。

  b.管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。

  c.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。

  d.线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。

  e.线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。

  f.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。

  g.对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。

  h.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

  一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生 “呢?也是不成立的,一个典型的例子就是指令重排序。所以时间上的先后顺序与happens-before原则之间基本没有什么关系,所以衡量并发安全问题一切必须以happens-before 原则为准。

JVM(一)JVM虚拟机内存结构 和 JAVA内存模型(JMM)相关推荐

  1. 区分 JVM 内存结构、 Java 内存模型 以及 Java 对象模型 三个概念

    本文由 简悦 SimpRead 转码, 原文地址 https://www.toutiao.com/i6732361325244056072/ 作者:Hollis 来源:公众号Hollis Java 作 ...

  2. 【转】JVM内存结构 VS Java内存模型 VS Java对象模型

    JVM内存结构 我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途. 其中有些区域随着虚拟机进程的启动而 ...

  3. JVM内存结构 VS Java内存模型 VS Java对象模型

    Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点.而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚.比如本文我们要讨论的JVM内存结构.Java内存模型和Java对象 ...

  4. JVM之深入理解JVM内存结构(Java内存结构/Java内存区域)、Java内存模型

    Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点.而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚.比如本文我们要讨论的JVM内存结构.JAVA内存结构.JAVA内存 ...

  5. 快速带你分清java内存结构,java内存模型,java对象模型和jvm内存结构!

    现如今你是否有这样的感觉,无论生活还是学习,节奏都是非常的快,每天面对海量的知识信息,自己感觉都要hold不住了,每天打开微信公众号,是不是发现有几十条未读,无论是技术文章还是其他类型的文章,我们大多 ...

  6. java 堆内存结构_JVM内存结构、Java内存模型和Java对象模型

    JVM内存结构 我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途.其中有些区域随着虚拟机进程的启动而存 ...

  7. jvm内存参数配置_JVM内存结构和Java内存模型

    一.JVM 首先看一张JVM结构图(某度找的) 主要看运行时数据区,里边有方法区,堆,java虚拟机栈,本地方法栈,程序计数器.其中方法区和堆是线程共享的,也是JVM进行垃圾收集的区域,java虚拟机 ...

  8. java对象模型是什么_蓝石榴_个人博客_JVM内存结构、Java内存模型、Java对象模型...

    JVM内存结构 我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途.其中有些区域随着虚拟机进程的启动而存 ...

  9. Java内存结构与Java内存模型

    Java内存结构.内存模型.对象模型这几个概念经常会被混为一谈.理论知识容易忘写下来帮助记忆,方便以后查看. 1.Java内存结构 Java内存结构其实说的是JVM在运行时内存区域的划分,是根据Jav ...

最新文章

  1. 云计算的未来,就是“打车模式” | CCF C³@亚马逊云科技
  2. python django 动态网页_使用Django创建动态页面
  3. 什么是前后端分离 前后端不分离
  4. [转载]-如何向妻子解释OOD
  5. Windows上快速在指定目录打开cmd.exe命令行的方法
  6. java 用面向接口编程的方式开发打印机_Java“打印机”模型理解面向接口编程。实现接口定义类,接口实现类,核心“业务”类分离...
  7. 计算机上相同的用户,php会话为不同计算机上的不同用户发出相同的session_id
  8. 2022最新版影视小程序源码支持josn官解+卡密系统
  9. php两张图片合成,php多张图片合成一张的方法及案例
  10. Shader学习7——法线贴图
  11. 实验三 mysql数据库与表的创建_实验二 数据库和表的创建与管理
  12. 28岁转行程序员?别想了、没戏。事实真就如此残酷?
  13. SQL语法 Access
  14. linux系统端口说明
  15. 慧都APS解决方案,点亮「照明灯具行业」精益化生产之路
  16. 安天每日安全简讯20160713
  17. 破解百度网盘屏蔽文件分享失效被和谐的独家秘籍
  18. 炫龙笔记本的gtx965m显卡玩游戏很卡
  19. MIPS汇编语言学习笔记23:if 语句分支指令
  20. Aizu-2200-floyd+dp

热门文章

  1. go 编译约束//go:build dev //+build
  2. 那些年,我加入的社区、博客和微博(2020版)
  3. 2022个人推荐好书
  4. playstation模拟器Mednafen/Beetle PSX HW Alpha测试版发布
  5. 均分01【解】--英雄会
  6. #实验吧整理#拐弯抹角小结
  7. c 语言 pthread_create_【励达外语】第八篇 语言学习的5个关键期(上)
  8. 玩水果忍者未能找到服务器,水果忍者无法连接服务器是什么原因
  9. PHP常用数组(Array)函数整理
  10. Kevin Mitnick