前言
深入理解JAVA虚拟机读书笔记及代码记录
Chapter Two:Java内存区域与内存溢出异常
2.1 运行时数据区内存分配概述
- 程序计数器:当前线程执行字节码的行号指示器,唯一不受OutOfMemoryError影响的内存区域
- 虚拟机栈:每个方法执行时会在栈内存中创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口灯信息,其中局部变量表存储基本数据类型、对象引用和returnAddress类型,线程请求的栈深度大于虚拟机允许的深度则抛出StackOverFlowError
- 本地方法栈:与虚拟机栈相似,用于执行native方法,同样会抛出StackOverFlowError
- java堆:存放对象实例(在栈上分配和标量替换技术中,并不是全部对象都在堆中分配),是GC的主要管理区域,分为新生代和老生代,其中新生代包括Eden、FromSurvivor和ToSurvivor区域
- 方法区:另称为永久带(Perm Gen)用于存储加载后的类信息、常量池、静态变量和JIT编译后的代码,但永久代内存回收机制比较落后,容易出现内存溢出问题,因此这部分内存在JDK8后已经被移植到堆中,称为元空间(Meta Space)
- 直接内存区(direct memory):NIO类可直接使用native函数库直接分配堆外内存进行Channel和Buffer的使用,称为直接内存区,这部分内存不受虚拟机GC管理
2.2 JVM对象特征
1. 创建对象
虚拟机为新建对象分配内存空间采用如下两种分配算法:
- 指针碰撞:堆内存完全规整,已用内存空间和未用内存空间中间由分界指针划开,新建内存时只需要将指针向未用内存区域挪动即可;带Compact过程的GC(Serial、ParNew等)采用该分配算法
- 空闲列表:堆内存随机分布,有一个列表用于记录堆中哪些内存未占用,新建对象时直接从列表上取一块内存进行使用;基于Mark-Sweep算法的GC(CMS等)采用该分配算法
如何保证分配内存过程的原子性:
- CAS同步:虚拟机采用CAS方式进行失败重试来分配内存
- TLAB:把内存分配动作按照线程划分在不同区域,各线程在堆中预先分配一小块内存(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存就在哪个线程的TLAB上进行分配,当TLAB用完并分配新的TLAB时才重新采用CAS方式
创建对象的整个过程:
graph TD
确保类已被加载并初始化-->获取对象大小
获取对象大小-->A{是否在TLAB中分配对象}
A{是否在TLAB中分配对象}-->|否|直接在Eden中分配对象
A{是否在TLAB中分配对象}-->|是|TLAB中分配
直接在Eden中分配对象-->CAS方式分配空间
CAS方式分配空间-->将分配的内存空间赋0值
TLAB中分配-->将分配的内存空间赋0值
将分配的内存空间赋0值-->编辑对象头
编辑对象头-->对象引用入栈
对象引用入栈-->初始化对象
2. 对象在内存中的结构
一个对象在内存中包括3个区域:对象头、实例数据、对齐填充(padding):
- 对象头:第一部分(Mark Word)包括HashCode、GC分代年龄、锁状态标志、线程当前持有锁、偏向线程ID、偏向时间戳等,在32位和64位JVM中大小分别为32bit和64bit;Mark Word在不同的状态下(状态用2bit的锁标志位来标记),存储结构不同,以下是5中状态时存储的不同内容:
存储内容 | 标志位 | 状态 |
---|---|---|
Hashcode和分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 重量级锁定 |
空 | 11 | GC标记 |
偏向线程ID、偏向时间戳、分代年龄 | 01 | 可偏向 |
对象头第二部分是类型指针,用于确定这个类是哪个类的实例
如果对象是数组,还要有一块记录数组长度的数据
- 实例数据:对象实际存储的数据,相同宽度的成员变量一般分配到一起
- 对其填充:通常作为占位符,因为Hotspot要求对象大小必须是8的整倍数
3. 对象访问定位
目前市面上的JVM对于对象地址的定位有两种处理方式:
- 句柄访问:栈中保存的引用指向的是堆中的句柄池,引用存储的是某个对象句柄的地址,句柄中包含对象数据当前实际地址和对象类型数据地址
- 直接指针访问:栈中的引用直接指向堆中对象数据的实际地址
目前Hotspot用的是第二种方式
网友评论