Java内存区域与内存溢出异常
1、运行时数据区域
1.1、程序计数器
-
是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能需要依赖这个计数器来完成。
-
它是线程私有的。
-
如果线程正在执行的是一个Java方法,则这个计数器记录的是正在执行的虚拟机的字节码指令的地址。
-
如果线程正在执行的是一个本地方法,则计数器值为空。
-
它是唯一一个没有规定任何OutOfMemoryError情况的区域。
1.2、Java虚拟机栈
-
它也是线程私有的。
-
它的生命周期和线程相同。
-
它描述的是Java方法执行的线程内存模型:每个方法都被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈桢在虚拟机栈中从入栈到出栈的过程。
-
局部变量表:存放各种编译期可知的Java基础数据类型(char、boolean、short、byte、int、folat、long、double)、对象应用和returnAdress类型。
- 这些数据类型在局部变量表里的存储空间以局部变量槽来表示。
- long、double占两个变量槽,其它占一个。
-
在《java虚拟机规范》在这个区域规定了两类异常状况:
- 栈深度溢出(线程请求栈的深度大于虚拟机锁允许的深度):StackOverflowError
- 栈扩展失败(虚拟机栈可以动态扩展,当栈扩展时无法申请到足够的内存):OutOfMemoryError
1.3、本地方法栈
与虚拟机栈相似,但本地方法栈是为虚拟机使用本地方法服务
异常情况和虚拟机栈相同。
1.4、堆
-
是虚拟机管理的内存里最大的区域。
-
是被所有线程共享的一块内存区域,虚拟机启动时自动创建。
-
所有的对象实例以及数组都应当在堆上分配
-
是垃圾回收集器管理的内存区域,也被称为“GC堆”;
-
Java堆细分的目的是:为了更好的回收内存,或者更快的分配内存。
-
Java堆处于在物理上不连续的内存空间,但在逻辑上它应该被视为连续的。
-
它既可以被实现成固定的大小,也可以是扩展的。
如果在堆中没有内存完成实例的分配你,并且堆也无法在扩展时,抛出OutOfMemoryError
1.5、方法区
- 是各个线程共享的内存区域
- 用于存储被虚拟机加载的类型信息、常量、静态变量等
如果在方法无法满足新的内存分配需求时,抛出OutOfMemoryError
运行时常量池是方法区的一部分,用于存放编译期产生的各种字面量和符号引用。
因为是方法区的一部分,所以受到方法区内存的限制,当常量池无法申请到内存时,抛出OutOfMemoryError
1.6、直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是《虚拟机规范》中定义的内存区域。
本机直接内存的分配不会受到Java堆大小的限制,但受到本机总内存大小以及处理器寻址空间的限制。
由于一般管理员设置虚拟机参数时,会根据实际的内存区设置-Xmx等参数,直接忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。
2、HotSpot虚拟机对象
2.1、对象的创建
-
虚拟机遇到一条字节码new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过,若没有则先执行类加载过程。
-
类加载检查通过后,虚拟机为新生对象分配内存。有两种分配方式:
- 指针碰撞:若Java堆中的内存是绝对规整的,所有被用过的内存都被放在一边,空闲的内存被放在一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把指针向空闲空间挪动一段与对象大小相等的距离。
- 空闲列表:若Java堆中的内存不是规整的,虚拟机就必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表中找到一块足够大的空间分给对象实例,并更新列表上的记录。
因为在虚拟机中中对象的创建是非常频繁的,及时仅仅修改一个指针的位置,在并发的情况下也并不是线 程安全的。解决方案有两种:
-
对分配内存空间的动作进行同步处理——实际上是虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
-
内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB,Thread Local Allocation Buffer),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有缓冲区用完了,分配新的缓存区时才需要同步锁定。
-
内存分配完成后,虚拟机必须将分配到的内存空间都初始化为零值,如果使用了TLAB可提前至TLAB分配时顺便进行
-
对对象进行一些必要的设置,如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码等等
以上工作完成后,在虚拟机的角度一个新的对象已经被创建,但在Java程序的角度,对象的创建才刚开始,只有当构造函数执行后,一个真正的对象才算完全的被构造出来。
2.2、对象的内存布局
在HotSpot虚拟机中,对象在堆内存的存储布局可以划分为三个部分:
- 对象头(Header)
- 用于储存对象自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等
- 类型指针
- 实例数据(Instance Data)
- 对其填充(Padding)
在32位的HotSpot虚拟机中,如果对象渭北同步锁定的状态下,MarkWord的32比特存储空间中:
25个用来存储对象哈希码
4个比特用来存储对象分代年龄
2比特用来存储锁标志位
1比特固定为0
2.3、对象的访问锁定
主流的访问方式有两种:
- 句柄
- 优点:reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
- 直接指针
- 优点:速度快,节省了一次指针定位的时间开销
- 优点:速度快,节省了一次指针定位的时间开销