第二章 java内存区域与内存溢出异常
2.2 运行时数据区域
- 程序计数器:线程私有,没有OOM,当前执行的java方法则是虚拟机字节码指令的地址,如果是native,则为空(undefined)。
- 虚拟机栈:线程私有,生命周期与线程相同,每个方法对应一个栈帧(局部变量表、操作数栈、动态链接、方法出口等),方法的执行就是栈帧的出栈和入栈。抛出栈溢出异常(线程申请的栈深度过大)和内存溢出(无法申请到足够的内存来扩展)异常。
- 本地方法栈:与虚拟机栈的区分是,为native方法服务。
- 堆:线程共享,存放对象的实例、数组,可以划分出多个线程私有的分配缓冲区。存储空间在逻辑上是连续的,但是对于大对象类似于数组对象,可能要求存储空间在物理上连续以方便管理。可以扩展(-Xmx、-Xms),不能扩展则抛出OOM。
- 方法区:线程共享,存放虚拟机加载的类型信息、常量、静态变量、即时编译产生的代码缓存等。"永久代":实现方法区的方式不同,HotSpot使用永久代来实现方法区,物理上还是存在于堆空间,方便使用HotSpot的垃圾管理器来管理方法区。在java8中,废弃了永久代的概念,在本地内存中实现方法区,称为元空间。这部分的内存回收主要指常量池的回收和类型的卸载,会抛出OOM。
- 运行时常量池:属于方法区的一部分。在Class文件中,有一项信息是常量池表,用于存放编译器生成的字面量和符号引用(一般还包括翻译出来的直接引用),这部分信息在类加载之后放到方法区的运行时常量池中。除了编译器确定的常量外,运行时产生的常量(String.intern())也会放到这里,会抛出OOM。
2.3 HotSpot虚拟机对象探秘
介绍HotSpot虚拟机在java堆中管理对象的方法。
2.3.1 对象的创建(不包括数组和Class对象)
new SomeObject();
- 类加载检查 在java虚拟机遇到new指令时,检查在常量池中能否找到一个类的符号引用,进而检查代表的类是否已经完成加载、解析和初始化,如果没有需要预先执行类的加载。
- 分配内存
- 指针碰撞:内存划分规整,例如使用Serial、ParNew垃圾回收器回收的内存
- 空闲列表:使用CMS回收的内存
- 同步问题:解决并发分配内存问题,1.CAS+失败重试 2.预先给每个线程分配缓存(TLAB)
- 在分配完内存后,将内存空间(不包括对象头)初始化为零值,使不需要在程序里赋初始值就能访问
- 设置对象头
- 执行<init>()构造器方法对对象初始化
2.3.2 对象的内存布局
- 对象头
- 第一部分:存储对象运行时的数据(Mark Word):HashCode、GC分代年龄、锁状态标志位(01未锁定、00轻量级锁定、10重量级锁定、11GC标记)、持有锁的线程在栈帧中对当前Mark Word的拷贝的地址、偏向线程ID、偏向时间戳
- 第二部分:类型指针,指向该对象所属的类型数据。
- 第三部分:如果对象是数组,记录数组的长度
- 实例数据
- 对齐填充 HotSpot虚拟机规定对象的大小必须是8字节的整数倍
2.3.3 对象的访问定位
通过栈的存储的对象引用操作堆上面的具体对象。具体访问的实现方式不在《java虚拟机规范》中限制。
- 句柄访问:Java堆中划分一部分内存作为句柄池。对象引用中存储的是句柄地址,句柄中存储了对象实例的地址(堆)和对象类型信息的地址(方法区)。
- 直接指针:Java堆中存储的是对象实例地址。在对象实例中,有对象类型信息的指针。
这两种方式各有优势,使用句柄访问,当对象实例移动时(垃圾回收时),不需要改变对象引用中的句柄地址,而直接指针访问则省去了一次访问。HotSpot主要使用第二种方式来进行对象访问。
人生就像蒲公英,看似自由,其实身不由己。