跳到主要内容

图解Jvm 8.对象实例化及直接内存

·2434 字·5 分钟

1. 对象实例化 #

1.1 创建对象的方式 #

Java中常见的对象创建方式包括:

  1. new关键字:最常规的实例化方式
  2. Class.newInstance():反射机制创建对象
  3. Constructor.newInstance():更灵活的反射创建方式
  4. Clone方法:对象复制机制
  5. 反序列化:从字节流恢复对象
  6. Unsafe类:直接操作内存的危险方式

示例代码:

// 反射创建实例
Class<?> clazz = Class.forName("com.example.User");
User user = (User) clazz.newInstance();

1.2 创建对象的步骤 #

1.2.1 类加载检查 #

JVM遇到new指令时,首先检查:

  1. 类元数据是否存在
  2. 是否完成链接(验证-准备-解析)
  3. 是否完成初始化

1.2.2 内存分配策略 #

  • 指针碰撞(Bump the Pointer):适用于Serial、ParNew等带压缩整理的收集器
  • 空闲列表(Free List):CMS这类基于标记-清除算法的收集器使用

1.2.3 并发处理 #

采用两种保证线程安全的方式:

  1. CAS+失败重试
  2. TLAB(Thread Local Allocation Buffer)

TLAB结构示例:

1.2.4 内存初始化 #

JVM将分配的内存空间初始化为零值(不包括对象头)

1.2.5 对象头设置 #

对象头包含两部分信息:

  1. Mark Word(存储对象的运行时数据)
  2. 类型指针(指向类元数据的指针)

1.2.6 执行init方法 #

执行顺序:

  1. 父类构造器
  2. 实例变量初始化
  3. 构造器代码块

2. 对象内存布局 #

2.1 对象头(Header) #

对象头包含两个核心部分:

  1. 运行时元数据(Mark Word):存储对象运行时状态
    • 哈希码(Identity HashCode)
    • GC分代年龄(4bit)
    • 锁状态标志(2bit)
    • 偏向线程ID(23bit)
    • 偏向时间戳(2bit)
  2. 类型指针(Klass Pointer):指向方法区的类元数据
    • 开启压缩指针时为4字节(-XX:+UseCompressedOops)
    • 未开启压缩指针时为8字节

锁状态演变示例

2.2 实例数据(Instance Data) #

存储策略特点:

  1. 相同宽度的字段分配在一起
  2. 父类字段在子类字段之前
  3. 开启字段重排列优化时(默认开启),允许字段顺序调整

2.3 对齐填充(Padding) #

作用原理:

  • JVM要求对象起始地址是8字节的整数倍
  • 对象大小必须是8字节的整数倍
  • 通过填充无效字节满足对齐要求

3. 对象的访问定位 #

3.1 句柄访问 #

优势与劣势:

  • GC时只需修改句柄指针
  • 需要两次指针访问
  • 内存空间额外消耗

3.2 直接指针(HotSpot采用) #

性能对比:

访问方式指针跳转次数内存占用GC效率
句柄访问2次较高
直接指针1次较低较低

4. 直接内存(Direct Memory) #

4.1 直接内存概述 #

关键特征:

  • 非JVM运行时数据区
  • 受操作系统管理的内存
  • 可通过NIO的ByteBuffer.allocateDirect()分配
  • 大小可通过-XX:MaxDirectMemorySize设置

4.2 非直接缓冲区 #

性能瓶颈:

  • 需要JVM堆与系统内存之间的数据拷贝
  • 增加GC压力

4.3 直接缓存区 #

优势对比:

指标直接缓冲区非直接缓冲区
内存占用位置系统内存JVM堆
创建/销毁成本较高较低
IO操作性能
内存管理手动自动

5. 对象实例化及直接内存的常见问题与解决方案 #

5.1 对象创建导致的OOM异常 #

解决方案:

  1. 调整JVM参数:<font style="background-color:rgb(252, 252, 252);">-Xmx4g -Xms4g</font>
  2. 使用内存分析工具(MAT、JProfiler)定位泄漏点
  3. 对大对象使用对象池技术

5.2 直接内存溢出问题 #

