Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

java内存模型.

2017-10-11
nivelle

JVM内存管理

物理内存和虚拟内存

物理内存就是我们常说的RAM(随机存储器).还有一个存储单元叫寄存器,它用于存储计算单元执行指令(浮点,整数等运算时)的中间结果.

我们运行程序,要向操作系统先申请内存地址.通常操作系统管理内存的申请空间是按照进程来管理的,每个进程拥有一段独立的地址空间,每个进程之间不会相互重合,操作系统也会保证每个进程只能访问自己的内存空间.

虚拟内存:虚拟内存使得多个进程在同时可以共享物理内存,这里的共享空间是空间上共享,在逻辑上任然不能互相访问.虚拟地址不但可以让进程共享物理内存,提高内存利用率,而且还能扩展内存的地址空间.虚拟地址可以被映射到一段物理内存,文件或者其他可以寻址的存储上.

一个进程在不活动的情况下,操作系统将这个物理内存中的数据移动一个磁盘文件中,而真正高效的物理内存留给正在活动的程序使用.重新唤醒一个很长时间没有使用的程序时,磁盘会吱吱作响,并且会有一个短暂的停顿得到印证,这时操作系统又会报磁盘上的数据重新交互到物理内存中.

主内存和工作内存

java虚拟机规范中试图定义一种java内存模型来屏蔽各种硬件和操作系统的内存访问的差异,以实现java程序在各个平台下能达到一致性内存访问效果.

java线程模型规定所有的变量都有存储在主内存,每条线程还有自己的工作内存,线程的工作内存中保存了被该使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值)都必须在工作内存中执行,而不能直接直接操作主内存的变量,不同的线程服务直接访问对方的工作内存中的变量,线程间变量值的传递需要通过主存来完成.

image

内存间交互操作

java内存模型定义了8种操作,来定义主内存和工作内存之间的交互协议,虚拟机实现时会保证这8种操作都是原子的、不可再分的(对于double和long类型变量来说,read,load,store,write操作在某些平台上允许例外)

  • lock(锁定):作用于主内存变量,它把一个变量标志位一条线程独占的状态.
  • unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用.
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入到工作内存的变量副本
  • use(使用):作用于工作内存的变量,它把工作内存中的一个变量值传递给执行引擎
  • assign(复制):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量
  • store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的write操作使用
  • write(写入):作用于主内存中的变量,它把store操作从主存中的到的变量值放主内存中的变量中

java内存模型还规定了在执行以上8种操作时必须满足如下的规则:

  • 不允许read,load和write操作之一单独出现.
  • 不允许一个只会线程丢弃它的最近的assign操作,即变量在工作内存中改变了必须把该变化同步到主内存中
  • 不允许一个线程无原因的把数据从线程的工作内存同步到主内存中.
  • 一个新的变量只能在主内存诞生,不允许在工作内存中直接使用一个未被初始化的变量,换句话说,就是一个变量实施use,store之前,必须先执行过了assign和load操作
  • 一个变量在同一时刻只允许一条线程对其执行lock操作,但lock操作可以被同一条线程重复执行多次,多次lock,只有执行相同次数的unlock,变量才会被解锁.
  • 如果对一个变量执行lock操作,那会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assign操作初始化变量的值
  • 对一个变量执行unlock之前,必须把此变量同步回主内存(执行store和write操作)

对于volatile型变量的特殊规则

当一个变量定义成volatile的时候,它将具备两种特性.第一是保证此变量对所有线程的可见性,这里的可见性是指当一条线程修改了变量值,新值对于其他线程来说是可以立即得知的.第二是用volatile可以禁止指令重排序,普通的变量仅仅会保证在该方法执行过程中所有依赖复制结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与代码中执行顺序一致.由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们任然要通过加锁来保证原子性.

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  • 运算不需要与其他的状态变量共同参与不变约束

对于long和double型变量的特殊规则

java内存模型要求lock、unlock、read、load、use、assign、store、write这8个操作都具有原子性,但是对于64位的数据类型(long和double),在模型中特别定义了一条相对宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写划分为两次32位操作来进行,即允许虚拟机实现跨越不保证(目前商用虚拟机都保证了)64位数据类型的load、store、read、write这四个原子性操作,这点就是所谓的long和double的非原子性协定。

由于商用的虚拟机保证64位的原子性,所以我们再平时编写代码的时候不需要对long和double变量专门声明volatile。

原子性、可见性与有序性

java内存模型是围绕着在并发过程中如何处理原子性、可见性、有序性这三个特征来建立的,我们逐个来看一下哪些操作实现了这三个特性。

  • 原子性: 我们大致可以认为基本数据类型的访问读写是具备原子性的(例外就是long和double的非原子协定,读者只要知道这件事就可以了,无序太过在意这些几乎不会发生的情况)如果应用场景需要一个更大范围的原子性保证,java内存模型还提供了lcok和unlock操作来满足这种需求,反应到java就是synchronized关键字,在synchronized之间的操作也是具有原子性的。
  • 可见性: 可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改.可以说volatile保证了多线程操作时变量的可见性,普通变量不能保证这一点.除了volatile只有synchronized和final这两个关键字也是可以实现的,普通变量不能保证这一点.synchronized的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write)”。final的可见性是指被final修饰的关键字一旦初始化完成,其他线程中就能看见final字段的值。
  • 有序性: java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。volatile关键字本身就包含了禁止指令重排序的语意,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获取的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。

