Java虚拟机运行时数据区分为以下几个部分:
方法区、虚拟机栈、本地方法栈、堆、程序计数器。如下图所示:
程序计数器
程序计数器可以理解为当前线程执行的字节码的行号指示器,字节码解释器就是通哟改变这个值来获取需要执行的下一条需要执行的字节码指令。对于多线程来说,每条线程都有自己的程序计数器,这样各线程之间的计数器互不影响,这类内存区域也叫作“私有内存”(可以看到其实并不是私有的),之所以这么设计,是因为在多线程的情况下,完全可能出现线程中断的情况,那么当被中断的线程需要回复执行的时候,怎么知道上次该线程执行到哪里了呢?这就需要程序计数器发挥作用了,由于每个线程都有自己的程序计数器,这样当CPU重新调度该线程的时候,从其计数器中取出下一条的字节码执行指令,于是就可以继续执行了。
Java虚拟机栈
也是线程私有的,生命周期与线程相同,Java虚拟机栈描述了Java的方法执行模型,每个方法执行时都会创建一个栈帧。会抛出StackOverFlowError和OOM
本地方法栈
与Java虚拟机栈类似,只不过其描述的是本地方法的执行模型,也会抛出StackOverFlowError和OOM
Java堆
与Java虚拟机栈不同,Java堆是所有线程共享的,在虚拟机启动的时候创建,此内粗区域的唯一目的就是存放对象实例。同时,这块内存区域也是垃圾收集器的主要区域,也被成为GC堆
Hotspot虚拟机对象
对象的创建
首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有没有就执行下一步
检查通过后,虚拟机会为新生的对象分配内存,主要由两种内存分配策略,一种是指针碰撞,一种是空闲列表。所谓指针碰撞就是把Java堆中的内存一分为二,一边是所有用过的内存(这部分内存不能被分配了),一边是空闲的内存,是可以被分配的,这样的话,在可用于不可用的内存之间会有一个分割点指示器,那么为对象分配内存实际上就是从这个分界点指示器往空闲内存的一边拨动一段空间就可以了。而空闲列表则没有这个假设,已使用的内存与空闲内存可能是交叉在一起的,那么使用指针碰撞的方式分配内存就会产生问题,但是虚拟机维护着一张列表,这张列表记录了哪些区域的内存是可用的,那么在分配内存的时候就从选择可以容纳对象要求大小的内存区域分配给这个对象。
虚拟机将分配到的内存空间都初始化为零(不包括对象头),这里的初始化不同于我们在Java中利用构造函数进行初始化的过程,这里的初始化时保证Java的一些原生数据类型在不重新赋值的时候就可以直接使用,程序在使用这些对象的时候可以直接使用零值。
接下来,虚拟机要对对象一些必要的设置,进行这些设置的目的是可以知道这些对象是哪个类的实例、对象的哈希码、对象的GC分代年龄等信息,这些信息都存储在对象头中。
上面这些工作完成之后,从虚拟机的角度看,一个对象已经构造完成,但是从开发人员的角度看,还需要进行new对象之后初始化,接着执行init方法,把对象按照程序员的意愿进行初始化。到这里一个对象才算真正创建完毕
对象的内存布局
主要包括三部分的信息:对象头、实例数据和对齐填充
对象头又包括两部分信息,第一部分用于存储对象自身的运行时数据,比如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等;第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。当然,类型指针不是必须的。可能有疑问,如果没有类型指针,这么知道这个对象是哪个类的实例呢?答案是知道对象属于哪个类并不一定需要通过对象本身
实例数据部分是对象真真好存储的有效信息,也是程序代码中所定义的各种类型的字段内容
第三部分不是必然存在的,只是起到占位符的作用,因为HotspotVM规定对象的起始地址必须是8字节的整数倍。所以很有可能以上两部分的大小不够8字节的整数倍,那么这个字段就可以发挥作用了。
对象的访问定位
创建了对象,要使用对象就必须定位这个对象,那么在VM中是如何定位一个对象呢?主要是通过Java栈中的reference数据,通过这个reference数据只是一个指向对象的引用,那么对象的访问方式就可以不同。目前主流的对象访问方式主要由句柄和直接指针两种。通过句柄访问的话,会在Java堆中划分出一块句柄池,句柄池中国句柄存放了对象的实例数据和类型指针,而reference数据则存放了句柄的地址引用。使用直接指针访问对象,那么reference数据存放的就是对象的地址。
使用句柄访问的最大好处是reference中存储的稳定的句柄地址,当对象的地址发生了改变可以不用去关心。而直接指针的最大好处是速度更快,在于节省了一次指针定位的时间。