# Java 面试模拟系列 🧠📘
该系列文档基于真实面试准备过程,通过 ChatGPT 辅助问答,对高频 Java 面试题进行整理与答疑。每篇文档聚焦一个主题,覆盖基础、集合、JVM、并发、网络等模块。
序号 文章标题 内容简介 01 Java 模拟面试题(基础篇01) Java 基础知识模拟问答 02 Java 模拟面试题(1) 集合框架、JMM 内存模型等基础进阶题 03 Java 模拟面试题(2) Java 四种引用类型等进阶题 ...... 🔄 持续更新中🔄 持续更新中 JVM、并发、Spring 等模块 JVM、并发、Spring 等模块
📌 推荐使用场景:
- 准备面试前的快速复盘
- 学习 Java 各模块的知识要点
- 与 ChatGPT 配合自测提问训练
🚀 持续优化中,欢迎 Star & 分享!
# 💡第一部分:Java 基础与进阶
# ❓问题 1:请说一下 Java 中的四种引用类型(强、软、弱、虚引用),它们的作用及使用场景?
# 1️⃣ 强引用(Strong Reference)
定义方式: Object obj = new Object();
特点:
- 默认引用类型。
- 只要强引用还在,GC 永远不会回收该对象。
使用场景: 大多数场景下使用,普通对象引用。
# 2️⃣ 软引用(Soft Reference)
定义方式: SoftReference<Object> ref = new SoftReference<>(new Object());
特点:
- 内存不足时 GC 会尝试回收它所引用的对象。
- 适合做缓存(如图片缓存)。
使用场景: 内存敏感型缓存系统,例如浏览器缓存、图片缓存等。
# 3️⃣ 弱引用(Weak Reference)
定义方式: WeakReference<Object> ref = new WeakReference<>(new Object());
特点:
- 只要 GC 扫描到,无论内存是否紧张,都会回收。
- 一般用于管理非必须的对象,例如 ThreadLocal 的 key 就是弱引用。
使用场景: ThreadLocal、Map 中防止内存泄漏的 key 引用等。
# 4️⃣ 虚引用(Phantom Reference)
定义方式: PhantomReference<Object> ref = new PhantomReference<>(new Object(), referenceQueue);
特点:
- 无法通过引用获取对象。
- 必须与
ReferenceQueue联合使用,GC 后可接收到通知。
使用场景: 管理堆外内存,或在对象被 GC 回收后做清理工作。
# 🧠 第二部分:JVM
# ❓问题 2:请你说一说类加载的整个过程,包括各个阶段的含义,以及什么是双亲委派模型?为什么要采用这种模型?
# ✅ 类加载过程(7 个阶段)详解:
- 加载(Loading)
- 从 class 文件或其他源中读取字节码,生成对应的
Class对象。 - 这一步会由类加载器完成(如 AppClassLoader)。
- 从 class 文件或其他源中读取字节码,生成对应的
- 验证(Verification)
- 确保字节码合法、安全,例如不会越界、不违反访问权限等。
- 准备(Preparation)
- 为静态变量分配内存,并设置默认值(不是初始化值)。
- 解析(Resolution)
- 将常量池中的符号引用(如类名、字段名)替换为直接引用(如内存地址)。
- 初始化(Initialization)
- 执行类的
<clinit>()方法:静态变量赋值、静态代码块执行。
- 执行类的
- 使用(Using)
- 正常使用这个类,比如 new 实例、调用方法。
- 卸载(Unloading)
- 由 GC 回收无用的类,通常只有在自定义类加载器场景下会被卸载。
# ✅ 双亲委派模型(Parent Delegation Model)
** 定义:** 类加载器加载类时,优先将请求委派给父加载器,一层层向上直到引导类加载器;如果父加载器无法完成,再由子类加载器自己尝试加载。
加载顺序: BootstrapClassLoader(引导类加载器) → ExtensionClassLoader(扩展类加载器) → AppClassLoader(系统类加载器) → 自定义加载器
目的与好处:
- 避免重复加载,保证唯一性(类隔离)。
- 防止核心类被覆盖或篡改,例如自定义 java.lang.String 就无法加载成功。
- 提升安全性与稳定性。
# 🔒 第三部分:并发编程
# ❓问题 3:请你详细说一下 synchronized 和 ReentrantLock 的区别,包括它们的实现机制、功能特性和适用场景。
# ✅ synchronized vs ReentrantLock 全面对比
| 特性 | synchronized |
ReentrantLock |
|---|---|---|
| 是否属于 Java 语法关键字 | 是 | 否(是一个类,需手动引入) |
| 实现方式 | JVM 层面实现(通过 monitor 对象) | JDK 实现( java.util.concurrent.locks 包) |
| 可重入性 | ✅ 支持 | ✅ 支持 |
| 公平锁 | ❌ 不支持 | ✅ 可选(构造函数传入 true 实现公平锁) |
| 可中断 | ❌ 不支持(不能中断正在等待的线程) | ✅ 支持 lockInterruptibly() |
| 超时获取锁 | ❌ 不支持 | ✅ 支持 tryLock(long timeout, TimeUnit unit) |
| 读写锁 | ❌ 不支持 | ✅ 支持 ReentrantReadWriteLock |
| 条件变量 | ❌ 只能使用 wait/notify |
✅ 使用 Condition 更灵活 |
| 性能 | JDK1.6 以后性能很好,低竞争场景下优化为偏向锁、轻量级锁 | 高并发下表现更好(如大量线程抢锁) |
# 🧠 使用建议:
- 简单同步场景(例如同步方法、代码块) → 用
synchronized,简单、稳定、无需手动释放。 - 高并发、需要灵活控制(中断、超时、公平性、读写分离等) → 用
ReentrantLock。
✅ 示例代码:
//synchronized 示例 | |
public synchronized void method() { | |
// 临界区代码 | |
} | |
// ReentrantLock 示例 | |
Lock lock = new ReentrantLock(); | |
try { | |
lock.lock(); | |
// 临界区代码 | |
} finally { | |
lock.unlock(); // 一定要手动释放 | |
} |
# 🗃️ 第四部分:MySQL 索引与优化
# ❓问题 4:请你说一说 MySQL 中 B+ 树索引的结构特点,为什么 MySQL 使用的是 B+ 树而不是 Hash 或 B 树?
# 🔸 1. B + 树结构特点
- 所有数据都存储在叶子节点,非叶子节点只做索引(即只存 key,不存 value)。
- 叶子节点之间通过双向链表连接,天然支持范围查询。
- B + 树的层级更少,磁盘访问次数少,I/O 效率更高。
# 🔸 2. 为什么不是 Hash?
| 特性 | Hash 索引 | B+ 树索引 |
|---|---|---|
| 支持范围查询 | ❌ 不支持 | ✅ 支持范围、排序查询 |
| 有序性 | ❌ 无序 | ✅ 有序性好 |
| 空间利用率 | ❌ 高冲突时效率下降 | ✅ 多路平衡树,效率高 |
| 磁盘友好 | ❌ 随机访问 | ✅ 顺序访问,减少磁盘 I/O |
结论:Hash 虽然 O (1),但无法支持范围查询、排序,非常不适合数据库索引需求。
# 🔸 3. 为什么不是 B 树?
- B 树每个节点会存储 key 和 value,导致节点更大,磁盘页利用率低,查询过程中会多次访问磁盘。
- B+ 树只在叶子节点存 value,内部节点只存 key,更高的扇出(一个节点能存更多 key),树更矮,查询更快。
# 🔸 4. 总结优点:
- I/O 效率高:多路平衡,扇出大,树高低。
- 范围查询友好:叶子节点链表结构。
- 支持排序:天然支持
ORDER BY和范围查询。 - 扫描效率高:适合数据库这种基于磁盘的海量数据检索场景。
# 🌱 第五部分:Spring 核心原理
# ❓问题 5:请你说一下 Spring Bean 的生命周期,包括在哪些阶段可以进行扩展(如 Aware 接口、BeanPostProcessor 等)?另外,Spring 的 AOP 是如何实现的?
# ✅ 一、Spring Bean 生命周期完整流程及扩展点
# 🌀 生命周期 7 大步骤(按容器层面梳理):
- 实例化
- 调用构造方法创建对象(通过反射)
- 对应扩展点:
InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation
- 属性注入(依赖注入)
- 使用
@Autowired、@Value注入依赖 - 扩展点:
InstantiationAwareBeanPostProcessor#postProcessProperties、postProcessAfterInstantiation
- 使用
- Aware 接口回调
- 例如:
BeanNameAware、ApplicationContextAware - 容器注入 Spring 环境信息到 Bean 中
- 例如:
- BeanPostProcessor 前置处理
- 调用
postProcessBeforeInitialization
- 调用
- 初始化阶段
- 执行
@PostConstruct、InitializingBean#afterPropertiesSet()、自定义init-method
- 执行
- BeanPostProcessor 后置处理
- 调用
postProcessAfterInitialization(这个阶段通常会生成代理对象,例如 AOP)
- 调用
- 销毁阶段
- 容器关闭前执行:
@PreDestroy、DisposableBean#destroy()、自定义destroy-method
- 容器关闭前执行:
# 🧩 常见扩展点总结:
| 阶段 | 扩展接口或注解 |
|---|---|
| 实例化前 | postProcessBeforeInstantiation() |
| 实例化后 | postProcessAfterInstantiation() |
| 属性填充 | postProcessProperties() |
| 初始化前 | postProcessBeforeInitialization() |
| 初始化后 | postProcessAfterInitialization() |
| 初始化逻辑 | InitializingBean , @PostConstruct , init-method |
| 销毁逻辑 | DisposableBean , @PreDestroy , destroy-method |
# ✅ 二、Spring AOP 实现原理
Spring AOP 采用的是代理机制,主要有两种方式:
| 代理方式 | 适用场景 | 原理说明 |
|---|---|---|
| JDK 动态代理 | 如果目标对象实现了接口 | 基于 java.lang.reflect.Proxy ,生成代理类,实现接口,并委托调用原始方法 |
| CGLIB 动态代理 | 如果目标对象没有实现接口 | 通过继承目标类并重写方法生成代理类,底层基于 ASM 字节码操作框架 |
# 🧠 核心流程概览:
- Bean 初始化时,
BeanPostProcessor#postProcessAfterInitialization()判断是否需要 AOP; - 如果需要,生成一个 代理对象(Proxy);
- 方法调用时进入
MethodInterceptor,执行增强逻辑(如@Around,@Before,@After); - 再调用原始方法,实现增强功能。
# 🏗️ 第六部分:系统设计与高可用架构
# ❓问题 6:请你结合实际项目,说一说微服务架构中是如何实现 “限流、熔断、降级” 的?各自的目的、实现方式以及你使用过的具体方案(如 Sentinel、Hystrix、Resilience4j 等)?
# 🚦 一、限流(Rate Limiting)
目的:
防止接口瞬时高并发被压垮,保护下游服务资源。
常见策略:
- 固定窗口计数法
- 滑动窗口法
- 令牌桶算法(常用)
- 漏桶算法
实现方式:
- 自定义注解 + AOP + Redis/Guava 缓存计数器
- 使用 Sentinel 设置 QPS、线程数等限流规则
- 网关限流(如 Nginx、Spring Cloud Gateway + Redis)
# 🔌 二、熔断(Circuit Breaker)
目的:
当下游接口连续失败,主动切断请求通道,快速失败,防止雪崩效应。
核心机制:
- 失败次数阈值 + 时间窗口 + 半开状态探活
- 熔断后进入 “休眠”,到期后尝试请求,如果恢复就关闭熔断器。
实现方案:
- Hystrix(已废弃)
- Sentinel 熔断规则
- Resilience4j(轻量、现代化)
# ⚙️ 三、降级(Fallback)
目的:
服务不可用或慢响应时,为用户提供备选方案或默认返回值,避免系统直接崩溃或页面卡死。
场景:
- 服务调用超时
- 接口熔断后自动降级
- 后台逻辑失败时优雅退化处理
实现方式:
- Sentinel:
@SentinelResource注解 +fallback方法 - Resilience4j:
@CircuitBreaker(name = "...", fallbackMethod = "...")
# 🛠️ 实战经验举例(你可以这样答):
在我们做 HRSaaS 系统时,为了防止批量导入、报表导出等高频接口压垮服务,我们接入了 Sentinel 做 QPS 限流,并结合降级处理返回友好的提示。对于调用外部保单系统的接口,我们配置了熔断机制,一旦失败率超过 60% 就熔断,并提供静态保单信息 fallback 返回,保证用户操作流畅。
# 🧠 第七部分:Redis 高级用法
# ❓问题 7:请你讲一讲 Redis 中常见的三大缓存问题:缓存穿透、缓存击穿、缓存雪崩,以及你在项目中是如何解决它们的?
# 1️⃣ 缓存穿透
定义: 查询一个数据库中不存在的数据,缓存没命中,每次都打到数据库。
常见场景: 用户恶意请求不存在的 ID,导致缓存永远不命中。
解决方案:
- 布隆过滤器(Bloom Filter):提前存储所有可能存在的 key,过滤掉无效请求;
- 空值缓存:将查询不到的结果也写入缓存(如
null,并设置短 TTL); - 参数校验:对 ID 做格式校验、合法性判断,提前拦截。
# 2️⃣ 缓存击穿
定义: 热点 key 在过期的一瞬间,大量并发请求打入数据库。
解决方案:
- 互斥锁(Mutex Lock):查询数据库前先获取锁,只有一个线程加载,其他线程等待;
- 逻辑过期 + 后台异步刷新:
- 缓存中加一个
expireTime字段; - 过期后仍返回旧值,后台线程异步刷新数据库数据。
- 缓存中加一个
项目举例:
在 CRM 系统中,为避免客户详情缓存击穿,我们使用了 Redis + 分布式锁(Redisson),只允许一个线程回源查询,其他线程等待数据加载。
# 3️⃣ 缓存雪崩
定义: 大量缓存同时过期,瞬时大量请求打爆数据库。
解决方案:
-
缓存过期时间加随机值:避免 key 同时过期;
redis.set("key", value, baseTtl + random.nextInt(5 * 60))
-
热点数据永久缓存 + 异步更新:核心数据不设置过期时间;
-
多级缓存(本地 + Redis):先查本地缓存(如 Caffeine),再查 Redis,最后数据库;
-
限流降级:设置限流 / 熔断措施,保护数据库。
# 🧰 实战总结可这样说:
在我负责的 HRSaaS 项目中,为了防止缓存击穿,我们在部分关键接口中引入了 Redisson 分布式锁,防止热点 key 同时过期时大量线程并发回源。针对穿透问题,我们为不存在的数据设置了短 TTL 的空值缓存,并在登录等接口前增加了参数校验。缓存雪崩方面,我们所有缓存过期时间都加了随机数,避免批量失效。
# 🌐 第八部分:注册中心与一致性
# ❓问题 8:请你说一说 Nacos 和 Eureka 的区别?它们分别如何实现服务注册与发现?它们之间的一致性模型有何不同?如何理解 CAP 理论在注册中心中的应用?
# 🔸一、Nacos 和 Eureka 的核心区别
| 对比项 | Eureka | Nacos |
|---|---|---|
| 所属项目 | Netflix(Spring Cloud) | 阿里巴巴(Spring Cloud Alibaba) |
| 服务健康检查 | 客户端自上报(心跳) | 支持客户端 + 服务端主动探测(TCP/HTTP) |
| 数据存储 | 内存 + Peer-to-Peer 同步 | 支持 AP 模式(集群中基于 Raft 一致性) |
| 一致性模型 | AP(可用性 + 分区容忍性) | AP 默认,也可切换为 CP(基于 Raft) |
| 支持配置中心 | ❌ 无 | ✅ 集成配置中心(一个系统两个功能) |
| 开发状态 | 官方已停止维护 | 社区活跃,国内主流 |
# 🔸二、CAP 理论在注册中心中的体现
CAP 理论:
- C 一致性(Consistency):数据在多个节点中始终一致;
- A 可用性(Availability):每次请求都能获得响应;
- P 分区容忍性(Partition tolerance):允许网络分区时系统仍能运行。
在分布式系统中,CAP 三者不可兼得,必须牺牲一个。
# 📌 Eureka:偏向 AP 模型
- 容忍网络分区(P),保持高可用(A)
- 牺牲强一致性:即使部分节点宕机,注册信息仍可访问
- 服务实例下线后有 90s 的保护期,这段时间注册中心不立即剔除服务(防止误判)
# 📌 Nacos:默认 AP,可配置为 CP 模式
- 单机或非持久化模式下:AP(可用性优先)
- 集群 + 开启持久化模式(如 MySQL)时:支持 Raft 协议,实现 CP 模式
- 多节点之间通过 Raft 投票选主,保证一致性
- 更灵活:你可以根据业务场景选择强一致还是高可用
# 🔍 项目实战举例(你可以这么说):
在我参与的 CRM 微服务拆分项目中,早期使用 Eureka 做服务发现,主要为了快速部署、高可用。但随着配置中心需求增加,我们迁移到了 Nacos,并通过开启 Raft + MySQL 模式,保障服务注册数据的一致性和持久性。我们也借助 Nacos 实现了注册 + 配置统一管理,运维效率提升明显。
# ⏱️ 第九部分:系统设计 —— 分布式任务调度设计
# ❓问题 9:如果让你从零设计一个分布式任务调度系统(用于定时任务执行、批处理、自动发薪等),你会如何考虑以下几个方面?
# 1. 任务的时间调度与精度如何保证?
- Cron 表达式调度: 类似 Linux crontab,适用于定时执行(如每日发薪、定时统计)。
- 延迟任务调度: 某任务在未来某个时间点执行(如 T+1 审批流程)。
- 手动触发 / 依赖触发: 任务之间支持依赖关系,满足 A 执行后触发 B。
🔧 常用调度引擎:
- Quartz(轻量,但单机)
- ElasticJob(支持分片、注册中心)
- XXL-JOB(UI 管理好,任务分布式执行)
- PowerJob(功能强大,支持分布式 + DAG 依赖)
# 2. 如何实现高可用性(主备 / 故障切换)?
-
注册中心 + 多节点调度器(Worker)
- 通过 Nacos/Zookeeper 注册服务,多个 Worker 实例负载均衡
-
主节点选举(Master):如使用数据库或 ZK 实现选主
-
支持 任务 Failover:某个 Worker 崩溃,任务自动转移到其他节点
-
状态持久化:任务状态、调度记录写入数据库,容器重启后可恢复
# 3. 如何避免任务重复执行或漏执行?
- 分布式锁:使用 Redis、Zookeeper 分布式锁确保同一时间只有一个实例执行某任务;
- 幂等性设计:任务本身执行逻辑需幂等(如:相同数据只处理一次);
- 任务日志 + 重试机制:任务失败可配置重试次数,并记录执行状态,支持告警通知;
- 事务支持:关键步骤要用事务确保数据一致性(如写库 + 发 MQ)
# 4. 如何支持分布式部署 + 任务分片并发执行?
任务分片(Sharding) = 把一个大任务拆分成多个小任务,由不同节点并行处理。
- Worker 启动时从注册中心获取任务分片列表;
- 每个分片带有 shardIndex、shardTotal 标识;
- Worker 只执行自己负责的分片;
- 支持动态扩容,避免热点 Worker 负载过高。
🔧 ElasticJob / PowerJob 原生支持任务分片。
# 5. 是否接触过类似的中间件(如 xxljob、Quartz、ElasticJob、PowerJob)?
在我们 HRSaaS 系统的发薪模块中,需要定时每天检查企业的工资发放计划,我们基于 XXL-JOB 实现了调度系统,使用 Redis 分布式锁防止重复执行,任务失败后自动告警并支持重试。同时支持多租户并行计算工资明细,通过任务分片按租户并发处理,提升执行效率。