java内存区域与内存溢出异常

-

运行时数据区域

  • 程序计数器(Program Counter Register)
    • 是一块很小的内存空间
    • 是当前线程所执行的字节码指示器,字节码解释器执行是通过改变它的值来选取下一跳执行命令
    • 单核处理器在确定时刻只能执行一条线程中的指令,为了让线程切换后能恢复到正确的位置,每个线程都有一个独立的程序计数器,这类内存区域称为“线程私有‘’
    • 线程执行java方法,计数器记录正在执行的虚拟机字节码指令地址,若执行native方法,计数器值为空。
    • 程序计数器是唯一规定没有OutofMenoryError情况的内存
  • 虚拟机栈(VM Stack)
    • 描述java方法执行内存模型(字节码)
    • 虚拟机栈是线程私有
    • 每个方法执行都会创建一个帧栈,帧栈保存局部变量,操作数栈等信息,每个方法的执行都是帧栈在虚拟机栈中的入栈与出栈的过程
      • 局部变量表存储的是基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用和returnAddress类型(指向一条字节码指令的地址)
      • long 和double 占用了2个局部变量,其他的数据类型占用1个
      • 局部变量表所需的内存空间在编译期间完成分配,在运行期间是不会被改变的
    • 当java虚拟机进行动态扩展时,无法申请足够的内存将抛出OutOfMemoryError异常
    • 线程请求的栈深度大于虚拟机允许的深度将抛出StackOverflowError异常
  • 本地方法栈(Native Method Stack)
    与虚拟机栈相似
    • 描述native方法执行的内存模型。
    • 对使用的语言,使用方式与数据结构没有强制要求,虚拟机可自由使用它。
    • 有的将本地方法栈与虚拟机栈合二为一(Sun HotSpot虚拟机)
    • 抛出OutOfMenmoryError与StackOverflowError异常
  • Java堆(Java Heap)
    • 是虚拟机管理的内存最大的一块
    • 唯一目的是存放对象实例
    • 几乎所有的对象实例都在堆上分配内存(随着JIT编译器的发展与逃逸分析技术的成熟,栈上分配与标量替换优化技术导致堆上分配不是那么绝对)
    • 垃圾收集器(GC)管理的主要区域,因此很多时候被称作“”GC堆“”
    • 可以处于物理上不连续的内存空间,但逻辑要连续
    • 在实现上,可实现固定大小,也可扩展(当前主流都可扩展)
    • 当堆中没有内存完成实例分配,且堆无法再扩展时,抛出OutOfMenmoryError异常
  • 方法区(Method Area)
    • 存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据
    • 在实现上,可实现固定大小,也可扩展(当前主流都可扩展)
    • 垃圾收集行为在这个区域比较少出现,回收目标主要是:常量池的回收和对类型的卸载(类型的卸载回收条件苛刻),这部分回收是非常必要的,否则会导致内存泄露
    • 当方法区无法满足内存分配需求时,抛出OutOfMenmoryError异常
    • 运行时常量池(Runtime Constant Pool)
      class文件除了有类型的版本,字段,方法,接口等描述信息外,还有常量池(Constant Pool Table)用于存放编译期生成的各种字面量和符号引用,这些内容在类加载后进入方法区的运行时常量池中保存
      • java虚拟机对Class文件每个文件(包括常量池)格式都有严格的规定,但是对运行时常量池没有做任何细节要求(不同的提供商实现的虚拟机可根据自己的要求来实现这部分)
      • 翻译出来的直接引用也存储在运行时常量池中
      • java语言不要求常量一定在编译期时产生,也就是并非与预置入 Class文件中常量池内容才能进入运行时常量池,运行期间也能将新的常量放入池中(String类的intern()方法)
      • 无法申请到内存时抛出OutOfMenmoryError异常
  • 直接内存(Direct Memory)
    • 并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但这部分内存被频繁使用
    • 在jdk1.4中新加入NIO(New Input/Output)类,引入基于通道(Channel)与缓冲区(Buffer)的I/O方式,它用Native函数库直接分配堆外内存,然后通过java堆中的DirectByteBuffer对象作为这块内存的引用操作,避免java堆和Native堆中来回复制数据
    • 直接内存不受java虚拟机的限制,受本机内存大小以及处理器寻址空间的限制,当内存区域大于物理内存限制,抛出OutOfMenmoryError异常

      对象

  • 对象创建
    • 虚拟机遇到new指令时,首先去检测这个指令参数是否能在常量池中定位到一个类的符号引用,检测这个符号引用代表的类是否已被加载,解析和初始化过,若没有则进行类加载过程
    • 类加载完成后(所需内存确定),虚拟机为对象分配内存(从java堆中分配一块内存)
    • 2种分配方式:
      • 指针碰撞(Bump the Pointer),若java堆中的内存时绝对规整的,所有用过的在一边,没有用过的在一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把那个指针向空闲的空间那边挪到一段与对象大小相等的距离
      • 空闲列表(Free List),如java堆内存不是规整的,使用和未使用的内存相互交错,虚拟机就必须维护一个列表记录那些内存可用,分配时从列表中找一块足够大的内存划分给对象实例,然后刷新列表
        • 内存分配完后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头),若使用TLAB,这个过程可提前在TLAB分配时进行
        • 对对象进行设置,例如对象是那个类的实例,如何找到类的元数据信息,对象的哈希码等,存放在对象的对象头
        • 以上完成后,从虚拟机计角度,对象已经创建,但是从java程序角度来看,对象创建刚开始,执行方法后,按照程序员的意愿初始化后,对象才正真创建
  • 对象的内存布局
    • 对象头(Header)
      • Mark Word,用于存储对象自身本身运行时的数据(哈希表,GC分代年龄,线程持有的锁等)
      • 类型指针,对象指向它的类元数据的指针,虚拟机通过这个来确定对象是那个类型的实例(不是所有虚拟机都必须在对象数据中保留类型指针,即查找对象不一定要经过对象本身),java数组还需要一个记录数组长度的数据
    • 实例数据(Instance Data)
      • 对象真正存储的有效信息,即代码中定义的各种类型的字段内容
      • 这部分存储顺序受到虚拟机分配策略参数(FieldsAllocationStyle)和java源码定义顺序影响
    • 对齐填充(Padding)
      这部分不是必要存在
      • 起占位符作用,对象起始地址必须是8字节的整数倍,当没有8字节整数倍时,就填充
  • 对象访问地址
    java程序通过栈中的reference类型数据来操作堆上的具体对象,reference类型被规定是一个指向对象的引用
    • 句柄访问,java堆划分一块内存做为句柄池,reference存储句柄地址,句柄包含对象实例和类型数据各自的具体地址信息(稳定)
    • 直接访问,reference存储是对象地址(快速)