---
title: JVM面试题,54道Java虚拟机八股文(2.3万字115张手绘图),面渣逆袭必看👍
shortTitle: 面渣逆袭-JVM
author: 三分恶
date: 2025-01-10
category:
- 面渣逆袭
tag:
- 面渣逆袭
description: 下载次数超 1 万次,2.3 万字 115 张手绘图,详解 54 道 Java 虚拟机面试高频题(让天下没有难背的八股),面渣背会这些 JVM 八股文,这次吊打面试官,我觉得稳了(手动 dog)。
head:
- - meta
- name: keywords
content: Java,Java虚拟机,JVM,Java面试题,JVM面试题,java虚拟机面试题,八股文,java
---

## 前言
2.3 万字 115 张手绘图,详解 54 道 Java 虚拟机面试高频题(让天下没有难背的八股),面渣背会这些 JVM 八股文,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/bHhqhl8mH3OAPt3EkaVc8Q),作者:三分恶,戳[原文链接](https://mp.weixin.qq.com/s/XYsEJyIo46jXhHE1sOR_0Q)。
亮白版本更适合拿出来打印,这也是很多学生党喜欢的方式,打印出来背诵的效率会更高。

2024 年 12 月 30 日开始着手第二版更新。
- 对于高频题,会标注在《[Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)》中出现的位置,哪家公司,原题是什么,并且会加🌟,目录一目了然;如果你想节省时间的话,可以优先背诵这些题目,尽快做到知彼知己,百战不殆。
- 区分八股精华回答版本和原理底层解释,让大家知其然知其所以然,同时又能做到面试时的高效回答。
- 结合项目([技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)、[pmhub](https://javabetter.cn/zhishixingqiu/pmhub.html))来组织语言,让面试官最大程度感受到你的诚意,而不是机械化的背诵。
- 修复第一版中出现的问题,包括球友们的私信反馈,网站留言区的评论,以及 [GitHub 仓库](https://github.com/itwanger/toBeBetterJavaer/issues)中的 issue,让这份面试指南更加完善。
- 增加[二哥编程星球](https://javabetter.cn/zhishixingqiu/)的球友们拿到的一些 offer,对面渣逆袭的感谢,以及对简历修改的一些认可,以此来激励大家,给大家更多信心。
- 优化排版,增加手绘图,重新组织答案,使其更加口语化,从而更贴近面试官的预期。

由于 PDF 没办法自我更新,所以需要最新版的小伙伴,可以微信搜【**沉默王二**】,或者扫描/长按识别下面的二维码,关注二哥的公众号,回复【**222**】即可拉取最新版本。
当然了,请允许我的一点点私心,那就是星球的 PDF 版本会比公众号早一个月时间,毕竟星球用户都付费过了,我有必要让他们先享受到一点点福利。相信大家也都能理解,毕竟在线版是免费的,CDN、服务器、域名、OSS 等等都是需要成本的。
更别说我付出的时间和精力了,大家觉得有帮助还请给个口碑,让你身边的同事、同学都能受益到。

我把二哥的 Java 进阶之路、JVM 进阶之路、并发编程进阶之路,以及所有面渣逆袭的版本都放进来了,涵盖 Java基础、Java集合、Java并发、JVM、Spring、MyBatis、计算机网络、操作系统、MySQL、Redis、RocketMQ、分布式、微服务、设计模式、Linux 等 16 个大的主题,共有 40 多万字,2000+张手绘图,可以说是诚意满满。
展示一下暗黑版本的 PDF 吧,排版清晰,字体优雅,更加适合夜服,晚上看会更舒服一点。

## 一、引言
### 1.什么是 JVM?
JVM,也就是 Java 虚拟机,它是 Java 实现跨平台的基石。
程序运行之前,需要先通过编译器将 Java 源代码文件编译成 Java 字节码文件;
程序运行时,JVM 会对字节码文件进行逐行解释,翻译成机器码指令,并交给对应的操作系统去执行。

这样就实现了 Java 一次编译,处处运行的特性。
#### 如果我们要执行hello world,那虚拟机干了什么呢?谁把字节码翻译成机器码,操作时机是什么?Java虚拟机是一个执行单元吗?
当我们写好一个 HelloWorld 程序,编译成 .class 文件后,执行 `java HelloWorld` 这条命令时,操作系统会创建一个 JVM 进程,JVM 进程启动起来后,会先初始化运行环境,包括创建类加载器、初始化内存空间、启动垃圾回收线程等等。
接下来,JVM 的类加载器会去找 HelloWorld.class 这个文件,读取它的字节码内容。
JVM 的执行引擎会将这些字节码翻译成机器码,有两种方式:
- 第一种是解释执行。JVM 的解释器会逐条读取字节码指令,然后把它翻译成机器码执行。这个过程是动态的,每次执行都要翻译一遍。所以解释执行的速度相对较慢。
- 第二种是即时编译。JVM 里有一个 JIT 编译器,他发现某段代码被频繁执行(比如循环里的代码或者热点方法)时,JIT 编译器就会把这段字节码直接编译成本地机器码并缓存到 codeCache 中,下次执行的时候不用再一行一行的解释,而是直接执行缓存后的机器码,执行效率会大幅提高。
至于执行的时机,解释器的执行是立即的,当 JVM 启动后,就开始解释执行字节码了。JIT 编译器的执行时机是延后的,它需要先监控哪些代码是热点代码(通常需要执行数千次或者数万次),然后才会启动编译。
操作系统层面,JVM 进程是一个执行单元,由操作系统调度执行。
#### 说说 JVM 的其他特性?
①、JVM 可以自动管理内存,通过垃圾回收器回收不再使用的对象并释放内存空间。
②、JVM 包含一个即时编译器 JIT,它可以在运行时将热点代码缓存到 codeCache 中,下次执行的时候不用再一行一行的解释,而是直接执行缓存后的机器码,执行效率会大幅提高。

③、任何可以通过 Java 编译的语言,比如说 Groovy、Kotlin、Scala 等,都可以在 JVM 上运行。

#### 为什么要学习 JVM?
学习 JVM 可以帮助我们开发者更好地优化程序性能、避免内存问题。
比如说了解 JVM 的内存模型和垃圾回收机制,可以帮助我们更合理地配置内存、减少 GC 停顿。
比如说掌握 JVM 的类加载机制可以帮助我们排查类加载冲突或异常。
再比如说,JVM 还提供了很多调试和监控工具,可以帮助我们分析内存和线程的使用情况,从而解决内存溢出内存泄露等问题。
> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东同学 10 后端实习一面的原题:有了解 JVM 吗
> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动同学 20 测开一面的原题:了解过 JVM 么?讲一下 JVM 的特性
memo:2025 年 11 月 6 日修改至此,[今天有球友](https://javabetter.cn/zhishixingqiu/)提问说拿到了京东测开的 offer,60 万总包;还有华为的 14a offer,真的恭喜他,太强了呀。

### 2.说说 JVM 的组织架构(补充)
> 增补于 2024 年 03 月 08 日。
推荐阅读:[大白话带你认识 JVM](https://javabetter.cn/jvm/what-is-jvm.html)
JVM 大致可以划分为三个部分:类加载器、运行时数据区和执行引擎。

① 类加载器,负责从文件系统、网络或其他来源加载 Class 文件,将 Class 文件中的二进制数据读入到内存当中。
② 运行时数据区,JVM 在执行 Java 程序时,需要在内存中分配空间来处理各种数据,这些内存区域按照 Java 虚拟机规范可以划分为方法区、堆、虚拟机栈、程序计数器和本地方法栈。
③ 执行引擎,也是 JVM 的心脏,负责执行字节码。它包括一个虚拟处理器、即时编译器 JIT 和垃圾回收器。
#### JVM 是个什么东西?
JVM 本质上就是一个进程。当我们执行 `java -jar application.jar` 命令时,操作系统会创建一个名为 JVM 的进程。这个进程在内存里运行着一个虚拟机,这个虚拟机有自己的指令集、内存模型、执行引擎等等。
#### Java虚拟机和操作系统的关系到底什么,假如我是个完全不懂技术的人,举例说明让我明白
想象操作系统是一个大剧院。这个剧院有舞台、灯光、音响、观众座位等各种资源。剧院的管理员负责分配这些资源,决定谁在什么时候用哪个舞台。
现在,一个话剧团想在这个剧院里演一场就在河南。话剧团就向剧院管理员说:"我需要租用你的舞台、灯光、音响,从下午 2 点到 5 点。"剧院管理员就给他们分配了一个舞台和相关资源。

操作系统是底层,它直接控制硬件资源,比如 CPU、内存、磁盘、网络等。操作系统的工作就是把这些硬件资源分配给各个应用程序使用。
JVM 是运行在操作系统上的一个应用程序。JVM 本身是一个进程,也就是说,从操作系统的角度看,JVM 就是一个普通的应用程序,并没有什么特别的。操作系统不知道也不关心 JVM 里面运行的是什么,它只是按照进程管理的规则来管理 JVM 这个进程。
### 一个操作系统有两个Java程序的话,有几个虚拟机?有没有单独的JVM进程存在?启动一个hello world编译的时候,有几个进程
1、每一个 Java 程序都需要一个独立的 JVM 进程来运行,两个 Java 程序就需要两个 JVM。这两个 JVM 进程是完全独立的,互不干扰。
2、JVM 不存在什么公共的、被所有 Java 程序共享的进程。每一个 Java 程序都必须启动自己的 JVM 进程。
3、如果只是编译,不运行,至少有两个进程。一个是 javac 的编译器进程。当执行 `javac HelloWorld.java` 时,操作系统会创建一个 javac 进程,这个进程会读取 .java 源文件,生成 .class 字节码文件。完成后 javac 进程就退出了。另一个是当前的 shell 进程,运行 javac 进程的终端,这是一直存在的。
#### JVM什么时候启动 比如执行一条Java命令的时候对应一个进程,然后这个JVM虚拟机到底是不是在这个进程里面,还是说要先启动一个JVM虚拟机的进程
当执行 `java HelloWorld` 这条命令的时候,操作系统就会创建 JVM 进程。没有"先启动 JVM"然后"再运行程序"这种操作。而是一步到位:操作系统启动了 JVM 进程,JVM 进程同时运行 Java 程序。
> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯 Java 后端实习一面原题:说说 JVM 的组织架构
> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 9 面试题目原题:JVM的架构,具体阐述一下各个部分的功能?
memo:2025 年 11 月 04 日修改至此,[今天有球友](https://javabetter.cn/zhishixingqiu/)私信说美团开奖了,白菜,但今年美团整体开的都是这个价,24k 左右,去年其实也差不多这个价,好在美团业务稳定,整体氛围还可以。

## 二、内存管理
### 3.🌟能说一下 JVM 的内存区域吗?
推荐阅读:[深入理解 JVM 的运行时数据区](https://javabetter.cn/jvm/neicun-jiegou.html)
按照 Java 虚拟机规范,JVM 的内存区域可以细分为`程序计数器`、`虚拟机栈`、`本地方法栈`、`堆`和`方法区`。

其中`方法区`和`堆`是线程共享的,`虚拟机栈`、`本地方法栈`和`程序计数器`是线程私有的。
#### 介绍一下程序计数器?
程序计数器也被称为 PC 寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码行号指示器。
#### 介绍一下 Java 虚拟机栈?
Java 虚拟机栈的生命周期与线程相同。
当线程执行一个方法时,会创建一个对应的[栈帧](https://javabetter.cn/jvm/stack-frame.html),用于存储局部变量表、操作数栈、动态链接、方法出口等信息,然后栈帧会被压入虚拟机栈中。当方法执行完毕后,栈帧会从虚拟机栈中移除。

#### 一个什么都没有的空方法,空的参数都没有,那局部变量表里有没有变量?
对于[静态方法](https://javabetter.cn/oo/static.html),由于不需要访问实例对象 this,因此在局部变量表中不会有任何变量。
对于非静态方法,即使是一个完全空的方法,局部变量表中也会有一个用于存储 this 引用的变量。this 引用指向当前实例对象,在方法调用时被隐式传入。
详细解释一下:
比如说有这样一段代码:
```java
public class VarDemo1 {
public void emptyMethod() {
// 什么都没有
}
public static void staticEmptyMethod() {
// 什么都没有
}
}
```
用 `javap -v VarDemo1` 命令查看编译后的字节码,就可以在 emptyMethod 中看到这样的内容:

这里的 `locals=1` 表示局部变量表有一个变量,即 this,Slot 0 位置存储了 this 引用。
而在静态方法 staticEmptyMethod 中,你会看到这样的内容:

这里的 locals=0 表示局部变量表为空,因为静态方法属于类级别方法,不需要 this 引用,也就没有局部变量。
#### 介绍一下本地方法栈?
本地方法栈与虚拟机栈相似,区别在于虚拟机栈是为 JVM 执行 Java 编写的方法服务的,而本地方法栈是为 Java 调用[本地 native 方法](https://javabetter.cn/oo/native-method.html)服务的,通常由 C/C++ 编写。
在本地方法栈中,主要存放了 native 方法的局部变量、动态链接和方法出口等信息。当一个 Java 程序调用一个 native 方法时,JVM 会切换到本地方法栈来执行这个方法。
#### 介绍一下本地方法栈的运行场景?
当 Java 应用需要与操作系统底层或硬件交互时,通常会用到本地方法栈。
比如调用操作系统的特定功能,如内存管理、文件操作、系统时间、系统调用等。
详细说明一下:
比如说获取系统时间的 `System.currentTimeMillis()` 方法就是调用本地方法,来获取操作系统当前时间的。

再比如 JVM 自身的一些底层功能也需要通过本地方法来实现。像 Object 类中的 `hashCode()` 方法、`clone()` 方法等。

#### native 方法解释一下?
推荐阅读:[手把手教你用 C语言实现 Java native 本地方法](https://javabetter.cn/oo/native-method.html)
native 方法是在 Java 中通过 [native 关键字](https://javabetter.cn/basic-extra-meal/48-keywords.html)声明的,用于调用非 Java 语言,如 C/C++ 编写的代码。Java 可以通过 JNI,也就是 Java Native Interface 与底层系统、硬件设备、或者本地库进行交互。
#### 介绍一下 Java 堆?
堆是 JVM 中最大的一块内存区域,被所有线程共享,在 JVM 启动时创建,主要用来存储 new 出来的对象。

Java 中“几乎”所有的对象都会在堆中分配,堆也是[垃圾收集器](https://javabetter.cn/jvm/gc-collector.html)管理的目标区域。
从内存回收的角度来看,由于垃圾收集器大部分都是基于分代收集理论设计的,所以堆又被细分为`新生代`、`老年代`、`Eden空间`、`From Survivor空间`、`To Survivor空间`等。

随着 [JIT 编译器](https://javabetter.cn/jvm/jit.html)的发展和逃逸技术的逐渐成熟,“所有的对象都会分配到堆上”就不再那么绝对了。
从 JDK 7 开始,JVM 默认开启了逃逸分析,意味着如果某些方法中的对象引用没有被返回或者没有在方法体外使用,也就是未逃逸出去,那么对象可以直接在栈上分配内存。
#### 堆和栈的区别是什么?
堆属于线程共享的内存区域,几乎所有 new 出来的对象都会堆上分配,生命周期不由单个方法调用所决定,可以在方法调用结束后继续存在,直到不再被任何变量引用,最后被垃圾收集器回收。
栈属于线程私有的内存区域,主要存储局部变量、方法参数、对象引用等,通常随着方法调用的结束而自动释放,不需要垃圾收集器处理。
#### 介绍一下方法区?
方法区并不真实存在,属于 Java 虚拟机规范中的一个逻辑概念,用于存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。
在 HotSpot 虚拟机中,方法区的实现称为永久代 PermGen,但在 Java 8 及之后的版本中,已经被元空间 Metaspace 所替代。
#### 变量存在堆栈的什么位置?
对于局部变量,它存储在当前方法栈帧中的局部变量表中。当方法执行完毕,栈帧被回收,局部变量也会被释放。
```java
public void method() {
int localVar = 100; // 局部变量,存储在栈帧中的局部变量表里
}
```
对于静态变量来说,它存储在 Java 虚拟机规范中的方法区中,在 Java 7 中是永久带,在 Java8 及以后 是元空间。
```java
public class StaticVarDemo {
public static int staticVar = 100; // 静态变量,存储在方法区中
}
```
> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东同学 10 后端实习一面的原题:堆和栈的区别是什么
> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的比亚迪面经同学 3 Java 技术一面面试原题:介绍一下 JVM 运行时数据区
> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学 1 Java 后端技术一面面试原题:讲一下 JVM 内存结构?
> 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 1 Java 技术一面面试原题:说说 JVM 运行时数据区
> 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 2 Java 后端技术一面面试原题:JVM 内存结构了解吗?
> 6. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手面经同学 1 部门主站技术部面试原题:请说一下 Java 的内存区域,程序计数器等?
> 7. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学 8 Java 后端实习一面面试原题:jvm 内存分布,有垃圾回收的是哪些地方
> 8. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 8 一面面试原题:说一说 jvm 内存区域
> 9. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 3 Java 后端技术一面面试原题:jmm 内存模型 栈 方法区存放的是什么
> 10. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的收钱吧面经同学 1 Java 后端一面面试原题:你提到了栈帧,那局部变量表除了栈帧还有什么?一个什么都没有的空方法,完全空的参数什么都没有,那局部变量表里有没有变量?
> 11. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的招银网络科技面经同学 9 Java 后端技术一面面试原题:Java堆内存和栈内存的区别
> 12. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的 OPPO 面经同学 1 面试原题:说一下JVM内存模型
> 13. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的深信服面经同学 3 Java 后端线下一面面试原题:JVM变量存在堆栈的位置?
> 14. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的TP联洲同学 5 Java 后端一面的原题:Jvm内存区域,本地方法栈的运行场景,Native方法解释一下
> 15. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动同学 17 后端技术面试原题:jvm结构 运行时数据区有什么结构 堆存什么
> 16. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 29 Java 后端一面原题:new一个对象存放在哪里?(运行时数据区),局部变量存在JVM哪里
### 4.说一下 JDK 1.6、1.7、1.8 内存区域的变化?
JDK 1.6 使用永久代来实现方法区:

JDK 1.7 时仍然是永久带,但发生了一些细微变化,比如将字符串常量池、静态变量存放到了堆上。

在 JDK 1.8 时,直接在内存中划出了一块区域,叫**元空间**,来取代之前放在 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 那样,只要系统内存足够,就可以一直用。
### 6.🌟对象创建的过程了解吗?
当我们使用 new 关键字创建一个对象时,JVM 首先会检查 new 指令的参数是否能在常量池中定位到类的符号引用,然后检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,就先执行类加载。

如果已经加载,JVM 会为对象分配内存完成初始化,比如数值类型的成员变量初始值是 0,布尔类型是 false,对象类型是 null。
接下来会设置对象头,里面包含了对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。
最后,JVM 会执行构造方法 `` 完成赋值操作,将成员变量赋值为预期的值,比如 `int age = 18`,这样一个对象就创建完成了。
#### 对象的销毁过程了解吗?
当对象不再被任何引用指向时,就会变成垃圾。垃圾收集器会通过可达性分析算法判断对象是否存活,如果对象不可达,就会被回收。
垃圾收集器通过标记清除、标记复制、标记整理等算法来回收内存,将对象占用的内存空间释放出来。
可以通过 `java -XX:+PrintCommandLineFlags -version` 和 `java -XX:+PrintGCDetails -version` 命令查看 JVM 的 GC 收集器。

可以看到,我本机安装的 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 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的比亚迪面经同学 3 Java 技术一面面试原题:对象创建到销毁的流程
> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 2 Java 后端技术一面面试原题:说说创建对象的流程?
> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的携程面经同学 1 Java 后端技术一面面试原题:对象创建到销毁,内存如何分配的,(类加载和对象创建过程,CMS,G1 内存清理和分配)
memo:2025 年 8 月 13 日修改到此,[就像球友说的](https://javabetter.cn/zhishixingqiu/),帮大家拿下暑期实习,也会帮大家拿下秋招。

### 7.堆内存是如何分配的?
在堆中为对象分配内存时,主要使用两种策略:指针碰撞和空闲列表。

指针碰撞适用于管理简单、碎片化较少的内存区域,如年轻代;而空闲列表适用于内存碎片化较严重或对象大小差异较大的场景如老年代。
#### 什么是指针碰撞?
假设堆内存是一个连续的空间,分为两个部分,一部分是已经被使用的内存,另一部分是未被使用的内存。
在分配内存时,Java 虚拟机会维护一个指针,指向下一个可用的内存地址,每次分配内存时,只需要将指针向后移动一段距离,如果没有发生碰撞,就将这段内存分配给对象实例。
#### 什么是空闲列表?
JVM 维护一个列表,记录堆中所有未占用的内存块,每个内存块都记录有大小和地址信息。
当有新的对象请求内存时,JVM 会遍历空闲列表,寻找足够大的空间来存放新对象。
分配后,如果选中的内存块未被完全利用,剩余的部分会作为一个新的内存块加入到空闲列表中。
> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的携程面经同学 1 Java 后端技术一面面试原题:对象创建到销毁,内存如何分配的,(类加载和对象创建过程,CMS,G1 内存清理和分配)
memo:2025 年 1 月 10 日修改到此
### 8.new 对象时,堆会发生抢占吗?
会。

new 对象时,指针会向右移动一个对象大小的距离,假如一个线程 A 正在给字符串对象 s 分配内存,另外一个线程 B 同时为 ArrayList 对象 l 分配内存,两个线程就发生了抢占。
#### JVM 怎么解决堆内存分配的竞争问题?
为了解决堆内存分配的竞争问题,JVM 为每个线程保留了一小块内存空间,被称为 TLAB,也就是线程本地分配缓冲区,用于存放该线程分配的对象。

当线程需要分配对象时,直接从 TLAB 中分配。只有当 TLAB 用尽或对象太大需要直接在堆中分配时,才会使用全局分配指针。
这里简单测试一下 TLAB。
可以通过 `java -XX:+PrintFlagsFinal -version | grep TLAB` 命令查看当前 JVM 是否开启了 TLAB。

如果开启了 TLAB,会看到类似以下的输出,其中 bool UseTLAB 的值为 true。
我们编写一个简单的测试类,创建大量对象并强制触发垃圾回收,查看 TLAB 的使用情况。
```java
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`,运行后可以看到这样的内容:

- 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 来说吧。对象在内存中包括三部分:对象头、实例数据和对齐填充。

#### 说说对象头的作用?
对象头是对象存储在内存中的元信息,包含了Mark Word、类型指针等信息。
Mark Word 存储了对象的运行时状态信息,包括锁、哈希值、GC 标记等。在 64 位操作系统下占 8 个字节,32 位操作系统下占 4 个字节。
类型指针指向对象所属类的元数据,也就是 Class 对象,用来支持多态、方法调用等功能。
除此之外,如果对象是数组类型,还会有一个额外的数组长度字段。占 4 个字节。
#### 类型指针会被压缩吗?
类型指针可能会被压缩,以节省内存空间。比如说在开启压缩指针的情况下占 4 个字节,否则占 8 个字节。在 JDK 8 中,压缩指针默认是开启的。
可以通过 `java -XX:+PrintFlagsFinal -version | grep UseCompressedOops` 命令来查看 JVM 是否开启了压缩指针。

如果压缩指针开启,输出结果中的 bool UseCompressedOops 值为 true。
#### 实例数据了解吗?
了解一些。
实例数据是对象实际的字段值,也就是成员变量的值,按照字段在类中声明的顺序存储。
```java
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 字节对齐,是为了效率的提高,以空间换时间的一种方案。

#### new Object() 对象的内存大小是多少?
推荐阅读:[高端面试必备:一个 Java 对象占用多大内存 ](https://www.cnblogs.com/rickiyang/p/14206724.html)
一般来说,目前的操作系统都是 64 位的,并且 JDK 8 中的压缩指针是默认开启的,因此在 64 位的 JVM 上,`new Object()`的大小是 16 字节(12 字节的对象头 + 4 字节的对齐填充)。

对象头的大小是固定的,在 32 位 JVM 上是 8 字节,在 64 位 JVM 上是 16 字节;如果开启了压缩指针,就是 12 字节。
实例数据的大小取决于对象的成员变量和它们的类型。对于`new Object()`来说,由于默认没有成员变量,因此我们可以认为此时的实例数据大小是 0。
假如 MyObject 对象有三个成员变量,分别是 int、long 和 byte 类型,那么它们占用的内存大小分别是 4 字节、8 字节和 1 字节。
```java
class MyObject {
int a; // 4 字节
long b; // 8 字节
byte c; // 1 字节
}
```
考虑到对齐填充,MyObject 对象的总大小为 12(对象头) + 4(a) + 8(b) + 1(c) + 7(填充) = 32 字节。
#### 用过 JOL 查看对象的内存布局吗?
用过。
[JOL](https://openjdk.org/projects/code-tools/jol/) 是一款分析 JVM 对象布局的工具。
第一步,在 pom.xml 中引入 JOL 依赖:
```xml
org.openjdk.jol
jol-core
0.9
```
第二步,使用 JOL 编写代码示例:
```java
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);
}
}
```
第三步,运行代码,查看输出结果:

可以看到有 OFFSET、SIZE、TYPE DESCRIPTION、VALUE 这几个信息。
- OFFSET:偏移地址,单位字节;
- SIZE:占用的内存大小,单位字节;
- TYPE DESCRIPTION:类型描述,其中 object header 为对象头;
- VALUE:对应内存中当前存储的值,二进制 32 位;
从上面的结果能看到,对象头是 12 个字节,还有 4 个字节的 padding,`new Object()` 一共 16 个字节。
#### 对象的引用大小了解吗?
推荐阅读:[Object o = new Object()占多少个字节?](https://www.cnblogs.com/dijia478/p/14677243.html)
在 64 位 JVM 上,未开启压缩指针时,对象引用占用 8 字节;开启压缩指针时,对象引用会被压缩到 4 字节。HotSpot 虚拟机默认是开启压缩指针的。

我们来验证一下:
```java
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());
}
}
```
运行代码,查看输出结果:

ReferenceHolder.reference 的大小为 4 字节。
> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的帆软同学 3 Java 后端一面的原题:Object a = new object()的大小,对象引用占多少大小?
> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的去哪儿面经同学 1 技术二面面试原题:Object 底层的数据结构(蒙了)
> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东同学 19 JDS 后端一面的原题:一个对象的结构是什么,包含哪些内容
memo:2025 年 8 月 14 日修改到此。[今天收到球友的反馈说](https://javabetter.cn/zhishixingqiu/),暑期拒了腾讯的客户端,暑期实习已经拿到了美团的意向,坐等 A 薪资。😄

### 10.JVM 怎么访问对象的?
主流的方式有两种:句柄和直接指针。
两种方式的区别在于,句柄是通过一个中间的句柄表来定位对象的,而直接指针则是通过引用直接指向对象的内存地址。
优点是,对象被移动时只需要修改句柄表中的指针,而不需要修改对象引用本身。

在直接指针访问中,引用直接存储对象的内存地址;对象的实例数据和类型信息都存储在堆中固定的内存区域。
优点是访问速度更快,因为少了一次句柄的寻址操作。缺点是如果对象在内存中移动,引用需要更新为新的地址。

HotSpot 虚拟机主要使用直接指针来进行对象访问。
### 11.说一下对象有哪几种引用?
四种,分别是强引用、软引用、弱引用和虚引用。

强引用是 Java 中最常见的引用类型。使用 new 关键字赋值的引用就是强引用,只要强引用关联着对象,垃圾收集器就不会回收这部分对象,即使内存不足。
```java
// str 就是一个强引用
String str = new String("沉默王二");
```
软引用于描述一些非必须对象,通过 SoftReference 类实现。软引用的对象在内存不足时会被回收。
```java
// softRef 就是一个软引用
SoftReference softRef = new SoftReference<>(new String("沉默王二"));
```
弱引用用于描述一些短生命周期的非必须对象,如 ThreadLocal 中的 Entry,就是通过 WeakReference 类实现的。弱引用的对象会在下一次垃圾回收时会被回收,不论内存是否充足。
```java
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
//节点类
Entry(ThreadLocal> k, Object v) {
//key赋值
super(k);
//value赋值
value = v;
}
}
```
虚引用主要用来跟踪对象被垃圾回收的过程,通过 PhantomReference 类实现。虚引用的对象在任何时候都可能被回收。
```java
// phantomRef 就是一个虚引用
PhantomReference phantomRef = new PhantomReference<>(new String("沉默王二"), new ReferenceQueue<>());
```
> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东同学 4 云实习面试原题:四个引用(强软弱虚)
### 12.Java 堆的内存分区了解吗?
了解。Java 堆被划分为**新生代**和**老年代**两个区域。

新生代又被划分为 Eden 空间和两个 Survivor 空间(From 和 To)。
新创建的对象会被分配到 Eden 空间。当 Eden 区填满时,会触发一次 Minor GC,清除不再使用的对象。存活下来的对象会从 Eden 区移动到 Survivor 区。
对象在新生代中经历多次 GC 后,如果仍然存活,会被移动到老年代。当老年代内存不足时,会触发 Major GC,对整个堆进行垃圾回收。
> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 8 一面面试原题:Java 中堆内存怎么组织的
> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 27 云后台技术一面面试原题:怎么来区分对象是属于哪个代的?
### 13.说一下新生代的区域划分?
新生代的垃圾收集主要采用标记-复制算法,因为新生代的存活对象比较少,每次复制少量的存活对象效率比较高。
基于这种算法,虚拟机将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾收集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。默认 Eden 和 Survivor 的大小比例是 8∶1。

### 14.🌟对象什么时候会进入老年代?
对象通常会在年轻代中分配,随着时间的推移和垃圾收集的进程,某些满足条件的对象会进入到老年代中,如长期存活的对象。

#### 长期存活的对象如何判断?
JVM 会为对象维护一个“年龄”计数器,记录对象在新生代中经历 Minor GC 的次数。每次 GC 未被回收的对象,其年龄会加 1。
当超过一个特定阈值,默认值是 15,就会被认为老对象了,需要重点关照。这个年龄阈值可以通过 JVM 参数`-XX:MaxTenuringThreshold`来设置。
可以通过 `jinfo -flag MaxTenuringThreshold $(jps | grep -i nacos | awk '{print $1}')` 来查看当前 JVM 的年龄阈值。

1. 如果应用中的对象存活时间较短,可以适当调大这个值,让对象在新生代多待一会儿
2. 如果对象存活时间较长,可以适当调小这个值,让对象更快进入老年代,减少在新生代的复制次数
#### 大对象如何判断?
大对象是指占用内存较大的对象,如大数组、长字符串等。
```java
int[] array = new int[1000000];
String str = new String(new char[1000000]);
```
其大小由 JVM 参数 `-XX:PretenureSizeThreshold` 控制,但在 JDK 8 中,默认值为 0,也就是说默认情况下,对象仅根据 GC 存活的次数来判断是否进入老年代。

G1 垃圾收集器中,大对象会直接分配到 HUMONGOUS 区域。当对象大小超过一个 Region 容量的 50% 时,会被认为是大对象。

Region 的大小可以通过 JVM 参数 `-XX:G1HeapRegionSize` 来设置,默认情况下从 1MB 到 32MB 不等,会根据堆内存大小动态调整。
可以通过 `java -XX:+UseG1GC -XX:+PrintGCDetails -version` 查看 G1 垃圾收集器的相关信息。

从结果上来看,我本机上 G1 的堆大小为 2GB,Region 的大小为 4MB。
#### 动态年龄判定了解吗?
如果 Survivor 区中所有对象的总大小超过了一定比例,通常是 Survivor 区的一半,那么年龄较小的对象也可能会被提前晋升到老年代。
这是因为如果年龄较小的对象在 Survivor 区中占用了较大的空间,会导致 Survivor 区中的对象复制次数增多,影响垃圾回收的效率。
> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的阿里面经同学 5 阿里妈妈 Java 后端技术一面面试原题:哪些情况下对象会进入老年代?
> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 7 Java 后端技术一面面试原题:新生代对象转移到老年代的条件
> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的拼多多面经同学 4 技术一面面试原题:对象什么时候进入老年代
memo:2025 年 1 月 13 日修改到此
### 15.STW 了解吗?
了解。
JVM 进行垃圾回收的过程中,会涉及到对象的移动,为了保证对象引用在移动过程中不被修改,必须暂停所有的用户线程,像这样的停顿,我们称之为`Stop The World`。简称 STW。
#### 如何暂停线程呢?
JVM 会使用一个名为安全点(Safe Point)的机制来确保线程能够被安全地暂停,其过程包括四个步骤:
- JVM 发出暂停信号;
- 线程执行到安全点后,挂起自身并等待垃圾收集完成;
- 垃圾回收器完成 GC 操作;
- 线程恢复执行。
#### 什么是安全点?
安全点是 JVM 的一种机制,常用于垃圾回收的 STW 操作,用于让线程在执行到某些特定位置时,可以被安全地暂停。
通常位于方法调用、循环跳转、异常处理等位置,以保证线程暂停时数据的一致性。
用个通俗的比喻,老王去拉车,车上的东西很重,老王累的汗流浃背,但是老王不能在上坡或者下坡时休息,只能在平地上停下来擦擦汗,喝口水。

推荐大家看看这个[HotSpot JVM Deep Dive - Safepoint](https://www.youtube.com/watch?v=JkbWPPNc4SI),对 safe point 有一个比较深入地解释。

### 16.对象一定分配在堆中吗?
不一定。
默认情况下,Java 对象是在堆中分配的,但 JVM 会进行逃逸分析,来判断对象的生命周期是否只在方法内部,如果是的话,这个对象可以在栈上分配。
举例来说,下面的代码中,对象 `new Person()` 的生命周期只在 `testStackAllocation` 方法内部,因此 JVM 会将这个对象分配在栈上。
```java
public void testStackAllocation() {
Person p = new Person(); // 对象可能分配在栈上
p.name = "沉默王二是只狗";
p.age = 18;
System.out.println(p.name);
}
```
#### 什么是逃逸分析?
逃逸分析是一种 JVM 优化技术,用来分析对象的作用域和生命周期,判断对象是否逃逸出方法或线程。
可以通过分析对象的引用流向,判断对象是否被方法返回、赋值到全局变量、传递到其他线程等,来确定对象是否逃逸。
如果对象没有逃逸,就可以进行栈上分配、同步消除、标量替换等优化,以提高程序的性能。
可以通过 `java -XX:+PrintFlagsFinal -version | grep DoEscapeAnalysis` 来确认 JVM 是否开启了逃逸分析。

#### 逃逸具体是指什么?
根据对象逃逸的范围,可以分为方法逃逸和线程逃逸。
当对象被方法外部的代码引用,生命周期超出了方法的范围,那么对象就必须分配在堆中,由垃圾收集器管理。
```java
public Person createPerson() {
return new Person(); // 对象逃逸出方法
}
```
比如说 `new Person()` 创建的对象被返回,那么这个对象就逃逸出当前方法了。

再比如说,对象被另外一个线程引用,生命周期超出了当前线程,那么对象就必须分配在堆中,并且线程之间需要同步。
```java
public void threadEscapeExample() {
Person p = new Person(); // 对象逃逸到另一个线程
new Thread(() -> {
System.out.println(p);
}).start();
}
```
对象 `new Person()` 被另外一个线程引用了,发生了线程逃逸。
#### 逃逸分析会带来什么好处?
主要有三个。
第一,如果确定一个对象不会逃逸,那么就可以考虑栈上分配,对象占用的内存随着栈帧出栈后销毁,这样一来,垃圾收集的压力就降低很多。
第二,线程同步需要加锁,加锁就要占用系统资源,如果逃逸分析能够确定一个对象不会逃逸出线程,那么这个对象就不用加锁,从而减少线程同步的开销。
第三,如果对象的字段在方法中独立使用,JVM 可以将对象分解为标量变量,避免对象分配。
```java
public void scalarReplacementExample() {
Point p = new Point(1, 2);
System.out.println(p.getX() + p.getY());
}
```
如果 Point 对象未逃逸,JVM 可以优化为:
```java
int x = 1;
int y = 2;
System.out.println(x + y);
```
> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的收钱吧面经同学 1 Java 后端一面面试原题:所有对象都在堆上对不对?
### 17.内存溢出和内存泄漏了解吗?
内存溢出,俗称 OOM,是指当程序请求分配内存时,由于没有足够的内存空间,从而抛出 OutOfMemoryError。
```java
List list = new ArrayList<>();
while (true) {
list.add("OutOfMemory".repeat(1000)); // 无限增加内存
}
```
可能是因为堆、元空间、栈或直接内存不足导致的。可以通过优化内存配置、减少对象分配来解决。
内存泄漏是指程序在使用完内存后,未能及时释放,导致占用的内存无法再被使用。随着时间的推移,内存泄漏会导致可用内存逐渐减少,最终导致内存溢出。
内存泄漏通常是因为长期存活的对象持有短期存活对象的引用,又没有及时释放,从而导致短期存活对象无法被回收而导致的。
```java
class MemoryLeakExample {
private static List