预防措施:

  1. 使用try-with-resources管理Buffer
  2. 定期调用<font style="background-color:rgb(252, 252, 252);">((DirectBuffer) buffer).cleaner().clean()</font>
  3. 监控直接内存使用:<font style="background-color:rgb(252, 252, 252);">jcmd <pid> VM.native_memory</font>

5.3 对象访问定位异常 #

典型表现:

  • <font style="background-color:rgb(252, 252, 252);">NullPointerException</font>
  • <font style="background-color:rgb(252, 252, 252);">ClassCastException</font>

根本原因:

调试方法:

  1. 使用HSDB(HotSpot Debugger)查看对象头
  2. 添加<font style="background-color:rgb(252, 252, 252);">-XX:+VerifyBeforeGC</font>参数验证内存
  3. 开启<font style="background-color:rgb(252, 252, 252);">-XX:+UseCompressedOops</font>优化指针

6. 对象实例化及直接内存的高频面试问题与解答 #

6.1 对象创建过程相关问题 #

Q1:new关键字创建对象时,类加载发生在哪个阶段?

解答:
当JVM执行到new字节码指令时,首先检查对应的类是否已经完成加载、链接和初始化。若未加载,则立即触发类加载过程,该过程完全同步执行。

Q2:TLAB如何解决并发分配问题?

技术原理:

解答:
每个线程在Eden区预先划分私有内存块(默认Eden的1%),通过<font style="background-color:rgb(252, 252, 252);">-XX:TLABSize</font>调整大小。当TLAB用尽时,才会使用CAS机制在公共区域分配,降低锁竞争。

6.2 内存布局相关问题 #

Q3:对象头中的Mark Word存储哪些关键信息?

![](../../assets/img/图解JVM/8/19对象头中的Mark Word存储哪些关键信息.png)

解答:
存储运行时数据包括:

  1. 无锁状态:25位哈希码 + 4位分代年龄
  2. 偏向锁:23位线程ID + 2位时间戳
  3. 轻量级锁:指向栈中锁记录的指针
  4. 重量级锁:指向监视器Monitor的指针

Q4:如何计算对象实际大小?

工具使用:

// 添加JVM参数
-javaagent:path/to/sizeofagent.jar

计算方式:
对象大小 = 对象头(12/16字节) + 实例数据 + 对齐填充

6.3 直接内存相关问题 #

Q5:为什么Netty使用直接内存进行IO操作?

性能优势:

  1. 避免JVM堆与Native堆之间的数据拷贝
  2. 使用DMA(Direct Memory Access)技术加速传输
  3. 减少GC停顿对IO的影响

Q6:如何监控直接内存使用情况?

监控命令:

jcmd <pid> VM.native_memory detail

输出解析:

Total: reserved=16384MB, committed=5120MB
-                 Java Heap (reserved=8192MB, committed=2048MB)
-                     Class (reserved=1060MB, committed=60MB)
-                    Thread (reserved=156MB, committed=156MB)
-                      Code (reserved=2496MB, committed=96MB)
-                        GC (reserved=624MB, committed=524MB)
-                  Internal (reserved=96MB, committed=96MB)
-                    Other (reserved=384MB, committed=384MB)
-                    Native (reserved=384MB, committed=384MB)

7. 总结优化建议 #

7.1 对象实例化优化 #

7.2 直接内存最佳实践 #

// 推荐使用模式
try (ByteBuffer buffer = ByteBuffer.allocateDirect(1024) {
    // 使用直接缓冲区
} 

// 显式释放内存
public void releaseDirectMemory(ByteBuffer buffer) {
if (buffer.isDirect() {
    Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
    if (cleaner != null) {
        cleaner.clean();
    }
}
}

全文总结
本文通过图解与文字结合的方式,系统剖析了JVM对象实例化的完整生命周期和直接内存的运行机制。掌握这些原理可以帮助开发者:

  1. 编写高性能的对象创建代码
  2. 合理设计对象内存结构
  3. 正确使用直接内存提升IO性能
  4. 快速定位内存相关异常问题
  5. 从容应对技术面试中的深度拷问
Anarkh
作者
Anarkh
博学之 审问之 慎思之 明辨之 笃行之