首页 > 代码库 > JMM java内存模型
JMM java内存模型
JMM对于一个想要深入了解java的程序猿来说是不可避免的一关,本文偏理论性,尽可能说的通俗易懂,如有不对的地方希望多多指正。
那我们先说一下jvm的主内存分配
1 java虚拟机栈(java virtual stack)
虚拟机栈是线程私有的,每个线程都有一个自己的虚拟机栈,是java方法执行的内存模型,每个方法执行的时候都会在虚拟机栈上创建一个栈帧,栈帧是一个数据结构,主要存储的是方法中的局部变量(基本类型,对象的引用,returnAddress类型(指向一条字节码指令的地址)),操作栈(指的就是方法编译后的操作指令的栈),动态链接,方法出口。通常所说的java内存分为栈和堆,其中所说的栈就是指的虚拟机栈。但java的内存分配并没有这么简单。
动态链接解释如下:
每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(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的资源)。
1、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
2、管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
4、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
5、线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
6、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
7、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
8、传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
JMM java内存模型