JVM对象模型详解
一、知识概述
在Java虚拟机中,对象的创建、存储和访问是Java程序运行的基础。理解JVM对象模型有助于我们编写更高效的代码,排查内存相关问题,以及进行性能优化。
对象的内存布局
在HotSpot虚拟机中,对象在内存中的存储布局可以分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
┌─────────────────────────────────────────────────────────────┐
│ Java 对象内存布局 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 对象头 (Header) │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Mark Word (标记字) │ │ │
│ │ │ 32位: 4字节 / 64位: 8字节 │ │ │
│ │ │ 存储: hashCode、锁状态、GC年龄等 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Class Pointer (类型指针) │ │ │
│ │ │ 指向方法区的类元数据 │ │ │
│ │ │ 开启指针压缩: 4字节,否则: 8字节 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Array Length (数组长度) │ │ │
│ │ │ 仅数组对象有此字段 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 实例数据 (Instance Data) │ │
│ │ │ │
│ │ 字段内容(继承的 + 自身的) │ │
│ │ - 基本类型: 按大小存储 │ │
│ │ - 引用类型: 存储 reference │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 对齐填充 (Padding) │ │
│ │ │ │
│ │ 保证对象大小是8字节的整数倍 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
对象大小计算
| 类型 | 大小 | 说明 |
|---|---|---|
| 对象头(普通对象) | 12字节(64位开启压缩) | Mark Word(8) + Class Pointer(4) |
| 对象头(数组对象) | 16字节(64位开启压缩) | 额外4字节数组长度 |
| 引用 | 4字节(开启压缩)/ 8字节 | 对象引用 |
| int | 4字节 | 整型 |
| long | 8字节 | 长整型 |
| 对齐填充 | 0~7字节 | 凑整到8的倍数 |
二、知识点详细讲解
2.1 对象头详解
对象头包含两部分信息:Mark Word(标记字)和类型指针。如果是数组,还有一个记录数组长度的部分。
Mark Word结构
Mark Word在32位和64位虚拟机中的长度不同,但存储的信息相似。它在不同的锁状态下存储的内容不同:
32位 JVM Mark Word 结构:
┌───────────────────────────────────────────────────────────────┐
│ Mark Word (32 bits) │
├───────────────────────────────────────────────────────────────┤
│ │
│ 无锁状态 (Normal): │
│ ┌─────────────────────────────┬─────┬─────┬─────┐ │
│ │ hashcode:25 │ age:4│ 0:1 │ 01 │ │
│ └─────────────────────────────┴─────┴─────┴─────┘ │
│ │
│ 偏向锁 (Biased): │
│ ┌─────────────────────────────┬─────┬─────┬─────┐ │
│ │ thread:23 | epoch:2 │ age:4│ 1:1 │ 01 │ │
│ └─────────────────────────────┴─────┴─────┴─────┘ │
│ │
│ 轻量级锁 (Lightweight Locked): │
│ ┌─────────────────────────────────────────┬─────┐ │
│ │ ptr_to_lock_record:30 │ 00 │ │
│ └─────────────────────────────────────────┴─────┘ │
│ │
│ 重量级锁 (Heavyweight Locked): │
│ ┌─────────────────────────────────────────┬─────┐ │
│ │ ptr_to_heavyweight_monitor:30 │ 10 │ │
│ └─────────────────────────────────────────┴─────┘ │
│ │
│ GC标记 (Marked for GC): │
│ ┌─────────────────────────────────────────┬─────┐ │
│ │ (空) │ 11 │ │
│ └─────────────────────────────────────────┴─────┘ │
│ │
└───────────────────────────────────────────────────────────────┘
字段说明:
hashcode: 对象的哈希码
age: 对象的GC年龄(4位,最大15)
thread: 持有偏向锁的线程ID
epoch: 偏向锁的时间戳
ptr_to_lock_record: 栈中锁记录的指针
ptr_to_heavyweight_monitor: 对象监视器的指针
最后2位: 锁状态标记
使用JOL工具分析对象
java
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
/**
* 使用JOL(Java Object Layout)分析对象布局
* 依赖: org.openjdk.jol:jol-core:0.16
*/
public class ObjectLayoutDemo {
public static void main(String[] args) {
// 打印JVM信息
System.out.println("=== JVM信息 ===");
System.out.println(VM.current().details());
System.out.println();
// === 1. 空对象 ===
System.out.println("=== 空对象布局 ===");
EmptyObject empty = new EmptyObject();
System.out.println(ClassLayout.parseInstance(empty).toPrintable());
// === 2. 基本类型字段 ===
System.out.println("\n=== 基本类型字段 ===");
PrimitiveFields primitives = new PrimitiveFields();
System.out.println(ClassLayout.parseInstance(primitives).toPrintable());
// === 3. 引用类型字段 ===
System.out.println("\n=== 引用类型字段 ===");
ReferenceFields references = new ReferenceFields();
System.out.println(ClassLayout.parseInstance(references).toPrintable());
// === 4. 数组对象 ===
System.out.println("\n=== 数组对象布局 ===");
int[] intArray = new int[5];
System.out.println(ClassLayout.parseInstance(intArray).toPrintable());
Object[] objArray = new Object[5];
System.out.println(ClassLayout.parseInstance(objArray).toPrintable());
// === 5. 继承关系 ===
System.out.println("\n=== 继承关系 ===");
ChildClass child = new ChildClass();
System.out.println(ClassLayout.parseInstance(child).toPrintable());
// === 6. 对象大小计算 ===
System.out.println("\n=== 对象大小 ===");
System.out.println("EmptyObject 大小: " +
VM.current().sizeOf(empty) + " 字节");
System.out.println("PrimitiveFields 大小: " +
VM.current().sizeOf(primitives) + " 字节");
System.out.println("int[5] 大小: " +
VM.current().sizeOf(intArray) + " 字节");
}
// 空对象
static class EmptyObject {
// 只有对象头
}
// 基本类型字段
static class PrimitiveFields {
byte b; // 1字节
short s; // 2字节
int i; // 4字节
long l; // 8字节
float f; // 4字节
double d; // 8字节
char c; // 2字节
boolean z; // 1字节
}
// 引用类型字段
static class ReferenceFields {
Object obj; // 引用,4字节(开启压缩)
String str; // 引用,4字节
int[] array; // 引用,4字节
}
// 父类
static class ParentClass {
int parentField;
}
// 子类
static class ChildClass extends ParentClass {
int childField;
}
}
/*
运行结果示例(64位JVM,开启指针压缩):
=== JVM信息 ===
# Running 64-bit HotSpot VM.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
=== 空对象布局 ===
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
=== 基本类型字段 ===
PrimitiveFields object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header) 01 00 00 00 00 00 00 00 (1)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 1 byte PrimitiveFields.b 0
13 1 boolean PrimitiveFields.z false
14 2 char PrimitiveFields.c
16 2 short PrimitiveFields.s 0
20 4 float PrimitiveFields.f 0.0
24 8 double PrimitiveFields.d 0.0
32 8 long PrimitiveFields.l 0
40 4 int PrimitiveFields.i 0
44 4 (loss due to the next object alignment)
Instance size: 48 bytes
*/
2.2 指针压缩
在64位JVM中,为了减少内存占用,可以使用指针压缩(Compressed Oops)。默认开启。
指针压缩原理
java
/**
* 指针压缩演示
*/
public class CompressedOopsDemo {
public static void main(String[] args) {
// 检查指针压缩是否开启
System.out.println("=== 指针压缩状态 ===");
String compressOops = getPropertyValue("UseCompressedOops");
String compressClassPointers = getPropertyValue("UseCompressedClassPointers");
System.out.println("UseCompressedOops: " + compressOops);
System.out.println("UseCompressedClassPointers: " + compressClassPointers);
// 计算内存差异
System.out.println("\n=== 内存占用对比 ===");
// 创建大量对象测试
int count = 1_000_000;
// 每个对象的内存差异
long referenceSizeWith = 4; // 开启压缩,引用4字节
long referenceSizeWithout = 8; // 关闭压缩,引用8字节
// 每个对象头的差异
long headerSizeWith = 12; // 开启压缩,对象头12字节
long headerSizeWithout = 16; // 关闭压缩,对象头16字节
System.out.println("创建 " + count + " 个对象:");
System.out.println();
// 空对象
long emptyWith = 16; // 12头 + 4填充
long emptyWithout = 16; // 16头,无需填充
System.out.println("空对象:");
System.out.println(" 开启压缩: " + (emptyWith * count / 1024 / 1024) + " MB");
System.out.println(" 关闭压缩: " + (emptyWithout * count / 1024 / 1024) + " MB");
// 包含引用的对象
ReferenceObject[] refs = new ReferenceObject[count];
long refObjWith = 16; // 12头 + 4引用
long refObjWithout = 24; // 16头 + 8引用
System.out.println("\n包含1个引用的对象:");
System.out.println(" 开启压缩: " + (refObjWith * count / 1024 / 1024) + " MB");
System.out.println(" 关闭压缩: " + (refObjWithout * count / 1024 / 1024) + " MB");
System.out.println(" 节省: " + ((refObjWithout - refObjWith) * count / 1024 / 1024) + " MB");
// 数组
Object[] array = new Object[count];
// 数组头 + 引用数组
long arrayWith = 16 + 4L * count; // 16头(含长度) + 4*count引用
long arrayWithout = 24 + 8L * count; // 24头 + 8*count引用
System.out.println("\n长度" + count + "的对象数组:");
System.out.println(" 开启压缩: " + (arrayWith / 1024 / 1024) + " MB");
System.out.println(" 关闭压缩: " + (arrayWithout / 1024 / 1024) + " MB");
System.out.println(" 节省: " + ((arrayWithout - arrayWith) / 1024 / 1024) + " MB");
// 指针压缩的地址范围
System.out.println("\n=== 指针压缩限制 ===");
System.out.println("压缩指针最大寻址: 4GB (2^32)");
System.out.println("配合对象对齐8字节: 最大32GB堆内存");
System.out.println("超过32GB需要关闭压缩或使用其他对齐");
System.out.println();
System.out.println("相关参数:");
System.out.println(" -XX:+UseCompressedOops 开启普通对象指针压缩");
System.out.println(" -XX:+UseCompressedClassPointers 开启类指针压缩");
System.out.println(" -XX:ObjectAlignmentInBytes=8 对象对齐字节数");
}
private static String getPropertyValue(String property) {
try {
return sun.misc.VM.getSavedProperty(property);
} catch (Exception e) {
return "unknown";
}
}
static class ReferenceObject {
Object ref;
}
}
/*
指针压缩工作原理:
地址编码:
压缩前:64位真实地址
压缩后:32位偏移量 × 8(对象对齐)
示例:
真实地址: 0x0000000123456780
压缩后: 0x02468ACF (偏移量/8)
限制:
32位 × 8字节对齐 = 32GB最大堆内存
超过此限制需要关闭压缩
*/
2.3 对象创建过程
java
/**
* 对象创建过程详解
*/
public class ObjectCreationDemo {
public static void main(String[] args) {
/*
对象创建步骤:
1. 类加载检查
- 检查类是否已加载
- 未加载则先加载类
2. 分配内存
- 指针碰撞(Bump the Pointer):堆规整时使用
- 空闲列表(Free List):堆不规整时使用
- 并发安全:CAS或TLAB
3. 初始化零值
- 将分配的内存初始化为零值
- 保证对象字段有默认值
4. 设置对象头
- 设置类元数据指针
- 设置对象哈希码、GC年龄等
5. 执行<init>方法
- 调用构造方法
- 初始化成员变量
- 执行构造代码块
*/
// 示例:跟踪对象创建
System.out.println("=== 对象创建过程 ===\n");
// 步骤1:类加载检查
System.out.println("1. 类加载检查");
System.out.println(" 检查Person类是否已加载");
// 步骤2:分配内存
System.out.println("\n2. 分配内存");
System.out.println(" 在堆中分配对象所需空间");
// 步骤3:初始化零值
System.out.println("\n3. 初始化零值");
Person p = new Person(); // 此时所有字段为零值
System.out.println(" name = null, age = 0");
// 步骤4:设置对象头
System.out.println("\n4. 设置对象头");
System.out.println(" 设置类指针、哈希码种子等");
// 步骤5:执行构造方法
System.out.println("\n5. 执行<init>方法");
p = new Person("张三", 25);
System.out.println(" 调用构造方法初始化");
System.out.println(" 结果: " + p);
// 对象创建的内存分配方式
System.out.println("\n=== 内存分配方式 ===\n");
memoryAllocationDemo();
// TLAB演示
System.out.println("\n=== TLAB (Thread Local Allocation Buffer) ===\n");
tlabDemo();
}
/**
* 内存分配方式演示
*/
private static void memoryAllocationDemo() {
/*
指针碰撞(Bump the Pointer):
┌─────────────────────────────────────────┐
│ 已分配区域 │ 空闲区域 │
│ │ │
│ obj1 obj2│←─ ptr ─→ │
└─────────────────────────────────────────┘
- 适用:堆内存规整(使用标记-整理、复制算法)
- 方式:指针向后移动对象大小距离
- 优点:效率高
- 使用:Serial、ParNew等收集器
空闲列表(Free List):
┌─────────────────────────────────────────┐
│ obj1 │空闲│ obj2 │ 空闲 │ obj3 │空闲 │
└─────────────────────────────────────────┘
↑ ↑ ↑
记录在空闲列表中
- 适用:堆内存不规整(使用标记-清除算法)
- 方式:从空闲列表找合适大小的空闲块
- 缺点:效率较低,可能产生碎片
- 使用:CMS收集器
*/
System.out.println("指针碰撞: 适用于规整堆(Serial, ParNew)");
System.out.println("空闲列表: 适用于不规整堆(CMS)");
}
/**
* TLAB演示
*/
private static void tlabDemo() {
/*
TLAB (Thread Local Allocation Buffer):
每个线程在堆中预先分配一小块私有区域
┌─────────────────────────────────────────────────────────┐
│ Java Heap │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Thread 1 │ │Thread 2 │ │Thread 3 │ │Thread 4 │ │
│ │ TLAB │ │ TLAB │ │ TLAB │ │ TLAB │ │
│ │ │ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 共享区域(需要同步) │
│ │
└─────────────────────────────────────────────────────────┘
优点:
- 避免多线程竞争
- 在TLAB中分配无需同步
- 使用指针碰撞方式,高效
参数:
-XX:+UseTLAB 开启TLAB(默认开启)
-XX:TLABSize=256k 设置TLAB大小
-XX:TLABRefillWasteFraction=64 TLAB refill阈值
*/
System.out.println("TLAB: 每个线程的私有分配缓冲区");
System.out.println("优点: 避免并发竞争,提高分配效率");
System.out.println("参数: -XX:+UseTLAB (默认开启)");
}
static class Person {
private String name;
private int age;
// 构造代码块
{
System.out.println(" 构造代码块执行");
}
public Person() {
System.out.println(" 无参构造执行");
}
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println(" 有参构造执行: name=" + name + ", age=" + age);
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
}
2.4 对象的访问定位
java
/**
* 对象访问定位方式
*/
public class ObjectAccessDemo {
public static void main(String[] args) {
/*
创建对象后,Java程序需要通过栈上的reference数据来操作堆上的具体对象。
访问方式取决于JVM实现,主要有两种:
1. 句柄访问
2. 直接指针访问
*/
// === 句柄访问 ===
System.out.println("=== 句柄访问 ===\n");
handleAccessDemo();
// === 直接指针访问 ===
System.out.println("\n=== 直接指针访问 ===\n");
directPointerDemo();
// === HotSpot的实现 ===
System.out.println("\n=== HotSpot实现 ===\n");
System.out.println("HotSpot使用直接指针访问");
System.out.println("优点: 访问速度快,减少一次间接访问");
System.out.println("缺点: 对象移动时需要修改reference");
}
/**
* 句柄访问演示
*/
private static void handleAccessDemo() {
/*
句柄访问:
┌──────────────┐
│ Java栈 │
│ reference ───┼────┐
└──────────────┘ │
↓
┌─────────────────────────────────────────┐
│ 句柄池 │
│ ┌─────────────────────────────────┐ │
│ │ 句柄 │ │
│ │ ┌───────────┬───────────┐ │ │
│ │ │对象实例数据│对象类型数据│ │ │
│ │ │ 指针 │ 指针 │ │ │
│ │ └─────┬─────┴─────┬─────┘ │ │
│ └────────│───────────│────────────┘ │
└───────────│───────────│─────────────────┘
↓ ↓
┌───────────────┐ ┌─────────────────┐
│ 堆(实例) │ │ 方法区(类型) │
│ Object │ │ Class数据 │
└───────────────┘ └─────────────────┘
优点:
- reference存储稳定句柄地址
- 对象移动时只需修改句柄,不动reference
缺点:
- 需要两次指针访问
- 需要维护句柄池空间
*/
System.out.println("句柄访问流程:");
System.out.println(" reference -> 句柄 -> 对象实例");
System.out.println(" reference -> 句柄 -> 对象类型");
System.out.println();
System.out.println("优点: 对象移动只需改句柄");
System.out.println("缺点: 两次指针访问,效率稍低");
}
/**
* 直接指针访问演示
*/
private static void directPointerDemo() {
/*
直接指针访问(HotSpot使用):
┌──────────────┐
│ Java栈 │
│ reference ───┼──────────────────────┐
└──────────────┘ │
↓
┌─────────────────────────────────────────────┐
│ 堆(对象实例) │
│ ┌─────────────────────────────────────┐ │
│ │ 对象头 │ │
│ │ 其中包含指向方法区的类型指针 │────┼──→ 方法区(类型数据)
│ ├─────────────────────────────────────┤ │
│ │ 实例数据 │ │
│ │ ... │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
优点:
- 只需一次指针访问
- 访问速度快
缺点:
- 对象移动需要修改reference
- GC时需要更新所有引用
*/
System.out.println("直接指针访问流程:");
System.out.println(" reference -> 对象实例 -> 类型数据(通过对象头)");
System.out.println();
System.out.println("优点: 一次访问,效率高");
System.out.println("缺点: 对象移动需要更新reference");
}
/**
* 实际代码访问对象
*/
public void accessObject() {
// 创建对象
User user = new User("张三", 25); // user是栈上的reference
// 访问对象字段
String name = user.getName(); // 通过reference访问堆中的对象
int age = user.getAge();
// 调用对象方法
user.sayHello();
// 对象作为参数传递
processUser(user); // 传递的是reference的副本
}
private void processUser(User user) {
// 仍然指向堆中同一个对象
user.setAge(26);
}
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public void sayHello() {
System.out.println("Hello, I'm " + name);
}
}
}
2.5 对象的分配策略
java
/**
* 对象分配策略详解
*/
public class ObjectAllocationDemo {
public static void main(String[] args) {
System.out.println("=== 对象分配策略 ===\n");
// === 1. 对象优先在Eden分配 ===
edenAllocation();
// === 2. 大对象直接进入老年代 ===
largeObjectAllocation();
// === 3. 长期存活对象进入老年代 ===
tenuringDemo();
// === 4. 动态对象年龄判定 ===
dynamicAgeDemo();
// === 5. 空间分配担保 ===
spaceGuaranteeDemo();
}
/**
* Eden区分配
*/
private static void edenAllocation() {
System.out.println("【1. Eden区分配】");
System.out.println("大多数对象首先在Eden区分配");
System.out.println();
/*
新生代结构:
┌─────────────────────────────────────────────┐
│ 新生代 │
│ ┌────────────┬─────────┬─────────┐ │
│ │ Eden │ S0 │ S1 │ │
│ │ (80%) │ (10%) │ (10%) │ │
│ └────────────┴─────────┴─────────┘ │
└─────────────────────────────────────────────┘
Eden满了触发Minor GC
*/
// 模拟Eden分配
byte[] allocation1 = new byte[1024 * 1024]; // 1MB
byte[] allocation2 = new byte[1024 * 1024]; // 1MB
System.out.println("allocation1, allocation2 在Eden分配");
System.out.println();
}
/**
* 大对象直接进老年代
*/
private static void largeObjectAllocation() {
System.out.println("【2. 大对象直接进老年代】");
System.out.println("超过阈值的大对象直接在老年代分配");
System.out.println();
/*
参数:
-XX:PretenureSizeThreshold=3145728 (3MB)
注意:
- 只对Serial和ParNew收集器有效
- G1有Humongous区域处理大对象
*/
// 假设阈值是3MB
byte[] largeObject = new byte[4 * 1024 * 1024]; // 4MB
// 大于阈值,直接在老年代分配
System.out.println("大对象: 4MB > 阈值(3MB) -> 老年代分配");
System.out.println("参数: -XX:PretenureSizeThreshold");
System.out.println("注意: 避免大对象,增加Eden区GC压力");
System.out.println();
}
/**
* 长期存活对象进入老年代
*/
private static void tenuringDemo() {
System.out.println("【3. 长期存活对象进入老年代】");
System.out.println("对象经过多次GC仍存活,年龄达到阈值后晋升");
System.out.println();
/*
年龄计数:
- 对象在Eden出生,经历一次Minor GC存活,年龄+1
- 年龄达到阈值,晋升老年代
参数:
-XX:MaxTenuringThreshold=15 (默认15)
-XX:InitialTenuringThreshold=7 (初始阈值,并行GC)
对象头中年龄占4位,最大值15
*/
// 模拟年龄增长
Object longLived = new Object();
System.out.println("对象经历GC次数 -> 年龄");
System.out.println("年龄达到 MaxTenuringThreshold(默认15) -> 晋升老年代");
System.out.println();
System.out.println("年龄记录在对象头Mark Word中:");
System.out.println("┌─────────────────────────────────┐");
System.out.println("│ ... │ age:4位 │ ... │");
System.out.println("└─────────────────────────────────┘");
System.out.println(" 4位最大值15,所以年龄阈值最大15");
System.out.println();
}
/**
* 动态对象年龄判定
*/
private static void dynamicAgeDemo() {
System.out.println("【4. 动态对象年龄判定】");
System.out.println("Survivor区相同年龄对象大小超过一半,直接晋升");
System.out.println();
/*
动态晋升规则:
如果Survivor空间中相同年龄所有对象大小的总和大于
Survivor空间的一半,年龄大于或等于该年龄的对象
直接进入老年代。
示例:
Survivor大小: 10MB
年龄1: 2MB
年龄2: 3MB
年龄3: 4MB (累计 2+3+4=9MB > 10MB/2)
-> 年龄3及以上的对象晋升老年代
*/
System.out.println("示例:");
System.out.println("Survivor大小: 10MB");
System.out.println("年龄1对象: 2MB");
System.out.println("年龄2对象: 3MB");
System.out.println("年龄3对象: 4MB (累计9MB > 5MB)");
System.out.println();
System.out.println("结果: 年龄≥3的对象晋升老年代");
System.out.println();
}
/**
* 空间分配担保
*/
private static void spaceGuaranteeDemo() {
System.out.println("【5. 空间分配担保】");
System.out.println("Minor GC前检查老年代是否有足够空间");
System.out.println();
/*
分配担保流程:
1. Minor GC前检查
老年代最大可用的连续空间 > 新生代所有对象总大小?
YES -> Minor GC安全
NO -> 检查 HandlePromotionFailure 设置
2. 允许担保失败 (HandlePromotionFailure=true)
老年代最大可用连续空间 > 历次晋升到老年代对象的平均大小?
YES -> 尝试Minor GC(有风险)
NO -> Full GC
3. 不允许担保失败
-> Full GC
参数:
-XX:-HandlePromotionFailure (JDK 6之后默认允许)
*/
System.out.println("检查流程:");
System.out.println(" 1. 老年代可用空间 > 新生代总大小?");
System.out.println(" YES -> Minor GC");
System.out.println(" NO -> 继续");
System.out.println();
System.out.println(" 2. 老年代可用空间 > 历史晋升平均值?");
System.out.println(" YES -> 尝试Minor GC");
System.out.println(" NO -> Full GC");
System.out.println();
}
}
三、可运行Java代码示例
完整示例:对象大小计算工具
java
import java.lang.reflect.*;
import java.util.*;
/**
* 对象大小计算工具
* 不使用JOL的情况下估算对象大小
*/
public class ObjectSizeCalculator {
// 对象头大小(64位JVM,开启指针压缩)
private static final int OBJECT_HEADER_SIZE = 12;
// 数组头额外大小
private static final int ARRAY_HEADER_EXTRA = 4;
// 引用大小(开启压缩)
private static final int REFERENCE_SIZE = 4;
// 对齐
private static final int ALIGNMENT = 8;
// 基本类型大小
private static final Map<Class<?>, Integer> PRIMITIVE_SIZES = new HashMap<>();
static {
PRIMITIVE_SIZES.put(byte.class, 1);
PRIMITIVE_SIZES.put(boolean.class, 1);
PRIMITIVE_SIZES.put(char.class, 2);
PRIMITIVE_SIZES.put(short.class, 2);
PRIMITIVE_SIZES.put(int.class, 4);
PRIMITIVE_SIZES.put(float.class, 4);
PRIMITIVE_SIZES.put(long.class, 8);
PRIMITIVE_SIZES.put(double.class, 8);
}
/**
* 计算对象大小
*/
public static long sizeOf(Object obj) {
if (obj == null) {
return 0;
}
Class<?> clazz = obj.getClass();
// 数组
if (clazz.isArray()) {
return sizeOfArray(obj);
}
// 普通对象
return sizeOfObject(clazz);
}
/**
* 计算普通对象大小
*/
private static long sizeOfObject(Class<?> clazz) {
long size = OBJECT_HEADER_SIZE;
// 收集所有字段(包括父类)
List<Field> fields = getAllFields(clazz);
for (Field field : fields) {
Class<?> fieldType = field.getType();
if (fieldType.isPrimitive()) {
size += PRIMITIVE_SIZES.get(fieldType);
} else {
size += REFERENCE_SIZE;
}
}
// 对齐
return align(size);
}
/**
* 计算数组大小
*/
private static long sizeOfArray(Object array) {
Class<?> componentType = array.getClass().getComponentType();
int length = java.lang.reflect.Array.getLength(array);
long size = OBJECT_HEADER_SIZE + ARRAY_HEADER_EXTRA;
if (componentType.isPrimitive()) {
size += (long) PRIMITIVE_SIZES.get(componentType) * length;
} else {
size += (long) REFERENCE_SIZE * length;
}
return align(size);
}
/**
* 获取所有字段
*/
private static List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
while (clazz != null && clazz != Object.class) {
for (Field field : clazz.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers())) {
fields.add(field);
}
}
clazz = clazz.getSuperclass();
}
return fields;
}
/**
* 对齐到8字节边界
*/
private static long align(long size) {
return (size + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT;
}
/**
* 深度计算对象大小(包括引用的对象)
*/
public static long deepSizeOf(Object obj) {
return deepSizeOf(obj, new IdentityHashMap<>());
}
private static long deepSizeOf(Object obj, Map<Object, Object> visited) {
if (obj == null || visited.containsKey(obj)) {
return 0;
}
visited.put(obj, null);
Class<?> clazz = obj.getClass();
long size = sizeOf(obj);
// 数组
if (clazz.isArray()) {
if (!clazz.getComponentType().isPrimitive()) {
int length = java.lang.reflect.Array.getLength(obj);
for (int i = 0; i < length; i++) {
Object element = java.lang.reflect.Array.get(obj, i);
size += deepSizeOf(element, visited);
}
}
return size;
}
// 对象字段
for (Field field : getAllFields(clazz)) {
if (!field.getType().isPrimitive()) {
try {
field.setAccessible(true);
Object fieldValue = field.get(obj);
size += deepSizeOf(fieldValue, visited);
} catch (IllegalAccessException e) {
// 忽略
}
}
}
return size;
}
// === 测试 ===
public static void main(String[] args) {
System.out.println("=== 对象大小计算工具 ===\n");
// 1. 空对象
Object empty = new Object();
System.out.println("空对象: " + sizeOf(empty) + " 字节");
// 2. 包含基本类型的对象
PrimitiveObject primitive = new PrimitiveObject();
System.out.println("基本类型对象: " + sizeOf(primitive) + " 字节");
// 3. 包含引用的对象
ReferenceObject reference = new ReferenceObject();
System.out.println("引用对象(浅): " + sizeOf(reference) + " 字节");
System.out.println("引用对象(深): " + deepSizeOf(reference) + " 字节");
// 4. 数组
int[] intArray = new int[100];
System.out.println("int[100]: " + sizeOf(intArray) + " 字节");
Object[] objArray = new Object[100];
System.out.println("Object[100](浅): " + sizeOf(objArray) + " 字节");
// 5. 复杂对象
Person person = new Person("张三", 25, new Address("北京", "朝阳"));
System.out.println("复杂对象(浅): " + sizeOf(person) + " 字节");
System.out.println("复杂对象(深): " + deepSizeOf(person) + " 字节");
// 6. 集合
List<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add("String" + i);
}
System.out.println("ArrayList(100个字符串)(深): " + deepSizeOf(list) + " 字节");
}
// 测试类
static class PrimitiveObject {
byte b;
short s;
int i;
long l;
float f;
double d;
char c;
boolean z;
}
static class ReferenceObject {
String str = "Hello, World!";
Object obj = new Object();
}
static class Person {
String name;
int age;
Address address;
Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
static class Address {
String city;
String district;
Address(String city, String district) {
this.city = city;
this.district = district;
}
}
}
完整示例:逃逸分析与标量替换
java
/**
* 逃逸分析与标量替换示例
*/
public class EscapeAnalysisDemo {
/**
* 逃逸分析:判断对象是否可能逃逸方法
*/
public static void main(String[] args) {
System.out.println("=== 逃逸分析示例 ===\n");
// 开启逃逸分析(JDK 7+默认开启)
// -XX:+DoEscapeAnalysis
// 1. 不逃逸
noEscape();
// 2. 方法逃逸
methodEscape();
// 3. 线程逃逸
threadEscape();
// 4. 标量替换
scalarReplacement();
// 5. 锁消除
lockElimination();
}
/**
* 不逃逸:对象仅在方法内使用
* 可以进行标量替换,在栈上分配
*/
private static void noEscape() {
// 对象不逃逸,可以被优化
Point p = new Point(1, 2);
int result = p.getX() + p.getY();
System.out.println("不逃逸: " + result);
/*
JIT编译后可能优化为:
int x = 1;
int y = 2;
int result = x + y;
// 对象根本不会在堆上创建
*/
}
/**
* 方法逃逸:对象被返回或传递给其他方法
*/
private static Point methodEscape() {
Point p = new Point(3, 4);
return p; // 逃逸:返回给调用者
}
/**
* 线程逃逸:对象被其他线程访问
*/
private static void threadEscape() {
Point p = new Point(5, 6);
// 逃逸:被其他线程访问
new Thread(() -> {
System.out.println(p.getX());
}).start();
}
/**
* 标量替换:将对象拆解为标量(基本类型)
*/
private static void scalarReplacement() {
System.out.println("\n=== 标量替换 ===");
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
// 这个对象不会逃逸
Point p = new Point(i, i + 1);
int x = p.getX();
int y = p.getY();
}
long end = System.currentTimeMillis();
System.out.println("耗时: " + (end - start) + "ms");
System.out.println("开启逃逸分析和标量替换后,对象不会在堆上创建");
System.out.println();
System.out.println("相关参数:");
System.out.println(" -XX:+DoEscapeAnalysis 开启逃逸分析(默认)");
System.out.println(" -XX:+EliminateAllocations 开启标量替换(默认)");
System.out.println(" -XX:+EliminateLocks 开启锁消除(默认)");
/*
验证:
java -XX:-DoEscapeAnalysis EscapeAnalysisDemo // 关闭,较慢
java -XX:+DoEscapeAnalysis EscapeAnalysisDemo // 开启,较快
*/
}
/**
* 锁消除:对象不逃逸时,锁操作可以消除
*/
private static void lockElimination() {
System.out.println("\n=== 锁消除 ===");
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
StringBuffer sb = new StringBuffer();
sb.append("a").append(i); // StringBuffer的方法都有synchronized
// 但sb不逃逸,锁可以消除
}
long end = System.currentTimeMillis();
System.out.println("StringBuffer耗时: " + (end - start) + "ms");
System.out.println("StringBuffer的synchronized锁被消除");
System.out.println("参数: -XX:+EliminateLocks (默认开启)");
}
// 测试类
static class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}
}
/*
逃逸分析优化效果:
1. 栈上分配
- 对象不逃逸时,可以在栈上分配
- 减少GC压力
2. 标量替换
- 对象拆解为标量(基本类型和引用)
- 完全消除对象创建
3. 锁消除
- 对象不逃逸时,synchronized锁可以消除
- 提高并发性能
相关JVM参数:
-XX:+DoEscapeAnalysis 开启逃逸分析(JDK7+默认)
-XX:+EliminateAllocations 开启标量替换(默认)
-XX:+EliminateLocks 开启锁消除(默认)
*/
四、实战应用场景
场景1:内存优化
java
import java.util.*;
/**
* 基于对象模型的内存优化
*/
public class MemoryOptimizationDemo {
public static void main(String[] args) {
System.out.println("=== 内存优化技巧 ===\n");
// === 1. 避免不必要的对象头开销 ===
System.out.println("【1. 减少小对象数量】");
// 不好的设计:每个属性都是对象
class BadDesign {
List<Integer> values = new ArrayList<>();
}
// 好的设计:使用数组
class GoodDesign {
int[] values;
}
System.out.println("List<Integer> 每个Integer都有对象头(16字节)");
System.out.println("int[] 只有数组头+数据,节省大量内存");
System.out.println();
// === 2. 使用基本类型替代包装类 ===
System.out.println("【2. 基本类型vs包装类】");
// 包装类
Integer[] integers = new Integer[1000];
// 每个: 16字节头 + 4字节int = 20字节 -> 对齐24字节
// 总计: 24 * 1000 = 24000字节 (还不算引用数组)
// 基本类型
int[] ints = new int[1000];
// 只有: 16字节头 + 4 * 1000 = 4016字节
// 节省约85%
System.out.println("Integer[1000]: 约24KB");
System.out.println("int[1000]: 约4KB");
System.out.println("节省约85%内存");
System.out.println();
// === 3. 字段顺序优化 ===
System.out.println("【3. 字段顺序优化】");
// 不好的顺序
class BadOrder {
byte b1; // 1字节
long l1; // 8字节 (需要8字节对齐,前面填充7字节)
byte b2; // 1字节
long l2; // 8字节 (前面填充7字节)
byte b3; // 1字节
// 总计: 1 + 7(填充) + 8 + 1 + 7(填充) + 8 + 1 = 33字节 -> 40字节
}
// 好的顺序
class GoodOrder {
long l1; // 8字节
long l2; // 8字节
byte b1; // 1字节
byte b2; // 1字节
byte b3; // 1字节
// 总计: 8 + 8 + 1 + 1 + 1 = 19字节 -> 24字节
}
System.out.println("BadOrder: 40字节(大量填充)");
System.out.println("GoodOrder: 24字节(字段按大小排序)");
System.out.println("建议: 长字段在前,短字段在后");
System.out.println();
// === 4. 使用更紧凑的数据结构 ===
System.out.println("【4. 紧凑数据结构】");
// 存储布尔值
boolean[] boolArray = new boolean[8]; // 8个字节
byte bitSet = 0; // 1个字节,用位存储8个布尔值
System.out.println("存储8个布尔值:");
System.out.println("boolean[8]: 16字节头 + 8字节 = 24字节");
System.out.println("位运算byte: 1字节");
System.out.println("节省约96%");
System.out.println();
// === 5. 对象池 ===
System.out.println("【5. 对象池复用】");
// 频繁创建销毁的对象使用对象池
ObjectPool<Point> pointPool = new ObjectPool<>(() -> new Point());
Point p = pointPool.borrow();
p.set(1, 2);
// 使用后归还
pointPool.returnObject(p);
System.out.println("对象池避免频繁创建销毁对象");
System.out.println("适用: 创建开销大、频繁使用的对象");
System.out.println();
// === 6. 使用原始类型集合 ===
System.out.println("【6. 原始类型集合】");
// 标准集合
// List<Integer> - 每个元素是Integer对象
// 第三方原始类型集合(如Eclipse Collections, Trove)
// IntArrayList - 内部使用int[]
System.out.println("使用Eclipse Collections等库的原始类型集合");
System.out.println("避免包装类开销");
}
static class Point {
int x, y;
void set(int x, int y) { this.x = x; this.y = y; }
}
static class ObjectPool<T> {
private final Queue<T> pool = new LinkedList<>();
private final java.util.function.Supplier<T> factory;
public ObjectPool(java.util.function.Supplier<T> factory) {
this.factory = factory;
}
public T borrow() {
T obj = pool.poll();
return obj != null ? obj : factory.get();
}
public void returnObject(T obj) {
pool.offer(obj);
}
}
}
场景2:对象布局调优
java
import org.openjdk.jol.info.ClassLayout;
import java.util.*;
/**
* 对象布局调优示例
*/
public class ObjectLayoutOptimization {
public static void main(String[] args) {
System.out.println("=== 对象布局调优 ===\n");
// === 1. 分析当前布局 ===
System.out.println("【1. 分析对象布局】");
// 使用JOL分析
// System.out.println(ClassLayout.parseClass(MyClass.class).toPrintable());
// === 2. 字段重排序 ===
System.out.println("\n【2. 字段重排序】");
// JVM会自动进行字段重排序优化
// 但最好还是按规范写
System.out.println("优化前(可能有填充):");
System.out.println(" byte, long, byte, long, byte");
System.out.println("\n优化后(减少填充):");
System.out.println(" long, long, byte, byte, byte");
// === 3. 使用@Contended避免伪共享 ===
System.out.println("\n【3. 避免伪共享】");
// 多线程场景下,不同线程访问同一缓存行的不同变量
// 会导致缓存行失效(伪共享)
// 使用@Contended注解让JVM添加填充
// @Contended
// volatile long value;
System.out.println("伪共享: 不同线程访问同一缓存行的不同变量");
System.out.println("解决: @Contended注解或手动填充");
System.out.println("参数: -XX:-RestrictContended");
// === 4. 对齐控制 ===
System.out.println("\n【4. 对齐控制】");
System.out.println("默认对齐: 8字节");
System.out.println("参数: -XX:ObjectAlignmentInBytes");
System.out.println();
System.out.println("增大对齐可以支持更大堆(指针压缩时):");
System.out.println(" 8字节对齐 -> 最大32GB堆");
System.out.println(" 16字节对齐 -> 最大64GB堆");
}
/**
* 手动填充避免伪共享
*/
static class PaddedLong {
volatile long value;
// 手动填充(确保value独占一个缓存行)
long p1, p2, p3, p4, p5, p6, p7;
}
/**
* 使用@Contended(需要JVM支持)
*/
// @jdk.internal.vm.annotation.Contended
static class ContendedLong {
volatile long value;
}
}
五、总结与最佳实践
核心要点回顾
| 概念 | 内容 |
|---|---|
| 对象头 | Mark Word + 类型指针 + 数组长度(可选) |
| 实例数据 | 字段内容,按大小排序减少填充 |
| 对齐填充 | 保证对象大小是8字节的倍数 |
| 指针压缩 | 64位JVM优化,减少内存占用 |
| 逃逸分析 | 判断对象是否逃逸方法 |
| 标量替换 | 对象拆解为标量,消除对象创建 |
最佳实践
-
内存优化
- 使用基本类型替代包装类
- 字段按大小排序(长字段在前)
- 减少小对象数量
- 使用对象池复用对象
-
避免伪共享
java// 多线程共享变量添加填充 @Contended volatile long counter; -
利用逃逸分析
- 让对象不逃逸方法
- JIT会自动优化栈上分配
-
合理设置指针压缩
bash# 堆内存 <= 32GB,开启指针压缩 -XX:+UseCompressedOops # 堆内存 > 32GB,关闭或增大对齐 -XX:ObjectAlignmentInBytes=16
对象大小速查表
| 对象类型 | 大小(开启压缩) | 大小(关闭压缩) |
|---|---|---|
| 空对象 | 16字节 | 16字节 |
| 包含1个int | 16字节 | 16字节 |
| 包含1个long | 16字节 | 24字节 |
| 包含1个引用 | 16字节 | 24字节 |
| Object[0] | 16字节 | 24字节 |
| int[0] | 16字节 | 16字节 |
| Integer | 16字节 | 16字节 |
| Long | 24字节 | 24字节 |
相关JVM参数
bash
# 指针压缩
-XX:+UseCompressedOops # 开启普通指针压缩(默认)
-XX:+UseCompressedClassPointers # 开启类指针压缩(默认)
# 逃逸分析
-XX:+DoEscapeAnalysis # 开启逃逸分析(默认)
-XX:+EliminateAllocations # 开启标量替换(默认)
-XX:+EliminateLocks # 开启锁消除(默认)
# 对齐
-XX:ObjectAlignmentInBytes=8 # 对象对齐字节数(默认8)
# TLAB
-XX:+UseTLAB # 开启TLAB(默认)
-XX:TLABSize=256k # TLAB大小
# 对象年龄
-XX:MaxTenuringThreshold=15 # 晋升年龄阈值
扩展阅读
- 《深入理解Java虚拟机》:周志明著,对象内存布局章节
- JOL工具:OpenJDK项目,分析对象布局
- 逃逸分析论文:Choi et al., "Escape Analysis for Java"
- HotSpot源码:oops模块,对象模型实现