jvm内存模型:
-
定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的底层细节。
- 此处的变量是指实例字段,静态字段和构成数组对象的元素,不包括局部变量与方法参数
基本结构:
- 类加载子系统:负责从文件系统或网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间。
- 方法区:存放类信息、常量信息、常量池信息、包括字符串字面量和数字常量等。
- Java堆:在虚拟机启动的时候建立,是Java程序最主要的内存工作区域,几乎所有的对象实例都放到堆里,堆空间是所有线程共享的。
- 直接内存:Java的NIO库允许Java程序使用直接内存,从而提高性能,通常直接内存速度会优于Java堆,读写频繁的场合可能会考虑使用。
- 栈:每个虚拟机线程都有一个私有的栈,一个线程的Java栈在线程创建的时候被创建,Java栈中保存着局部变量、方法参数、同时Java的方法调用、返回值等。
- 本地方法栈:类似于Java栈,最大不同为本地方法栈用于本地方法调用,Java虚拟机允许Java直接调用本地方法。
- 垃圾收集:Java有一套自己进行垃圾清理机制。
- PC寄存器:每个线程私有空间,一个Java线程在执行一个方法(当前方法),如果不是本地方法,则PC寄存器值为undefined,寄存器存放当前执行环境指针、程序计数器、操作栈指针、计算的变量指针等。
- 执行引擎:负责执行虚拟机的字节码,一般先进性编译成机器码后执行。
关于Java堆,完全自动化管理,通过垃圾回收机制,垃圾对象会自动清理:所有的对象实例以及数组都要在堆上分配。是垃圾收集器管理的主要区域。
- 根据垃圾回收机制不同,Java堆有可能拥有不同的结构,最常见的分为“新生代”和“老年代”。
- 新生代分为eden区、s0区(from)、s1区(to),s0和s1是两块大小相等并且可以互换角色的空间。绝大多数情况下对象首先分配到eden区,在一次新生代回收后,如果对象还存活则会进入s0或s1区(复制算法),之后每经过一次新生代回收,如果对象存活则它的年龄就加1,当对象达到一定的年龄后,则进入老年代。
- 局部变量表:用于报错函数的参数及局部变量。存放了编译期可知的基本类型、对象引用类型和returnAddress类型(指向一条字节码指令的地址。即程序就是存储在方法区的字节码指令,而 returnAddress 类型的值就是指向特定指令内存地址的指针)
- 操作数栈:保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数是一个后入先出栈,JVM所有的操作码都是对操作数栈上的数据进行操作,对于每一个方法的调用,JVM会建立一个操作数栈,以供计算使用。
- 帧数据区:保存着访问常量池的指针来支持常量池的解析。另外当函数返回或者出现异常时,虚拟机必须有一个异常处理表,方便发送异常的时候找到异常代码,因此异常处理表也是帧数据区的一部分。
- 动态链接:运行期间转化为直接引用,就称为动态链接。Class字节码的常量池中存有大量的符号引用,在运行期才将符号引用变成直接引用(也就是指向数据),可以是方法或者字段的引用。
- 方法出口:即本方法执行后下一步指令的地址,方法正常退出时,调用者PC计数器的值就可以作为返回地址,异常退出时,返回地址是要通过异常处理器来确定。
虚拟机参数问题:
堆分配参数
- -XX:+PrintGC 使用该参数,虚拟机启动后,只要遇到GC就会打印日志。
- -XX:+UseSerialGC 配置串行回收器。
- -XX:+PrintGCDetails 可以查看详细信息,包括各个区的情况。
- -Xms: 设置Java程序启动时初始堆大小。
- -Xmx: 设置Java程序能获得的最大堆大小。
- -Xmx20m -Xms5m -XX:+PrintCommandLineFlags :可以将隐式或者显示传给虚拟机的参数输出。
-
新生代配置:
方法区配置:
从源码到最终执行的指令序列的示意图
重排序:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
- 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
double pi = 3.14 //A double r = 1.0 //B double area = pi * r * r //C这是一个计算圆面积的代码,由于A,B之间没有任何关系,对最终结果也不会存在关系,它们之间执行顺序可以重排序。因此可以执行顺序可以是A->B->C或者B->A->C执行最终结果都是3.14,即A和B之间没有数据依赖性。具体的定义为:如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作就存在数据依赖性这里就存在三种情况:1. 读后写;2.写后写;3. 写后读,者三种操作都是存在数据依赖性的,如果重排序会对最终执行结果会存在影响。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序。
as-if-serialas-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。比如上面计算圆面积的代码,在单线程中,会让人感觉代码是一行一行顺序执行上,实际上A,B两行不存在数据依赖性可能会进行重排序,即A,B不是顺序执行的。as-if-serial语义使程序员不必担心单线程中重排序的问题干扰他们,也无需担心内存可见性问题。
happens-before规则
happens-before定义
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
上面的1)是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!
上面的2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。
下面来比较一下as-if-serial和happens-before:
as-if-serial VS happens-before
- as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
- as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
- as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
具体的一共有六项规则:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
- 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
下面以一个具体的例子来讲下如何使用这些规则进行推论:
依旧以上面计算圆面积的进行描述。利用程序顺序规则(规则1)存在三个happens-before关系:1. A happens-before B;2. B happens-before C;3. A happens-before C。这里的第三个关系是利用传递性进行推论的。A happens-before B,定义1要求A执行结果对B可见,并且A操作的执行顺序在B操作之前,但与此同时利用定义中的第二条,A,B操作彼此不存在数据依赖性,两个操作的执行顺序对最终结果都不会产生影响,在不改变最终结果的前提下,允许A,B两个操作重排序,即happens-before关系并不代表了最终的执行顺序。
上面已经聊了关于JMM的两个方面:1. JMM的抽象结构(主内存和线程工作内存);2. 重排序以及happens-before规则。接下来,我们来做一个总结。从两个方面进行考虑。1. 如果让我们设计JMM应该从哪些方面考虑,也就是说JMM承担哪些功能;2. happens-before与JMM的关系;3. 由于JMM,多线程情况下可能会出现哪些问题?JMM的设计
JMM是语言级的内存模型,在我的理解中JMM处于中间层,包含了两个方面:(1)内存模型;(2)重排序以及happens-before规则。同时,为了禁止特定类型的重排序会对编译器和处理器指令序列加以控制。而上层会有基于JMM的关键字和J.U.C包下的一些具体类用来方便程序员能够迅速高效率的进行并发编程。站在JMM设计者的角度,在设计JMM时需要考虑两个关键因素:
- 程序员对内存模型的使用 程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码。
- 编译器和处理器对内存模型的实现 编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型。
另外还要一个特别有意思的事情就是关于重排序问题,更简单的说,重排序可以分为两类:
- 会改变程序执行结果的重排序。
- 不会改变程序执行结果的重排序。
JMM对这两种不同性质的重排序,采取了不同的策略,如下。
- 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
- 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种 重排序)
JMM的设计图为:
从图可以看出:
- JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不一定真实存在,比如上面的A happens-before B)。
- JMM对编译器和处理器的束缚已经尽可能少。从上面的分析可以看出,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。例如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。再如,如果编译器经过细致的分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当作一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提高程序的执行效率。