面渣逆袭 JVM篇 V2.1




⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
前⾔ No. 1 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
前⾔
2.3 万字 115 张⼿绘图,详解 54 道 Java 虚拟机⾯试⾼频题(让天下没有难背的⼋股),⾯渣背会这些 JVM ⼋股
⽂,这次吊打⾯试官,我觉得稳了(⼿动 dog)。整理:沉默王⼆,戳转载链接,作者:三分恶,戳原⽂链接。
亮⽩版本更适合拿出来打印,这也是很多学⽣党喜欢的⽅式,打印出来背诵的效率会更⾼。
2024 年 12 ⽉ 30 ⽇开始着⼿第⼆版更新。
对于⾼频题,会标注在《Java ⾯试指南(付费)》中出现的位置,哪家公司,原题是什么,并且会加 ,⽬
录⼀⽬了然;如果你想节省时间的话,可以优先背诵这些题⽬,尽快做到知彼知⼰,百战不殆。
区分⼋股精华回答版本和原理底层解释,让⼤家知其然知其所以然,同时⼜能做到⾯试时的⾼效回答。
结合项⽬(技术派、pmhub)来组织语⾔,让⾯试官最⼤程度感受到你的诚意,⽽不是机械化的背诵。
修复第⼀版中出现的问题,包括球友们的私信反馈,⽹站留⾔区的评论,以及 GitHub 仓库中的 issue,让这
份⾯试指南更加完善。
增加⼆哥编程星球的球友们拿到的⼀些 offer,对⾯渣逆袭的感谢,以及对简历修改的⼀些认可,以此来激励
⼤家,给⼤家更多信⼼。
优化排版,增加⼿绘图,重新组织答案,使其更加⼝语化,从⽽更贴近⾯试官的预期。
No. 2 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
码,关注⼆哥的公众号,回复【222】即可拉取最新版本。
No. 3 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
当然了,请允许我的⼀点点私⼼,那就是星球的 PDF 版本会⽐公众号早⼀个⽉时间,毕竟星球⽤户都付费过了,
我有必要让他们先享受到⼀点点福利。相信⼤家也都能理解,毕竟在线版是免费的,CDN、服务器、域名、OSS
等等都是需要成本的。
更别说我付出的时间和精⼒了,⼤家觉得有帮助还请给个⼝碑,让你身边的同事、同学都能受益到。
我把⼆哥的 Java 进阶之路、JVM 进阶之路、并发编程进阶之路,以及所有⾯渣逆袭的版本都放进来了,涵盖 Java
基础、Java集合、Java并发、JVM、Spring、MyBatis、计算机⽹络、操作系统、MySQL、Redis、RocketMQ、分
布式、微服务、设计模式、Linux 等 16 个⼤的主题,共有 40 多万字,2000+张⼿绘图,可以说是诚意满满。
展示⼀下暗⿊版本的 PDF 吧,排版清晰,字体优雅,更加适合夜服,晚上看会更舒服⼀点。
No. 4 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
⼀、引⾔
1.什么是 JVM?
JVM,也就是 Java 虚拟机,它是 Java 实现跨平台的基⽯。
程序运⾏之前,需要先通过编译器将 Java 源代码⽂件编译成 Java 字节码⽂件;
程序运⾏时,JVM 会对字节码⽂件进⾏逐⾏解释,翻译成机器码指令,并交给对应的操作系统去执⾏。
No. 5 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
这样就实现了 Java ⼀次编译,处处运⾏的特性。
说说 JVM 的其他特性?
①、JVM 可以⾃动管理内存,通过垃圾回收器回收不再使⽤的对象并释放内存空间。
②、JVM 包含⼀个即时编译器 JIT,它可以在运⾏时将热点代码缓存到 codeCache 中,下次执⾏的时候不⽤再⼀⾏
⼀⾏的解释,⽽是直接执⾏缓存后的机器码,执⾏效率会⼤幅提⾼。
No. 6 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
③、任何可以通过 Java 编译的语⾔,⽐如说 Groovy、Kotlin、Scala 等,都可以在 JVM 上运⾏。
为什么要学习 JVM?
No. 7 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
学习 JVM 可以帮助我们开发者更好地优化程序性能、避免内存问题。
⽐如说了解 JVM 的内存模型和垃圾回收机制,可以帮助我们更合理地配置内存、减少 GC 停顿。
⽐如说掌握 JVM 的类加载机制可以帮助我们排查类加载冲突或异常。
再⽐如说,JVM 还提供了很多调试和监控⼯具,可以帮助我们分析内存和线程的使⽤情况,从⽽解决内存溢出内存
泄露等问题。
1. Java ⾯试指南(付费)收录的京东同学 10 后端实习⼀⾯的原题:有了解 JVM 吗
2. Java ⾯试指南(付费)收录的字节跳动同学 20 测开⼀⾯的原题:了解过 JVM 么?讲⼀下 JVM 的特性
2.说说 JVM 的组织架构(补充)
增补于 2024 年 03 ⽉ 08 ⽇。
推荐阅读:⼤⽩话带你认识 JVM
JVM ⼤致可以划分为三个部分:类加载器、运⾏时数据区和执⾏引擎。
① 类加载器,负责从⽂件系统、⽹络或其他来源加载 Class ⽂件,将 Class ⽂件中的⼆进制数据读⼊到内存当中。
No. 8 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
② 运⾏时数据区,JVM 在执⾏ Java 程序时,需要在内存中分配空间来处理各种数据,这些内存区域按照 Java 虚拟
机规范可以划分为⽅法区、堆、虚拟机栈、程序计数器和本地⽅法栈。
③ 执⾏引擎,也是 JVM 的⼼脏,负责执⾏字节码。它包括⼀个虚拟处理器、即时编译器 JIT 和垃圾回收器。
1. Java ⾯试指南(付费)收录的腾讯 Java 后端实习⼀⾯原题:说说 JVM 的组织架构
2. Java ⾯试指南(付费)收录的得物⾯经同学 9 ⾯试题⽬原题:JVM的架构,具体阐述⼀下各个部分的功
能?
⼆、内存管理
3. 能说⼀下 JVM 的内存区域吗?
推荐阅读:深⼊理解 JVM 的运⾏时数据区
按照 Java 虚拟机规范,JVM 的内存区域可以细分为 程序计数器 、 虚拟机栈 、 本地⽅法栈 、 堆 和 ⽅法区 。
其中 ⽅法区 和 堆 是线程共享的, 虚拟机栈 、 本地⽅法栈 和 程序计数器 是线程私有的。
No. 9 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
介绍⼀下程序计数器?
程序计数器也被称为 PC 寄存器,是⼀块较⼩的内存空间。它可以看作是当前线程所执⾏的字节码⾏号指示器。
介绍⼀下 Java 虚拟机栈?
Java 虚拟机栈的⽣命周期与线程相同。
当线程执⾏⼀个⽅法时,会创建⼀个对应的栈帧,⽤于存储局部变量表、操作数栈、动态链接、⽅法出⼝等信息,
然后栈帧会被压⼊虚拟机栈中。当⽅法执⾏完毕后,栈帧会从虚拟机栈中移除。
⼀个什么都没有的空⽅法,空的参数都没有,那局部变量表⾥有没有变量?
对于静态⽅法,由于不需要访问实例对象 this,因此在局部变量表中不会有任何变量。
No. 10 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
对于⾮静态⽅法,即使是⼀个完全空的⽅法,局部变量表中也会有⼀个⽤于存储 this 引⽤的变量。this 引⽤指向当
前实例对象,在⽅法调⽤时被隐式传⼊。
详细解释⼀下:
⽐如说有这样⼀段代码:
public class VarDemo1 {
public void emptyMethod() {
// 什么都没有
}
public static void staticEmptyMethod() {
// 什么都没有
}
}
⽤ javap -v VarDemo1 命令查看编译后的字节码,就可以在 emptyMethod 中看到这样的内容:
这⾥的 locals=1 表示局部变量表有⼀个变量,即 this,Slot 0 位置存储了 this 引⽤。
⽽在静态⽅法 staticEmptyMethod 中,你会看到这样的内容:
No. 11 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
这⾥的 locals=0 表示局部变量表为空,因为静态⽅法属于类级别⽅法,不需要 this 引⽤,也就没有局部变量。
介绍⼀下本地⽅法栈?
本地⽅法栈与虚拟机栈相似,区别在于虚拟机栈是为 JVM 执⾏ Java 编写的⽅法服务的,⽽本地⽅法栈是为 Java 调
⽤本地 native ⽅法服务的,通常由 C/C++ 编写。
在本地⽅法栈中,主要存放了 native ⽅法的局部变量、动态链接和⽅法出⼝等信息。当⼀个 Java 程序调⽤⼀个
native ⽅法时,JVM 会切换到本地⽅法栈来执⾏这个⽅法。
介绍⼀下本地⽅法栈的运⾏场景?
当 Java 应⽤需要与操作系统底层或硬件交互时,通常会⽤到本地⽅法栈。
⽐如调⽤操作系统的特定功能,如内存管理、⽂件操作、系统时间、系统调⽤等。
详细说明⼀下:
⽐如说获取系统时间的 System.currentTimeMillis() ⽅法就是调⽤本地⽅法,来获取操作系统当前时间的。
再⽐如 JVM ⾃身的⼀些底层功能也需要通过本地⽅法来实现。像 Object 类中的 hashCode() ⽅法、 clone() ⽅
法等。
No. 12 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
native ⽅法解释⼀下?
推荐阅读:⼿把⼿教你⽤ C语⾔实现 Java native 本地⽅法
native ⽅法是在 Java 中通过 native 关键字声明的,⽤于调⽤⾮ Java 语⾔,如 C/C++ 编写的代码。Java 可以通过
JNI,也就是 Java Native Interface 与底层系统、硬件设备、或者本地库进⾏交互。
介绍⼀下 Java 堆?
堆是 JVM 中最⼤的⼀块内存区域,被所有线程共享,在 JVM 启动时创建,主要⽤来存储 new 出来的对象。
No. 13 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
Java 中“⼏乎”所有的对象都会在堆中分配,堆也是垃圾收集器管理的⽬标区域。
从内存回收的⻆度来看,由于垃圾收集器⼤部分都是基于分代收集理论设计的,所以堆⼜被细分为 新⽣代 、 ⽼年
代 、 Eden空间 、 From Survivor空间 、 To Survivor空间 等。
随着 JIT 编译器的发展和逃逸技术的逐渐成熟,“所有的对象都会分配到堆上”就不再那么绝对了。
从 JDK 7 开始,JVM 默认开启了逃逸分析,意味着如果某些⽅法中的对象引⽤没有被返回或者没有在⽅法体外使
⽤,也就是未逃逸出去,那么对象可以直接在栈上分配内存。
堆和栈的区别是什么?
堆属于线程共享的内存区域,⼏乎所有 new 出来的对象都会堆上分配,⽣命周期不由单个⽅法调⽤所决定,可以
在⽅法调⽤结束后继续存在,直到不再被任何变量引⽤,最后被垃圾收集器回收。
栈属于线程私有的内存区域,主要存储局部变量、⽅法参数、对象引⽤等,通常随着⽅法调⽤的结束⽽⾃动释放,
不需要垃圾收集器处理。
介绍⼀下⽅法区?
⽅法区并不真实存在,属于 Java 虚拟机规范中的⼀个逻辑概念,⽤于存储已被 JVM 加载的类信息、常量、静态变
量、即时编译器编译后的代码缓存等。
在 HotSpot 虚拟机中,⽅法区的实现称为永久代 PermGen,但在 Java 8 及之后的版本中,已经被元空间
Metaspace 所替代。
变量存在堆栈的什么位置?
对于局部变量,它存储在当前⽅法栈帧中的局部变量表中。当⽅法执⾏完毕,栈帧被回收,局部变量也会被释放。
public void method() {
int localVar = 100; // 局部变量,存储在栈帧中的局部变量表⾥
}
No. 14 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
对于静态变量来说,它存储在 Java 虚拟机规范中的⽅法区中,在 Java 7 中是永久带,在 Java8 及以后 是元空间。
public class StaticVarDemo {
public static int staticVar = 100; // 静态变量,存储在⽅法区中
}
1. Java ⾯试指南(付费)收录的京东同学 10 后端实习⼀⾯的原题:堆和栈的区别是什么
2. Java ⾯试指南(付费)收录的⽐亚迪⾯经同学 3 Java 技术⼀⾯⾯试原题:介绍⼀下 JVM 运⾏时数据区
3. Java ⾯试指南(付费)收录的字节跳动⾯经同学 1 Java 后端技术⼀⾯⾯试原题:讲⼀下 JVM 内存结
构?
4. Java ⾯试指南(付费)收录的京东⾯经同学 1 Java 技术⼀⾯⾯试原题:说说 JVM 运⾏时数据区
5. Java ⾯试指南(付费)收录的美团⾯经同学 2 Java 后端技术⼀⾯⾯试原题:JVM 内存结构了解吗?
6. Java ⾯试指南(付费)收录的快⼿⾯经同学 1 部⻔主站技术部⾯试原题:请说⼀下 Java 的内存区域,
程序计数器等?
7. Java ⾯试指南(付费)收录的字节跳动⾯经同学 8 Java 后端实习⼀⾯⾯试原题:jvm 内存分布,有垃
圾回收的是哪些地⽅
8. Java ⾯试指南(付费)收录的得物⾯经同学 8 ⼀⾯⾯试原题:说⼀说 jvm 内存区域
9. Java ⾯试指南(付费)收录的美团⾯经同学 3 Java 后端技术⼀⾯⾯试原题:jmm 内存模型 栈 ⽅法区存
放的是什么
10. Java ⾯试指南(付费)收录的收钱吧⾯经同学 1 Java 后端⼀⾯⾯试原题:你提到了栈帧,那局部变量
表除了栈帧还有什么?⼀个什么都没有的空⽅法,完全空的参数什么都没有,那局部变量表⾥有没有变
量?
11. Java ⾯试指南(付费)收录的招银⽹络科技⾯经同学 9 Java 后端技术⼀⾯⾯试原题:Java堆内存和栈内
存的区别
12. Java ⾯试指南(付费)收录的 OPPO ⾯经同学 1 ⾯试原题:说⼀下JVM内存模型
13. Java ⾯试指南(付费)收录的深信服⾯经同学 3 Java 后端线下⼀⾯⾯试原题:JVM变量存在堆栈的位
置?
14. Java ⾯试指南(付费)收录的TP联洲同学 5 Java 后端⼀⾯的原题:Jvm内存区域,本地⽅法栈的运⾏场
景,Native⽅法解释⼀下
15. Java ⾯试指南(付费)收录的字节跳动同学 17 后端技术⾯试原题:jvm结构 运⾏时数据区有什么结构
堆存什么
16. Java ⾯试指南(付费)收录的腾讯⾯经同学 29 Java 后端⼀⾯原题:new⼀个对象存放在哪⾥?(运⾏
时数据区),局部变量存在JVM哪⾥
4.说⼀下 JDK 1.6、1.7、1.8 内存区域的变化?
JDK 1.6 使⽤永久代来实现⽅法区:
No. 15 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
JDK 1.7 时仍然是永久带,但发⽣了⼀些细微变化,⽐如将字符串常量池、静态变量存放到了堆上。
在 JDK 1.8 时,直接在内存中划出了⼀块区域,叫元空间,来取代之前放在 JVM 内存中的永久代,并将运⾏时常量
池、类常量池都移动到了元空间。
No. 16 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
5.为什么使⽤元空间替代永久代?
客观上,永久代会导致 Java 应⽤程序更容易出现内存溢出的问题,因为它要受到 JVM 内存⼤⼩的限制。
HotSpot 虚拟机的永久代⼤⼩可以通过 -XX:MaxPermSize 参数来设置,32 位机器默认的⼤⼩为 64M,64 位的
机器则为 85M。
⽽ J9 和 JRockit 虚拟机就不存在这种限制,只要没有触碰到进程可⽤的内存上限,例如 32 位系统中的 4GB 限制,
就不会出问题。
主观上,当 Oracle 收购 BEA 获得了 JRockit 的所有权后,就准备把 JRockit 中的优秀功能移植到 HotSpot 中。
如 Java Mission Control 管理⼯具。
但因为两个虚拟机对⽅法区实现有差异,导致这项⼯作遇到了很多阻⼒。
考虑到 HotSpot 虚拟机未来的发展,JDK 6 的时候,开发团队就打算放弃永久代了。
JDK 7 的时候,前进了⼀⼩步,把原本放在永久代的字符串常量池、静态变量等移动到了堆中。
JDK 8 就终于完成了这项移出⼯作,这样的好处就是,元空间的⼤⼩不再受到 JVM 内存的限制,⽽是可以像 J9 和
JRockit 那样,只要系统内存⾜够,就可以⼀直⽤。
No. 17 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
6. 对象创建的过程了解吗?
当我们使⽤ new 关键字创建⼀个对象时,JVM ⾸先会检查 new 指令的参数是否能在常量池中定位到类的符号引
⽤,然后检查这个符号引⽤代表的类是否已被加载、解析和初始化。如果没有,就先执⾏类加载。
如果已经加载,JVM 会为对象分配内存完成初始化,⽐如数值类型的成员变量初始值是 0,布尔类型是 false,对
象类型是 null。
接下来会设置对象头,⾥⾯包含了对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。
最后,JVM 会执⾏构造⽅法 <init> 完成赋值操作,将成员变量赋值为预期的值,⽐如 int age = 18 ,这样⼀
个对象就创建完成了。
对象的销毁过程了解吗?
当对象不再被任何引⽤指向时,就会变成垃圾。垃圾收集器会通过可达性分析算法判断对象是否存活,如果对象不
可达,就会被回收。
垃圾收集器通过标记清除、标记复制、标记整理等算法来回收内存,将对象占⽤的内存空间释放出来。
可以通过 java -XX:+PrintCommandLineFlags -version 和 java -XX:+PrintGCDetails -version 命令查
看 JVM 的 GC 收集器。
No. 18 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
可以看到,我本机安装的 JDK 8 默认使⽤的是 Parallel Scavenge + Parallel Old 。
不同参数代表对应的垃圾收集器表单:
新⽣代 ⽼年代 JVM参数
Serial Serial -XX:+UseSerialGC
Parallel Scavenge Serial -XX:+UseParallelGC -XX:-UseParallelOldGC
Parallel Scavenge Parallel Old -XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel New CMS -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
G1 -XX:+UseG1GC
1. Java ⾯试指南(付费)收录的⽐亚迪⾯经同学 3 Java 技术⼀⾯⾯试原题:对象创建到销毁的流程
2. Java ⾯试指南(付费)收录的美团⾯经同学 2 Java 后端技术⼀⾯⾯试原题:说说创建对象的流程?
3. Java ⾯试指南(付费)收录的携程⾯经同学 1 Java 后端技术⼀⾯⾯试原题:对象创建到销毁,内存如
何分配的,(类加载和对象创建过程,CMS,G1 内存清理和分配)
7.堆内存是如何分配的?
在堆中为对象分配内存时,主要使⽤两种策略:指针碰撞和空闲列表。
No. 19 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
指针碰撞适⽤于管理简单、碎⽚化较少的内存区域,如年轻代;⽽空闲列表适⽤于内存碎⽚化较严重或对象⼤⼩差
异较⼤的场景如⽼年代。
什么是指针碰撞?
假设堆内存是⼀个连续的空间,分为两个部分,⼀部分是已经被使⽤的内存,另⼀部分是未被使⽤的内存。
在分配内存时,Java 虚拟机会维护⼀个指针,指向下⼀个可⽤的内存地址,每次分配内存时,只需要将指针向后移
动⼀段距离,如果没有发⽣碰撞,就将这段内存分配给对象实例。
什么是空闲列表?
JVM 维护⼀个列表,记录堆中所有未占⽤的内存块,每个内存块都记录有⼤⼩和地址信息。
当有新的对象请求内存时,JVM 会遍历空闲列表,寻找⾜够⼤的空间来存放新对象。
分配后,如果选中的内存块未被完全利⽤,剩余的部分会作为⼀个新的内存块加⼊到空闲列表中。
1. Java ⾯试指南(付费)收录的携程⾯经同学 1 Java 后端技术⼀⾯⾯试原题:对象创建到销毁,内存如
何分配的,(类加载和对象创建过程,CMS,G1 内存清理和分配)
No. 20 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
memo:2025 年 1 ⽉ 10 ⽇修改到此
8.new 对象时,堆会发⽣抢占吗?
会。
new 对象时,指针会向右移动⼀个对象⼤⼩的距离,假如⼀个线程 A 正在给字符串对象 s 分配内存,另外⼀个线
程 B 同时为 ArrayList 对象 l 分配内存,两个线程就发⽣了抢占。
JVM 怎么解决堆内存分配的竞争问题?
为了解决堆内存分配的竞争问题,JVM 为每个线程保留了⼀⼩块内存空间,被称为 TLAB,也就是线程本地分配缓
冲区,⽤于存放该线程分配的对象。
No. 21 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
当线程需要分配对象时,直接从 TLAB 中分配。只有当 TLAB ⽤尽或对象太⼤需要直接在堆中分配时,才会使⽤全
局分配指针。
这⾥简单测试⼀下 TLAB。
可以通过 java -XX:+PrintFlagsFinal -version | grep TLAB 命令查看当前 JVM 是否开启了 TLAB。
如果开启了 TLAB,会看到类似以下的输出,其中 bool UseTLAB 的值为 true。
我们编写⼀个简单的测试类,创建⼤量对象并强制触发垃圾回收,查看 TLAB 的使⽤情况。
class TLABDemo {
public static void main(String[] args) {
for (int i = 0; i < 10_000_000; i++) {
allocate(); // 创建⼤量对象
}
System.gc(); // 强制触发垃圾回收
}
private static void allocate() {
// ⼩对象分配,通常会使⽤ TLAB
byte[] bytes = new byte[64];
}
}
在 VM 参数中添加 -XX:+UseTLAB -XX:+PrintTLAB -XX:+PrintGCDetails -XX:+PrintGCDateStamps ,运⾏
后可以看到这样的内容:
No. 22 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
waste:未使⽤的 TLAB 空间。
alloc:分配到 TLAB 的空间。
refills:TLAB 被重新填充的次数。
可以看到,当前线程的 TLAB ⽬标⼤⼩为 10,496 KB( desired_size: 10496KB );未发⽣慢分配( slow
allocs: 0 );分配效率直接拉满( alloc: 1.00000 52494KB )。
当使⽤ -XX:-UseTLAB -XX:+PrintGCDetails 关闭 TLAB 时,会看到类似以下的输出:
直接出现了两次 GC,因为没有 TLAB,Eden 区更快被填满,导致年轻代 GC。年轻代 GC 频繁触发,⼀部分⻓⽣
命周期对象被晋升到⽼年代,间接导致⽼年代 GC 触发。
9.能说⼀下对象的内存布局吗?
好的。
对象的内存布局是由 Java 虚拟机规范定义的,但具体的实现细节各有不同,如 HotSpot 和 OpenJ9 就不⼀样。
就拿我们常⽤的 HotSpot 来说吧。
对象在内存中包括三部分:对象头、实例数据和对⻬填充。
No. 23 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
说说对象头的作⽤?
对象头是对象存储在内存中的元信息,包含了Mark Word、类型指针等信息。
Mark Word 存储了对象的运⾏时状态信息,包括锁、哈希值、GC 标记等。在 64 位操作系统下占 8 个字节,32
位操作系统下占 4 个字节。
类型指针指向对象所属类的元数据,也就是 Class 对象,⽤来⽀持多态、⽅法调⽤等功能。
除此之外,如果对象是数组类型,还会有⼀个额外的数组⻓度字段。占 4 个字节。
类型指针会被压缩吗?
类型指针可能会被压缩,以节省内存空间。⽐如说在开启压缩指针的情况下占 4 个字节,否则占 8 个字节。在 JDK
8 中,压缩指针默认是开启的。
可以通过 java -XX:+PrintFlagsFinal -version | grep UseCompressedOops 命令来查看 JVM 是否开启了压
缩指针。
No. 24 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
如果压缩指针开启,输出结果中的 bool UseCompressedOops 值为 true。
实例数据了解吗?
了解⼀些。
实例数据是对象实际的字段值,也就是成员变量的值,按照字段在类中声明的顺序存储。
class ObjectDemo {
int age;
String name;
}
JVM 会对这些数据进⾏对⻬/重排,以提⾼内存访问速度。
对⻬填充了解吗?
由于 JVM 的内存模型要求对象的起始地址是 8 字节对⻬(64 位 JVM 中),因此对象的总⼤⼩必须是 8 字节的倍
数。
如果对象头和实例数据的总⻓度不是 8 的倍数,JVM 会通过填充额外的字节来对⻬。
⽐如说,如果对象头 + 实例数据 = 14 字节,则需要填充 2 个字节,使总⻓度变为 16 字节。
为什么⾮要进⾏ 8 字节对⻬呢?
因为 CPU 进⾏内存访问时,⼀次寻址的指针⼤⼩是 8 字节,正好是 L1 缓存⾏的⼤⼩。如果不进⾏内存对⻬,则可
能出现跨缓存⾏访问,导致额外的缓存⾏加载,CPU 的访问效率就会降低。
⽐如说上图中 obj1 占 6 个字节,由于没有对⻬,导致这⼀⾏缓存中多了 2 个字节 obj2 的数据,当 CPU 访问 obj2
的时候,就会导致缓存⾏刷新。
也就说,8 字节对⻬,是为了效率的提⾼,以空间换时间的⼀种⽅案。
No. 25 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
new Object() 对象的内存⼤⼩是多少?
推荐阅读:⾼端⾯试必备:⼀个 Java 对象占⽤多⼤内存
⼀般来说,⽬前的操作系统都是 64 位的,并且 JDK 8 中的压缩指针是默认开启的,因此在 64 位的 JVM 上, new
Object() 的⼤⼩是 16 字节(12 字节的对象头 + 4 字节的对⻬填充)。
对象头的⼤⼩是固定的,在 32 位 JVM 上是 8 字节,在 64 位 JVM 上是 16 字节;如果开启了压缩指针,就是 12
字节。
实例数据的⼤⼩取决于对象的成员变量和它们的类型。对于 new Object() 来说,由于默认没有成员变量,因此我
们可以认为此时的实例数据⼤⼩是 0。
No. 26 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
假如 MyObject 对象有三个成员变量,分别是 int、long 和 byte 类型,那么它们占⽤的内存⼤⼩分别是 4 字节、8
字节和 1 字节。
class MyObject {
int a; // 4 字节
long b; // 8 字节
byte c; // 1 字节
}
考虑到对⻬填充,MyObject 对象的总⼤⼩为 12(对象头) + 4(a) + 8(b) + 1(c) + 7(填充) = 32 字节。
⽤过 JOL 查看对象的内存布局吗?
⽤过。
JOL 是⼀款分析 JVM 对象布局的⼯具。
第⼀步,在 pom.xml 中引⼊ JOL 依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
第⼆步,使⽤ JOL 编写代码示例:
public class JOLSample {
public static void main(String[] args) {
// 打印JVM详细信息(可选)
System.out.println(VM.current().details());
// 创建Object实例
Object obj = new Object();
// 打印Object实例的内存布局
String layout = ClassLayout.parseInstance(obj).toPrintable();
System.out.println(layout);
}
}
第三步,运⾏代码,查看输出结果:
No. 27 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
可以看到有 OFFSET、SIZE、TYPE DESCRIPTION、VALUE 这⼏个信息。
OFFSET:偏移地址,单位字节;
SIZE:占⽤的内存⼤⼩,单位字节;
TYPE DESCRIPTION:类型描述,其中 object header 为对象头;
VALUE:对应内存中当前存储的值,⼆进制 32 位;
从上⾯的结果能看到,对象头是 12 个字节,还有 4 个字节的 padding, new Object() ⼀共 16 个字节。
对象的引⽤⼤⼩了解吗?
推荐阅读:Object o = new Object()占多少个字节?
在 64 位 JVM 上,未开启压缩指针时,对象引⽤占⽤ 8 字节;开启压缩指针时,对象引⽤会被压缩到 4 字节。
HotSpot 虚拟机默认是开启压缩指针的。
No. 28 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
我们来验证⼀下:
class ReferenceSizeExample {
private static class ReferenceHolder {
Object reference;
}
public static void main(String[] args) {
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseClass(ReferenceHolder.class).toPrintable());
}
}
运⾏代码,查看输出结果:
No. 29 / 95
⾯渣逆袭 JVM篇第⼆版-让天下所有的⾯渣都能逆袭
ReferenceHolder.reference 的⼤⼩为 4 字节。
1. Java ⾯试指南(付费)收录的帆软同学 3 Java 后端⼀⾯的原题:Object a = new object()的⼤⼩,对象
引⽤占多少⼤⼩?
2. Java ⾯试指南(付费)收录的去哪⼉⾯经同学 1 技术⼆⾯⾯试原题:Object 底层的数据结构(蒙了)
memo:2025 年 1 ⽉ 11 ⽇修改到此
10.JVM 怎么访问对象的?
主流的⽅式有两种:句柄和直接指针。
两种⽅式的区别在于,句柄是通过⼀个中间的句柄表来定位对象的,⽽直接指针则是通过引⽤直接指向对象的内存
地址。
优点是,对象被移动时只需要修改句柄表中的指针,⽽不需要修改对象引⽤本身。
No. 30 / 95