JVM-JVM内存模型(Java Memory Model)

版权声明:中华人民共和国持有版权 https://blog.csdn.net/Fly_Fly_Zhang/article/details/89706328

JVM内存划分:

在这里插入图片描述

  • 方法区(线程共享): class对象,常量,静态变量,运行时常量池,JIT编译后的机器码也在方法区存放。
  • 堆区(线程共享): 对象,字符串常量池在堆区,垃圾回收的主要场地。
  • 程序计数器(PC寄存器,线程私有): 当前线程执行的字节码的位置指示器,也就是说指向当前正在运行的指令地址。
  • java虚拟机栈(栈内存,线程私有): 保存局部变量表,基本数据类型,堆内存中对象的引用变量,以及方法的返回地址。
  • 本地方法栈(c栈): 为JVM提供使用native方法的服务。
    在这里插入图片描述
jdk1.7和1.8JMM的区别:

最大的区别就是: 元数据取代了永久代 ,元空间的本质和永久代相似,都是对JVM规范中方法区的实现,其元空间和永久代之间的最大区别在于:元数据空间不再虚拟机中,而是在本地内存中

1,程序计数器(PC寄存器):

程序计数器定义:

程序计数器是一块较小的内存空间,是当前线程正在执行哪一条字节码指令的地址,若正在执行的是一个本地方法,那么此时程序计数器的值就为空Undefined

程序计数器的作用:
  • 字节码解释器通过改变程序计数器来依次获取指令,从而实现代码的流程控制。
  • 在多线程情况下,程序计数器记录的是当前线程执行的执行位置情况,在线程上下文切换回来后,就直到上次执行到哪了。
程序计数器的特点:
  • 一块较小的内存空间。
  • 线程私有。
  • 生命周期:随着线程创建而创建,消亡而消亡。
  • 唯一一个不会抛出OutOfMemoryError的内存区域

2,java虚拟机栈:

定义:

描述java方法运行过程的内存模型。

栈是什么:

栈也称栈内存,主要负责java程序的运行,在线程创建时被创建,它的生命周期和线程的生命周期保持同步,线程结束栈内存也随之释放。 栈不存在垃圾回收问题 ,只要线程一结束该栈就消亡,方法执行完毕,栈帧就被弹出。

栈存储什么:

栈帧主要保存三类数据;

  • 本地变量: 数据参数和输出参数以及方法中的局部变量。
  • 栈操作: 记录出栈,入栈操作。
  • 栈帧数据: 类文件,方法等。
栈运行原理:

栈中的数据以栈帧的格式存在(一个方法产生一个栈帧),栈帧是一个内存区块,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进…F3栈帧,再弹出F2栈帧,再弹出F1栈帧。
遵循“先进后出原则”

压栈出栈过程示例:

java虚拟机会为每一个即将运行的java方法创建一个叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息:局部变量表,操作数栈,动态链接,方法出口信息等。

局部变量表:

存放了编译器可知的基本数据类型,对象引用(reference类型,它不等同于对象本身,可能是指向对象起始地址的引用指针,也可能是指向一个对象代表的句柄或其它与此对象相关的位置)和returnAddress类型(指向了一条字节码指令地址)。long和double类型的数据会占用两个局部变量空间(slot),其余数据类型只占用1个, 局部变量表所需要的内存空间在编译期间完成分配。 所以在运行期间不会改变局部变量表的大小。

在这里插入图片描述

压栈出栈过程: 每个虚拟机栈中,活动栈帧只有一个。

当方法运行过程中需要局部变量时,就将局部变量的值存入栈帧的局部变量表中。
java虚拟机栈的栈顶是当前正在执行的活动栈,也就是正在执行的方法,PC寄存器也会指向这个地址只有活动的栈帧的本地变量才可以被操作数栈操作,当前栈帧调用另一个方法时,与之对应的栈帧又会被创建,新创建的栈帧压入栈顶,变成当前活动栈帧,方法执行结束后,当前栈帧的返回值变成新的活动栈帧的操作数栈中的一个操作数 ,如果没有返回值,那么新的活动栈帧中操作数栈的操作数没有发生变化。

  • 由于java虚拟机栈是线程私有的,数据不会共享,因此不用关心数据一致性问题,也不会存在同步锁的问题。
