运行时数据区域
java虚拟机在执行java程序的时候会把它管理的内存分为若干不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间。根据java虚拟机规范规定,虚拟机所管理的内存区域包括以下几个区域:
1.程序计数器(线程私有)
程序计数器是一块较小的内存区域,它是当前线程所执行的字节码的行号指示器,在虚拟机的实现里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理等功能都要依赖这个计数器来完成。
在多处理器计算机中,虚拟机需要处理多个线程同时执行的情况,因此,需要在线程切换回来的时候,仍然需要从上次执行的字节码处继续往下执行。因此,每个线程都需要一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。因此,程序计数器是线程私有的的内存区域。
如果正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计数器值则为空。此内存区域是唯一一个java虚拟机规范中没有任何规定OutOfMemoryError的情况。
2.java虚拟机栈
1.线程私有,生命周期与线程相同
2.描述了java方法执行的内存模型:每个方法在执行时会创建一个栈帧用户存储局部变量表(8种基本数据类型和引用类型——对象的内存地址)、操作数栈、动态链接、方法出口等信息
3.每一个方法从调用到执行的过程对应着一个栈帧在虚拟机中入栈道出栈的过程
这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的栈深度(Xss,虚拟机默认为1024k),将抛出StackOverFlowError,一般情况是在方法中错误地执行了递归调用
如果虚拟机栈可以动态扩展,当错误地进行了递归调用的时候,会抛出OutOfMemoryError
3.本地方法栈
与虚拟机栈相同,本地方法栈为虚拟机使用到的Native方法服务
4.java堆
1.堆是所有线程共享的java内存区域,用于存放对象实例,几乎所有的对象实例(Object Instant Data以及数组)都在这里分配(随着栈上分配技术、标量替换技术等优化手段的发展,对象也不一定都存储在堆上了)
2.堆是垃圾收集器管理的主要区域。因此很多时候也被称为GC堆。由于现在的收集器基本都采用分代收集算法。所以java堆中还可以细分为:新生代和老年代,再细致一点,可以分为Eden空间,From survivor ,To Survivor空间
3.堆可以是由不连续的物理内存空间组成的,并且既可以固定大小,也可以设置为可扩展的(Scalable),可以通过-Xms 和 Xmx控制,如果堆中没有可用的内存时会抛出OutOfMemoryError异常
5.方法区/常量池
1.方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息(Object Class Data),常量,静态变量,即时编译器编译后的代码等数据
2.虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,正因为方法去存储的数据与堆有一种类比关系,所以还被称为Non-Heap。
3.方法区也可以是内存不连续的区域组成的,并且可设置为固定大小,也可以设置称为可扩展的,这点与堆一样。
4.方法区内部有一个非常重要的区域,叫做运行时常量池(Runtime Constant Pool,简称RCP)。在字节码文件中常量池(Constant Pool Table),用于存储编译器产生的字面量和符号引用。每个字节码文件中的常量池在类被加载后,都会存储到方法区中。值得注意的是,运行时产生的新常量也可以被放入常量池中,比如String类中的intern()方法产生的常量。
6.直接内存区
直接内存区并不是JVM管理的内存区域的一部分,而是其之外。该区域也会在Java开发中使用到,并且存在导致内存溢出的隐患。NIO中是可以使用Native Methods来使用直接内存区的。
对象的创建
对象创建的过程:
1.虚拟机遇到一个new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用
2.检查这个符号引用代表的类是否已经被加载,解析和初始化,如果没有就先执行相关类的加载过程
3.类加载检查通过后,虚拟机会为新生对象分配内存。对象所需的内存大小在类加载完成后就可以完全确定,为对象分配内存的任务等同于把一块确定大小的内存从java堆中划分出来
4.内存分配的方法可以分为指针碰撞(假设java堆中的内存是绝对规整的,所有使用过的内存放在一边,空闲的内存放在另外一边,中间放一个指针作为分界点的指示器)和空闲列表(虚拟机维护一个列表来记录哪些内存块是可用的,分配的时候从列表中找到一个足够大的内存划分给对象)两种方式。选择哪种方式由java堆是否规整来决定。
5.虚拟机对对象进行设置:对象是哪个类的实例,如何找到类的元数据信息,对象的hash码,对象的gc分代年龄等信息。这些信息存放在对象头(Object Header)中
6.初始化对象内成员。
对象的内存布局
对象在内存中的布局可以分为三个区域:对象头(Header),实例数据(Instance Data),对齐填充(Padding)
对象头(Header)
HotSpot虚拟机的对象头包括两部分的信息
第一部分存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳。这部分数据的长度在32位和64位虚拟机中的长度为32bit和64bit
另一部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。如果Java对象是一个数组,那在对象头中还必须有一块用于记录数组长度的数据。
实例数据(Instance Data)
真正存储的有效信息。也是程序代码中定义的各种类型的字段内容。无论是从父类继承下来的,还是从子类中定义的,都需要记录起来。
这部分的存储信息会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义的顺序的影响。
对齐填充(Padding)
对齐填充不是必然存在的,没有特别的含义,仅仅起着占位符的作用。HotSpot要求对象其实地址必须是8的整数倍。对象头部分正好是8字节的倍数,而实例数据部分则不一定,所以这部分需要对齐填充。
对象的访问定位
句柄访问方式:java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
指针访问方式:reference变量中直接存储的就是对象的地址,而java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据。