当前位置: 首页 > article >正文

11.2 java语言执行浅析3美团面试追魂七连问

美团面试追魂七连问:关于Object o = New Object() ,1请解释一下对象的创建过程(半初始化) 2,加问DCL要不要volatile 问题(指令重排) 3.对象在内存中的存储布局(对象与数组的存储不同),4.对象头具体包括什么.5.对象怎么定位.6.对象怎么分配(栈-线程本地-Eden-Old)7.在内存中占用多少字节


1. 请解释一下对象的创建过程(包括“半初始化”问题)

在 Java 中,当我们执行 Object o = new Object(); 时,JVM 会经历以下步骤来创建一个对象:

1. 类加载检查

  • 首先检查这个类是否已经被加载、解析和初始化。
  • 如果没有,则进行类加载(ClassLoader.loadClass -> 验证 -> 准备 -> 解析 -> 初始化)

2. 分配内存

  • 在堆中为对象分配内存空间。
  • 内存分配方式取决于 GC 算法:
    • 指针碰撞(适用于 Serial / ParNew)
    • 空闲列表(适用于 CMS)

3. 初始化零值

  • 将分配到的内存空间初始化为默认值(如 int=0,boolean=false,引用=null)

4. 设置对象头

  • 设置对象头信息(Mark Word、Klass Pointer 等),用于支持 JVM 的运行时机制。

5. 执行构造函数 <init>

  • 调用类的构造方法,对对象进行初始化。

关键点:指令重排导致“半初始化”问题

  • 在多线程环境下,由于 CPU 和编译器的指令重排序,可能导致对象未完全初始化就被其他线程访问。
  • 例如,在 DCL(Double Check Locking)单例模式中,如果不对实例变量加 volatile,可能会看到一个“半初始化”的对象。

2. 加问 DCL 要不要 volatile?为什么?

DCL 示例代码:

public class Singleton {private static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) {if (instance == null) { // 第二次检查instance = new Singleton(); // 创建对象}}}return instance;}
}

回答:需要 volatile

  • 因为 instance = new Singleton() 这个操作不是原子的,它分为三个步骤:

    1. 分配内存空间
    2. 调用构造函数初始化对象
    3. 将 instance 引用指向该内存地址
  • 如果不加 volatile,可能发生指令重排,变成顺序是:

    • 分配内存 → 指向地址 → 构造函数(还没完成构造)
    • 此时另一个线程读取到 instance != null,但对象尚未构造完毕,出现“半初始化”状态。

解决办法:

使用 volatile 关键字修饰 instance 变量:

private static volatile Singleton instance;

作用:

  • 禁止指令重排序
  • 保证可见性

3. 对象在内存中的存储布局(对象 vs 数组)

Java 对象在堆内存中的布局由三部分组成:

1. 对象头(Object Header)

  • 存储元数据信息,比如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID 等。
  • 大小通常是 12~16 字节(32/64位系统不同)

2. 实例数据(Instance Data)

  • 对象真正存储的有效数据,即我们在类中定义的各种字段内容。
  • 包括从父类继承下来的字段。

3. 对齐填充(Padding)

  • 为了满足 JVM 的内存对齐要求(通常 8 字节对齐),可能添加一些填充字节。

数组对象的特殊布局:

除了上述三部分外,数组对象还有一个额外的部分:

长度字段(Length)

  • 表示数组长度,占用 4 字节(int 类型)

所以数组对象结构如下:

对象头长度字段实例数据填充
MarkWord + KlassPointer4字节数组元素8字节对齐

4. 对象头具体包括什么?

对象头主要包含两个部分:

1. Mark Word(标记字段)

