资讯动态

22 面试官:你 new 一个对象,JVM 到底做了哪些事?

发布时间:2026/8/10 11:30:36 来源:尧图企业网站定制
面试现场。来写一行代码。面试官把白板笔递给我。我在白板上写了Object obj new Object();这行代码背后JVM做了哪些事给你三分钟。我当时心里一紧。说实话new一个对象谁不会啊但要说清楚底层每一步大部分人只能说出分配内存、调用构造方法就卡住了。今天就掰开揉碎了讲讲new一个对象JVM到底在背后忙什么。一、五个大步骤从new Object()到你能用obj这个引用JVM内部走了五个大步骤1. 类加载检查2. 分配内存3. 初始化零值4. 设置对象头5. 执行init方法每一步里面都有坑。二、第一步类加载检查虚拟机遇到new指令第一件事不是分配内存——是检查这个类加载了没有。// 字节码层面 0: new #2 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object.init:()V 7: astore_1new指令的参数#2指向常量池中java/lang/Object的符号引用。虚拟机先去方法区找这个类有没有加载过。没加载先走一遍类加载流程加载→验证→准备→解析→初始化。这五个阶段里面最有意思的是准备和初始化。准备阶段给静态变量分配内存并设零值初始化阶段才执行你写的赋值语句。比如public static int count 100;准备阶段count0初始化阶段count100。两者之间有时间差。如果别的线程在准备阶段读了count读到的是0而不是100——这就是为什么静态变量在多线程环境中有可见性问题。Object类肯定加载过了JVM启动就加载但如果是你自己写的类这一步可能会触发类加载。而类加载的初始化阶段会执行static{}块和静态变量赋值——这就是为什么面试官爱追问类加载时机。什么时候会触发new对象、反射、访问静态变量、初始化子类先初始化父类……每个时机展开都能讲十分钟。三、第二步分配内存类加载完毕确定对象需要多大内存下一步就是在堆上划一块地。这里涉及两个问题怎么划划在哪3.1 怎么划地指针碰撞 vs 空闲列表取决于堆内存是否规整。堆是否规整又取决于垃圾收集器有没有压缩整理功能。指针碰撞堆内存是规整的用过的在一边空闲的在另一边中间一根指针当分界线。分配内存就是指针往空闲那边挪一段等于在纸上画一道线。空闲列表堆内存不规整虚拟机维护一个列表记录哪些块空闲。分配时从列表里找一块够大的。Serial、Parallel用指针碰撞因为它们有压缩CMS用空闲列表。G1和ZGC各有各的玩法G1是Region级别管理ZGC用彩色指针。3.2 划在哪栈上 vs TLAB vs 堆这是面试的加分点。很多人以为对象一定在堆上其实不一定。栈上分配逃逸分析发现对象不会逃出方法直接在栈帧里分配方法结束自动销毁不用GC。这玩意听起来很美但实际触发条件很苛刻JDK 8之后默认开启但实际栈上分配的场景不多。public void test() { // 这个Point对象只在这个方法里用可能直接分配在栈上 Point p new Point(1, 2); System.out.println(p.x p.y); }TLABThread Local Allocation Buffer这是最常用的优化。堆是共享的多个线程同时new对象会有锁竞争。TLAB的思路是给每个线程在堆上预先申请一小块专属区域线程在自己的TLAB里分配不用加锁只有TLAB用完了才去堆上申请新的。JDK 8默认开启了-XX:UseTLAB。你可以加-XX:-UseTLAB关掉试试高并发场景下吞吐量直接腰斩。这就是为什么JVM团队搞了这个优化——太有用了。TLAB不够用了或者对象太大比如一个大数组超过TLAB剩余空间才走堆上的慢速分配——这一步要加CAS锁性能差不少。所以写代码的时候尽量别在循环里new超大对象会让TLAB频繁换新的等于把TLAB优化的好处全丢了。四、第三步初始化零值内存分配好了但在构造方法执行之前虚拟机会把分配到的这块内存全部清零除了对象头。public class User { private int age; // 这一步后被设为 0 private String name; // 这一步后被设为 null private boolean vip; // 这一步后被设为 false }这就是为什么Java不会有野指针问题。C/C里如果不初始化局部变量的值是不确定的——那块内存之前存过什么就是什么。Java直接给你清零了。注意这一步是虚拟机做的不是构造方法做的。所以你在构造方法里读到的字段值已经是0/null/false了不是乱码。五、第四步设置对象头这是最有意思的地方——对象头Object Header里藏了对象的所有身份证信息。一个Java对象在内存中的布局------------------ | Mark Word | 8字节64位JVM ------------------ | Klass Pointer | 4字节压缩后 ------------------ | 实例数据 | 变长 ------------------ | 对齐填充 | 凑到8字节的倍数 ------------------Mark Word里存了哈希码、GC分代年龄、锁状态标志、偏向锁信息、线程ID...这就是为什么能synchronized(obj)——锁信息就写在对象的Mark Word里。// HotSpot源码markOop.hpp // Mark Word的位布局64位 // // 无锁状态 // unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 // // 偏向锁 // thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 // // 轻量级锁 // ptr_to_lock_record:62 | lock:2 // // 重量级锁 // ptr_to_monitor:62 | lock:2你看那lock:2字段两位二进制表示四种锁状态01无锁、00轻量锁、10重量锁、11GC标记。这就是上一篇讲的锁升级——升级的不是别的东西改的就是Mark Word里的这几个bit。Klass Pointer指向方法区中类的元数据告诉虚拟机我是哪个类的对象。六、第五步执行init方法最后才轮到构造方法。但要注意这里执行的init不是只执行你写的构造方法。它是编译器自动生成的执行顺序是1. 父类构造方法递归到Object 2. 实例变量初始化 3. 实例初始化块 {} 4. 你写的构造方法体这个顺序很重要——很多诡异bug都跟它有关。看个例子public class Child extends Parent { private int x 10; // 第2步 { System.out.println(init); } // 第3步 public Child() { super(); // 第1步隐式编译器插入 System.out.println(x); // 第4步此时x已经是10了 } }如果父类构造方法里调用了一个被子类重写的方法那子类重写的方法执行时子类的实例变量还是默认值0/null不是你在声明时写的那个值。因为第2步还没执行。这是Java面试的经典陷阱题——构造方法里调用可重写方法会出问题根源就在init的执行顺序。另一个知识点init方法和clinit方法的区别。clinit是类构造器执行静态变量赋值和静态代码块由JVM保证线程安全只会执行一次。init是实例构造器每次new都执行。有个细节很多高级开发都不知道——如果类里没有任何构造方法编译器会自动生成一个无参构造方法。但如果只定义了有参构造方法编译器就不会生成无参的了。这时候用反射newInstance()就会报NoSuchMethodException。Spring容器创建Bean时默认调无参构造所以Spring管理的Bean最好保留无参构造。 面试官视角的标准回答当面试官问new一个对象做了什么他想要听到的完整答案是首先虚拟机遇到new指令去检查类是否加载过没加载会触发类加载。然后根据类的大小分配内存——具体怎么分取决于GC器Serial用指针碰撞CMS用空闲列表。分配的时候会优先使用TLAB来避免线程竞争大的或者TLAB放不下的对象走堆上慢速分配。分配完后虚拟机把内存空间清零保证字段有默认值。接着设置对象头包括Mark Word和Klass Pointer。Mark Word里存hashCode、GC年龄和锁标志——这就是synchronized能用的原因。最后执行init方法按照父类构造、实例变量、初始化块、构造方法体的顺序执行。到这里对象才算真正创建完成。说到这面试官大概率会追问那你讲讲对象头里的锁状态变化过程你看下一篇的选题都帮你想好了。锁升级对象头串起来讲。下一篇对象头里的锁状态到底怎么变的轻量锁膨胀成重量锁的那一刻JVM做了什么唠点键盘之外的 · 第 22 篇

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价