Skip to Content

JVM内存区域

根据《Java虚拟机规范》的规定, Java虚拟机所管理的内存将会包括以下几个运行时数据区

jvm1

程序计数器

程序计数器是一块比较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器. 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要程序计数器来完成.

由于Java虚拟机的多线程是通过轮流切换,分配处理器执行时间的方式来实现的. 在任何一个确定的时刻,一个处理器只会执行一条线程中的一个命令. 因此为了保证线程切换后能够恢复到正确的执行位置,每条线程都会有对应一个独立的程序计数器.

  • 如果线程正在执行的是一个Java方法,那么这个计数器记录的是正在执行的虚拟机字节码
  • 如果正在执行的是一个本地(Native)方法,这个计数器值为空(Undefined)

Java虚拟机栈

与程序计数器一样,虚拟机栈也是线程私有的,它的生命周期与线程相同.

虚拟机栈描述的是Java方法的执行模型: 每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息. 每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程.

局部变量表存放了编译器可知的各种数据对象:

  1. Java虚拟机基本数据类型(boolean,byte,char,short,int,float,double,long)
  2. 对象引用(并不等于对象本身,可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)
  3. returnAddress(指向一条字节码指令)

这些数据类型在局部变量表中的存储空间以局部变量槽来表示,其中64位长度的long和double类型数据,会占用两个变量槽,其余只占用一个. 局部变量表所需要的空间大小可以在编译器阶段得到确定,当进入一个方法后,当前栈帧需要多大的局部变量空间是完全确定的.

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是类似的,区别在于虚拟机栈执行Java方法,本地方法栈则是执行本地(Native)方法

堆(Heap)

此内存区域唯一的作用就是存放对象实例,Java世界里”几乎”所有的对象实例都在这里分配内存.

从内存分配的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB),用于提升对象分配效率.

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储被虚拟机加载的类型信息,常量,静态变量以及即使编译器编译后代码缓存. 虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但它却有一个别名叫做非堆(Non-Heap),目的是与Java堆区域分开.

一般来说,很多人更愿意把方法区称之为永久代(Permanent Generation),或者将二者混为一谈. 但本质上来说这两者是不等价的,因为仅仅是当时的HotSpot设计团队选择把垃圾收集器的永久代拓展至方法区,或者说使用永久代来实现方法区而已.

在JDK6的时候,HotSpot开始逐步放弃永久代,逐步改为采用本地内存来实现方法区.

JDK7中,原本放在永久代的字符串常量池,静态变量等移至Java堆中,而到了JDK8中,则完全废弃了永久代的概念,而是用元空间(Meta-Space)代替,主要存放类型相关信息

运行时常量池

运行时常量池是方法区的一部分.Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分将在类加载后放到方法区的运行常量池中

运行时常量池一个重要特性就是具备动态性, Java语言并不要求常量一定只有编译期才产生,运行期也可以将新的常量放入池中.

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域.

在JDK1.4中新加入NIO后,引入了一种基于通道和缓冲区的IO方式,它可以直接使用Native函数分配堆外内存,然后通过DirectByteBuffer对象引用到这块内存区域. 这样可以显著提高性能,避免了在Java堆和Native堆中来回复制数据

对象的创建过程

一般情况下,Java中创建对象是基于new关键字.

  1. 当Java虚拟机遇到new指令时,首先要检查能否定位到对应类的符号引用,并检查这个类是否被加载且初始化完成
  2. 类加载完成后,接下来虚拟机将为即将创建的对象分配内存.对象所需的内存大小在类加载完成后即可确定.
    • 如果Java堆中的内存是完全规整的,即使用过的内存放在一边,未使用的内存放在另外一边,中间存放一个指针,那么分配内存的过程就是把指针向空闲区域挪动一段距离
    • 如果Java堆中的内存是不规整的,那么虚拟机就必须维护一块列表,记录那些内存区域是可用的,这个列表被称为”空闲列表(Free List)“,分配完成后更新列表
  3. 可以看到,对象在堆中的分配内存会频繁改变内存指针的位置,在并发情况下并不是线程安全的,比如正在给对象A分配内存,指针还没来得及修改,B对象又使用了原来的指针分配内存.为了解决这个问题,一般有两种方案:
    • 对内存分配的动作进行同步处理(也可以利用CAS失败重试的方式保证操作的原子性)
    • 内存的划分按线程划分在不同的空闲之中,即每个线程在Java堆中预先分配一小块内存,即TLAB,每个线程在分配内存时,优先在其对应的缓冲区中分配,缓冲区分配完成后会产生新的缓冲区,新的缓存区产生的过程是同步锁定的
  4. 内存分配完成后,虚拟机还需要对创建出来的对象进行必要的设置,比如关联对应类的元信息,计算对象的hash码,GC分代年龄等信息
  5. 从虚拟机的角度来看,此刻对象已经创建好了,但是从Java程序的角度来看,对象的创建才刚刚开始.此时构造函数和对象代码块还未执行,因此会按顺序执行对象代码块和构造函数

对象内存布局

在HotSpot虚拟机里,对象在堆内存的布局这个划分为三个部分: 对象头(Header),实例数据和对齐填充

对象头(Header)

对象头包含两类信息

  • 对象自身的数据,如哈希码,GC分代年龄,锁状态标准,线程持有锁,偏向线程id,偏向时间戳.这一部分官方称之为”Mark Word”,考虑到虚拟机的空间效率,Mark Word被设计成动态定义的数据结构,规则如下(以32位虚拟机为例子):
    1. 25个比特用于存储对象哈希码
    2. 4个比特用于存储对象分代年龄
    3. 2个比特用于存储锁标志位
    4. 1个比特固定0
  • 类型指针,执行类型元数据的指针
  • 如果对象类型是数组,那么在对象头中还需要一块用于记录数组长度的数据

对象实例数据

实例数据部分是对象真正存储的有效信息,即我们在程序代码中所定义的所有类型的字段,无论是从父类继承下来的,还是在子类中定义的. 这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle)和定义先后顺序的影响. HotSpot的默认分配顺序为:

  • long/double
  • int
  • short/char
  • byte/boolean
  • 对象指针

对齐补充

对齐补充,这不是必然存在的,也没有什么特殊的含义,只是起到占位符的作用. 在HotSpot虚拟机中,对对象内存的起始地址要求必须是8字节的整数呗,所以对象的内存占用大小也要设计成8字节的整数倍,所以对于不满足条件的对象就需要通过对齐来补全

对象的访问定位

在线程栈上对对象的访问一般有两种规则:

  • 使用句柄访问. Java堆中划分一部分区域作为句柄池,线程栈中reference指向的位置就是句柄池中的指针.然后在句柄池中的指针分别包含了对象的实例数据和对象的类型数据的指针
  • 使用直接指针访问. 这种方式下,线程池的reference直接指向堆内存中的对象数据,然后对象数据指向方法区中的对象类型数据

jvm2

Last updated on