java虚拟机运行时数据区的组成
java虚拟机在执行字节码文件时会根据java线程的运行周期产生一片供线程使用的运行时数据区,这个数据区的组成如下:方法区、本地方法栈、虚拟机栈、程序计数器、堆、以及本不属于这个区域的直接内存(我们自学到了就带着写一下)。他们分别存放一个线程执行过程中需要准备以及运行时使用、运行后处理的相应资源与记录信息。
程序计数器
程序计数器,相信学过计算机操作系统或者微机原理的小伙伴对于线程切换亦或是进程间切换机制有一定的了解,就计算机操作系统而言,在一个进程运行的过程中,操作系统需要保护这个进程运行的上下文、资源使用记录等相关现场信息,做这些工作的原因是它为了在进程切换的过程中,可以做到等另一个优先级或者紧迫程度高的进程运行结束之后能够安全的回复原进程的进度继续运行。这个暂停将自己的处理机资源给其他进程直至它结束之后又获取处理机继续之前的进度运行的过程在操作系统中叫做中断。而特地用来保存运行环境上下文的数据结构就叫做"中断向量表",这个表里的每一条数据就是一个进程当前运行代码的地址与相关的指针信息,以确保进程在让出处理机的时候可以恢复它之前正常的进度。我们的程序计数器里存放的就是当前的java线程运行的字节码地址指针,随着程序运行指针也不断的自加,所以叫做计数器,需要注意的是每一个java线程都拥有一个程序计数器,它的内存很小,随着线程创建而创建,消亡而消亡,它是每一个java线程私有的资源。
注:如果当前执行的线程是一个java方法,那么程序计数器指向的就是当前正在执行的虚拟机字节码指令的地址,如果执行的是一个本地方法(native方法),则程序计数器值为null(原因在下面的本地方法栈里会有体现,当然也可以解释为jvm能自由使用本地java类库中的方法,自然也就不害怕丢失本地方法的现场信息)。
虚拟机栈
虚拟机栈,每一个java线程所私有的一部分栈资源,它是虚拟机线程运行时的内存模型,这个栈结构由一个个栈帧组成,这个栈帧其实就是一个个方法组成,栈帧存储的内容分别是:局部表量表,操作数表,动态链接(调用其他非本地法方法地址的指向),方法出口等。其中在局部变量表中可以存放一个特殊的引用reference(当前方法存在new出一个对象变量或者是new一个字符串抑或是new一个数组对象时),这个时候对象访问定位的方法就成为了"使用句柄"和"直接指针"两种方式。还需要说明的就是方法的调用时,虚拟机栈会根据执行顺序先后入栈,释放内存时依次出栈。
本地方法栈
本地方法栈与虚拟机栈的结构一致,栈帧只关于native的本地方法。
方法区
方法区的结构很简单,大体上是分为两块,一块是字符串常量池(稍后的内存溢出测试会通过它测试堆内存溢出),方法区是线程们共有的一部分资源,主要存放静态变量,常量,字面量,类的类型信息,字节码文件的代码信息,可以通过不断的生成类的类型信息使方法区的内存溢出。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,存放编译期的各种字面量和符号引用,这部分就存放在方法区中的java运行时字符串常量池里。
堆
java堆,几乎所有的java实例化对象都存放在这个地方,它也是垃圾回收的主要关注区域。后面的对象创建,对象生死,垃圾回收算法等等都围绕着堆。
直接内存
NIO类引入的基于通道与缓冲区的IO方式,它可以直接通过native函数库直接分配堆外内存,虽然直接内存不是java堆内存的一部分,但是它受到物理内存的限制,也会出现内存溢出。
java虚拟机对象的创建过程
对象的创建
我将对象的创建过程总结为:
-
检查常量池是否存在该对象的符号引用并确定是否经过类加载过程,都没有则进行类加载过程。
-
为新生对象分配内存(两种方式:指针碰撞和空闲列表<指针碰撞涉及到当指针调动频繁时为了避免出现脏读,采取本地线程分配缓冲TLAB的优先分配情况>)并将除对象头外的其他内存空间赋值W为0。
-
设置对象头。
-
对象的初始化,这个就是执行你的构造方法的过程,给你需要的字段赋值你想要定义的值。
补充一下其中的细节:为新生对象分配内存过程中,首先一个对象在类加载完成后它所需要的内存大小是完全确定的,分配内存的过程实际上就是在java堆里划分一块等大的内存给它,但是该怎么划分呢?如果java堆的内存布局是严格的顺序分配,即一边是使用过的,一边是空闲的,那么就会采取指针碰撞的方式分配内存,所谓的指针在空闲区与使用区的分界线处,收到内存需求时,指针向后移动直到移动所覆盖的长度等于java对象所需要的内存大小时停止并进行分配。但如果java堆的内存布局是碎片化的不连续的呢?我们就只能维护一个列表,这个列表记录了所有java堆空闲区的大小与位置信息,分配时只需要查找最适合新生对象的区域分配即可。
注意:java堆是否规整是由垃圾收集器的能力决定的,是否带有空间压缩整理的能力。当我们采用的收集器是Serial与Parnew时是用指针碰撞的方式分配的,当采用的是CMS垃圾收集器的时候,则是需要使用麻烦的空闲区表分配。
对象的内存布局
对象在堆内存的布局可以分成:对象头,实例数据,对其填充。
-
对象头:HOTSPOT虚拟机对象的对象头部分分为两类信息。第一类是用于存储自身的运行时数据,如哈希码,GC分代年龄,锁标志,偏向锁线程ID,偏向时间戳等。实际上就是一个拥有32个比特位的名叫MarkWord的动态数据结构。第二类是类型指针,即对象指向它的类型元数据的指针,告诉虚拟机它是哪一个类的实例。如果它是一个数组那么我们还需要在对象头中加入一个显示数组大小的标记。
-
实例数据:我们在程序代码里定义的各种类型的字段内容,无论是继承得来的还是子类定义的都必须按照顺序存储在这个区域,相同宽度的数据一起存放。
-
对齐填充:虚拟机的自动内存管理系统规定所有的对象起止地址必须是8的倍数,对象头存储在markWord数据结构中,已经被严格控制在8字节的倍数上,但是前面的实例数据部分的结束地址可能不是8的倍数,这时就需要对齐填充去补齐。它就相当于占位符的作用。
对象的访问定位
之前的虚拟机栈部分中提到了一个叫做reference的引用,它是指向方法调用的实例的位置或者是地址,那么它是采取什么方式去定位这个调用的对象呢?
访问定位分为两种方式:使用句柄与直接指针。
-
使用句柄:句柄是一条存储真实地址信息(可以是到对象实例数据的指针,也可以是到对象类型数据的指针)的数据。如果采取使用句柄访问的话,java堆中可能会划出一部分内存作为句柄池,reference中存储的就是对象的句柄地址。
-
直接指针:如果采取直接指针的话,reference可以直接指向java堆中到对象实例数据的地址,但是对象类型数据在方法区中,就需要建立二次指针访问去定位方法区中的对象类型数据了。(java堆需要考虑的就是:这个指向方法区对象类型数据地址的指针入如何存放和布局?)