一、Java内存区域与内存溢出异常

1. 运行时数据区

根据Java虚拟机规范的规定,Java虚拟机所管理的内存将会包括以下几个运行区域:

  • 方法区

    • 作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
    • 异常:

      • OutOfMemoryError:当方法区无法满足内存分配需求时将会抛出。
    • 线程共享
    • 作用:存放对象实例,几乎所有的对象实例都在这里分配内存。
    • 异常:

      • OutOfMemoryError:如果对堆中没有内存完成实例分配,并且堆也无法在拓展的时候,将会抛出。(堆空间可以通过-Xmx和-Xms来控制)
    • 线程共享
  • 虚拟机栈

    • 作用:为方法创建栈帧(用于存储局部变量表、操作数栈、动态链接、方法出口等信息)。
    • 异常

      • StackOverflowError:线程请求的栈深度大于虚拟机规定的深度。
      • OutOfMemoryError:如果虚拟机栈可以动态拓展,如果拓展无法申请到足够的内存就会产生。、
    • 线程私有
  • 本地方法栈

    • 作用:为本地方法服务,创建本地方法栈帧,分配相关内存
    • 异常

      • StackoverflowError:线程请求栈的深度大于虚拟机所规定的深度。
      • OutOfMemoryError:如果拓展的时候无法申请到足够的内存。
    • 线程私有
  • 程序计数器

    • 作用:通过改变计数器的值选取下一条执行的字节码指令
    • 异常:无(唯一不会产生OutOfMemoryError的区域)
    • 线程私有
  • 直接内存(由于NIO的引入Java对于内存有了直接操作)

    • 作用:由于NIO引入通道和缓存区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景显著提高性能,因为避免了Java对和Native堆来回的内存拷贝。
    • 异常:

      • OutOfMemoryError:各个内存区域总和大于物理内存的限制,从而导致动态拓展出现异常。

运行时数据区

2. HotSpot虚拟机对象探秘

2.1 对象的创建过程

  • 类若未加载必须将类先进行加载
  • 为新生对象分配内存

    • 指针碰撞法:假设Java堆内存是绝对规整的,所有用过的内存都放在一边,空闲的内存都放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅把哪个指针向空闲空间那边挪动一段距与对象大小相同的距离。(并发环境下指针可能出现异常,采用CAS加上retry的方式保证原子性)
    • 空闲列表:虚拟机必须维护一个列表,记录上那些内存块是可用的,在分配的时候从列表中找一块足够大的内存空间划分给对象实例,并更新列表上的记录。

对象创建的过程

2.2 对象的内存布局

在Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例对象(Instance Data)、对齐填充(Padding)。

  • 对象头:

    • 存储对象自身的运行时数据:如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等

      • 长度:32bit(32位虚拟机)、64bit(64位虚拟机),官方称为Mark Word

    | 存储内容 | 标志位 | 状态 |

    | :----------------------------------: | :----: | :--------------: |
    |       对象哈希码、对象分代年龄       |   01   |      未锁定      |
    |           指向锁记录的指针           |   00   |    轻量级锁定    |
    |          指向重量级锁的指针          |   10   | 膨胀(重量级锁定) |
    |          空、不需要记录信息          |   11   |      GC标记      |
    | 偏向线程ID、偏向时间戳、对象分代年龄 |   01   |      可偏向      |
    
    • 类型指针:对象指向它的类元数据的指针(虚拟机通过这个指针确定这个对象是哪个类的实例)
  • 实例数据:对象真正存储的有效信息,也就是程序代码中所定义的各个类型的字段内容。
  • 对齐填充:HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍。(如果不对齐,你要取一部分数据,但是他们不再同一行,你就要取两次,导致效率下降)

2.3 对象的访问定位

建立对象就是为了使用对象,我们都知道对象使用过存储在栈中的引用(Reference)来调用。但是JVM规范并没有规定如何定位和访问堆中的对象的具体位置。目前主流的两种方式分为句柄和直接访问两种方式。

  • 句柄:Java堆中划分出一部分作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址。

    • 优点:对象被移动(垃圾收集移动对象是非常普遍的事情)时只会改变句柄中的实例数据指针,而reference本身不需要修改
    • 缺点:增加了一次指针定位的开销,速度较慢
  • 直接内存:reference直接存储对象的地址

    • 优点:节省了一次指针定位开销,速度较快
    • 缺点:同上句柄优点的反面
最后修改:2022 年 02 月 23 日
如果觉得我的文章对你有用,请随意赞赏