目录:
1.运行时数据区域
2.对象的创建
3.对象的内存布局
4.对象的访问定位
一、运行时数据区域
基本的java虚拟机运行时数据区如下图:
下面我们就来逐个认识这几个运行时的数据区域
1.程序计数器(Program Counter Register)
它是一块比较小的内存,可以看做是当前线程执行的字节码行号指示器,每个线程都需要有一个独立的线程计数器,各线程间计数器互不影响,独立存储,因此也称此类内存区域为“线程私有”的内存
情景:
1)当线程正在执行一个java方法,则这个计数器记录的是正在执行的虚拟机字节码指令的地址
2)正在执行的是Native方法,则这个计数器值为空
2.Java虚拟机栈(Java Virtual Machine Stacks)
此运行时数据区也是“线程私有”的,它的生命周期与线程相同。虚拟机栈描述的是Java执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程就对应着一个栈帧在虚拟机中的入栈到出栈的过程。
通常我们习惯将Java内存分为堆内存和占内存,这种分法比较粗糙,实际的内存区域远比这种复杂,而这里所说的栈就是我们的虚拟机栈
3.本地方法栈(Native Method Stack)
作用与虚拟机栈所发挥的作用相似,他们之间的区别不过在于虚拟机栈为虚拟机执行Java方法或字节码服务,而本地方法栈则为虚拟机使用到的Native方法服务
4.Java堆(Java Heap)
Java堆是被所有线程所共享的一块内存区域,在虚拟机启动时创建,目的就是用来存放对象实例,几乎所有的对象都是在这里分配(但随着技术发展,“所有”不是那么绝对了)
Java堆是垃圾收集器的主要管理区域,也叫“GC”堆
从内存回收的角度可以将Java堆分为“新生代”,“老年代”
5.方法区(Method Area)
也是各个线程共享的存储区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
6.运行时常量池(Runtime Constant Pool)
它是方法区的一部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,存放编译期生成的各种字面量和符号引用,这部分内容会在类加载后进入方法区的运行时常量池中存放
7.直接内存(Direct Memory)
并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也会被频繁的使用
二、对象的创建
1.在语言层面对象的创建仅仅是一个new关键字而已,而在虚拟机中对象的创建却很复杂
2.当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析、初始化过。如果没有,那必须先执行相应的类加载过程,在类加载完成后,接下来虚拟机将会为对象分配内存,对象所需的内存大小在类加载完成后就可完全确定(如何确定在后面的对象内存布局部分会提及),为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来
3.分配空间的方式主要有两种,根据Java堆是否规整来决定选择哪种分配方式,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
1)指针碰撞(Bump the Pointer):Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就是仅仅把指针向空闲空间那边挪动一段与对象大小相等的距离
2)空闲列表(Free List):Java中内存不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须要维护一个列表,记录那些快是可以使用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
4.内存分配完后,虚拟机需要将分配到的内存空间都初始化为0值(不包括对象头),如果使用了TLAB(本地线程分配缓冲Thread Local Allocation Buffer,每个线程在Java堆中预先分配的一小块内存),这一工作过程也可以提前至TLAB分配时进行。这一步骤保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
5.接下来虚拟机要为对象进行必要的设置,如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)中。根据虚拟机当前的运行状态的不同,对象头会有不同的设置方式
6.在上面步骤完成后,从虚拟机的角度来看对象已经创建完成,但从Java程序的视角来看,对象的创建才刚刚开始,初始化init还没有执行,执行new指令后接着执行init方法,把对象按照我们自己的意愿来初始化后,这样一个可用对象才算完全创建出来
三、对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
1.对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说即是查找对象的元数据信息不一定要经过对象本身。此外,如果对象是一个Java数组,那在对象头中还必须有一块用来记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是数组的元数据中无法确定数组的大小
2.实例数据部分,它是真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容
3.对齐填充,不是必然存在的,它也没有特殊的含义,它的存在仅仅是为了起到占位的作用,因为HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须要是8字节的整数倍,而对象头部分正好是8字节的整数倍,因此,在对象实例数据部分没有对齐时,就需要通过对齐填充来补全
四、对象的访问定位
1.建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象,在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问对中对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有以下两种:
1)如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体地址信息
2)如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类数据类型的基本信息,而reference中存储的直接就是对象地址
2.两种访问方式的比较:
1)使用句柄的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改
2)使用直接访问的最大好处就是速度更快,它节省了一次指针定位的时间开销
补充:
本博客是对《深入理解Java虚拟机》的学习总结,整理了第二章一些最为关键重要又易于理解的部分,在整理的同时也加深我对Java内存区域的理解,总之,希望本博客可以对大家理解Java的内存区域有所帮助
您的支持是对博主学习总结深入思考的最大帮助