JVM定义的虚拟机内存模型到底是怎样的?

作为一名java开发,从入门以来在内存上的关注度就不是很高,,看了好几遍的文章介绍虚拟机内存,等到自己回想起来总是不那么清楚,还是落笔记下来吧,都是干货快快点进来!

作为一门高级语言,java自出生开始就运行在虚拟机上,作为一门语言的跨平台关键所在,JVM虚拟机也帮我们进行了内存管理,幸好有虚拟机把javaer们从内存管理中解放出来,使得大家有更多的精力放到业务实现上去。正是因此,一旦出现内存泄漏或溢出问题,如果不了解JVM的内存管理原理,那么将会对问题的排查带来极大的困难。

JVM在执行java程序的过程中,会将管理的内存划分为不同的区域,这些区域的用途也各有不同,根据JVM的内存规定,内存包含如下几个区域。

程序计数器

程序计数器是是一个很小的内存区域,直接划分到cpu上,用于JVM在解析执行字节码时存储当前线程的字节码行号,每个线程都有自己独立的程序计数器,独立存储互不影响。
此内存区域也是JVM规范中没有规定任何OutOfMemoryError的区域。

堆 (heap)

堆,是规范中定义的最大的一块内存区域,所有线程共享,在虚拟机启动时创建,此区域时用来存放 对象实例 的。

  • 从内存回收角度来看,java中的堆分为新生代和老年代,新生代又分为Eden和两块Survivor空间。
  • 从内存分配角度俩看,java堆可能分为多个线程私有的分配缓冲区。如果有线程无法完成实例对象的内存分配,且堆的大小也无法拓展时(-Xmx 参数限制堆的最大内存),将会抛出OutOfMemoryError异常。

虚拟机栈

虚拟机栈也是线程私有的,每个方法在执行是都会创建一个栈帧(Stack Frame)用来存储局部变量表。操作数栈,动态链接,方法出口等信息,方法执行时栈帧入栈,方法结束时栈帧出站。

局部变量表存放的编译器可知的各种基本类型数据,对象的引用及returnAddress类型,局部变量表所需的内存空间在编译器间就去确定了,在运行期间不会改变。

虚拟机栈规定了两种异常:如果线程请求的栈深度大于虚拟机允许的最大栈深度则会抛出StackOverflow异常;如果虚拟机可以动态扩展扩展栈深度,在拓展时无法申请足够的内存则会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈是虚拟机为Native方法分配的,用来存储Native方法的执行状态,同样这个也是线程私有的。
与虚拟机栈一样,本地方法栈也会抛出StackOverflow及OutOfMemoryError异常。

元空间/方法区

方法区 Method Area

先看方法区,在java8之前虚拟机中还有一块内存区域叫做方法区,同样是线程之间共享的,用来存放虚拟机加载的类信息,常量,静态变量等,大家也会称这个区域为永久代(Permanent Generation),但永久代内的数据并非真的永久存在,该区域也是会有较少的GC发生的,GC的目标就是卸载未使用的类信息。
此空间在内存不足时也会抛出OutOfMemoryError异常。

在java7时常量池已经从方法区移动到了堆中,到了java8时方法区便被移除了,自java8起不子再有方法区,取而代之的是 元空间。

元空间 Metaspace

元空间(Metaspace) 并不在虚拟机内存中,而是使用本地内存,因此具体大小取决于系统的可用内存,当然我们可以通过配置参数(-XX:MetaspaceSize -XX:MaxMetaspaceSize),控制元空间的大小。

此区域也是会存在OutOfMemoryError风险的。

直接内存

直接内存也是虚拟机运行过程中可能会用到的内存区域,但是直接内存并不是虚拟机运行时数据区的一部分,比较典型的就是NIO,其引入了一种基于通道于缓冲区的I/O方式,使用Native方法直接分配操作堆外内存,在虚拟机内通过一个DirectByteBuffer对象可对这块内存区域进行操作。
该空间由于是直接分配是系统上的,所以不会受到java堆大小的限制,但是当系统内存分配不足时,同样就会抛出OutOfMemoryError异常。

对象的访问定位

这里多说一点点,对象的创建是为了使用,java程序执行时需要通过栈上的reference数据来找到堆上的具体对象数据进行操作,目前有常用的两种访问方式:句柄访问、直接指针访问。

句柄访问

java堆中将分配一块空间用来作为句柄池,栈中的reference指向的便是实例对象的句柄地址。
句柄包含两个指针,一个指针指向对象实例的内存地址,另一个指针指向对象类型数据的地址。
使用句柄访问对象的方式需要进行两次指针定位,但是优点在于,在GC时如果对象被移动,只需要修改句柄中对象实例的指针地址即可。

直接指针访问

栈中reference直接指向对象实例的内存地址,而对象类型数据的地址存放在对象实例数据中。

使用直接指针访问的好处在于访问速度快,其只需要一次指针定位,但是在GC过程中对象被移动时,需要将所有指向该对象实例的reference值都更改为新的内存地址。