深入理解JVM线程模型
1、 jvm内存模型
在描述jvm线程模型之前,我们先深入的理解下,jvm内存模型。在jvm1.8之前,jvm的逻辑结构和物理结构是对应的。即Jvm在初始化的时候,会为堆(heap),栈(stack),元数据区(matespace)分配指定的内存大小,Jvm线程启动的时候会向服务器申请指定的内存地址空间进行分配。在jdk1.8之后,使用了G1垃圾回收器,逻辑上依然存在堆,栈,元数据区。但是在物理结构上,G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
1.1、 jvm内存模型介绍
1.2、 Jvm堆
Java堆是和Java应用程序最密切的内存空间,几乎所有的对象都放到堆中。并且堆完全由Jvm管理,通过垃圾回收机制,垃圾对象会被自动清理,而不需显式的释放。
根据垃圾回收机制的不同,Java堆通常被分为以下的集中不同的结构。
构成 | 描述 |
New Generation | 由 Eden + Survivor (From Space + To Space)组成 |
Eden | 所以的new出来的新对象都存放到Eden区 |
Survivor Space | Eden每次垃圾清理过后,任然没又被清理的对象,会转移到交换区中 |
Old Generation | 在交换区中未被清理的对象(默认清理18次标记),将转移到老年代。 |
1.3、 Jvm栈
Java栈是一块线程私有的内存空间,Java栈和线程执行密切相关。线程的执行基本单位就是函数调用,每次函数调用的数据就会通过Java栈传递。
Java栈与数据结构上的栈有着类似的含义,它是一块先进后出的数据结构,只支持出栈和入栈的两种操作。在Java栈中保存的主要内容为栈帧。每次调用一个函数,都会有一个对应的栈帧被压入Java栈。每一个函数调用结束,都会有一个栈帧被弹出Java栈。例如:
如图所示,每次调用一个函数都会被当做栈帧压入到栈中。其中每一个栈帧对应一个函数。由于每次调用函数都会生成一个栈帧,从而占用一定的栈空间。如果线程中存在大量的递归操作,会频繁的压栈,导致栈的深入过于深入,当栈的空间被消耗殆尽的时候,会抛出StackOverflowError栈溢出错误。
当函数执行结束返回时,栈帧从Java栈中被弹出。Java方法有两种返回的方式,一种是正常函数返回,即使用 return; 另外一种是抛出异常。不管哪种方式,都会导致栈帧被弹出。
1.3.1、 局部变量表
局部变量表示栈帧的重要组成部分之一。它用于保存函数已经局部变量。局部变量表中的变量只有在当前的函数中调用有效,当调用函数结束以后,随着函数栈帧的销毁,局部变量表也随之销毁。
1.3.2、 操作数栈
操作数栈也是栈帧中重要的内容之一,它主要保存计算过程中的结果,同事作为计算过程临时变量的存储空间。
操作数栈也是一个先进后出的数据结构,只支持入栈和出栈的两种操作,Java的很多字节码指令都是通过操作数栈进行参数传递的。比如iadd指令,它就会在操作数栈中弹出两个整数进行加法计算,计算结果会被入栈。入下图所示:
1.3.3、 帧数据区
每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接
1.4、 Jvm方法区(jdk1.8元数据区)
它主要存放一些虚拟机加载的类信息,常量,静态变量,即使编译器后的代码等数据。根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
1.4.1、 运行时常量池
运行时常量区是方法区的一部分。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。还会有一些符号引用转换的直接引用一保存在运行时常量池中。
运行时常量池具备动态性,也就是运行期间也可以将新的常量放入池中,例如String.intern()方法。当常量池无法再申请到内存时,会抛出OutOfMemoryError异常
2、 jvm线程模型
通过Jvm内存模型,我们可以发现,Jvm其实就是操作系统的一种镜像。是软件层次的虚拟机。其中我们队Jvm内存模型分析可知:堆,方法区是线程共有的;栈是每个线程私有的。
讨论Java内存模型和线程之前,先简单介绍一下硬件的效率与一致性
由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,如下图所示:多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。Java虚拟机内存模型中定义的内存访问操作与硬件的缓存访问操作是具有可比性的,后续将介绍Java内存模型。
除此之外,为了使得处理器内部的运算单元能竟可能被充分利用,处理器可能会对输入代码进行乱起执行(Out-Of-Order Execution)优化,处理器会在计算之后将对乱序执行的代码进行结果重组,保证结果准确性。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Recorder)优化。
2.1、 Java内存模型
定义Java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在JDK1.5发布后,Java内存模型就已经成熟和完善起来了。
2.1.1、 主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示,和上图很类似
2.1.2、 java内存变量的交互操作
主内存变量和工作内存变量之间的交互过程如下:
3、 线程安全
通过上面分析,可以发现,每个线程都拥有自己的工作内存,工作内存是线程私有的。所以每个线程对堆中的共享变量进行修改对其他的线程而言是不可见的。
Java内存模型中,程序(进程)拥有一块内存空间,可以被所有的线程共享,即MainMemory(主内存:堆);而每个线程又有一块独立的内存空间,即WorkingMemory(工作内存:栈)。普通情况下,当线程需要对某一共享变量进行修改时,通常会进行如下的过程:
1.从主内存中拷贝变量的一份副本,并装载到工作内存中;
2.在工作内存中执行代码,修改副本的值;
3.用工作内存中的副本值更新主存中的相关变量值。
所谓“线程安全”,即多个线程同时执行同一段代码时,不会出现不确定的或者与单线程条件下不一致的结果。通常,下列三种条件居其一的并发访问被JVM认为是线程安全的:
有final关键字修饰且已被赋值;
有volatile关键字修饰;
有锁保护(synchronized、ReentrantLock等)。