虚拟机栈特点:
  • 局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可,在方法运行过程中,局部变量表的大小不会发生变化。
  • java虚拟机会出现两种异常:StackOverFlowError和OutOfMemoryError。
  • StackOverFlowError: 当前虚拟机栈的大小不允许动态扩展 ,那么当前线程请求的栈的深度超过当前的虚拟机栈的最大深度时,会抛出次异常。
  • OutOfMemoryError: 当前虚拟机栈的大小允许动态扩展 ,那么当前线程请求的栈的深度用完无法在动态扩展时,会抛出此异常。
  • java虚拟机是线程私有的,随着线程的创建而创建,消亡而消亡。

3,本地方法栈(c栈):

定义:

是为了JVM运行native方法准备的空间,由于很多native方法都是c语言实现,所以也叫c栈,与虚拟机栈实现的功能类似,只不过本地方法栈是用来描述本地方法运行过程的内存模型

栈帧变化过程:

本地方法被执行时,在本地方法栈也会创建一块栈帧用来存放该方法的局部变量表/操作数栈/动态链接/方法出口信息 ;方法结束后,相应的栈帧也会出栈并且释放内存空间。也会抛出StackOverFlowError(未动态扩展)OutOfMemoryError(可动态扩展) 异常。

4, 堆:

定义:

堆是用来存放对象的内存空间,几乎所有的对象都存储在堆里。
堆的内存空间是JVM中最大的 ,应用的对象和数据都存储在堆中,这块区域是线程共享的,也是gc主要的回收区,一个JVM只有一个堆内存 ,堆内存的大小是可以调节的 ,类加载器读取类文件后,需要把类,方法,常变量放到堆内存 中,以便执行器执行,

堆内存主要分三部分:

在这里插入图片描述

新生区(新生代):

新生区是类诞生,成长,消亡的区域 ,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分伊甸区(Eden space)和幸存者区(Survivor space) 。所有的类都是在eden区被new出来的。
Survivor有两个: 0区和1区。当eden区快用完时,垃圾回收器就会对eden区进行垃圾回收(Minor GC),将eden区中剩余存活对象移动到Survivor 0区中,若0区也满了,则再次进行GC然后移动到1区,如果1区也满了,则移动到老年代。若老年代也满了,那么将进行Major GC(Full FC) 对老年代内存进行清理,若老年代执行Full GC之后依然发现无法进行对象的保存,就会产生OutOfMemoryError 异常
如果出现java.lang.OutOfMemoryError: java heap space异常,说明java虚拟机的堆内存不够。

  • 原因:
  • java 虚拟机的堆内存设置不够,可以通过参数**-Xms(设置初始分配大小),-Xmx(设置最大分配大小)** 来调整。
  • 代码中创建了大量的大对象,并且长时间不能被GC(存在被引用)。
养老区(老年代):

老年代用于保存从新生代筛选出来的对象,一般对象都在这个区域活跃。

永久区(永久代):

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的class,interface的元数据 也就是说存储的是运行环境必须的类信息 ,被装载进此区域的数据是不会被GC回收掉的,关闭JVM才会释放此区域所占用的内存。
如果出现java.lang.OutOfMemoryError: PermGen space 说明java虚拟机对perm内存设置不够。

  • 原因:
  • 程序启动需要加载大量第三方的jar包。 例如:在一个Tomcat下部署太多的应用。
  • 大量动态反射生成的类不断被加载,最终导致Perm区被占满。
注意:
  • jdk1.6之前:常量池分配在永久代。
  • jdk1.7:有,但已经逐步“去永久代”。
  • jdk1.8及以后:无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。
    在这里插入图片描述
方法区和堆内存的异议:

