一、对象的创建流程详解
1.1 对象创建的完整流程
类加载检查 → 分配内存 → 初始化 → 设置对象头 → 执行<init>方法
1.2 五步流程深度解析
1. 类加载检查
当JVM遇到new指令时,首先检查:
-
能否在常量池中定位到类的符号引用
-
该类是否已被加载、解析和初始化
触发场景 :new关键字、对象克隆、对象序列化等。
2. 分配内存
类加载检查通过后,JVM为新生对象分配内存。这里有两个核心问题:
内存划分方法:
// 根据堆内存是否规整,采用不同策略
if (堆内存规整) {
// 使用"指针碰撞"(Bump the Pointer)
// 所有用过的内存放一边,空闲内存放另一边
// 分配时只需移动分界指针
} else {
// 使用"空闲列表"(Free List)
// 维护一个可用内存块列表
// 分配时从列表中找到足够大的空间
}
并发问题解决方案:
// 方案1:CAS + 失败重试
// 保证内存分配操作的原子性
// 方案2:TLAB(线程本地分配缓冲)
// 每个线程在堆中预先分配一小块私有内存
// 参数:-XX:+UseTLAB(默认开启),-XX:TLABSize指定大小
3. 初始化
内存分配完成后,JVM将分配到的内存空间初始化为零值(不包括对象头)。这保证了实例字段在不赋初值的情况下也能直接使用。
java
// 示例:所有基本类型字段被设为0或false,引用类型设为null
public class User {
private int id; // 初始化为0
private String name; // 初始化为null
private boolean flag; // 初始化为false
}
4. 设置对象头
初始化零值后,JVM设置对象头信息,包括:
-
对象哈希码
-
GC分代年龄
-
锁状态标志
-
类型指针(指向类元数据)
对象头结构(64位系统):
|-------------------------------------------------|
| Mark Word (8字节) |
|-------------------------------------------------|
| 类型指针(4字节,开启压缩) / 类指针(8字节) |
|-------------------------------------------------|
| 数组长度(4字节,仅数组对象) |
|-------------------------------------------------|
锁状态在对象头中的存储:
| 锁状态 | 存储内容 | 标志位 |
|----------|--------------------------------------|--------|
| 无锁 | 哈希码、分代年龄、偏向锁标识等 | 01 |
| 偏向锁 | 持有锁的线程ID、偏向时间戳等 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 |
| 重量级锁 | 指向重量级锁(互斥量)的指针 | 10 |
| GC标记 | 空 | 11 |
5. 执行<init>方法
最后执行对象的构造方法,按照程序员的意愿进行初始化。这是对象创建的最后一步,也是程序员唯一能控制的部分。
二、对象内存分配策略
2.1 对象分配流程图

