JVM内存模型


程序计数器

程序当前运行到的字节码行号,可通过这个来进行跳转、循环等,
唯一不会出现OOM的区域,为每个线程私有的。当执行的是Native方法时计数器的值为空

JAVA虚拟机栈

线程私有的,即每个线程都会存在一个虚拟机栈,栈中存放的东西为栈帧,当进入一个方法时便创建一个栈帧,方法结束栈帧出栈。

栈帧中存放着局部变量表 操作数栈等信息。其中数据占用空间以slot为单位,long和double占2个,其余占一个。局部变量表需要的内存空间会在编译期间就完成分配,所需大小在编译期间就是是完全确定的。
当所请求的栈的深度大于所允许的最大深度时会出现stackoverflow异常,当扩展无法申请足够的内存时,会抛出OOM.

本地方法栈

和虚拟机栈十分相似,只不过里面是以本地方法服务的。所以也会发生stackoverflow和OOM。

堆区

为线程共享的。所有的对象实例和数组都在堆上分配,但是随着JIT编译器的发展,并不是那么绝对了.

堆是垃圾收集的主要区域,大部分虚拟机采用分代收集,即将堆分为新生代和老生代,对新生代采用复制清理,对老生代采用标记清理方法

方法区

也是线程共享的,里面存放虚拟机加载的类信息,常量和静态变量等信息,也会抛出OOM异常

运行时常量池

是方法区的一部分。Class文件中一项信息是常量池,用于存放编译期间生成的各种字面量和符号引用。这部分内容在类加载后进入方法区的运行时常量池中存放。

直接内存

并不是虚拟机运行时数据区的一部分,也不是JAVA虚拟机规范中定义的内存区域,但是这部分区域被频繁使用,同时也会出现OOM。

NIO中,引入了一种基于通道和缓冲区的IO方式,使用Native库直接分配堆外内存,然后通过一个存储在JAVA堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能用显著提升性能,避免了在Java堆和Native堆中来回复制数据。

直接内存不会受到JAVA堆大小的限制,会受到本机中内存的限制。

对象创建过程

举个例子,当我们new Person()时,虚拟机遇到new这个指令,会去运行时常量池中查找是否存在Person这个类的符号引用,如果没有,则抛NoClassFound异常,并且检查此类是否已经加载,解析和初始化过,如果没有先进行加载过程。之后再进行分配内存,一种是利用指针碰撞,即通过移动指针来分配相应的内存,另一种是维护一个空闲列表用来分配,内存分配后会将分配到的内存空间都初始化为零值,因此就算类的成员变量没有初始化也可以直接使用。

接下来,虚拟机要对对象进行必要的设置,例如该对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,分代信息等。这些信息放在对象头中 ,这些工作都完成后再进行init方法(构造函数)进行初始化

对象的内存布局

一个空对象在内存占用空间大小:

在32位虚拟机上 一个指针占4个字节,同时在堆中分配的对象会存在64bit的对象头 其中32位是对象标志位,另32bit是指向类元数据的指针 所有在32位中一个空对象一共占12个字节

一个对象在堆中的存储包含三部分:对象头实例数据对其填充

  • 对象头包括:Mark Word部分,这部分主要是存储对象自身运行时数据,如哈希码 GC分代年龄,线程持有的锁,锁状态标记等。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来判断这个对象是哪个类的实例。如果是数组对象还需要数组长度的数据,因为从数组的元数据中无法确定数组的大小。
  • 实例数据部分按照一定的分配策略来分配,相同宽度的字段总是分配在一起,在满足这个前提下,父亲定义的变量在子类前面。
  • 对其填充。并不是必然存在的。

对象的访问定位

存在两种方式:
1.通过句柄的方式,会在堆中单独划出一块内存当做句柄池

大部分虚拟机采用第二种。节省了一次指针定位的时间开销。