方法区和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+JIT编译后的机器码等 虽然JVM规范将方法区描述为堆的一个逻辑部分,但是它有一个别名叫Non_Heap(非堆),目的就是要和堆分开。
永久代是方法区的一个实现,jdk1.7中,已经将原本放在永久代的字符串常量池移到堆里面
常量池是方法区的一部分,class文件除了有类的版本,字段,方法,接口等描述信息外,还有一项就是常量信息(字面量),这部分内存将在类加载后进入方法区的运行时常量池存放

堆的特点:
  • 线程共享,整个java虚拟机只有一个堆,所有线程都访问同一个堆。
  • 在JVM实例启动时创建。
  • 是垃圾回收的主要场地。
  • 分为新生代和老年代;
    不同的区域存放着不同生命周期的对象,这样可以根据不同区域使用不同的垃圾回收算法,更具针对性。堆的大小也可以固定也可以扩展 ,对于主流虚拟机,大小是可以扩展的,因此当线程请求分配内存,但是堆已满且内存无法在扩展,就抛出OutOfMemoryError异常。
下面是堆java堆介绍的总结:
  • 在虚拟机启动时就被创建。
  • 是所有线程共享的内存区域。
  • 存储了被自动内存管理系统所管理的各种对象。
  • 这些受管理的对象无需,也无法显示地被销毁。
  • 自动内存管理系统: Automatic StorageManagement System, 也就是常说的Garbage Collector(垃圾收集器)
  • 并未指名用什么具体的技术去实现自动内存管理系统。
  • java堆的容量是可以固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。
  • java 堆所使用的内存不需要保证连续性。
  • 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那么就会抛出OutOfMemoryError异常。
  • 实现者应当提供给程序猿或者最终用户调节java初始堆容量的手段。
  • 对于可以动态扩展或者收缩java堆来说,则应该提供调节其最大最小容量的手段
  • 所有对象的实例以及数据都要分配在堆上。
    在这里插入图片描述

5,方法区:

定义:

JVM规范定义方法区是堆的一个逻辑部分,方法区存放以下信息,已被虚拟机加载的类信息/常量/静态变量/JIT编译后的机器码

特点:
  • 线程共享:方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的,一个JVM实例只有一个方法区。
  • 永久代 : 方法区中信息一般需要长期存在,而它又是堆的逻辑分区,因此用堆划分的方法,把方法区称为永久代。 jdk1.8之后永久代被元空间所代替,字符串常量池方法堆中。
  • 内存回收的效率低,方法区中的信息一般需要长期存在,回收一遍只有少量无效信息,主要回收目标:对常量池的回收,对类型的卸载
  • JVM规范对方法区的要求比较宽松,和堆一样,允许固定大小,也允许动态扩展,还允许不实现垃圾回收
运行时常量池:

方法区存放信息:类信息,常量,静态变量,JIT编译后的机器码。常量就存放在运行时常量池中,当类被JVM加载后,class文件中的常量就存放在运行时常量池,而且在运行期间,可以向常量池中添加新的常量, 如String类的intern()方法就能在运行期间向常量池中添加字符串常量。 注:字符串常量池1.8移到堆中,这里针对1.7

6,直接内存(堆外内存):

直接内存是除JVM之外的内存,但是可能会被使用。

操作直接内存:

在NIO中引入了一种基于通道和缓存的IO方式,它可以调用本地方法直接分配JVM之外的内存,然后通过一个存储在堆中的DirectByteBuffer对象直接操作该内存,而无需将外部内存中数据复制到堆中在进行操作,从而提高数据操作的效率,而直接内存的大小不受JVM限制,但是依然会抛出OutOfMemoryError异常。

直接内存和堆内存比较:
  • 直接内存申请空间耗费更高的性能。
  • 直接内存读取I/O性能优于普通的堆内存。
  • 直接内存作用链:本地IO–>直接内存–>本地IO
  • 堆内存的作用链:本地IO–>直接内存–>非直接内存–>直接内存–>>本地IO

服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存,从而导致动态扩展时出现OutOFMemoryError。

猜你喜欢

转载自blog.csdn.net/Fly_Fly_Zhang/article/details/89706328