文章目录
运行时数据区域
程序计数器
程序计数器是一块很小的内存空间。它是当前线程所执行的字节码的行号指示器,字节码解释器工作器通过改变这个计数器的值来选取下一条需要执行的字节码执行。像条件分支,循环,异常处理等基础功能都需要依赖程序计数器来实现。
程序计数器是线程私有的,每个线程都有一个独立的程序计数器。并且它是为Java方法服务的,如果是Native方法,计数器值为空(Undefined)。它是Java虚拟机规范中唯一没有OutOfMemoryError的区域
Java虚拟机栈
Java虚拟机栈也是线程私有的,并且其生命周期与线程相同。虚拟机栈描述的是方法执行的内存模型,每个方法在执行的时候都会生成一个栈帧,用于存放局部变量表,方法出口等信息。所以方法的开始执行与执行完毕,在其中就是一个栈帧入栈出栈的过程。
对Java虚拟机规范中,对这个区域规定了两种异常状况:
1.StackOverFlowError:当线程请求的栈深度超过了虚拟机允许的深度,就会抛出该异常。
2.OutOfMemoryError:如果虚拟机栈在动态扩展的时候无法申请到足够的内存,则会抛出该异常。
本地方法栈
本地方法栈和虚拟机栈其实非常相似,区别就是虚拟机栈是为Java方法服务的,而本地方法栈是为虚拟机执行的Native方法服务的(Native方法就是JVM源码中的一些C/C++方法)。有的虚拟机(例如Sun HotSpot虚拟机)会直接将这两个栈合二为一。
同样也有StackOverFlowError和.OutOfMemoryError这两个错误抛出。
堆
这是由所有线程共享的一个区域,也是在JVM所管理的内存中最大的一块。堆的作用就是存放对象实例和数组,同时也是进行GC的主要区域。
若堆无法扩展,且内存不足时,会抛出.OutOfMemoryError异常
方法区
这也是一个由所有线程共享的一个区域。用于存放已经倍虚拟机加载的类信息,常量,静态变量等信息。
若方法区无法满足内存分配时,也会抛出.OutOfMemoryError异常。
运行时常量池
运行时常量池是方法区的一部分。用于存放在运行时产生的常量。典型的用法就是String.intern()方法,也就是若在常量池中存有该字符串,则直接返回,否则创建一个到次列车中再返回
HotSpot虚拟机创建对象的流程
在语言层面,我们创建一个新的对象仅仅需要使用new关键字就可以,而在Java虚拟机中,这是怎么样的一个流程呢?
类加载检查
虚拟机遇到一条new指令,首先去检查这个指令的参数是否能在方法区的常量池中定位到一个类的符号引用,并且检查这类是否已经被加载,解析,初始化过,若没有,则必须先进行类加载过程。
内存分配
若类加载检查通过后,接下来虚拟机将为新对象分配内存。对象所需的内存在类加载完成之后便可以确定了,那分配内存的方法有两种:
指针碰撞
若Java堆中的内存是规整的,也就是正在使用的内存放在一边,空闲内存放在另一边,中间有一个指针作为分界。那么只需要将指针移动需要的内存大小就可以了。这就叫"指针碰撞"
空闲列表
若Java堆中的内存不是规整的,也就是占用的内存和空闲的内存是交错分布的。此时需要维护一个空闲列表,用于记录堆中可用的内存块,然后挑选出一个合适大小的进行分配,这就是"空闲列表"。
这两种分配方式要采用哪一种,具体还得看要使用哪一个垃圾收集器。
对象信息初始化
内存分配完成后,虚拟机会将分配到的内存空间都初始化为零值。这一步操作保证了对象实例在Java代码中即使不赋初始值,也可以直接使用。
然后就是要对对象头进行初始化。比如说这个实例是哪一个类的实例,hashCode,GC标记,锁标记,偏向线程id等信息。
那么到这一步,在虚拟机角度,对象的创建工作就已经完成了。而在Java程序角度来看,对象的创建才刚刚开始,因为还没有进行初始化,要进行初始化,比如说通过构造函数对属性进行赋值等工作完成后,一个真正的对象才算完全产生出来。
对象在虚拟机中的内存布局
在HotSpot虚拟机中,对象在内存中的布局可以分为三块区域:对象头,实例数据,对齐填充。
对象头
对象头中其实又包含两个区域:Mark Word,类型指针
Mark Word
Mark Word主要用来存放对象自身运行时的数据:hashCode,GC标记,锁标记,偏向线程ID等信息。在32位虚拟机的无锁状态下,Mark Word的32bit空间中有25bit用于储存hashCode,4bit用于储存GC分代年龄,2bit用于储存锁标记位,1bit固定为0
类型指针
类型指针指向当前对象的类元数据,虚拟机通过这个指针可以得知该对象是哪一个类的实例,也可以得知该对象占用内存的大小。而如果该对象是数组,那么在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机无法从数组的类型指针得知对象的大小。
实例数据
实例数据是对象中真正储存的有效信息,也就是在Java代码中定义的各个字段,包括自己定义的属性和父类中的属性。
对齐填充
并不是必然存在的,因为Java虚拟机规定对象的起始地址必须是8字节的整数倍,所以说如果当前对象长度不满足这个条件,就会使用占位符来进行补全。
对象的定位访问
那对象建立了,怎么样进行访问呢?Java程序是通过栈上的reference数据来操作栈上的具体对象,这个reference只是一个指针。那具体的定位方式可以由虚拟机自主实现。主流的有使用句柄和直接指针这两个方法
句柄
如果使用句柄访问的话,堆中还会划出一部分内存作为句柄池,句柄中就包含了对象实例和类数据的具体地址,而reference储存的就是句柄地址
优点:reference储存稳定的句柄地址,如果对象地址被移动,那么只需要改变句柄中的指针,reference本身无需修改。
缺点:多了一次指针定位的操作,速度较慢。
直接指针
顾名思义,reference直接储存了对象的地址
优点:只需要一次指针定位,速度较快
缺点:在对象被移动时,reference也要进行修改。
现在采用直接指针居多