JMM java内存模型

  JMM对于一个想要深刻了解java的程序猿来讲是不可避免的一关,本文偏理论性,尽量说的通俗易懂,若有不对的地方但愿多多指正。java

  那咱们先说一下jvm的主内存分配c++

  

 

  1 java虚拟机栈(java virtual stack)算法

  虚拟机栈是线程私有的,每一个线程都有一个本身的虚拟机栈,是java方法执行的内存模型,每一个方法执行的时候都会在虚拟机栈上建立一个栈帧,栈帧是一个数据结构,主要存储的是方法中的局部变量(基本类型,对象的引用,returnAddress类型(指向一条字节码指令的地址)),操做栈(指的就是方法编译后的操做指令的栈),动态连接,方法出口。一般所说的java内存分为栈和堆,其中所说的栈就是指的虚拟机栈。但java的内存分配并无这么简单。spring

  动态连接解释以下:编程

  每一个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接(Dynamic Linking)。数组

Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用做为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态链接缓存

  方法出口的解释以下:安全

  • 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),通常来讲,调用者的PC计数器能够做为返回地址。
  • 当执行遇到异常,而且当前方法体内没有获得处理,就会致使方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要经过异常处理器表来肯定。

  虚拟机栈会出现两种异常,一种就是常见的OOM 另外一种就是StackOverFlowError。StackOverflowError通常是递归调用所致使的,栈深度在虚拟机中也是有限制的,不然无限制的递归调用虚拟机会哭的。OOM就不用说了,当所请求的内存大于当前虚拟机栈所持有的就会出现OOM(虚拟机栈空间能够动态扩展,但分配给jvm的内存也是有限的,因此虚拟机栈也不是无限扩展的)。数据结构

  2 本地方法栈多线程

  本地方法栈和虚拟机栈基本是相似的,只不过虚拟机栈中执行的是class字节码,而本地方法栈中执行的就是本地方法的服务,其实就是调用一些由c或c++根据不一样的os平台所写的同一个方法的不一样的实现。

  3 方法区(method area)

  方法区是线程共享的区域,用于存储已经被虚拟机加载的类信息(类的字节码数据,这里要注意若是你同时加载的类不少的话须要调大方法区的空间,不然会OOM,只是对于类较少的状况下能够那么作。若是类特别多,那么能够用懒加载等机制进行处理,如spring的懒加载机制,尽可能避免同时加载过多的类),常量,静态变量和即时编译器(JIT)编译后的代码等数据。方法区其实就是咱们所说的永久代区域(只限于hotspot虚拟机的实现机制),之因此说是永久代,是此处的数据几乎不多进行垃圾回收,缘由是加载的类并非一时半刻就会消亡,不少方法会根据类在堆中建立对象,而静态变量通常是,gc的跟搜索算法的root节点,而常量是根本不会变的数据,因此都不多进行清理。

      Java虚拟机规范对这个区域的限制也很是宽松,除了能够是物理不连续的空间外,也容许固定大小和扩展性,还能够不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的(因此常量和静态变量的定义要多注意)。方法区的内存收集仍是会出现,不过这个区域的内存收集主要是针对常量池的回收和对类型的卸载。

      通常来讲方法区的内存回收比较难以使人满意。当方法区没法知足内存分配需求时将抛出OutOfMemoryError异常。

  4 运行时常量池

  JDK1.6以前字符串常量池位于方法区之中。 
  JDK1.7字符串常量池已经被挪到堆之中。

 

  java是一种动态链接的语言,常量池的做用很是重要,常量池中除了包含代码中所定义的各类基本类型(如int、long等等)和对象型(如String及数组)的常量值还,还包含一些以文本形式出现的符号引用,好比:

  类和接口的全限定名;

  字段的名称和描述符;

  方法和名称和描述符。

  在C语言中,若是一个程序要调用其它库中的函数,在链接时,该函数在库中的位置(即相对于库文件开头的偏移量)会被写在程序中,在运行时,直接去这个地址调用函数;

  在Java语言中这样,一切都是动态的。编译时,若是发现对其它类方法的调用或者对其它类字段的引用的话,记录进class文件中的,只能是一个文本形式的符号引用,在链接过程当中,虚拟机根据这个文本信息去查找对应的方法或字段。

  因此,与Java语言中的所谓“常量”不一样,class文件中的“常量”内容很非富,这些常量集中在class中的一个区域存放,一个紧接着一个,这里就称为“常量池”。

  java中的常量池技术,是为了方便快捷地建立某些对象而出现的,当须要一个对象时,就能够从池中取一个出来(若是池中没有则建立一个),则在须要重复建立相等变量时节省了不少时间。常量池其实也就是一个内存空间,不一样于使用new关键字建立的对象所在的堆空间。

  整个常量池会被JVM的一个索引引用,如同数组里面的元素集合按照索引访问同样,JVM针对这些常量池里面存储的信息也是按照索引方式进行,实际上常量池在Java程序的动态连接过程起到了一个相当重要的做用(上面有说到),下文摘自《深刻理解java虚拟机》。  

  Class文件中除了有类的版本,字段,方法,接口等信息之外,还有一项信息是常量池用于存储编译器生成的各类字面量和符号引用,这部分信息将在类加载后存放到方法区的运行时常量池中。Java虚拟机对类的每一部分(包括常量池)都有严格的规定,每一个字节用于存储哪一种数据都必须有规范上的要求,这样才可以被虚拟机承认,装载和执行。通常来讲,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

      运行时常量池相对于Class文件常量池的另一个重要特征是具有动态性,Java虚拟机并不要求常量只能在编译期产生,也就是并不是预置入Class文件常量池的内容才能进入方法区的运行时常量池中,运行期间也可将新的常量放入常量池中。

      常量池是方法区的一部分,因此受到内存的限制,当没法申请到足够内存时会抛出OutOfMemoryError异常

  5 堆(heap)

  堆就是内存中最大的一块区域,惟一用于存储对象实例的地方。这个地方也是gc算法主要的战场。不过随着JIT(即时编译)的发展和逃逸技术成熟,并非全部的对象都在堆上建立。下文摘自《深刻理解java虚拟机》。

  在Java编程语言和环境中,即时编译器(JIT compiler,just-in-time compiler)是一个把Java的字节码(包括须要被解释的指令的程序)转换成能够直接发送给处理器的指令的程序。当你写好一个Java程序后,源语言的语句将由Java编译器编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的指令代码(好比,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是能够发送给任何平台而且能在那个平台上运行的独立于平台的代码。

  java的内存分配大体就是这个样子,jvm中也配有不少的参数对上面的数据进行调节。这里就不进行列举,会在单独的一篇gc相关的文章中进行详细的说明。下面说一下在多核处理器的时代,jvm是如何处理并发带来的问题的。

  并发控制

  多核的cpu能够并发的执行多个线程,而每一个线程都有一个本身的本地工做区(其实就是分配给每一个核的系统缓存和寄存器),存储从上面主内存获取的数据做副本在工做区中运行,若是数据是多线程中共享的,并且线程之间是不能进行数据交换,这就涉及了共享变量数据不一致的问题。java经过sychronized volatile Lock锁等机制控制共享变量的可见性。

  synchronized和lock会有单独的章节分别讲解实现机制, 这两个不用说在可见性和原子性上都得道了保障。而volatile仅保证了数据的可见性,仅当数据在read 和load的时候数据在其余线程中改变会在当前线程中有所感知,若是过了这两个阶段,那只能很差意思了,数据不一致,(其实volatile所作的就是避免使用缓存不将主存上的数据存储到线程工做内存中,在read和load阶段都是从主存中获取数据这样就可以感知到其余线程对变量的修改)。volatile仅仅是在早期的jdk版本中,因为synchronized的性能很差而出现的一个保证可见性的一个解决方案。如今的jdk版本的synchronized和lock都获得了必定的优化,因此通常的状况下是不建议采用volatile变量的,除非你知道你如今用volatile到底在干什么,由于它并不能保证并发的正确性。

read and load 从主存复制变量到当前工做内存,use and assign  执行代码,改变共享变量值,store and write 用工做内存数据刷新主存相关内容,其中use and assign 能够屡次出现。volitale适合一些幂等操做。这个会在lock的nofairsync的实现中解说。

说到这里就不得不说一下happen before原则了。它是Java内存模型中定义的两项操做之间的偏序关系,若是操做A先行发生于操做B,其意思就是说,在发生操做B以前,操做A产生的影响都能被操做B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的前后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。

  下面是Java内存模型中的八条可保证happen—before的规则,它们无需任何同步器协助就已经存在,能够在编码中直接使用。若是两个操做之间的关系不在此列,而且没法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机能够对它们进行随机地重排序(jvm为了可以充分的利用cpu,提升利用率,jvm会将先后无关的代码或者说是操做进行重排序,让那些须要等待IO或者其余资源的操做排在后面,而其余可以瞬间完成的操做放在前面先执行,充分的利用cpu的资源)。

    一、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操做happen—before(时间上)后执行的操做。

    二、管理锁定规则:一个unlock操做happen—before后面(时间上的前后顺序,下同)对同一个锁的lock操做。

    三、volatile变量规则:对一个volatile变量的写操做happen—before后面对该变量的读操做。

    四、线程启动规则:Thread对象的start()方法happen—before此线程的每个动做。

    五、线程终止规则:线程的全部操做都happen—before对此线程的终止检测,能够经过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

    六、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。

    七、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。

    八、传递性:若是操做A happen—before操做B,操做B happen—before操做C,那么能够得出A happen—before操做C。

相关文章
相关标签/搜索