  • 用于存储对象自身的运行时数据,如:
    • 哈希码(HashCode)
    • GC 分代年龄(Age)
    • 锁状态标志(biased, lightweight, heavyweight)
    • 线程持有的锁
    • 偏向线程 ID(Biased Locking)
    • 持有锁的计数器等
以 HotSpot VM 为例(64位系统):
状态内容大小
无锁态哈希码 + 分代年龄 + 0164 bits
偏向锁线程ID + 时间戳 + 分代年龄 + 10164 bits
轻量级锁指向栈中锁记录的指针64 bits
重量级锁指向互斥量(monitor)的指针64 bits
GC 标记64 bits

2. Klass Pointer(类型指针)

  • 指向对象对应的 Class 对象(元数据),用于确定对象是哪个类的实例。
  • 占用 4 或 8 字节(取决于是否开启压缩指针 -XX:+UseCompressedOops

5. 对象怎么定位?

在 Java 中,当我们通过一个引用变量(如 Object o = new Object();)来访问对象时,JVM 是如何定位堆中的对象的?这个问题涉及到 JVM 的对象访问方式内存模型


 一、Java 对象的访问机制

Java 堆中存储的是对象的实例,而栈上的局部变量表中保存的是对象的引用(reference)。这个引用并不是直接指向对象本身,而是通过某种机制找到对象的实际地址。

JVM 规范并没有强制规定引用如何实现,不同的虚拟机可以有不同的实现方式,但主流的 HotSpot 虚拟机中通常使用以下两种方式:


1. 句柄访问(Handle)

结构:
  • 栈中的 reference 指向一个句柄池中的句柄。
  • 句柄中包含两个指针:
    • 对象实例指针:指向堆中真正的对象
    • 类型指针:指向方法区中的类元数据(Class 对象)
优点:
  • 在对象被移动(GC 时)时,只需要修改句柄中的对象实例指针,不需要修改栈上的引用。
缺点:
  • 多了一次间接访问,效率略低。
栈引用 --> [句柄] --> 实例数据 | 类型指针

 2. 直接指针访问(Direct Pointer,HotSpot 默认)

结构:
  • 栈中的 reference 直接指向堆中的对象实例。
  • 对象头中包含一个指向类元数据的指针(Klass Pointer)。
优点:
  • 访问速度快,省去一次中间跳转。
缺点:
  • 如果对象被 GC 移动,所有引用都需要更新(由 GC 算法自动处理)。
栈引用 --> 对象实例(包含对象头、实例数据)

 HotSpot 中的对象定位方式

HotSpot 使用的是直接指针访问的方式。对象布局简述:

内容描述
对象头(Object Header)包含 Mark Word 和 Klass Pointer
实例数据(Instance Data)成员变量等实际数据
对齐填充(Padding)保证对象大小为 8 字节的倍数
  • 其中 Klass Pointer 指向方法区中的类元数据(即 Class 对象)。
  • 所以,当引用变量指向对象后,JVM 就可以通过这个引用来访问对象的实例数据以及其类型信息。
方式定位方式是否需要句柄池访问速度GC 移动代价
句柄访问reference → 句柄池 → 对象需要稍慢
直接指针访问reference → 对象实例不需要大(需更新引用)

🔍 HotSpot 使用直接指针访问,这是为了提高性能。虽然 GC 时需要更新引用,但现代 GC 算法已经能高效地完成这项任务。


 补充:数组对象的定位

数组对象与普通对象略有不同,除了对象头、实例数据外,还多了一个长度字段(Length),位于对象头之后、实例数据之前。

例如一个 int[4] 数组的布局如下:

对象头长度字段(4)数据部分(4个 int)填充

这样就可以通过引用快速获取数组长度。

Java 中通过引用变量访问对象时,JVM 使用直接指针访问方式(HotSpot),引用直接指向堆中的对象实例,对象头中包含指向类元数据的指针,从而实现对象的快速定位和访问。


6. 对象怎么分配?(栈、线程本地、Eden、Old)

Java 中的对象主要分配在堆中,但根据对象的大小、生命周期、线程使用等特性,JVM 会采取不同的分配策略。

我们按以下几个维度来分析:


一、栈上分配(Stack Allocation)

场景:
  • 小对象
  • 作用域明确且不会逃逸出方法的作用域(即不会被其他线程访问或返回给外部)
  • 开启了 逃逸分析(Escape Analysis)
原理:
  • 如果一个对象只在方法内部使用,并且没有“逃逸”出去(比如被返回或赋值给全局变量),JVM 可以通过逃逸分析优化,将这个对象分配在当前线程的栈上。
  • 这样做的好处是:不走堆分配流程,减少 GC 压力,提升性能
 如何开启?
-XX:+DoEscapeAnalysis -XX:+UseCompressedOops

注意:默认情况下 HotSpot 是开启了逃逸分析的(JDK 6u23+),但是否启用栈上分配取决于具体实现和 JVM 版本。


 二、线程本地分配(Thread Local Allocation Buffer, TLAB)

 场景:
  • 多线程环境下,为每个线程预先分配一小块内存空间(TLAB)
  • 线程优先在自己的 TLAB 中分配对象
 原理:
  • 每个线程都有一个私有的 TLAB(位于 Eden 区)
  • 线程创建对象时优先尝试在 TLAB 中分配
  • 避免多线程竞争 Eden 区的锁(eden_lock),提高并发性能
相关参数:
-XX:+UseTLAB                  # 是否启用 TLAB,默认开启
-XX:TLABSize=                # 设置初始 TLAB 大小
-XX:+ResizeTLAB              # 是否动态调整 TLAB 大小
 小结:
  • TLAB 是线程私有的 Eden 区域
  • 提高多线程下对象分配效率
  • 不适合大对象(超过 TLAB 剩余空间的对象会被直接分配到 Eden)

三、Eden 区分配(Young Generation)

 场景:
  • 普通对象默认分配在 Eden 区(属于新生代的一部分)
  • Eden 区满了之后触发 Minor GC
原理:
  • 新生对象大多数都分配在 Eden 区
  • 经过一次 GC 后仍然存活的对象会被移动到 Survivor 区
  • 经历多次 GC 后晋升到老年代(Tenured/OLD)
 分配过程:
  1. 线程尝试在 TLAB 中分配
  2. TLAB 不够 → 分配到 Eden 区(需加锁)
  3. Eden 区不够 → 触发 Minor GC
  4. GC 后仍存活的对象进入 Survivor 区
  5. 经过一定年龄(MaxTenuringThreshold)后晋升到老年代

 四、老年代分配(Old Generation / Tenured)

 场景:
  • 大对象(如长数组、大字符串)
  • 存活时间较长的对象(经过多次 GC 依然存活)
  • 动态年龄判断(Survivor 区相同年龄对象总和大于一半,该年龄及以上的对象直接进入老年代)
原理:
  • 老年代存放的是生命周期较长的对象
  • GC 类型是 Full GC 或 Mixed GC(G1 收集器)
  • 老年代空间不足时可能触发 Full GC,代价较高

  五、大对象直接分配:

  • 使用 -XX:PretenureSizeThreshold 参数设置阈值,大于该值的对象直接分配到老年代(仅适用于 Serial 和 ParNew 收集器)
  • 示例:
    -XX:PretenureSizeThreshold=1048576  # 大于 1MB 的对象直接进老年代
分配方式条件优点缺点
栈上分配小对象 + 未逃逸快速、无 GC依赖逃逸分析,有限制
TLAB线程局部分配减少锁竞争,提高并发占用 Eden 空间,不适合大对象
Eden 区普通对象生命周期短,GC 效率高需要频繁 GC
Survivor 区Minor GC 存活对象年龄增长后晋升老年代临时过渡区
老年代大对象、长期存活对象稳定存储Full GC 成本高

对象创建↓
是否可栈上分配? → 是 → 分配在栈上↓否
是否可TLAB分配? → 是 → 分配在TLAB(Eden)↓否
是否为大对象?    → 是 → 分配在老年代(Old)↓否
分配到 Eden 区↓
Minor GC → 存活 → Survivor 区↓
再次 GC → 存活 → 晋升到老年代

 六、补充说明

  • 分配担保机制(Handle Promotion Failure)

    • 当 Minor GC 发生时,如果 Survivor 区无法容纳所有存活对象,JVM 会向老年代发起分配担保,将部分对象提前晋升到老年代。
  • 动态年龄判定(Adaptive Tenuring)

    • Survivor 区中相同年龄的对象总和 ≥ Survivor 区容量的一半时,这些对象都会晋升到老年代,而不需要等到 MaxTenuringThreshold。

Java 对象的分配遵循栈上 > TLAB > Eden > Old 的顺序,结合逃逸分析、TLAB 技术、GC 算法与对象生命周期进行智能调度,以达到性能最优。


 7. 在内存中占用多少字节?

我们以 HotSpot 虚拟机为例来分析。Java 对象在堆中的内存结构主要由三部分组成:


 一、对象头(Object Header)

1. Mark Word(标记字段)
  • 存储对象运行时信息,如哈希码、GC 分代年龄、锁状态标志等。
  • 占用 8 字节(64位系统) / 4 字节(32位系统)
2. Klass Pointer(类型指针)
  • 指向该对象对应的类元数据(Class 对象)。
  • 默认是 8 字节(64位系统)
  • 如果开启压缩指针(-XX:+UseCompressedOops),则为 4 字节

⚠️ UseCompressedOops 是 JDK 6 及以后版本默认开启的。

📌 对象头总大小:
系统UseCompressedOops对象头大小
64位开启12 字节
64位关闭16 字节
32位-8 字节

 二、实例数据(Instance Data)

这部分是你定义的对象字段所占用的空间。

class Person {int age;boolean isMale;
}
  • int age:4 字节
  • boolean isMale:1 字节

合计:5 字节

但 JVM 会对字段进行排序并填充,以满足对齐要求(通常是 8 字节对齐)。


三、对齐填充(Padding)

为了提高访问效率,JVM 要求对象大小是 8 字节的整数倍。

所以即使实际数据只有 5 字节,也会补到 8 字节。


示例分析:计算一个对象的实际大小

示例 1:空对象
class Empty {}
  • 对象头:12 字节(64位 + 压缩指针)
  • 实例数据:0 字节
  • 对齐后:12 → 不是 8 的倍数,需要填充4个字节
  • 总大小:16 字节

验证:(需要导入https://mvnrepository.com/artifact/org.openjdk.jol/jol-core)

import org.openjdk.jol.vm.VM;
import org.openjdk.jol.info.ClassLayout;public class Test {static class Empty {}public static void main(String[] args) {System.out.println(VM.current().details());System.out.println(ClassLayout.parseClass(Empty.class).toPrintable());}
}


示例 2:简单字段对象
class Person {int age;           // 4Bboolean isMale;    // 1B
}
  • 对象头:12 字节
  • 实例数据:4 + 1 = 5 字节
  • 总计:12 + 5 = 17 字节
  • 对齐填充:+7 字节(使总大小为 24 字节)

总大小:24 字节


示例 3:包含引用类型的对象
class User {String name;       // 引用类型,默认 4 字节(压缩指针)int age;
}
  • 对象头:12 字节
  • 实例数据:4 (name) + 4 (age) = 8 字节
  • 总计:20 字节
  • 对齐填充:+4 字节(使总大小为 24 字节)

 总大小:24 字节


示例 4:数组对象(特别注意长度字段)
int[] arr = new int[10]; // 10个元素 × 4B = 40B 数据
  • 对象头:12 字节
  • 长度字段:4 字节(int 类型)
  • 实例数据:10 × 4 = 40 字节
  • 总计:12 + 4 + 40 = 56 字节
  • 对齐检查:56 已是 8 的倍数

总大小:56 字节

 如何验证?使用 Instrumentation.getObjectSize()

你可以通过以下方式验证对象大小:

步骤:
  1. 编写一个 Agent 类获取 Instrumentation
  2. 使用 getObjectSize() 方法查看对象大小
示例代码:
public class ObjectSizeAgent {private static Instrumentation inst;public static void premain(String args, Instrumentation i) {inst = i;}public static long sizeOf(Object o) {return inst.getObjectSize(o);}
}
public class Main {public static void main(String[] args) {int[] arr = new int[10]; // 10个元素 × 4B = 40B 数据String str = "Hello, World!";long size = ObjectSizeFetcher.getObjectSize(str);System.out.println("Size of string: " + size + " bytes");long size2 = ObjectSizeFetcher.getObjectSize(arr);System.out.println("Array of int[10]: " + size2 + " bytes");}}

 需要把他们打成jar包. (我已经做了一份jar包.下载地址https://download.csdn.net/download/chxii/90938677?spm=1001.2101.3001.9500)

然后通过命令行启动程序并指定 agent 参数即可。


 补充:字段重排序优化

JVM 会自动对字段进行重排序,以减少填充空间,提升空间利用率。

class Example {byte b1;int i;byte b2;
}

实际存储顺序可能变为:

b1 | b2 | padding(2) | i(4)

这样可以减少填充空间,节省内存。

一个 Java 对象的大小 = 对象头(12~16字节)+ 实例数据 + 对齐填充;数组还额外包含长度字段(4字节);合理设计字段顺序和类型可以有效降低内存开销。

降到这里,就不在深入下去了.如果你还想了解更多.可以通过 JOL(Java Object Layout)工具分析对象内存布局

http://www.lryc.cn/news/2398824.html

相关文章:

  • MySQL 全量、增量备份与恢复
  • 【25.06】FISCOBCOS使用caliper自定义测试 通过webase 单机四节点 helloworld等进行测试
  • MonoPCC:用于内窥镜图像单目深度估计的光度不变循环约束|文献速递-深度学习医疗AI最新文献
  • 如何计算H5页面加载时的白屏时间
  • SpringAI系列 - MCP篇(三) - MCP Client Boot Starter
  • 【深度学习新浪潮】以Dify为例的大模型平台的对比分析
  • Asp.net core 使用EntityFrame Work
  • isp中的 ISO代表什么意思
  • AI Coding 资讯 2025-06-03
  • 2024年12月 C/C++(三级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • 3d GIS数据来源与编辑工具
  • NeRF PyTorch 源码解读 - 体渲染
  • SpringBoot 数据库批量导入导出 Xlsx文件的导入与导出 全量导出 数据库导出表格 数据处理 外部数据
  • 解决:install via Git URL失败的问题
  • OpenCV CUDA模块特征检测------创建Harris角点检测器的GPU实现接口cv::cuda::createHarrisCorner
  • 【氮化镓】钝化层对p-GaN HEMT阈值电压的影响
  • C++:优先级队列
  • 睡眠分期 html
  • Java求职者面试:Spring、Spring Boot、Spring MVC与MyBatis技术深度解析
  • Github 2025-05-29 Go开源项目日报Top9
  • 前端项目种对某个文件夹进行大小写更改,git识别不到差异导致无变化
  • AWS VPC 网络详解:理解云上专属内网的关键要素
  • Ubuntu24.04.2 + kubectl1.33.1 + containerdv1.7.27 + calicov3.30.0
  • 循环神经网络(RNN)全面教程:从原理到实践
  • uniapp 键盘顶起页面问题
  • 利用TOA与最小二乘法直接求解
  • SpringBoot系列之RabbitMQ 实现订单超时未支付自动关闭功能
  • 【C++高级主题】命令空间(五):类、命名空间和作用域
  • ArcGIS Pro 3.4 二次开发 - 地图创作 1
  • 2.1HarmonyOS NEXT开发工具链进阶:DevEco Studio深度实践