Java内存模型

Java虚拟机的大部分操作都不需要特别关注,关键性的问题在于运行时数据区的结构组成上,我们平常进行堆栈分析都是这个区内。

JVM运行时数据区

JVM中运行时数据区包括五个部分:

  • 程序计数器【寄存器】
  • Java栈【运行时的单位】
  • Java堆【存储的单元】
  • 方法区
  • 本地方法栈

运行时数据区模型图

在这里插入图片描述
注意:不同的JDK版本,ClassLoader是有所不同的。

运行时数据区各个区域的作用

程序计数器

作用如下:

  • 用来指示执行哪条指令,是一块很小的内存区域,小到可以忽略不记。
  • 在程序编译时会为字节码的每一行的都分配一个程序计数器。
  • 多线程并行执行时,每一个线程对象都会有自己独立的程序计数器,以保证CPU切换后可以取得当前程序的执行位置。
  • JVM中规定,如果当前线程执行的是非native方法,程序计数器保存的是当前需要执行的指令的地址,如果线程执行的是native方法,程序计数器保存的值是undefined。

因为程序计数器中存储的数据所占的空间大小不会随着程序的执行而发生变化,因此,程序计数器不会发生内存溢出现象【OOM】。

Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型。执行一个方法时会产生一个栈帧,随后将其保存到栈【先进后出】的顶部,方法执行完毕后会自动将此栈帧进行出栈。顶部的栈帧就表示的当前执行的方法。

  • 栈结构分为栈顶和栈底,遵循先进后出的(FILO)的规则。
  • 栈内存是线程私有的,其生命周期和线程相同。

注意 :

  1. 如果当前请求的栈的深度【栈帧数量过多】过大,虚拟机可能会抛出 StackOverflowError 异常。 【反映在代码上就是出现了死循环,递归没有结束】
  2. 不同的虚拟机实现不同,有的虚拟机允许栈动态扩展,当内存不足以扩展栈的时候,会抛出OutOfMemoryError异常。

上面已经说过了栈的基本单位是栈帧,那么接下来再来看一下栈帧的结构

栈帧

栈帧主要由以下部分组成

  1. 局部变量表
    方法的局部变量和形参,以变量槽【solt】为基本单位,只允许保存32位长度的变量,如果超过32位长度则会开辟两个连续的solt【64位长度,long和double】。
  2. 操作数栈
    表达式的计算是在栈中完成的。
  3. 方法返回地址
    方法执行完毕后需要返回调用此方法的位置,所以需要在栈帧中保存方法返回地址。
  4. 指向当前方法所属的类的运行时常量池的引用
    引用其他类的常量或使用String池中的字符串。
栈和栈帧模型图

在这里插入图片描述

方法区

方法区是JVM中非常重要的一块内存区域,一般而言,在方法区很少执行垃圾回收。

  • 该区域所有线程对象共享。
  • 该区域保存了每一个类的信息【类名称、方法信息、成员信息、接口信息、静态变量、常量和常量池等】。

本地方法栈

本地方法栈与Java栈内存的功能类似,唯一的区别是本地方法栈是为本地方法服务的。

堆内存

此区域所有线程对象共享!堆内存主要保存具体的数据信息【对象】,在JVM启动时将被自动创建。Java开发人员无需管理此区域的空间释放,其会由Java垃圾收集器自动释放,此空间是垃圾收集器的主要管理区域。该区域又细分如下几个区域。

  • 年轻代:所有新创建的对象都保存在年轻代里面。
  • 老年代:所有常用的对象【多次清理之后还被保留的对象】,新建的超大对象【将会占用大量内存的对象】。
  • 元空间:JDK1.8之后,新增了元空间的概念,元空间的本质描述的就是直接内存。【在JDK1.8以前,是没有元空间的,对应的内存被称为“永久代”(方法区:方法区的内容无法回收)】
  • 伸缩区:当模块内存空间不足时,会释放伸缩区中的内存空间,进行更多对象的存储,所以伸缩区是一块可以被不断修改大小的内存区域,所有的内存代都有此区域
堆内存模型图

在这里插入图片描述

年轻代内存划分

年轻代分为两个部分:伊甸园区、存活区(存活区都会为其分为两块大小相同的内存区域),年轻代的内存分配如下。

  1. 伊甸园区:新生对象。
  2. 存活区:经过垃圾回收后仍存活的对象。
年轻代内存模型图

在这里插入图片描述
可以发现,伊甸园区在整个年轻代中的比重是最大的,而这个比例的关系默认情况下大致为 8:1:1

元空间

上边已经介绍过了,元空间的本质描述的就是直接内存。那么直接内存又是什么呢?
所谓的直接内存,指的是虚拟机之外的主机内存【假设,我的服务器有8G内存,其中2G内存给了JVM,剩下的6G就是直接内存】。直接内存并不会受到JVM的控制,但在JDK中提供有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,这种方式会将native函数库分配在直接内存,并且会通过存储在JVM堆内存中的“DirectByteBuffer”来引用它。由于直接内存会受到本机系统内存的限制,所以也有可能出现“OOM”错误。

正因为直接内存的存在,所以我们断然不能把服务器的所有内存都分配给JVM。

OOM【OutOfMemoryError】总结

在进行多线程开发访问的过程里面,所有的线程一定可以共享某一个线程执行体的堆内存空间(这个堆内存空间可以使用Runnable或者是Callable进行表述),同时方法上也可以实现线程操作的共享,正是因为方法上可以共享,所以在进行同步的时候往往就要在方法里面追加同步锁等相关定义。如果要进行内存区域的理解,那么首先就必须考虑程序代码之中可能出现的OOM问题。OOM分为两种情况:栈内存溢出【Stackoverflow】、堆内存溢出。
在这里插入图片描述
从开发角度来讲,最为常见的OOM肯定是堆内存溢出(产生了大量无用对象),栈内存发生数据溢出主要出现在递归操作上。正是因为所有的递归操作在执行的时候有可能产生栈溢出的问题,所以在项目编写之中往往都会建议使用while循环来代替递归操作。

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2015-2020 臣服Romantic”
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信