快捷搜索:  汽车  科技

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内存模型完整图解(从jvm内存模型到原理)(1)

栈区:栈=虚拟机栈 本地方法栈

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的效率,但算法复杂,系统消耗较大;

猜您喜欢: