java内存模型面试回答及内存与cpu之间的关系

java 内存模型与JVM的内存模型是一个意思吗? - 知乎有问题,上知乎。知乎作为中文互联网最大的知识分享平台,以「知识连接一切」为愿景,致力于构建一个人人都可以便捷接入的知识分享网络,让人们便捷地与世界分享知识、经验和见解,发现更大的世界。2被浏览71分享邀请回答赞同 添加评论分享收藏感谢收起写回答jvm内存模型和内存分配
jvm内存模型和内存分配
1.什么是jvm?
(1)jvm是一种用于计算设备的规范,它是一个虚构出来的机器,是通过在实际的计算机上仿真模拟各种功能实现的。
(2)jvm包含一套字节码指令集,一组寄存器,一个栈,一个垃圾回收堆和一个存储方法域。
(3)JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
2.jdk、jre、jvm是什么关系?
(1)JRE(Java Runtime Environment),也就是java平台。所有的java程序都要在JRE环境下才能运行。
(2)JDK(Java Development Kit),是开发者用来编译、调试程序用的开发包。JDK也是JAVA程序需要在JRE上运行。
(3)JVM(Java Virtual Machine),是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
(1)jvm是java的核心和基础,在java编译器和os平台之间的虚拟处理器,可在上面执行字节码程序。
(2)java编译器只要面向jvm,生成jvm能理解的字节码文件。java源文件经编译成字节码程序,通过jvm将每条指令翻译成不同的机器码
,通过特定平台运行。
4. JVM执行程序的过程
1) 加载.class文件
2) 管理并分配内存
3) 执行垃圾收集
JRE(java运行时环境)由JVM构造的java程序的运行环,也是Java程序运行的环境,但是他同时一个操作系统的一个应用程序一个进程,
因此他也有他自己的运行的生命周期,也有自己的代码和数据空间。
JVM在整个jdk中处于最底层,负责于操作系统的交互,用来屏蔽操作系统环境,
提供一个完整的Java运行环境,因此也就虚拟计算机。
操作系统装入JVM是通过jdk中Java.exe来完成,
通过下面4步来完成JVM环境:
1) 创建JVM装载环境和配置
2) 装载JVM.dll
3) 初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例
4) 调用JNIEnv实例装载并处理class类。
5. JVM的生命周期
1) JVM实例对应了一个独立运行的java程序它是进程级别
a) 启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void
main(String[] args)函数的class都可以作为JVM实例运行的起点
b) 运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以表明自己创建的线程是守护线程
c) 消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出
2) JVM执行引擎实例则对应了属于用户运行程序的线程它是线程级别的
6、JVM内存模型
(1)java代码具体执行过程如下图,
(2)运行时数据区,即jvm内存结构图如下图
(3)运行时数据区存储了哪些数据?
a) 程序计数器(PC寄存器)
由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,
因此,为了能够使得每个线程都在线程切换后能够恢复在切 换 之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,
否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,
因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables) 、 操作数栈(Operand Stack) 、
指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到) 的引用 (Reference to runtime constant pool)、
方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。
c)本地方法栈
本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的
Java中的堆是用来存储对象本身的以及数组(数组引用是存放在Java栈中的)。堆是被所有线程共享的,在JVM中只有一个堆。
与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,
对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
7、JVM内存溢出的情况
a) 程序计数器(Program Counter Register)
每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。该内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM(内存溢出:OutOfMemoryError)情况的区域。
b)Java虚拟机栈(Java Virtual Machine Stacks)
在Java虚拟机规范中,对这个区域规定了两种异常情况:
1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
2、如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这两种情况存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。
在单线程的操作中,无论是由于栈帧太大,还是虚拟机栈空间太小,当栈空间无法分配时,虚拟机抛出的都是StackOverflowError异常,而不会得到OutOfMemoryError异常。
而在多线程环境下,则会抛出OutOfMemoryError异常。
c)堆Java Heap
Java Heap是Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域。几乎所有的对象实例和数组都在这类分配内存。Java Heap是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。
根据Java虚拟机规范的规定,Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。
d)方法区域,又被称为“永久代”,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
没有更多推荐了,JVM之内存构成(二)--JAVA内存模型与并发
这部分内容,跟并发有关
我们知道,多任务处理,在现代操作系统几乎是必备功能。让计算机同时去做几件事情,不仅因为CPU运算能力太强大了,还有一个重要原因,CPU的运算速度远远高于它的存储和通信子系统的速度,大量时间耗费在磁盘I/O,网络I/O,数据库访问
虚拟机层面,如何实现多线程,多线程之间因数据共享或竞争而引发的一系列问题及解决方案
物理机中的并发–硬件效率与一致性
物理机遇到的并发与虚拟机中的情况,有不少相似之处,再扩展到分布式系统,我发现,其实也有不少相似之处。这之间有许多值得玩味的地方。
让计算机并发执行多个运算任务
这里面,不可能仅仅靠CPU计算就搞定的。CPU至少要跟内存交互,读取运算数据,存储运算结果,这个IO很难消除。当然,也无法仅仅靠CPU内的寄存器完成所有运算任务
CPU与存储设备之间的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写尽可能接近CPU速度的高速缓存(Cache),作为内存与CPU间的缓冲。将运算需要的数据复制到缓冲,让运算快速进行,完后将缓存同步到内存。如此,CPU就无需等待缓慢的内存读写
在速度差距很大时,利用缓存来缓冲,用空间换时间;但同时会带来数据同步问题
引入了缓存一致性(Cache Coherence)问题
多核处理器里,每个CPU都有自己的高速缓存(一级、二级、三级),而它们又共享同一主内存。
当多个CPU的运算任务都涉及同一块主内存区域,可能导致各自的缓存数据不一致;数据同步回主存时,以谁的缓存数据为准呢?
为解决一致性问题,需要CPU访问缓存时都遵循一些协议,读写时,根据操作协议来。如MSI、MESI、MOSI、Synapse、Firefly、Dragon、Protocol
内存模型: 可以理解为,在特定操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
同时为了使得处理器充分被利用,CPU可能会对输入代码进行乱序执行(Out-of-Order Execution)优化,CPU会在计算后将结果重组,保证结果与顺序执行一致。Java虚拟机的即时编译器也有类似的指令重排序(Instruction Reorder)优化
若一个计算任务依赖另一计算任务的中间结果,那其顺序性,不能靠代码的先后顺序来保证
Java线程执行的内存模型
Java虚拟机使用定义种Java内存模型,以屏蔽各种硬件和OS的内存访问差异,以实现让Java程序在各个平台下都能达到一致的并发效果。C/C++直接使用物理硬件和OS的内存模型。
定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题
每条线程都有自己的工作内存(可与高速缓存类比)
线程读写变量,必须在自己工作的工作内存中进行
工作内存保存主内存变量的值的拷贝
不能直接读写主内存的变量
不同线程间,无法直接访问对方工作内存的变量
线程间变量值的传递,需要通过主内存
所有的变量存在主内存(虽然名字跟物理机的主内存一样,可类比,但此主内存只是虚拟机内存的一部分)
内存间交互
一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存,Java内存模型定义了8种操作。这些操作,都是原子操作
Instruction
Main Memory
将变量标识为一条线程独占状态
Main Memory
释放被锁定的变量,释放后的变量才能被其他线程锁定
Main Memory
变量值从主内存读取到线程的工作内存,以便紧接着的load操作
Working Memeory
将read操作得到变量值放入工作内存的变量副本中
Working Memeory
将工作内存的变量值传递给线程执行引擎
Working Memory
將一个从执行引擎接收到的值,赋给工作内存的变量
Working Memory
将工作内存中的一个变量的值,传送到主内存,以便紧接着的write操作
Main Memory
将store操作的变量值,放入主内存的变量中
变量从主内存复制到工作内存,顺序执行read和load
变量从工作内存同步到主内存,顺序执行store和write
long和double的非原子性协定
Nonatomic Treatment of double and long Variables
Java内存模型要求8个操作都具有原子性,但对64位的数据类型,long和double,模型定义了相对宽松
允许虚拟机将没有被volatile修饰的64位数据的读写操作,划分为2次32位的操作。
允许,并强烈建议,虚拟机将这些操作实现为原子性操作。
目前商用Java虚拟机几乎都选择把64位数据的读写作为原子操作来对待
编写代码时,一般不需为long或double专门声明为volatile
Volatile类型变量的特殊规则和语义
前面说过,Java内存模型,其实是定义读写内存变量的规则。
有些类型的变量比较特殊,除了上面所述的8个基本操作原则外,有特殊的规则。
read、load、use操作,须连续一起出现,每次use时,都从主内存read,工作内存load主内存的值,相当于每次use都从主内存中获取变量的最新值。保证能看见其他线程对变量的修改
assign、store、write操作,须连续一起出现,工作内存中的每次修改,须立刻同步回主内存。保证其他线程可以看到自己对变量的修改
两条线程,若A线程对变量a的use/assign操作,先于B线程对变量b的use/assign操作,那么A线程对a变量的read/write操作,先于B线程对变量b的read/write操作。该规则要求变量不被指令重排序优化,保证代码执行顺序与程序的顺序相同
保证可见性
禁止指令重排优化
保证可见性
volatile是轻量级的synchronized,在多CPU开发中,保证了共享变量的“可见性”。
指当一条线程修改了变量的值,新的值可以被其他线程立即知道
volatile只能保证可见性,但无法保证原子性,which is a necessity for synchronization.
因此,如果不符合下面两个规则的运算场景,我们需要通過加锁,如synchronized关键字和java.util.concurrent包下的原子类,来保证源自性。如果符合,volatile就能保证同步
运算结果不依赖当前值,或者能够确保只有单一线程修改变量的值
变量不需要与其他的状态变量共同参与不变约束
如下面的代码,就非常适合用volatile变量来控制并发
volatile boolean shutdownR
public void shutdown() {
shutdownRequested = true;
public void doWork() {
while(!shutdownRequest) {
禁止指令重排优化
被volatile修饰的变量,多执行了lock addl $0x0,(%esp)操作
这个操作,相当于一个内存屏障(Memory Barrier/Memory Fence),意思是,重排序时,不能把后面的指令重排序到內存屏障之前的位置
lock addl $0x0,(%esp)汇编指令,把ESP寄存器的值加0,这个是空操作。其作用,是使得本CPU的Cache写入内存,该写入动作,也会引起别的CPU或别的内核无效化(Invalidate)其Cache,相当于对Cache中的变量,做了一次如Java内存模型中的”Store且Write操作”。所以,通过这样一个空操作,可让volatile变量的修改,对其他CPU立即可见
硬件架构上讲,指令重排序,是指CPU采用了允许将多条指令不按程序规定的顺序,分开发送给各个相应电路单元处理,同时保证结果正确,与程序顺序执行的结果一致。
高效并发的原则
Java内存模型,围绕着并发过程中如何实现原子性、可见性和有序性,3个特征来建立。我们来看看哪些操作,实现了这些特征
可见性、有序性和原子性
原子性(Atomicity)
对基本数据类型的访问和读写是具备原子性的。
对于更大范围的原子性保证,Java内存提供lock,unlock操作,但未直接开发给用户使用
更高层次,可以使用字节码指令monitorenter和monitorexit来**隐式使用**lock和unlock操作。这两个字节码指令反映到Java代码中,就是同步块——synchronized关键字。因此synchronized块之间的操作也具有原子性。
可见性(Visibility)
当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取之前从主内存刷新变量值来实现
可见性的。volatile的特殊规则保证了新值能够立即同步到主内存,每次使用前立即从主内存刷新。
synchronized和final也能实现可见性。unlock前,先同步数据到主存。final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么其他线程中就能看见final字段的值
有序性(Ordering)
Java程序的有序性可以总结为一句话,如果在本线程内观察,所有的操作都是有序的(线程内表现为串行的语义);如果在一个线程中观察另一个线程,所有的操作都是
无序的(指令重排序和工作内存与主内存同步延迟线性)
先行发生(Happens-Before)
如果Java内存模型中所有的有序性,仅仅靠volatile和synchronized来完成,那么一些操作会很繁琐,但我们没有感觉得到,因为有happens-before原則。
该原则是判断数据是否存在竞争、线程是否安全的主要依据
Java内存模型中定义的两项操作之间的偏序关系。如果操作A Happens-Before 操作B,意思是,B发生时,A产生的影响能被B观察到
如果操作A和操作C之间,不存在先行发生关系,C出现在A和B之间,那么,C线程对变量j的修改,B线程不一定观察得到,此时,B读取到的数据可能不是最新的,不是线程安全的
Java内存模型中的先行发生
程序次序规则(Program Order Rule)
一个线程内, 按照控制流顺序,写在前面的操作先行发生与写在后面的操作
管程锁定规则(Monitor Lock Rule)
一个unlock操作先行发生于后面对同一个锁的lock操作(就是拿到一个同步监视器的锁后,其他线程在这个锁被释放前,必须等待)
Volatile变量规则(Volatile Variable Rule)
对一个volatile变量的写操作先行发生于后面对这个变量的读操作,“后面”指时间上的先后
线程启动规则(Thread Start Rule)
Thread对象的start()方法先行发生于此线程每一个动作
线程终止规则(Thread Termination Rule)
线程中的所有操作都先行发生于对此线程的终止检测
线程中断规则(Thread Interruption Rule)
对线程的interrupt()方法的调用,先行发生于被中断线程的代码检测到中断事件的发生
对象终结规则(Finalizer Rule)
对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法
传递性(Transitivity)
A先行发生于B,B先行发生于C,那么,A先行发生于C
时间上的先后,不等于“先行发生”。
一操作先行发生,推不出时间上先发生。有指令重排序存在。
时间先后顺序与happens-before基本没太大关系,衡量并发安全问题,一切以happens-before原则为准,不要受到时间顺序的干扰
infoq 深入理解Java内存模型
infoq Java并发编程的艺术
并发编程网
没有更多推荐了,博观而约取。
Java 线程通信内存模型---主内存与工作内存
多任务和高并发是衡量一台计算机处理器的能力重要指标之一。一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per Second,TPS)这个指标比较能说明问题,它代表着一秒内服务器平均能响应的请求数,而TPS值与程序的并发能力有着非常密切的关系。在讨论Java内存模型和线程之前,先简单介绍一下硬件的效率与一致性。
硬件的效率与一致性
  由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。
  基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,如下图所示:多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。Java虚拟机内存模型中定义的内存访问操作与硬件的缓存访问操作是具有可比性的,后续将介绍Java内存模型。
  除此之外,为了使得处理器内部的运算单元能竟可能被充分利用,处理器可能会对输入代码进行乱起执行(Out-Of-Order Execution)优化,处理器会在计算之后将对乱序执行的代码进行结果重组,保证结果准确性。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Recorder)优化。
Java中的内存模型
Java中的内存模型与硬件中的内存模型类似.
主内存与工作内存
  Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
  Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示,和上图很类似。
NOTE: 工作内存只会拷贝对象的引用,而不是直接拷贝整个对象,对象在堆中生成,对所有对象都是可见的。
主内存与工作内存之间的通信
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
内存之间的交互操作
  关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
八种线程之间的交互指令:
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状 。
unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
JMM对交互指令的约束
如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。
Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
不允许read和load、store和write操作之一单独出现
不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
主内存与工作内存之间的通信
两个线程之间进行通信时,必须经过下面两个步骤
线程A把本地内存A中更新过的共享变量刷新到主内存中去。
线程B到主内存中去读取线程A之前已更新过的共享变量。
这两个过程就是通过JMM内存之间的交互指令实现的。
如上图所示,本地内存A和B有主内存中共享变量x的副本。
假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。
随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
ref: 《深入理解JVM虚拟机》
没有更多推荐了,java内存模型及GC原理&&
前段时间在一个项目的性能测试中又发生了一次OOM(Out of swap sapce),情形和以前网店版的那次差不多,比上次更奇怪的是,此次搞了几天之后啥都没调整系统就自动好了,死活没法再重现之前的OOM了!问题虽然蹊跷,但也趁此机会再次对JVM堆模型、GC垃圾算法等进行了一次系统梳理;
JVM管理的内存叫堆;在32Bit操作系统上有4G的限制,一般来说Windows下为2G,而Linux下为3G;64Bit的就没有这个限制。JVM初始分配的内存由-Xms指定,默认是物理内存的1/64但小于1G。JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4但小于1G。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制,可以由-XX:MinHeapFreeRatio=指定。&默认空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制,可以由-XX:MaxHeapFreeRatio=指定。服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小,所以上面的两个参数没啥用。
分代/堆模型
分代是Java垃圾收集的一大亮点,根据对象的生命周期长短,把堆分为3个代:Young,Old和Permanent,根据不同代的特点采用不同的收集算法,可以扬长避短。可参考如下的模型图:
Young(Nursery):年轻代
研究表明大部分对象都是朝生暮死,随生随灭的。所以对于年轻代在GC时都采取复制收集算法,具体算法参考下面的描述;Young的默认值为4M,随堆内存增大,约为1/15,JVM会根据情况动态管理其大小变化。Young里面又分为3个区域,一个Eden,所有新建对象都会存在于该区,两个Survivor区,用来实施复制算法。-XX:NewRatio= 参数可以设置Young与Old的大小比例,-server时默认为1:2,但实际上young启动时远低于这个比率?如果信不过JVM,也可以用-Xmn硬性规定其大小,有文档推荐设为Heap总大小的1/4。-XX:SurvivorRatio= 参数可以设置Eden与Survivor的比例,默认为32。Survivio大了会浪费,小了的话,会使一些年轻对象潜逃到老人区,引起老人区的不安,但这个参数对性能并不太重要。
Old(Tenured):年老代
年轻代的对象如果能够挺过数次收集,就会进入老人区。老人区使用标记整理算法。因为老人区的对象都没那么容易死的,采用复制算法就要反复的复制对象,很不合算,只好采用标记清理算法,但标记清理算法其实也不轻松,每次都要遍历区域内所有对象,所以还是没有免费的午餐啊。-XX:MaxTenuringThreshold= 设置熬过年轻代多少次收集后移入老人区,CMS中默认为0,熬过第一次GC就转入,可以用-XX:+PrintTenuringDistribution查看。
Permanent:持久代
装载Class信息等基础数据,默认64M,如果是类很多很多的服务程序,需要加大其设置-XX:MaxPermSize=,否则它满了之后会引起fullgc()或Out of Memory。 注意Spring,Hibernate这类喜欢AOP动态生成类的框架需要更多的持久代内存。一般情况下,持久代是不会进行GC的,除非通过-XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled进行强制设置。
当每个代满了之后都会自动促发collection,各收集器触发的条件不一样,当然也可以通过一些参数进行强制设定。主要分为两种类型:
Minor Collection:GC用较高的频率对young进行扫描和回收,采用复制算法。
Major Collection:同时对Young和Old进行内存收集,也叫Full GC;因为成本关系对Old的检查回收频率要比Young低很多,采用标记清除/标记整理算法。可以通过调用代码System.gc()引发major collection,使用-XX:+DisableExplicitGC禁止它,或设为CMS并发-XX:+ExplicitGCInvokesConcurrent。
更为具体的阐述如下:由于年轻代进进出出的人多而频繁,所以年轻代的GC也就频繁一点,但涉及范围也就年轻代这点弹丸之地内的对象,其特点就是少量,多次,但快速,称之为Minor Collection。当年轻代的内存使用达到一定的阀值时,Minor Collection就被触发,Eden及某一Survior space(from space)之内存活的的对象被移到另一个空的Survior space(to space)中,然后from space和to space角色对调。当一个对象在两个survivor space之间移动过一定次数(达到预设的阀值)时,它就足够old了,够资格呆在年老代了。当然,如果survivor space比较小不足以容下所有live objects时,部分live objects也会直接晋升到年老代。Survior spaces可以看作是Eden和年老代之间的缓冲,通过该缓冲可以检验一个对象生命周期是否足够的长,因为某些对象虽然逃过了一次Minor Collection,并不能说明其生命周期足够长,说不定在下一次Minor Collection之前就挂了。这样一定程度上确保了进入年老代的对象是货真价实的,减少了年老代空间使用的增长速度,也就降低年老代GC的频率。当年老代或者永久代的内存使用达到一定阀值时,一次基于所有代的GC就触发了,其特定是涉及范围广(量大),耗费的时间相对较长(较慢),但是频率比较低(次数少),称之为Major Collection(Full Collection)。通常,首先使用针对年轻代的GC算法进行年轻代的GC,然后使用针对年老代的GC算法对年老代和永久代进行GC。
基本GC收集算法
复制(copying):将堆内分成两个相同空间,从根(ThreadLocal的对象,静态对象)开始访问每一个关联的活跃对象,将空间A的活跃对象全部复制到空间B,然后一次性回收整个空间A。因为只访问活跃对象,将所有活动对象复制走之后就清空整个空间,不用去访问死对象,所以遍历空间的成本较小,但需要巨大的复制成本和较多的内存。可参考如下的示例图:
标记清除(mark-sweep):收集器先从根开始访问所有活跃对象,标记为活跃对象。然后再遍历一次整个内存区域,把所有没有标记活跃的对象进行回收处理。该算法遍历整个空间的成本较大暂停时间随空间大小线性增大,而且整理后堆里的碎片很多。可参考如下的示例图:
标记整理(mark-sweep-compact):综合了上述两者的做法和优点,先标记活跃对象,然后将其合并成较大的内存块。可参考如下的示例图:
GC收集器类型
古老的串行收集器(Serial Collector)-XX:+UseSerialGC:策略为年轻代串行复制,年老代串行标记整理。可参考如下的示例图:
吞吐量优先的并行收集器(Throughput Collector)-XX:+UseParallelGC:这是JDK5 -server的默认值。策略为:年轻代:暂停应用程序,多个垃圾收集线程并行的复制收集,线程数默认为CPU个数,CPU很多时,可用-XX:ParallelGCThreads= 设定线程数。年老代:暂停应用程序,与串行收集器一样,单垃圾收集线程标记整理。如上可知该收集器需要2+的CPU时才会优于串行收集器,适用于后台处理,科学计算。可以使用-XX:MaxGCPauseMillis= 和 -XX:GCTimeRatio 来调整GC的时间。可参考如下的示例图:
暂停时间优先的并发收集器(Concurrent Low Pause Collector-CMS)-XX:+UseConcMarkSweepGC:这是以上两种策略的升级版,策略为:年轻代:同样是暂停应用程序,多个垃圾收集线程并行的复制收集。年老代:则只有两次短暂停,其他时间应用程序与收集线程并发的清除。若要采用标记整理算法,则可以通过设置参数实现;可参考如下的示例图:
增量并发收集器(Incremental Concurrent-Mark-Sweep/i-CMS):虽然CMS收集算法在最为耗时的内存区域遍历时采用多线程并发操作,但对于服务器CPU资源不够的情况下,其实对性能是没有提升的,反而会导致系统吞吐量的下降,为了尽量避免这种情况的出现,就有了增量CMS收集算法,就是在并发标记、清理的时候让GC线程、用户线程交叉运行,尽量减少GC线程的全程独占式执行;可参考如下的示例图:
对于以上的GC收集器的详细设置参数,可以参考JVM选项的超完整收集《》,这里就不一一详述了。
并行、并发的区别
并行(Parallel)与并发(Concurrent)仅一字之差,但体现的意思却完全不同,这可能也是很多同学非常困惑的地方,要想深刻体会这其中的差别,可以多揣摩下上面关于GC收集器的示例图;
并行:指多条垃圾收集线程并行,此时用户线程是没有运行的;
并发:指用户线程与垃圾收集线程并发执行,程序在继续运行,而垃圾收集程序运行于另一个个CPU上。
并发收集一开始会很短暂的停止一次所有线程来开始初始标记根对象,然后标记线程与应用线程一起并发运行,最后又很短的暂停一次,多线程并行的重新标记之前可能因为并发而漏掉的对象,然后就开始与应用程序并发的清除过程。可见,最长的两个遍历过程都是与应用程序并发执行的,比以前的串行算法改进太多太多了!!!串行标记清除是等年老代满了再开始收集的,而并发收集因为要与应用程序一起运行,如果满了才收集,应用程序就无内存可用,所以系统默认68%满的时候就开始收集。内存已设得较大,吃内存又没有这么快的时候,可以用-XX:CMSInitiatingOccupancyFraction=恰当增大该比率。
年轻代的痛
由于对年轻代的复制收集,依然必须停止所有应用程序线程,原理如此,只能靠多CPU,多收集线程并发来提高收集速度,但除非你的Server独占整台服务器,否则如果服务器上本身还有很多其他线程时,切换起来速度就..... 所以,搞到最后,暂停时间的瓶颈就落在了年轻代的复制算法上。因此Young的大小设置挺重要的,大点就不用频繁GC,而且增大GC的间隔后,可以让多点对象自己死掉而不用复制了。但Young增大时,GC造成的停顿时间攀升得非常恐怖,据某人的测试结果显示:默认8M的Young,只需要几毫秒的时间,64M就升到90毫秒,而升到256M时,就要到300毫秒了,峰值还会攀到恐怖的800ms。谁叫复制算法,要等Young满了才开始收集,开始收集就要停止所有线程呢
被转藏 : 1次
被转藏 : 1次

我要回帖

更多关于 单cpu下的java内存模型 的文章

 

随机推荐