一、栈上的数据存储
1.1 基本数据类型在栈上的实现
Java的8大基本数据类型在虚拟机中的实现方式与内存占用:
| 数据类型 | 堆内存占用(字节) | 栈中slot数 | 虚拟机内部符号 |
|---|---|---|---|
| byte | 1 | 1 | B |
| short | 2 | 1 | S |
| int | 4 | 1 | I |
| long | 8 | 2 | J |
| float | 4 | 1 | F |
| double | 8 | 2 | D |
| char | 2 | 1 | C |
| boolean | 1 | 1 | Z |
- 空间换时间:Java虚拟机采用空间换时间方案,在栈上不存储具体类型,只根据slot槽处理数据
- 64位特殊处理:long和double类型在64位系统中占用2个slot,共16字节,但高8字节未使用,实际满足8字节需求
1.2 boolean类型的内部实现
- 在栈上boolean类型与int类型相同处理,值1代表true,0代表false
- 可通过ASM框架修改字节码指令,验证boolean在栈上实际可存储超过1的值
- 从栈保存到堆上时,boolean类型只取低位的最后一位保存
- 在堆上,boolean类型只占用1个字节,无符号,低位复制,高位补0
1.3 栈与堆数据转换规则
- 堆→栈 :由于栈上空间大于或等于堆上空间,直接处理,注意符号位
- boolean、char为无符号:低位复制,高位补0
- byte、short为有符号:低位复制,非负补0,负则补1
- 栈→堆 :高位需要截断
- boolean特殊:只取低位的最后一位
二、对象在堆上的内存布局
2.1 对象内存结构
对象在堆中主要包含三部分:
-
对象头:包含Mark Word和Klass Pointer
-
实例数据:对象的实际字段
-
对齐填充:保证对象大小为8字节的倍数
普通对象结构:
┌──────────────┐
│ Mark Word │ 32位4字节, 64位8字节,保存锁、GC等信息
├──────────────┤
│Klass Pointer │ 指向方法区中InstanceKlass对象的指针
├──────────────┤
│ 实例数据 │ 字段重排序,保证内存对齐
├──────────────┤
│ 对齐填充 │ 保证对象大小为8字节的倍数
└──────────────┘数组对象额外包含:
┌──────────────┐
│ 数组长度 │ 4字节(int)
└──────────────┘
2.2 Mark Word(标记字段)
Mark Word在不同状态下存储不同内容,64位开启指针压缩布局:
未锁定状态:
- 2.5位未使用
- 31位Hashcode
- 1位未使用
- 4位分代年龄
- 1位偏向锁标记
- 2位锁状态(01)
偏向锁状态:
- 54位线程ID
- 2位epoch
- 1位偏向锁标记
- 4位分代年龄
- 2位锁状态(01)
轻量级锁:
- 62位指向栈中锁记录的指针
- 2位锁状态(00)
重量级锁:
- 62位指向Monitor对象的指针
- 2位锁状态(10)
GC标记:
- 62位空
- 2位锁状态(11)
2.3 指针压缩
- 64位JVM中,堆中原本8字节的指针可压缩为4字节
- 默认开启,可通过
-XX:-UseCompressedOops关闭 - 适用条件:堆大小不超过32GB(2^35字节)
- 内存对齐:将对象起始地址对齐到8字节边界,便于指针压缩
2.4 内存对齐
- 每个对象字节数必须是8的倍数
- 字段偏移量(offset)需是字段长度的整数倍
- 通过字段重排序和对齐填充实现
- 目的:避免伪共享,提高CPU缓存效率
三、方法调用原理
3.1 五种字节码指令
JVM提供5种字节码指令执行方法调用:
-
invokestatic:调用静态方法
- 静态绑定
- 编译期确定方法地址
-
invokespecial:调用私有方法、构造方法,以及super关键字调用父类方法
- 静态绑定
- final修饰的invokevirtual也使用此方式
-
invokevirtual:调用非私有实例方法
- 非final方法使用动态绑定
- 通过虚方法表(vtable)查找方法地址
-
invokeinterface:调用接口方法
- 动态绑定
- 通过接口方法表(itable)查找方法地址
-
invokedynamic:调用动态方法
- 主要用于lambda表达式
- 机制复杂,JVM 7引入
3.2 静态绑定 vs 动态绑定
-
静态绑定:
- 编译期间确定方法地址
- 适用于static、private、final方法
- 方法第一次调用时,符号引用替换为直接内存地址
-
动态绑定:
- 运行时确定方法地址
- 适用于非static、非private、非final的实例方法
- 通过虚方法表实现多态
3.3 虚方法表(vtable)
- 每个类包含一个虚方法表,记录方法地址
- 子类方法表包含父类所有方法
- 重写方法时,用子类方法地址替换父类方法地址
- 查找步骤:
- 根据对象头中的类型指针找到InstanceKlass
- 从InstanceKlass获取虚方法表
- 通过索引找到方法地址
- 调用方法
四、异常捕获原理
4.1 异常表
- 编译期生成,存储异常处理信息
- 包含四个关键字段:
- 起始PC:异常捕获范围开始位置
- 结束PC:异常捕获范围结束位置
- 跳转PC:捕获异常后跳转的指令位置
- 捕获类型:可捕获的异常类型
4.2 异常处理流程
- 异常发生时,JVM从上至下遍历异常表
- 检查异常发生位置是否在捕获范围内
- 检查异常类型是否匹配
- 如匹配,跳转到"跳转PC"位置
- 如不匹配,弹出当前栈帧,在上层栈帧继续查找
4.3 finally的实现
- finally代码块会被复制到try和catch执行路径之后
- 异常表增加额外条目处理Throwable等未捕获异常
- 通过局部变量表保存异常,执行完finally后再抛出
五、JIT即时编译器
5.1 JIT基本原理
- 热点代码:执行频率高的字节码
- 将字节码编译成机器码,直接在CPU执行
- 热点代码优化:方法内联、逃逸分析等
5.2 分层编译
JDK7后,HotSpot采用分层编译,5个优化级别:
| 等级 | 组件 | 描述 | 保存的信息 |
|---|---|---|---|
| 0 | 解释器 | 解释执行,记录方法/循环次数 | 无 |
| 1 | C1编译器 | 基础优化 | 优化后代码 |
| 2 | C1编译器 | 基础优化+收集信息 | 优化后代码+方法/循环次数 |
| 3 | C1编译器 | C1完整优化+收集所有额外信息 | 类型、分支概率等 |
| 4 | C2编译器 | 深度优化,服务端代码优化 | 优化后代码 |
- C1:编译速度快,优化效果较弱
- C2:编译速度慢,优化效果强
- Graal:新一代JIT编译器,替代C2
5.3 方法内联
- 将方法体直接复制到调用方
- 节省创建栈帧开销
- 内联条件:
- 方法字节码<325字节且是热点方法(-XX:FreqInlineSize)
- 方法字节码<35字节直接内联(-XX:MaxInlineSize)
- 生成机器码<1000字节(-XX:InlineSmallCode)
- 接口实现类<3个
5.4 逃逸分析
- 分析对象是否被外部方法/线程引用
- 优化技术:
- 锁消除:对象不逃逸,消除同步锁
- 标量替换:将对象拆分为基本类型,在栈上分配
- 栈上分配:不逃逸对象分配在栈上,避免GC
六、垃圾回收器原理
6.1 G1垃圾回收器
6.1.1 年轻代回收
- 只扫描Eden+Survivor区域
- 问题:老年代对象可能引用年轻代对象
- 解决方案:记忆集(RememberedSet) + 卡表(CardTable)
记忆集优化:
- 记录区域而非对象级别的引用
- 将内存划分卡页(512字节),记录卡页引用
- 通过写屏障维护卡表,标记"脏卡"
执行流程:
- Root扫描
- 处理脏卡队列,更新记忆集
- 标记存活对象
- 选择回收集合(Collection Set)
- 复制存活对象
- 处理引用和JNI弱引用
6.1.2 混合回收
- 触发条件:堆占用率>45%
- 步骤:
- 初始标记:STW,三色标记法标记GC Root可达对象
- 并发标记:与用户线程并发,继续标记
- 最终标记:STW,处理SATB(初始快照)队列
- 清理:STW,清除无存活对象的区域
- 转移:将存活对象复制到新区域
6.1.3 三色标记与SATB
- 三色标记 :
- 黑色:存活,引用关系已处理
- 灰色:待处理,引用关系部分处理
- 白色:可回收,不在GC Root链上
- SATB技术 :
- 初始创建快照,记录所有对象
- 采用写前屏障,将旧引用对象加入SATB队列
- 避免漏标,可能产生浮动垃圾
6.2 ZGC垃圾回收器
6.2.1 低延迟特性
- STW时间始终<1ms
- 支持堆大小:几百MB到16TB
- 对象地址空间:44位,最大16TB
6.2.2 着色指针
将8字节指针拆分为三部分:
- 最低44位:对象地址
- 中间4位:颜色位,同一时间只有一位是1
- Marked0/Marked1:标记可达对象
- Remap:重映射位,引用关系已变更
- Finalizable:只能通过终结器访问
- 高16位:未使用
6.2.3 读屏障
- 读取对象引用时触发
- 检查是否需要重映射
- 将引用指向转移后的对象
- 用户线程协助GC工作
6.2.4 执行流程
- 初始标记:STW,标记GC Root直接引用对象
- 并发标记:标记所有可达对象
- 并发处理:选择转移区域,创建转移表
- 转移开始:STW,转移GC Root引用对象
- 并发转移:转移剩余对象
- 重映射:修正引用关系,用户线程协助
6.2.5 ZPage区域
- 小区域:2MB,保存<256KB对象
- 中区域:32MB,保存256KB-4MB对象
- 大区域:保存>4MB单个对象
6.3 ShenandoahGC
6.3.1 1.0版本
- 每个对象增加8字节前向指针
- 读前屏障:根据前向指针访问转移后对象
- 缺点:内存占用增加5-10%,性能影响大
6.3.2 2.0版本(主流)
- 仅转移阶段将前向指针放入Mark Word
- CAS确保并发安全
- 执行流程与ZGC类似
6.3.3 分代支持
- JDK21+支持分代Shenandoah
- 年轻代和老年代回收可并行执行
- 减少全堆扫描频率,提升性能
七、最佳实践建议
7.1 代码优化
- 小方法设计:便于JIT内联
- 控制接口实现类数量:不超过2个,保证内联
- 避免对象逃逸:高频方法中创建临时对象
- 自定义热点方法:JDK标准库中复杂方法可能无法内联
7.2 JVM参数调优
-
JIT优化:
-XX:MaxInlineSize:控制内联方法大小-XX:FreqInlineSize:控制热点方法内联大小-XX:InlineSmallCode:控制机器码内联大小
-
G1回收器:
-XX:MaxGCPauseMillis:期望最大停顿时间-XX:InitiatingHeapOccupancyPercent:触发混合GC的堆占用率
-
ZGC:
-XX:+UseZGC:启用ZGC-XX:ZAllocationSpikeTolerance:分配峰值容忍度
7.3 GC选择指南
- 低延迟需求:ZGC或Shenandoah
- 大堆应用(>64GB):ZGC
- 吞吐量优先:G1或Parallel GC
- JDK8环境:G1或CMS(即将废弃)
- 容器化环境:ZGC(对容器友好)
八、总结
JVM是Java语言的核心运行环境,理解其内部原理对性能优化和问题排查至关重要。关键要点包括:
-
内存管理:
- 栈上slot管理保证执行效率
- 堆上对象布局优化空间利用
- 指针压缩减少64位系统内存开销
-
执行优化:
- JIT编译提升热点代码执行效率
- 方法内联消除调用开销
- 逃逸分析优化对象分配
-
垃圾回收演进:
- 从分代设计(G1)到并发转移(ZGC, Shenandoah)
- 从Stop-The-World到亚毫秒级停顿
- 从对象追踪到指针着色等创新技术
-
持续发展:
- ZGC支持TB级堆内存
- 分代ZGC/Shenandoah提升吞吐量
- Valhalla项目将带来值类型,进一步优化内存和性能