java 将控制内存的权利交给了java虚拟机,一旦出现内存泄漏和内存溢出的问题,如果不了解虚拟机是怎么使用内存的,排查错误修正问题就回变的很困难。
运行时数据区域
根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图
程序计数器
程序计数器是一块较小的内存空间,属于线程私有的,记录着线程需要执行的字节码指令。如果执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果是本地(Native)方法,这个计数器为空。此内存域是唯一一个没有规定任何OOM情况的区域。
虚拟机栈
java虚拟机栈是线程私有的,生命周期与线程相同。
虚拟机栈描述的是java方法执行的线程内存模型:每个方法被执行的时候,java虚拟机都会同步创建一个栈帧用户存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从被调用到执行完毕的过程就对应这个一个栈帧在虚拟机栈中从入栈道出栈的过程。
局部变量表存放了编译期可知的java虚拟机基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽表示。64位长度的long和dubble会占用两个变量槽,其余的数据类型占用一个。局部变量表所需的内存空间在编译期分配完成,这个空间在进入方法是是完全确定的,在方法运行期间不会改变局部变量表的大小。这里指的槽的数量。具体一个槽占用多少内存空间是有不同的虚拟机实现的。
StackOverflowError异常: 线程请求栈的深度大雨虚拟机所允许的深度
OutOfMemoryErro异常:如果Java虚拟机栈容量可以动态扩展[2],当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
本地方法栈
本地方法栈与虚拟机栈的作用是非常相似的,区别就是前者服务本地方法,后者服务java方法。在Hot-Spot中二者是合二为一的。
java堆
java堆是被所有线程所共享的一块儿内存区域,虚拟机启动时创建。作用是存放实例对象。java堆是垃圾收集器所管理的区域。堆的大小可以通过(-Xmx 和-Xms设定)如果堆中没有内存完成实例分配,并且堆也无法再扩展时会抛出 OutOfMemoryErroy异常。
方法区
线程共享的内存区域,存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的缓存数据等。1.8之后,元空间(Meta-space)。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
直接内存
HotSpot虚拟机对象
对象的创建过程
-
虚拟机收到字节码new指令时,首先会检查指令对应的参数是否能在常量池中定位到一个类的符号引用、并检查这个类是否已被加载、解析、初始化过。若没有就执行类加载过程
-
类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存大小在类加载阶段就可以确定。分配方式有"指针碰撞"、"空闲列表"两种。
指针碰撞: 假设java堆在内存中时绝对规整的,所用使用过的内存在一边、空闲的在另外一边,中间放着一个指针作为分界点的指示器。那么分配内存就是把指针向空闲空间方向移动一段与对象大小相等的距离。 (Serial、ParNew收集器下使用该分配方式)
空闲列表: 虚拟机维护一个列表记录哪块儿是可以用的。分配的时候找一个足够大的空间划分给实例,并更新列表上的记录。(CMS 拿到大的分配缓冲区后里面仍然是指针碰撞的方式)
-
虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
-
设置新生对象的对象头。(如:对象是哪个类的实例、元数据信息、hash码(延后到真正调用Object::hashCode()方法)、GC分代年龄等)
-
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始 执行 构造函数,即Class文件中的()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说new指令之后会接着执行 ()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
对象的内存布局
HotSpot中,对象在对内存中的存储布局可以划分为三个部分:对象头、实例数据、对其填充
对象头包含两类信息:第一类用于存储对象自身的运行时数据,如hashCode、GC分代年龄、锁的状态标志、线程持有的锁、偏向线程的ID、偏向时间戳等。在32位和64位虚拟机中分别占用32比特和64比特。另一部分是类型指针,即对象指向它类型元数据的的指针,java虚拟机通过这个指针来确定对象是哪个类的实例,若对象是java数组,对象头中还必须有一块用于记录数组长度的数据。
实例数据:存储我们在程序代码中定义的各种类型的字段内容,包括从父类继承下来的和子类中定义的字段。
对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
对象的访问定位
java程序会通过栈上的reference数据来操作堆上的具体对象。虚拟机规范中只定义了reference是一个指向对象的引用,具体这个引用如何定位到、访问对象的具体位置,是有各种虚拟机自己实现的。主流的方式是句柄、和直接指针
如果使用的是句柄:java堆中就划分出来一块内存作为一个句柄池,reference记录了句柄的地址,句柄中包含了对象的实例数据 和类型数据的的地址。如图所示
如果使用的是直接指针:java堆中的对象布局就需要考虑如何放置访问类型相关的信息,reference中记录的就是对象的直接地址。如图所示:
上述两种方式各有优劣:使用句柄最大的优势就是reference中存储的是最稳定的句柄地址,对象被移动时(垃圾清理过程中)只需要修改句柄中实例数据的地址,reference本身不需要被修改;使用直接指针最大的好处速度更快节省了一次时间定位的开销。常用的HotSpot使用的是直接指针。