2.2 栈上分配与逃逸分析
逃逸分析:分析对象动态作用域,判断对象是否会被外部方法引用。
// 示例1:对象未逃逸(可栈上分配)
public void test1() {
User user = new User(); // 对象未逃逸,可在栈上分配
user.setId(1);
user.setName("张三");
// 方法结束,对象随栈帧销毁
}
// 示例2:对象逃逸(需堆上分配)
public User test2() {
User user = new User(); // 对象逃逸,返回给外部
user.setId(1);
user.setName("李四");
return user; // 对象逃逸出方法作用域
}
JVM参数:
-
-XX:+DoEscapeAnalysis:开启逃逸分析(JDK7+默认开启) -
-XX:-DoEscapeAnalysis:关闭逃逸分析
2.3 标量与聚合量替换
标量替换:当对象不会被外部访问且可分解时,JVM不会创建该对象,而是将其成员变量分解为标量在栈上分配。
// 优化前:创建User对象
public void test() {
User user = new User();
user.id = 1;
user.name = "test";
System.out.println(user.id);
}
// 优化后:标量替换
public void test() {
int id = 1; // 标量分配在栈上
String name = "test";
System.out.println(id);
}
JVM参数 :-XX:+EliminateAllocations(开启标量替换,JDK7+默认开启)
2.4 实战:栈上分配性能对比
public class AllotOnStack {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100_000_000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");
}
private static void alloc() {
User user = new User(); // 未逃逸对象
user.setId(1);
user.setName("test");
}
}
// 测试结果对比
// 参数1:-Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
// 结果:耗时约5ms,几乎无GC
// 参数2:-Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:-EliminateAllocations
// 结果:耗时约2000ms,频繁GC
三、堆内存分配策略
3.1 Eden区分配(默认策略)
大多数对象在新生代Eden区分配。当Eden区空间不足时,触发Minor GC。
新生代内存布局:
默认比例 8:1:1
┌─────────────┬─────────────┬─────────────┐
│ Eden区 │ Survivor0区 │ Survivor1区 │
│ (8/10) │ (1/10) │ (1/10) │
└─────────────┴─────────────┴─────────────┘
3.2 大对象直接进入老年代
大对象:需要大量连续内存空间的对象(如长字符串、大数组)。
// 参数设置:-XX:PretenureSizeThreshold=1000000(单位字节)
// 仅对Serial和ParNew收集器有效
public class BigObjectTest {
public static void main(String[] args) {
// 对象大小超过1MB,直接进入老年代
byte[] bigArray = new byte[2 * 1024 * 1024]; // 2MB数组
}
}
为什么大对象直接进入老年代?
避免在Eden区和Survivor区之间发生大量内存复制,降低GC效率。
3.3 长期存活对象进入老年代
对象年龄计数器:
-
对象在Eden出生,经过第一次Minor GC后存活,移动到Survivor区,年龄设为1
-
每次Minor GC后年龄+1
-
年龄达到阈值(默认15)后,晋升到老年代
JVM参数 :-XX:MaxTenuringThreshold=15(设置年龄阈值)
3.4 对象动态年龄判断
规则 :Survivor区中,一批对象的总大小超过该区内存的50%(-XX:TargetSurvivorRatio可调整),则年龄大于等于这批对象中最大年龄的对象直接进入老年代。
目的:让可能长期存活的对象尽早进入老年代。
3.5 老年代空间分配担保机制
流程:
// Minor GC前检查
if (老年代可用空间 < 年轻代所有对象总大小) {
if (设置了-XX:-HandlePromotionFailure) {
// 检查老年代可用空间是否大于历次晋升到老年代对象的平均大小
if (老年代可用空间 < 平均晋升大小) {
// 触发Full GC
fullGC();
}
} else {
// 触发Full GC
fullGC();
}
}
触发条件:
-
Minor GC后存活对象大小 > 老年代可用空间
-
历次晋升对象平均大小 > 老年代可用空间
四、对象内存回收机制
4.1 判断对象是否可回收
方法1:引用计数法(JVM未采用)
// 问题:无法解决循环引用
public class ReferenceCountingGC {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB; // 循环引用
objB.instance = objA;
objA = null;
objB = null;
// 引用计数都不为0,但实际已不可达
}
}
方法2:可达性分析算法(JVM采用)
从GC Roots对象出发,向下搜索,形成引用链。不在引用链上的对象被认为是垃圾。
GC Roots包括:
-
虚拟机栈中引用的对象
-
方法区中静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中JNI引用的对象
4.2 引用类型详解
// 1. 强引用(Strong Reference)
Object obj = new Object(); // 强引用,不会被GC回收
// 2. 软引用(Soft Reference)
SoftReference<Object> softRef = new SoftReference<>(new Object());
// 内存不足时会被回收,适合做缓存
// 3. 弱引用(Weak Reference)
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 只要GC就会回收
// 4. 虚引用(Phantom Reference)
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// 几乎不用,用于对象回收跟踪
4.3 finalize()方法的最后机会
对象被标记为可回收后,还有一次"复活"的机会:
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize方法执行");
FinalizeEscapeGC.SAVE_HOOK = this; // 重新建立引用,避免被回收
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
// 第一次GC,finalize方法会执行
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
System.out.println("第一次GC后存活");
}
// 第二次GC,finalize方法不会再次执行
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK == null) {
System.out.println("第二次GC后被回收");
}
}
}
注意 :finalize()方法只会被执行一次!
4.4 如何判断一个类是无用的类
方法区(元空间)回收的主要目标是无用的类,需同时满足:
-
该类所有实例都已被回收
-
加载该类的ClassLoader已被回收
-
该类对应的
java.lang.Class对象没有被任何地方引用
五、指针压缩技术详解
5.1 什么是指针压缩?
-
JDK1.6 update14开始支持
-
64位系统中,使用32位指针表示对象地址
-
参数:
-XX:+UseCompressedOops(默认开启)
5.2 为什么要使用指针压缩?
优势:
-
减少内存占用(64位指针→32位指针)
-
减少GC压力
-
提高缓存命中率
限制:
-
堆内存≤4GB:直接使用低32位地址
-
4GB<堆内存≤32GB:压缩指针有效
-
堆内存>32GB:压缩指针失效,强制使用64位指针
5.3 查看对象布局示例
// 添加依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
// 查看对象布局
public class JOLSample {
public static void main(String[] args) {
// 查看普通对象布局
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
// 查看数组对象布局
System.out.println(ClassLayout.parseInstance(new int[3]).toPrintable());
// 查看自定义对象布局
System.out.println(ClassLayout.parseInstance(new User()).toPrintable());
}
}
六、总结与最佳实践
6.1 对象创建优化建议
-
减少大对象创建:避免长字符串、大数组的频繁创建
-
利用栈上分配:尽量使用局部变量,让对象不逃逸
-
对象复用:使用对象池或缓存,减少GC压力
-
及时释放引用 :使用完对象后及时置为
null
6.2 JVM参数调优建议
# 基础配置
-Xms2g -Xmx2g # 堆大小,避免频繁扩容
-Xmn1g # 新生代大小,根据业务调整
-XX:SurvivorRatio=8 # Eden与Survivor比例
-XX:MaxTenuringThreshold=15 # 对象晋升老年代年龄
# 指针压缩(默认开启)
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers
# 优化选项
-XX:+UseTLAB # 启用线程本地分配缓冲
-XX:+DoEscapeAnalysis # 启用逃逸分析
-XX:+EliminateAllocations # 启用标量替换
# 大对象处理
-XX:PretenureSizeThreshold=1M # 大对象直接进入老年代
6.3 监控与诊断
-
JOL工具:分析对象内存布局
-
VisualVM:监控对象创建和回收情况
-
GC日志分析:了解对象生命周期
-
堆转储分析:定位内存泄漏