JVM运行时数据区域

仅供学习交流,如有错误请指出,如要转载请加上出处,谢谢

Java虚拟机(英语:Java Virtual Machine,缩写为JVM),一种能够运行Java bytecode的虚拟机,以堆栈结构机器来进行实做。最早由太阳微系统所研发并实现第一个实现版本,是Java平台的一部分,能够运行以Java语言写作的软件程序(来自wiki)

运行时数据区域

JVM运行时数据区域由java栈,PC寄存器,本地方法栈,堆和方法区等五大数据区域组成,其结构图如下:

线程私有

java栈

java栈是线程私有的,它的生命周期会随着线程的创建而开始,摧毁而结束,它描述着java执行方法的内存模型:每个线程在执行方法时,会创建一个栈帧,该栈帧存储着方法的所有信息(局部变量表,操作数栈,栈数据区和方法出口等信息),一个方法的调用就好像是JVM对栈帧进行进栈到出栈的过程,由于其生命周期和线程一样,所以不需要GC

PC寄存器

PC寄存器也是线程私有的,是一块很小的内存,它是存储JVM当前方法执行的指令地址,就像是执行字节码的指示器,控制着当前程序的执行方向,JVM执行方法时就需要获取该计数器的指令来进行下一步的操作

本地方法栈

本地方法栈与java栈类似,不同的是java栈执行的是java方法服务(字节码),而本地方法栈执行的JVM自己定义的本地方法服务,也就native修饰的方法

线程共享

堆是线程共享的,所以它的声明周期由虚拟机创建时开始,主要存放的是对象的实例和数组,是JVM内存分配最大的,由于其生命周期较长,所以被垃圾回收收集器管理,由于现代的垃圾收集器都采用分代收集算法,所以还能以8比1的比例细分成一个Eden区和两个Survivor区(FromSurvivor和ToSurvivor)

方法区

方法区也是线程共享的,它用于存储类的信息,常量,静态变量等信息(类的元数据在JAVA1.8已经移动到一块本地内存空间,也就是元空间),还有运行时常量池是方法区很重要的部分,存放编译器编译期间各种class中的常量值和类的描述信息。

对象在内存区域是如何运行

对象在内存中是如果创建

我们在代码中直接用new来表示创建一个对象,而在虚拟机中,会碰到一个new指令,首先会检查指令的参数在常量池中是否有这个类的符号引用,再检查此类是否已经被加载,解析和初始化过了,如果没有就会执行相应的加载过程,然后就会在堆中为对象分配内存,分配内存有两种方式:

  1. 指针碰撞:如果GC使用的是复制算法,把堆内存一分为二,一边是已分配内存,一边是空闲内存,没有内存碎片,那么只需从中间开始,指针往空闲内存空间移动相应大小的距离即可
  2. 空闲列表:如果GC是没有使用标记-整体的算法,存在内存碎片,这时候会维护一种记录可用内存的表,再从表中寻找一块足够大小的内存空间分配给对象实例。

由于堆是线程共享的,在分配内存时存在着线程安全,比如T1线程准备在表中选取A内存分配,还没有开始分配,这时T2线程也准备在表中选取A内存分配,这就会造成冲突,解决线程安全问题有下面两个方案:

  1. CAS同步,在分配操作上使用CAS操作保证分配内存的原子性
  2. 每个线程在堆上预先分配自己的一小块内存(本地线程分配缓冲 TLAB),只需要在该内存用完了,再同步分配新的TLAB

对象在内存中的结构组成

对象存储在内存中分为三个部分:对象头,实例数据,对齐填充

对象头

对象头包括两个部分,第一部分是存储对象自身的运行时数据(哈希码,GC分代年龄,锁标志等),也叫Mark Word,第二部分是类型指针,也叫元数据指针,来确定该对象是哪个类的实例

实例数据

实例数据存储的是该对象内部所定义的类型的字段信息,包括从父类继承下来的,还是在子类定义的

对齐填充

这是一个占位符,JVM规定对象的大小为8字节的整数倍,如果不是就用对齐填充来补全

对象在内存中如何被访问

在内存中访问对象有两种方式:句柄池和直接引用

句柄池

句柄池访问是指在栈中的reference引用指向的是句柄池上的地址,再由句柄池指向相应的对象信息,句柄池的内容包括对象实例数据的指针和对象类型数据的指针,在java堆划分一小块内存来存储它,具体访问过程如下图:

直接引用

直接引用访问是指栈中的reference引用直接指向堆中对象实例的地址,访问过程如下图:

以上两种访问方式各有优劣,句柄池在对象实例频繁的更替时,不需要改动reference中的指针,只需改变句柄池的指针即可,但是直接引用的访问速度比句柄池更快,省去了句柄池的指针指向对象实例的消耗,总结出来就是,对象更替频繁的使用句柄池,对象访问频繁的使用直接引用

参考

https://book.douban.com/subject/24722612/