先发性原则

先行发生是java内存模型中定义的亮相操作之间的偏序关系,如果操作A先行发生于操作B,其实就是说在发生B操作之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存变量的值、发送了消息、调用了方法等。下面是Java内存模型的一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来,他们就没有顺序性保障,虚拟机可以对他们随意地进行重排序。

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

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

3)volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是时间上的先后顺序。

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

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

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

7)对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始

8)传递性:如果A先行发生于操作B,操作B先行发生于操作C,那么可以得出操作A先行发生于操作C的结论。

java与线程

线程实现:

实现线程主要有三种方式:使用内核线程实现,使用用户线程实现,使用用户现场加轻量级进程混合实现.

使用内核线程实现

内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,负责将线程的任务映射到各个处理器上。程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口—-轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上的线程。

局限性:由于基于内核线程实现,所以各种线程操作如创建、析构及同步都需要进行系统调用,而系统调用的代价比较高,需要再用户态和内核态进行切换;其次每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程需要消耗一定的内核资源,因此一个系统支持轻量级进程的数量是有限的。

image

使用用户线程实现

狭义的用户线程指的是完全建立在用户空间的线程库上的,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户状态完成,不需要内核的帮助,如果实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且消耗低,也可以支持规模更大的线程数量,部分高性能数据库的多线程就是用用户线程实现的。优势:不需要系统内核支援

劣势:由于没有系统内核支援,需要自己考虑线程的创建、切换、调度,实现复杂,较为困难。

image

java线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种:协同式线程调度和抢占式线程调度.由于协同式线程调度容易发生阻塞导致系统奔溃,所以目前多线程的调度方式多是抢占式调度。

线程转换

java语言定义了五种线程状态,在任意时间点,一个线程只能有其中一种状态:

  • 新建(new):创建后尚未启动的线程处于这种状态
  • 运行(Runable):Runable包括了操作系统线程状态中的Runing和Ready状态,也就是处于该状态的线程可能在执行,也可能在等待CPU分配执行时间
  • 无限等待(Wating):处于这种状态的线程不会被分配CPU执行时间,他们要等待被其他线程显示唤醒,以下方法进入无限期等待状态:没有设置TimeOut参数的Object.wait() 和没有设置TimeOut参数的Thread.join()方法,LockSupport.park()方法。
  • 限期等待(Time Waiting):处于这种状态的也不会分配CPU执行时间,不过无需等待被其他线程显示唤醒,在一定时间之后他们会由系统自动唤醒,以下方法会让线程进入限期等待状态:Thread.sleep() ,设置了Timeout参数的Object.wait(),设置了Timeout参数的Thread.join(),LockSupport.parkNanos()方法,LockSupport.parkUntil()方法
  • 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:”阻塞状态”在等待获取一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而等待则是在等待一段时间或者唤醒动作的发生。程序等待进入同步区域的时候,线程进进入这种状态。
  • 结束(Terminated):已终止的线程状态,线程已经结束

image

jvm板块

image

Heap就是我们经常说的”堆”,它主要包括两大板块:Yong和Old,两个板块在分配内存的方法和Heap类似,但是回收的方式区别很大.通常来说,Yong空间会小很多,具体比例是多少要看应用场景和配置.Yong(年轻代)和Old(老年代)的划分.

Heap总大小使用-Xms来设置它的初始值,使用-Xmx来设置它的上限,在服务器端我们通常将这两个值设置为一样大.

一般Yong空间都用-Xmn来设置,Yong空间被封为两个部分,3个板块,即”一个Eden区 + 2个Survivor区”

  • Eden:我们使用new()或者newInstance()等方法创建的对象,默认都是先将对象空间放置到Eden区域的(除非这个对象太大了Eden区域放不下,或设置了一个对象阈值:-XX:PretenureSizeThreadhold,这样对象就会直接进入Old区域)
  • 2个Survivor区域通常简称为S0,S1,理论上可以认为S0和S1是一样大的,当设置某些动态参数设置后,这两个区域的大小可能会发生一些变化.
工作原理
  • 在不断创建对象过程中,Eden区域会满,满的时候就会开始做Yong GC(也称为Minor GC),而Yong 空间的的第一次GC就是找出”Eden区域活着的对象”,将这些活着的对象向S0或S1其中一个区域存放.

假设第一次选择了S0,它会逐步将活着的对象拷贝到S0区域,但是如果S0空间放满了,剩下活着的对象只能放在Old区域了,接下来的动作是直接将Eden区域清空,此时S1区域是空的

  • 第二次Eden区域满的时候,就将”Eden中活着的对象+S0活着的对象”迁移到S1,迁移方式也是一样的(同样,如果S1放不下,对象就会放到Old区域),只是这一次对象来源增加了S0区域的对象,最后被清空的区域是Eden区域和S0区域.
  • 第三次,第四次操作以此类推,S0和S1始终有个一是空的.

image

转载至:http://blog.csdn.net/fighterandknight/article/details/55806075


上一篇 应用级缓存

评论