# 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 个阶段)详解:

  1. 加载(Loading)
    • 从 class 文件或其他源中读取字节码,生成对应的 Class 对象。
    • 这一步会由类加载器完成(如 AppClassLoader)。
  2. 验证(Verification)
    • 确保字节码合法、安全,例如不会越界、不违反访问权限等。
  3. 准备(Preparation)
    • 为静态变量分配内存,并设置默认值(不是初始化值)。
  4. 解析(Resolution)
    • 将常量池中的符号引用(如类名、字段名)替换为直接引用(如内存地址)。
  5. 初始化(Initialization)
    • 执行类的 <clinit>() 方法:静态变量赋值、静态代码块执行。
  6. 使用(Using)
    • 正常使用这个类,比如 new 实例、调用方法。
  7. 卸载(Unloading)
    • 由 GC 回收无用的类,通常只有在自定义类加载器场景下会被卸载。

#双亲委派模型(Parent Delegation Model)

** 定义:** 类加载器加载类时,优先将请求委派给父加载器,一层层向上直到引导类加载器;如果父加载器无法完成,再由子类加载器自己尝试加载。

加载顺序: BootstrapClassLoader(引导类加载器)ExtensionClassLoader(扩展类加载器)AppClassLoader(系统类加载器)自定义加载器

目的与好处:

  1. 避免重复加载,保证唯一性(类隔离)
  2. 防止核心类被覆盖或篡改,例如自定义 java.lang.String 就无法加载成功
  3. 提升安全性与稳定性

# 🔒 第三部分:并发编程

# ❓问题 3:请你详细说一下 synchronizedReentrantLock 的区别,包括它们的实现机制、功能特性和适用场景。

#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 大步骤(按容器层面梳理):
  1. 实例化
    • 调用构造方法创建对象(通过反射)
    • 对应扩展点: InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation
  2. 属性注入(依赖注入)
    • 使用 @Autowired@Value 注入依赖
    • 扩展点: InstantiationAwareBeanPostProcessor#postProcessPropertiespostProcessAfterInstantiation
  3. Aware 接口回调
    • 例如: BeanNameAwareApplicationContextAware
    • 容器注入 Spring 环境信息到 Bean 中
  4. BeanPostProcessor 前置处理
    • 调用 postProcessBeforeInitialization
  5. 初始化阶段
    • 执行 @PostConstructInitializingBean#afterPropertiesSet() 、自定义 init-method
  6. BeanPostProcessor 后置处理
    • 调用 postProcessAfterInitialization (这个阶段通常会生成代理对象,例如 AOP)
  7. 销毁阶段
    • 容器关闭前执行: @PreDestroyDisposableBean#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 字节码操作框架
# 🧠 核心流程概览:
  1. Bean 初始化时, BeanPostProcessor#postProcessAfterInitialization() 判断是否需要 AOP;
  2. 如果需要,生成一个 代理对象(Proxy)
  3. 方法调用时进入 MethodInterceptor ,执行增强逻辑(如 @Around , @Before , @After );
  4. 再调用原始方法,实现增强功能。

# 🏗️ 第六部分:系统设计与高可用架构

# ❓问题 6:请你结合实际项目,说一说微服务架构中是如何实现 “限流、熔断、降级” 的?各自的目的、实现方式以及你使用过的具体方案(如 Sentinel、Hystrix、Resilience4j 等)?

# 🚦 一、限流(Rate Limiting)

目的:
防止接口瞬时高并发被压垮,保护下游服务资源。

常见策略:

  • 固定窗口计数法
  • 滑动窗口法
  • 令牌桶算法(常用)
  • 漏桶算法

实现方式:

  1. 自定义注解 + AOP + Redis/Guava 缓存计数器
  2. 使用 Sentinel 设置 QPS、线程数等限流规则
  3. 网关限流(如 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 分布式锁防止重复执行,任务失败后自动告警并支持重试。同时支持多租户并行计算工资明细,通过任务分片按租户并发处理,提升执行效率。