jvm内存模型完整图解(从jvm内存模型到原理)
jvm内存模型完整图解(从jvm内存模型到原理)同步是指程序用于控制不同线程之间操作发生相对顺序的机制。2.线程之间的同步线程之间的通信机制有两种共享内存和消息传递。1).在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。2).在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。
不管是日常工作还是面试中用到的问到的都是java的内存模型,越是底层的知识越应该深入了解,这样在工作中才能更好的了解系统原理,处理故障问题。剖析内存结构我们先引入个问题,就是并发编程问题。(这里其实是指 jvm运行时内存模型)
并发编程的两个关键问题:线程之间的通信和同步。
1.线程之间的通信
线程的通信是指线程之间以何种机制来交换信息。
线程之间的通信机制有两种共享内存和消息传递。
1).在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。
2).在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。
2.线程之间的同步
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
Java内存模型即Java Memory Model(JMM)决定一个线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
一、在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区
栈区:栈=虚拟机栈 本地方法栈
JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。
线程栈还包含了当前方法的所有本地变量信息。一个线程只能读取自己的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。即使两个线程执行的是同一段代码,它们也会各自在自己的线程栈中创建本地变量,因此,每个线程中的本地变量都会有自己的版本。一个本地变量如果是原始类型,那么它会被完全存储到栈区。
所有原始类型(boolean byte short char int long float double)的本地变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的本地变量,一个线程可以传递一个副本给另一个线程,当它们之间是无法共享的。
堆区:堆=堆 方法区
堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。一个本地变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。
对象的创建:类加载、分配内存、内存区域的初始化、虚拟机对对象进行必要的设置。
对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。
对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。
Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。
堆中的对象可以被多线程共享。如果一个线程获得一个对象的应用,它便可访问这个对象的成员变量。如果两个线程同时调用了同一个对象的同一个方法,那么这两个线程便可同时访问这个对象的成员变量,但是对于本地变量,每个线程都会拷贝一份到自己的线程栈中。
线程在硬件内存写入过程:
CPU-->CPU寄存器-->CPU缓存器-->主内存
当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存,进而在读取CPU缓存到寄存器。当CPU需要写数据到主存时,同样会先flush寄存器到CPU缓存,然后再在某些节点把缓存数据flush到主存。
volatile关键字:
保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile原理是基于CPU内存屏障指令实现的。
synchronization关键字:
synchronized代码块可以保证同一个时刻只能有一个线程进入代码竞争区,synchronized代码块也能保证代码块中所有变量都将会从主存中读,当线程退出代码块时,对所有变量的更新将会flush到主存,不管这些变量是不是volatile类型的。
二、在了解了内存模型后,看下内存的分配过程:
内存申请过程:
1.JVM 会试图为相关Java对象在Eden中初始化一块内存区域
2.当Eden空间足够时,内存申请结束。否则到下一步
3.JVM 试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收) 释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
4.Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区
5.当OLD区空间不够时,JVM 会在OLD区进行完全的垃圾收集(0级)
6.完全垃圾收集后,若Survivor及Old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”
三、GC(Garbage Collection),是JAVA中的垃圾收集器。
JVM内存模型中Heap区分两大块,一块是 Young Generation,另一块是Old Generation
1) 在Young Generation中,有一个叫Eden Space的空间,主要是用来存放新生的对象,还有两个SurvivorSpaces(from、to), 它们的大小总是一样,它们用来存放每次垃圾回收后存活下来的对象。
2) 在Old Generation中,主要存放应用程序中生命周期长的内存对象。
3) 在Young Generation块中,垃圾回收一般用Copying的算法,速度快。每次GC的时候,存活下来的对象首先由Eden拷贝到某个SurvivorSpace,当Survivor Space空间满了后,剩下的live对象就被直接拷贝到OldGeneration中去。因此,每次GC后,Eden内存块会被清空。
4) 在Old Generation块中,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求。
5) 垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,
只会回收Young中的垃圾,内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
当每个代满了之后都会自动促发collection,各收集器触发的条件不一样,当然也可以通过一些参数进行强制设定。
主要分为两种类型:
Minor Collection:GC用较高的频率对young进行扫描和回收,采用复制算法。
Major Collection:同时对Young和Old进行内存收集,也叫Full GC;因为成本关系对Old的检查回收频率要比Young低很多,采用标记清除/标记整理算法。
常见OOM:
1.老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
2.Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
常用排查指令:
1.jstack -l [pid]把线程dump出来查看栈情况 配合top先找出最耗费CPU的线程,再定位。
2.jmap -heap [pid]把内存使用情况打印出来看看
3.jstat -gcutil [pid] 1000 10来跟踪GC情况。
4.jmap -histo [pid]把程序的对象情况打印出来
5.jmap -histo:live 21711
6.jps主要用来输出JVM中运行的进程状态信息。jps -m -l
堆内存 = 年轻代 年老代 永久代
年轻代 = Eden区 两个Survivor区(From和To)
FGC、FGCT:Full GC次数和Full GC耗时
jvm启动参数:
JAVA_OPTS="-Xms2048m -Xmx2048m -Xmn1024m -Xss1024K -XX:PermSize=128m -XX:MaxPermSize=512m"
1:-Xms 堆空间初始大小
2:-Xmx 堆空间最大数值
3:-Xmn 年轻代的堆大小
4:-Xss 每个线程堆大小
5:-XX:PermSize 设置持久代(perm gen)初始值
6:-XX:MaxPermSize 设置持久代最大值
java启动参数共分为三类
其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
其三是非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;
重点介绍:非标准参数又称为扩展参数
-Xms512m 设置JVM促使内存为512m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmx512m ,设置JVM最大可用内存为512M。
-Xmn200m:设置年轻代大小为200M。整个堆大小=年轻代大小 年老代大小 持久代大小。
持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。
JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-Xloggc:file
与-verbose:gc功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。
若与verbose命令同时出现在命令行中,则以-Xloggc为准。
-Xprof 跟踪正运行的程序,并将跟踪数据在标准输出输出;适合于开发环境调试。
jvm中GC执行的三种方式,即串行、并行、并发;
串行(SerialGC)是jvm的默认GC方式,一般适用于小型应用和单处理器,算法比较简单,GC效率也较高,但可能会给应用带来停顿;
并行(ParallelGC)是指GC运行时,对应用程序运行没有影响,GC和app两者的线程在并发执行,这样可以最大限度不影响app的运行;
并发(ConcMarkSweepGC)是指多个线程并发执行GC,一般适用于多处理器系统中,可以提高GC的效率,但算法复杂,系统消耗较大;