1 运行时数据区域
(1)程序计数器:线程私有,当前线程所执行的字节码的行号指示器。执行java方法时,计数器指示的是字节码指令的地址;执行Native方法时,计数器值为null。
并且此区域是唯一一个java虚拟机规范中没有规定任何OOM情况的区域。
(2)java虚拟机栈:线程私有,生命周期与线程相同,描述的是java方法执行的内存模型:每个方法在执行是都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译器可知的各种基本数据类型、对象引用和returnAddress类型。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间完全确定,在方法运行期间不会改变局部变量表的大小。
该区域的两种异常:
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机可以动态扩展,但扩展时无法申请到足够的内存。
(3)本地方法栈:线程私有,和java虚拟机栈很相似,不过是为虚拟机使用到本地Native方法服务。也会抛出StackOverflowError 和OutOfMemoryError异常。
(4)java堆:在虚拟机启动时创建,被所有线程共享,是虚拟机所管理的内存中最大的一块,唯一目的就是存放实例对象,是垃圾收集器管理的主要区域。
GC堆的划分:(1)新生代和老年代;(2)Eden区、From Survivor区和To Survivor区。从内存分配的角度来看,线程共享的java堆可能划分出多个线程私有的分配缓冲区。
如果在堆中没有内存完成实例分配,并且也无法再扩展时,会抛出OOM异常。
(5)方法区:线程共享,用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OOM异常。
运行时常量池也是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用来存放编译期生成的各种字面量和符号信息,这部分内容将在类加载后进入方法区的运行时常量池中存放。Class文件中的直接引用也会放在运行时常量池中。运行时常量池具备动态性,运行期间也可能产生常量进入常量池,String.intern()。常量池无法再申请到内存时也会抛出OutOfMemoryError异常。
Ps:直接内存不属于java虚拟机运行时数据区域的一部分,但也可能抛出OutOfMemoryError异常。NIO类引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,可以使用Native函数库直接俄分配堆外内存,然后通过一个存储再Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。它也可能导致OutOfMemoryError异常。
2 HotSpot虚拟机对象
2.1 对象的结构
在HotSpot中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充。
- 对象头:对象头由“Mark Word”和 类型指针组成。“Mark Word”用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。类型指针即指向它的类元数据的指针,虚拟机通过这个类来确定这个对象是属于那个类的实例。如果对象是一个java数组,那么对象头中还必须有一块用于记录数组长度的数据。但类型指针不是必须的,因为查找对象的元数据信息并不一定要经过对象本身。
- 实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从弗雷继承下来的,还是在子类中定义的,都需要记录下来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、opps(Ordinary Object Pointers),相同宽度的字段总是分配到一起。在满足这个前提的条件下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那么子类中较窄的变量可能会插入到父类变量的空隙之中。
- 对齐填充。仅仅起着占位符补齐的作用。
2.2 对象的创建过程
(1)检查类是否加载。虚拟机遇到一个new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查符号引用代表的类是否已经被加载、解析和初始化过。如果没有,这必须先执行相应的类加载过程。
(2)为新生对象分配内存。对象所需内存的大小,在类加载完后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。
如果java堆中内存时绝对规整(所有用过的内存都放在一遍,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器)的,那所分配的内存就仅仅时把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种方式称为“指针碰撞”;如果java堆中的内存并不是规整的,虚拟机就必须维护一个表,记录上哪些内存是可用的,在分配的时候从列表中找一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称之为“空闲列表”。
为解决分配内存并时的线程安全,有两种做法:
<1>对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
<2>把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓存(TLAB),那个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁定。
(3)将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,可以移到TLAB之前;
(4)对对象头信息进行必要的设置;
(5)执行<init>方法,把对象按照程序员的意愿进行初始化;
2.3 对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在java虚拟机规范中只规定了一个只想对象的引用,所以对象访问方式取决于虚拟机自己的实现。目前主流的访问方式有两种:句柄和直接指针。
- 使用句柄访问的话,java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自具体地址信息;
- 使用直接指针访问的话,那么java堆对象的布局中就必须考虑如何放置对象类型数据的相关信息,而reference中直接存储的就是对象地址。
使用句柄的优势是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改;使用直接指针的好处是速度更快,节省了一次指针定位的时间开销。
3 OOM
(1)Java堆溢出
(2)虚拟机栈和本地方法栈溢出
(3)方法区和运行时常量池溢出
(4)本地直接内存溢出