面试题目
一、Java 基础语法 & 关键字
1. JDK、JRE 和 JVM 的区别
- JVM(Java Virtual Machine):Java 虚拟机,负责执行字节码(
.class)。提供类加载、字节码校验、内存管理(堆、方法区)、执行引擎、垃圾回收、线程调度等。是抽象的规范,具体由不同厂商实现(HotSpot、OpenJ9 等)。 - JRE(Java Runtime Environment):运行环境,包含 JVM、核心类库(rt.jar 或 modules)、以及运行时需要的其它资源。用于运行 Java 程序。
- JDK(Java Development Kit):开发工具包,包含 JRE + 开发工具(
javac、jar、javadoc、调试工具等)。用于开发 Java 程序。
总结:JDK ⊃ JRE ⊃ JVM(JDK 包含 JRE,JRE 包含 JVM)。
2. Java 的跨平台原理是什么?
- Java 程序先由
javac编译成与平台无关的 字节码(.class)。字节码不是机器码,而是供 JVM 执行的指令集。 - 每个平台(Windows/Linux/macOS)有相应的 JVM 实现,JVM 把字节码翻译成该平台的机器指令(通过解释器、JIT 编译器等)。因此同一套
.class可以在不同平台的 JVM 上运行 —— “一次编写,处处运行”(Write Once, Run Anywhere)。 - 其它保证跨平台的要点:标准库隐藏平台差异(I/O、线程等),需避免使用与平台绑定的本地代码(JNI)。
3. Java 的基本数据类型有哪些?各自的默认值?
8 种基本类型及默认值(成员变量/数组元素;局部变量无默认值必须初始化):
byte:1 byte,默认值0short:2 bytes,默认值0int:4 bytes,默认值0long:8 bytes,默认值0Lfloat:4 bytes,默认值0.0fdouble:8 bytes,默认值0.0dchar:2 bytes(UTF-16 code unit),默认值'\u0000'(即 0)boolean:JVM 语义上用 1 bit 表示,默认值false
另外,引用类型(对象、数组等)的默认值是 null。
4. byte、short、int、long 各占多少字节?
byte:1 字节(8 位)short:2 字节(16 位)int:4 字节(32 位)long:8 字节(64 位)
(上述为 Java 语言规范定义,与平台无关。)
5. float 和 double 的区别?
- 精度与存储:
float:32 位单精度浮点(IEEE 754),约 6~7 位有效数字。double:64 位双精度浮点(IEEE 754),约 15~16 位有效数字。
- 精度更高的
double更常用;float用于节省内存或与特定 API(如图形库)兼容。 - 注意:浮点数有精度误差,不适合做精确货币计算(应使用
BigDecimal)。 - 默认浮点字面量为
double(例如1.2是double,写成1.2f才是float)。
6. char 类型占几个字节?能存汉字吗?
char占 2 字节(16 位),表示 UTF-16 的一个 code unit。- 能否存汉字:多数常用汉字(位于基本多文种平面 BMP)可以用单个
char表示(例如'你');但有些罕见汉字或 emoji 属于补充平面(Supplementary Planes),它们需要 两个 char(称为 surrogate pair) 来表示。 - 因此,单个
char能表示一个 UTF-16 code unit,而不是完整的 Unicode code point。处理 Unicode 字符时注意使用int codePoint/Character的相关 API。
7. 自动装箱(autoboxing)和拆箱(unboxing)的原理是什么?
概念:
- 装箱:把基本类型自动转换成对应的包装类型(如
int→Integer)。 - 拆箱:把包装类型自动转换回基本类型(如
Integer→int)。
- 装箱:把基本类型自动转换成对应的包装类型(如
编译器行为:自动装箱/拆箱是在编译期由 Java 编译器插入对应的调用,示例:
1
2Integer a = 10; // 编译器会转换为 Integer.valueOf(10)
int b = a; // 编译器会转换为 a.intValue()valueOf 缓存:包装类(如
Integer.valueOf(int))会对小整数(默认 -128 到 127)做缓存以重用对象,减少频繁创建对象。注意事项:
拆箱
null会抛出NullPointerException:1
2Integer x = null;
int y = x; // NPE装箱/拆箱会有性能开销(对象创建、装箱/拆箱方法调用),在性能敏感场景尽量使用基本类型或手动优化。
比较时注意:
Integer a = 100; Integer b = 100; a==b在缓存范围内可能为true,超出范围则通常为false。
8. == 和 equals() 的区别?
- ==:
- 对于基本类型:比较 值(数值是否相等)。
- 对于引用类型:比较 引用地址(是否是同一个对象)。
- equals():
- 是
Object的方法,默认实现也是比较引用(等价于==)。 - 许多类(如
String、Integer、集合类等)重写了equals(),用于比较逻辑/内容相等。
- 是
- 使用建议:
- 比较对象内容用
equals()(需检查null),比较是否同一实例用==。 - 与
equals()配套应重写hashCode()(见下题)。
- 比较对象内容用
9. hashCode 和 equals 的关系?
- 合同(Contract)(重要):
- 如果两个对象通过
equals()被判定为相等(a.equals(b)为true),那么a.hashCode() == b.hashCode()必须成立。 - 反之不要求:
hashCode相等的对象不一定equals()相等(哈希冲突允许)。
- 如果两个对象通过
- 在哈希集合中的角色:
- 哈希表(
HashMap/HashSet)先用hashCode()找到桶(bucket),若桶中有多个元素,再用equals()逐个比较确认相等或冲突。 - 如果只重写
equals()而不重写hashCode()会破坏集合行为(例如放入HashSet后无法正确查找)。
- 哈希表(
- 实现要点:
hashCode()要尽量分散(降低冲突),并在对象不可变字段上基于相同规则计算。- 若对象可变,若用于哈希集合要小心:修改字段会导致
hashCode()改变,破坏集合内部结构。
10. String、StringBuffer、StringBuilder 的区别?
- String:
- 不可变(immutable),每次修改都会产生新的对象(或新内部 char/byte 数组)。
- 线程安全(因为不可变),适合频繁读取、少量修改的场景。
- StringBuffer:
- 可变的字符序列(内部有缓冲区
char[]/byte[]),几乎与StringBuilder接口相同。 - 线程安全,其方法大多使用
synchronized,因此在多线程下可以被多个线程安全使用。 - 相对较慢(同步开销)。
- 可变的字符序列(内部有缓冲区
- StringBuilder(Java 5+):
- 可变,非线程安全(没有同步),比
StringBuffer快。 - 推荐在单线程或外部已同步的场景下使用。
- 可变,非线程安全(没有同步),比
- 选择建议:
- 多线程需要可变字符串:
StringBuffer(或外部同步)。 - 单线程/局部构造字符串:
StringBuilder。 - 常量字符串或少量拼接:
String(编译器对+会优化为StringBuilder)。
- 多线程需要可变字符串:
11. 为什么 String 是不可变的(immutable)?
- 安全性:
String经常用于关键场景(类加载器、网络地址、文件名、权限检查、数据库连接字符串等)。不可变保证在传递引用时不被恶意或意外修改。 - 线程安全:不可变对象固有线程安全,多个线程可共享同一
String实例而无需同步。 - 性能(缓存 hashCode):
String的哈希值可缓存(hash字段),便于作为 Map 的 key,避免重复计算。 - 字符串常量池:可安全地将字面量放入池中重用,不用复制或担心修改。
- 优化:JVM 可进行共享、常量折叠等优化(更易于实现某些编译期/运行期优化)。
12. String 常量池的实现机制?
- 概念:编译期和运行期维护一个字符串池(String Intern Pool),用于存放字符串字面量(literal)和显式
intern()后的字符串,以便重用相同内容的String实例。 - 编译期:源代码中的字符串字面量(如
"abc")会被放到常量池(编译后的 class 文件常量池),类加载后这些字面量会放入运行时的字符串池。 - 运行期:
- 以前(Java 6 及更早):字符串常量池在 PermGen(方法区)中。
- Java 7 起:常量池迁移到 Java 堆(运行时常量池也在堆里),避免 PermGen 问题。
- intern():当调用
s.intern()时,JVM 会检查池中是否已有相同内容的字符串:- 若存在,返回池中的引用;
- 若不存在,将该字符串的引用加入池并返回它。
- 编译时优化:字符串常量的拼接(编译时常量)会在编译期合并,如
"a" + "b"→"ab",直接放入常量池;而运行时拼接(变量参与)则使用StringBuilder。
13. new String("abc") 创建了几个对象?
- 通常情形:
- 若
"abc"字面量尚未在常量池中存在,执行new String("abc")会导致 两个对象 被创建:常量池中的"abc"(一个String)和堆中通过new创建的新的String实例(内容通常是对常量池中字符数组的复制或共享,具体实现随 Java 版本而异)。 - 如果字面量
"abc"已经存在于常量池中(例如之前被加载过),那么new String("abc")只会创建 一个堆对象(new 的那个String)。
- 若
- 注意:自 Java 7/9 后
String内部实现改变(压缩字符串、byte[] 存储等),但逻辑上上述结论成立:通常 1 或 2 个对象,取决于常量池中是否已存在该字面量。
14. final 关键字的作用?
- 用于类(final class):类不可被继承(如
String)。 - 用于方法(final 方法):方法不能被子类重写(override),用于确保行为不可变。
- 用于变量(final 字段/局部变量):
- 对基本类型:赋值后值不可改变(常量)。
- 对引用类型:引用不可改变(不能指向另一个对象),但所指向对象的内部状态仍可变(除非对象本身不可变)。
static final常用来定义常量(编译期常量)。
- 用于参数(方法形参可以声明为
final):代表方法内不能修改该参数引用/值。 - 其它用途:在多线程中,
final字段的写-构造过程被 JMM(Java 内存模型)处理,确保构造完成后其他线程可见(安全发布方面有好处)。
15. static 关键字的作用?
- 用于声明类级别成员(字段、方法、初始化块、嵌套类),不依赖实例。
- static 变量:类变量,所有实例共享一份。内存中仅有一份副本。
- static 方法:类方法,可通过
ClassName.method()调用;不能直接访问非静态成员(因无 this)。 - static 代码块:类加载时执行一次(用于静态初始化)。
- static 嵌套类:可以声明为静态的内部类(静态嵌套类),没有外部实例引用。
16. static 修饰变量、方法、代码块分别意味着什么?
- static 变量(类变量):
- 随类加载而创建,所有对象共享同一份数据。
- 可通过
ClassName.field或instance.field(不推荐)访问。
- static 方法:
- 属于类,调用时无需实例。
- 不能使用
this、不能直接访问非静态成员。 - 可作为工具方法(如
Math.abs())。
- static 代码块:
- 在类加载阶段执行一次,用于复杂静态初始化(比如初始化静态常量、加载本地库等)。
- 执行顺序:静态块按定义顺序执行,类加载时运行(在实例化之前)。
17. static 和 final 能一起用吗?
能。
static final常用于定义类常量(尤其是基本类型和String),例如:1
2public static final int MAX = 100;
public static final String NAME = "abc";区别:
- 如果是编译期常量(
static final基本类型或String且在编译时可以确定),编译器会将其内联到使用处(被引用的类编译后看到的是常量值),注意跨模块修改可能导致需要重新编译引用方。 - 如果是
static final引用对象且不是编译期常量,则引用本身不可变,但对象内容可能可变。
- 如果是编译期常量(
18. static 内部类和非静态内部类的区别?
- 静态内部类(static nested class):
- 相当于外部类的一个静态成员。
- 没有对外部类实例的隐式引用(不能直接访问外部类的非静态成员)。
- 可以像普通类那样实例化:
Outer.StaticInner inner = new Outer.StaticInner();
- 非静态内部类(inner class):
- 每个实例隐式持有一个外部类实例引用(
Outer.this),可以直接访问外部类的所有成员(包括私有成员)。 - 创建方式:
Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); - 占用外部类对象的内存引用,可能导致内存泄露(如果长期持有内部类实例导致外部类不能被回收)。
- 每个实例隐式持有一个外部类实例引用(
- 选择原则:如果内部类不需要访问外部实例成员,优先使用
static嵌套类,避免额外引用。
19. Java 中的 this 和 super 的区别?
- this:
- 引用当前对象的引用。
- 用于访问当前类的实例变量、调用当前类的其他构造器(
this(...))、或传递当前对象引用。
- super:
- 引用当前对象的父类部分。
- 用于访问父类被覆盖的方法(
super.method())、父类字段(若有同名字段)、以及在子类构造器第一行调用父类构造器(super(...))。
- 注意:
this()或super()必须在构造器的第一行(两者不能同时出现)。super不能用于访问父类的 private 成员(编译期限制)。
20. Java 中构造方法能否被重写?
- 不能。重写(override)适用于实例方法,构造方法不是继承的成员,子类不能重写父类的构造器。
- 可以重载(overload):在同一类中同名构造器有不同参数列表属于重载。
- 子类构造器可通过
super(...)显式调用父类构造器来完成父类初始化。
21. 接口和抽象类的区别?
- 接口(interface):
- 早期(Java 7 之前):只包含抽象方法(默认是
public abstract)和public static final常量。 - Java 8+:可以包含
default方法与static方法(可以有部分实现);Java 9+ 允许private方法。 - 支持多继承(一个类可以实现多个接口)。
- 没有实例字段(除
static final常量外)。
- 早期(Java 7 之前):只包含抽象方法(默认是
- 抽象类(abstract class):
- 可以有实例字段(非静态成员变量)、已有实现的方法(具体方法)、构造器。
- 适合表示一类有共同行为并共享状态的类层次。
- 类只能单继承(只能继承一个抽象类)。
- 选择建议:
- 如果需要多继承行为、只定义方法契约且不需要状态,使用接口。
- 如果需要提供共有实现和状态,使用抽象类。
22. 接口中可以有 default 方法和 static 方法吗?
- 可以(Java 8 起)。
default方法:为实现类提供默认实现,避免破坏已有实现(接口演化)。实现类可以覆盖(override)。static方法:属于接口自身,可以通过InterfaceName.method()调用,不能通过实例调用。
- Java 9 及以后还可有
private方法,供接口内部复用实现逻辑。
23. Java 8 接口新增了什么?
- Java 8 在接口中新增了 default 方法 和 static 方法,允许接口提供带实现的方法,从而便于接口的演化(向后兼容)。这也是 Java 8 引入函数式编程支持的基础之一(允许接口作为函数式接口)。
24. Java 8 有什么新特性,并详细介绍(重点)
一、Java 8 新特性深入解析
1. Lambda 表达式
概念
- 匿名函数,允许把行为作为参数传递。
- 简化匿名内部类写法,实现函数式编程。
示例
1 | List<String> list = Arrays.asList("a", "bb", "ccc"); |
底层原理
- 编译器生成静态/实例方法 +
invokedynamic指令。 - JVM 调用
LambdaMetafactory动态生成函数对象,实现 函数式接口。 - 捕获外部变量通过闭包机制存储(必须是
effectively final)。
特点
- 语法简洁
- 可作为参数传递行为
- 性能高于匿名内部类(减少对象创建)
2. 函数式接口
概念
- 仅有一个抽象方法的接口,用作 Lambda 表达式的目标类型。
- 可用
@FunctionalInterface标识。
示例
1 | @FunctionalInterface |
底层原理
- Lambda 表达式编译时生成实现函数式接口的函数对象。
- 捕获变量通过闭包对象保存。
特点
- 简化匿名类
- 支持函数式编程
- 可与 Stream / Optional 等结合
3. Stream API
概念
- 对集合进行声明式操作(过滤、映射、归约)。
- 支持 惰性求值 和 并行处理。
示例
1 | List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); |
底层原理
- Stream 是流水线对象,中间操作返回新的 Stream,终端操作触发计算。
- 串行流:顺序迭代器处理
- 并行流:ForkJoinPool 分块处理
特点
- 声明式、链式操作
- 支持并行
- 可与 Lambda / 方法引用结合
4. 方法引用
概念
- Lambda 表达式的简化写法,引用现有方法。
- 类型:
- 静态方法引用:
ClassName::staticMethod - 实例方法引用:
instance::method - 构造器引用:
ClassName::new
- 静态方法引用:
示例
1 | List<String> names = Arrays.asList("a", "bb", "ccc"); |
底层原理
- 编译器转成 Lambda 表达式 + invokedynamic。
- JVM 生成实现函数式接口的函数对象,内部持有方法引用。
特点
- 简洁直观
- 可减少 Lambda 代码
- 与 Stream / Optional 配合使用
5. java.time API
概念
- 替代
Date/Calendar,不可变、线程安全。 - 核心类:
LocalDate/LocalTime/LocalDateTime(无时区)ZonedDateTime(带时区)Duration/Period(时间段)
示例
1 | LocalDate today = LocalDate.now(); |
底层原理
- 内部字段
final保存值,不可变。 - 工厂方法创建对象,链式操作返回新对象。
- 使用 enum + 整型/字节优化存储(如 LocalDate 年月日用 int)。
特点
- 不可变,线程安全
- 链式操作,易组合
- 支持各种历法和时区计算
6. Optional
概念
- 容器对象,防止
NullPointerException。 - 可以包含值或为空(empty)。
示例
1 | Optional<String> opt1 = Optional.of("Hello"); |
底层原理
- 内部
value字段存储非空值,空值使用单例EMPTY。 map/flatMap/filter返回新的 Optional,对象不可变。- 与 Lambda / 函数式接口结合实现链式安全操作。
特点
- 避免显式 null 判断
- 支持链式调用
- 可与 Stream / Lambda 配合
Java 8 六大核心特性底层对比表
| 特性 | 底层实现 | 核心原理 | 优势 |
|---|---|---|---|
| Lambda | invokedynamic + LambdaMetafactory | 动态生成函数对象 | 简洁、性能高 |
| 函数式接口 | 单抽象方法接口 | 捕获变量通过闭包存储 | 简化匿名类,函数式编程 |
| Stream API | 流水线 + 延迟求值 | 中间操作惰性,终端触发 | 声明式、可并行 |
| 方法引用 | Lambda 简化语法 + invokedynamic | 内部持有方法引用 | 简洁直观,减少代码 |
| java.time | 不可变对象 + 工厂方法 | final字段保存值,链式返回新对象 | 线程安全,可组合 |
| Optional | 包装对象 + 单例 EMPTY | 不可变容器,函数式链式调用 | 避免 NPE,安全链式操作 |
二、synchronized 底层原理深入解析
1. JVM 锁的类型
| 锁类型 | 特点 | 性能 |
|---|---|---|
| 偏向锁 (Biased Lock) | 无竞争时偏向第一个线程,轻量 | 高 |
| 轻量级锁 (Lightweight) | 竞争少时使用 CAS 升级锁 | 中 |
| 重量级锁 (Monitor) | 高竞争时使用 OS mutex,阻塞线程 | 低 |
2. synchronized 对象头结构
- 每个对象在 JVM 内存中都有 对象头(Mark Word),存储锁信息、哈希码、GC 标记等。
1 | 对象头 Mark Word (32/64bit) |
- 锁标记位:
- 00:无锁
- 01:偏向锁
- 10:轻量级锁
- 11:重量级锁
3. synchronized 执行流程
a) 偏向锁(无竞争)
1 | 线程获取锁 -> 在对象头打上线程ID -> 执行 -> 释放锁 -> 保持偏向状态 |
b) 轻量级锁(CAS竞争)
1 | 线程尝试CAS抢锁 -> 成功:持有锁 -> 执行 -> 释放 |
c) 重量级锁(阻塞)
1 | 线程无法获取锁 -> 阻塞(OS等待队列) -> 被唤醒 -> 获取锁 -> 执行 -> 释放锁 |
图示
1 | +------------------+ |
4. synchronized 方法示例
1 | class Counter { |
- 锁对象:实例方法锁住
this,静态方法锁住类对象ClassName.class。 - 效果:同一时间只有一个线程能进入
increment(),保证count++操作的原子性。
5. JVM 优化
- 锁消除:编译器或 JIT 可检测无竞争情况,消除同步锁。
- 锁粗化:把多次连续的小范围锁合并,减少加锁次数。
- 偏向锁 / 轻量级锁 / 重量级锁升级:动态适应竞争情况,兼顾性能与正确性。
25.多态的实现机制是什么?
- 概念:同一操作作用于不同对象时表现出不同的行为(主要表现为方法的动态绑定)。
- 实现机制:
- 编译时:根据引用类型检查方法签名(静态类型检查)。
- 运行时:实际调用的方法根据对象的实际类型(运行时类型)决定;即 动态绑定(dynamic dispatch)。
- JVM 通常通过 虚方法表(vtable)/方法查找机制 实现。调用一个非
static、非private、非final的方法时,会在运行时查找实际类的实现并调用。
- 结果:子类可以覆盖父类方法,调用者使用父类引用指向子类对象时,调用的是子类的覆盖方法(运行时决定)。
- 注意:
static、private、final方法不会被动态绑定(是静态绑定)。
26. 方法重载(overload)和方法重写(override)的区别?
方法重载(Overloading):
- 同一类中方法名相同、参数列表不同(参数类型/个数/顺序),返回类型可以不同。
- 编译时决定(重载解析在编译期完成),与继承关系无关。
方法重写(Overriding):
- 子类定义与父类相同方法签名(方法名 + 参数类型相同)的实现,目的是改变/扩展父类行为。
- 运行时动态绑定,必须满足访问权限不能更严格,抛出的受检异常不能超过父类版本等规则。
示例:
1
2
3
4
5
6// overload
void f(int x) {}
void f(String s) {}
// override
class A { void m() {} }
class B extends A { void m() {} }
27. Java 支持多继承吗?如何实现类似效果?
- 类的多继承:Java 不支持类的多继承(不能继承多个类),以避免菱形继承问题(diamond problem)。
- 实现类似效果的方法:
- 接口多实现:一个类可以实现多个接口(Java 8 的
default方法也带来类似多继承方法实现的可能,但有冲突解决规则)。 - 组合/委托(composition/delegation):在类中持有其它类的实例并把调用委托给它们(优于继承的面向对象设计原则)。
- 接口多实现:一个类可以实现多个接口(Java 8 的
- 接口冲突解决:若多个接口提供相同默认方法,类必须重写该方法并明确调用哪个接口的默认实现(
InterfaceName.super.method())。
28. Java 的四种访问修饰符?
public:对所有类可见(任何包)。protected:对同包类和子类可见(即同包或子类可以访问)。- 默认(包私有,package-private)(不写修饰符):对同包类可见,包外不可见。
private:仅在本类内可见,包外/子类不可访问(子类无法直接访问父类 private 成员)。- 注意:对类(顶层类)只能使用
public或默认(包私有),不能声明为private/protected。
29. transient 关键字的作用?
- 用于标记字段在 Java 序列化(
Serializable)过程中不被序列化。 - 被标记为
transient的字段在序列化后不会写入序列化流,反序列化时这些字段会以默认值恢复(基本类型 0,引用类型null)。 - 常用于敏感信息(密码)、不需要持久化的缓存字段、或可重建的状态字段。
- 注意:
static字段本身也不会被序列化(因为是类级别,不属于实例状态)。
30. volatile 关键字的作用?
- 可见性:保证对
volatile变量的写入对其它线程立即可见(读取总是从主内存而不是线程缓存读取)。 - 禁止指令重排序(部分):读/写
volatile存在的内存屏障能保证一定的有序性(写volatile发生在后续读的可见性上,更多细节参考 JMM)。 - 不保证原子性:对单次读/写操作是原子的(对
long/double在 Java 5 后也保证原子),但非原子操作(如i++)不是原子的。 - 典型用途:
- 状态标志(如
volatile boolean running)。 - 用于实现双重检查锁定(DCL)单例中的
instance(在 Java 5+ 环境下有效)。
- 状态标志(如
- 与 synchronized 比较:
volatile轻量,只保证可见性与部分有序性,不保证互斥;适用于简单状态通信。synchronized提供互斥和可见性保证(更重),并可配合wait/notify。
31. synchronized 的作用及底层原理?
作用
- 互斥(排它):保证同一时间只有一个线程可以执行被
synchronized修饰的代码块或方法(针对同一把锁)。 - 可见性:进入/退出同步块会建立 happens-before 关系,确保锁释放前的写对随后获取该锁的线程可见。
- 用途:保护临界区、确保多个线程对共享可变状态的安全访问。
使用方式
- 方法级别:
synchronized void m() { ... }:等价于synchronized(this)(实例方法)或synchronized (ClassName.class)(静态方法)。
- 代码块级别:
synchronized(this) { ... }或synchronized(lockObj) { ... }更灵活,能减小锁的粒度。
底层原理(HotSpot 实现概要)
- JVM 使用对象头(object header)中的 mark word 和关联数据结构维护锁状态。
- 锁优化策略(为提升性能,HotSpot 引入若干优化):
- 偏向锁(Biased Locking):在没有竞争的情况下,锁会偏向于第一次获得它的线程,重复获取无需 CAS,减少开销。
- 轻量级锁(Lightweight Locking):使用 CAS 操作在栈上记录加锁记录,避免进入重量级监视器(monitor)。
- 重量级锁(Monitor/Mutex):当竞争激烈或 CAS 失败时,升级为重量级锁,使用操作系统互斥量(可能涉及线程阻塞/唤醒)。
monitorenter/monitorexit是字节码指令(由编译器/字节码生成器生成)。- JIT 编译器可进行锁消除、锁粗化、锁优化等(当能证明无并发访问或已外部同步时)。
wait/notify/notifyAll
Object.wait()、notify()、notifyAll()必须在持有对象监视器(即在synchronized块内)时调用,用于线程间协作(条件等待/通知)。wait()会释放锁并进入等待队列;notify()唤醒等待队列中的一个线程(被唤醒线程在重新获得锁后继续)。
注意与陷阱
- 锁粒度:避免用过大锁(如
synchronized在方法头部锁住大量操作),谨慎使用String或装箱对象作为锁(可能会导致多个实例共用同一锁或锁被外部持有)。 - 死锁:多线程锁顺序不当可能死锁;设计时谨防。
- 性能:在高并发下可考虑使用
java.util.concurrent包(ReentrantLock、ConcurrentHashMap、AtomicXxx)等更细粒度、高性能的并发工具。
二、面向对象编程(OOP)
1. 面向对象的三大特性是什么?
- 封装(Encapsulation):隐藏实现细节,只暴露必要接口。
- 继承(Inheritance):子类复用父类属性和方法,扩展功能。
- 多态(Polymorphism):同一接口,不同实现。表现为方法重写、方法重载。
2. 封装的作用是什么?
- 隐藏对象内部实现细节,只暴露必要接口。
- 提高代码复用性、安全性,避免数据被随意修改。
- 例如:类的成员变量用
private,提供getter/setter访问。
3. 多态的优点是什么?
- 接口统一:不同实现类可通过父类/接口操作。
- 可扩展性强:新增子类不影响原有代码。
- 解耦:调用方只依赖抽象,而不依赖具体实现。
- 例:
List list = new ArrayList();,后续可换成LinkedList。
4. 重写方法时返回值能否不同?
- 不能完全不同。
- Java 允许 协变返回类型:子类方法的返回值类型可以是父类方法返回值的子类。
- 例如:父类返回
Number,子类可以返回Integer。
5. 构造函数能否被继承?
- 不能继承,因为构造函数名必须与类名相同。
- 但子类可通过
super(...)调用父类构造方法。
6. 构造函数能否 private?
- 可以,常见于单例模式(饿汉/懒汉/枚举单例)。
- 作用:禁止外部
new,只能通过类提供的方法获取实例。
7. 单例模式的几种实现方式?
- 饿汉式(类加载即实例化,线程安全,但可能浪费内存)。
- 懒汉式(延迟加载,需加
synchronized保证线程安全)。 - 双重检查锁(DCL,volatile + synchronized,性能优)。
- 静态内部类(推荐,利用类加载机制,线程安全)。
- 枚举单例(最佳实践,防止反射和反序列化攻击)。
8. 饿汉式和懒汉式单例的区别?
- 饿汉式:类加载时实例化 → 线程安全,启动时可能浪费内存。
- 懒汉式:第一次使用时才实例化 → 节省资源,但需加锁保证线程安全。
9. 为什么要使用内部类?
- 内部类可以 访问外部类的私有成员。
- 更好地组织代码,使逻辑更紧密。
- 在需要回调或事件监听时常用。
10. 成员内部类、局部内部类、静态内部类的区别?
- 成员内部类:依附于外部类实例,可以访问外部类实例变量。
- 局部内部类:定义在方法内部,作用范围仅在方法内。
- 静态内部类:不依赖外部类实例,只能访问外部类静态成员。
11. Java 中对象的创建方式有哪些?
new关键字。- 反射
Class.newInstance()。 Constructor.newInstance()。- 通过
clone()。 - 通过反序列化
ObjectInputStream.readObject()。
12. 对象之间的浅拷贝和深拷贝的区别?
- 浅拷贝:只复制对象本身,引用字段仍指向同一对象。
- 深拷贝:连引用对象也复制一份,完全独立。
13. clone() 方法的原理?
- 来自
Object类,默认是 浅拷贝。 - 必须实现
Cloneable接口,否则抛出CloneNotSupportedException。 - 可重写
clone()实现深拷贝。
14. 为什么 Java 不支持多继承?
- 为了避免 菱形继承问题(多个父类方法冲突)。
- Java 使用 接口 来实现多继承特性。
15. Java 是值传递还是引用传递?
- Java 只有值传递。
- 对象参数传递的是 引用的副本(值),但指向同一对象。
16. 方法参数传递时是如何处理对象的?
- 方法内修改对象的字段 → 会影响外部对象。
- 方法内给引用变量重新赋值 → 不会影响外部对象。
17. 封装性体现在哪些方面?
- 使用
private修饰成员变量。 - 提供
getter/setter控制访问权限。 - 使用访问修饰符(public/protected/default/private)控制类和方法的可见性。
18. 为什么需要继承?
- 代码复用(减少重复)。
- 提高扩展性(子类扩展父类功能)。
- 实现多态,解耦业务逻辑。
19. 重写 Object 类的 toString 方法的意义?
- 提高可读性,方便调试和日志记录。
- 默认
toString()打印类名+hashcode,不直观。 - 重写后可打印对象核心字段信息。
20. 重写 Object 类的 equals 方法时需要注意什么?
- 必须满足 自反性、对称性、传递性、一致性。
- 与
hashCode()保持一致:相等对象必须有相同的 hashCode。 - 避免
NullPointerException。
三、异常处理
1. Java 的异常体系结构?
- 顶层类:
Throwable- Error(错误,程序无法处理,通常由 JVM 抛出)
- 如:
OutOfMemoryError、StackOverflowError
- 如:
- Exception(异常,程序可处理)
- Checked Exception(受检异常,编译期检查)
- 如:
IOException、SQLException
- 如:
- Unchecked Exception(运行时异常,编译器不强制处理)
- 如:
NullPointerException、ArrayIndexOutOfBoundsException
- 如:
- Checked Exception(受检异常,编译期检查)
- Error(错误,程序无法处理,通常由 JVM 抛出)
👉 结构图:
1 | Throwable |
2. Checked Exception 和 Unchecked Exception 的区别?
- Checked Exception(受检异常)
- 必须显式处理(try-catch 或 throws 声明)。
- 编译器强制检查,否则无法通过编译。
- 典型场景:IO、数据库、网络调用。
- Unchecked Exception(非受检异常 / 运行时异常)
- 不强制处理,编译器不检查。
- 通常由程序逻辑错误导致。
- 典型场景:NPE、除零、数组越界。
3. throw 和 throws 的区别?
- throw
- 用于方法体内部,抛出具体的异常对象。
- 语法:
throw new Exception("msg");
- throws
- 用于方法声明处,标识该方法可能抛出的异常类型。
- 语法:
public void test() throws IOException {}
👉 总结:
throw= 抛出异常实例。throws= 声明可能抛出的异常类型。
4. try-catch-finally 的执行顺序?
try块先执行。- 如果发生异常,跳到对应
catch执行。 finally一定会执行(除非System.exit())。- 如果
try或catch有return,会先执行finally,再返回结果。
5. finally 中的 return 会覆盖 try 中的 return 吗?
✅ 会覆盖。
- 如果
try中有return,但finally中也有return,最终返回的是finally的结果。
👉 因此实际开发中 不推荐在 finally 中写 return。
6. try-with-resources 的作用?
- Java 7 引入,简化资源管理(如流、数据库连接)。
- 自动关闭实现了
AutoCloseable或Closeable接口的资源。 - 避免忘记
finally { resource.close(); }。
示例:
1 | try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) { |
7. 自定义异常类如何实现?
- 继承
Exception(Checked)或RuntimeException(Unchecked)。 - 提供构造方法:无参、带 message、带 cause。
示例:
1 | public class MyException extends RuntimeException { |
8. Error 和 Exception 的区别?
- Error:系统级错误,JVM 无法恢复,程序不应该捕获。
- 如
OutOfMemoryError、StackOverflowError。
- 如
- Exception:应用级异常,程序应该处理。
- 如
IOException、SQLException、NullPointerException。
- 如
9. NullPointerException 常见场景有哪些?
- 调用空对象的方法:
obj.toString()。 - 访问空数组:
arr.length。 - 访问空集合:
list.size()。 - 自动拆箱:
Integer num = null; int n = num;。 - Map 查找为 null,再调用方法:
map.get("key").toString()。 - 依赖注入/未初始化的对象。
10. 运行时异常需要捕获吗?为什么
- 通常不需要显式捕获,因为它们多由逻辑错误导致(如 NPE、除零)。
- 捕获运行时异常并不能解决问题,应该通过修改代码逻辑避免。
- 但在某些关键服务(如 API 层)可以用统一异常处理(如 Spring @ControllerAdvice)拦截,避免程序直接崩溃。
四、集合框架(Collections)
1. Collection 和 Collections 的区别?
- Collection:集合接口,是所有集合类的父接口(List、Set)。
- Collections:工具类,提供操作集合的静态方法(排序、搜索、同步包装等)。
2. List、Set、Map 的区别?
- List:有序、可重复,按索引访问(ArrayList、LinkedList)。
- Set:无序、不可重复,基于哈希或树(HashSet、TreeSet)。
- Map:键值对存储,Key 不可重复,Value 可重复(HashMap、TreeMap)。
3. ArrayList 和 LinkedList 的区别?
- ArrayList:基于动态数组,查询快(O(1)),增删慢(O(n))。
- LinkedList:基于双向链表,增删快(O(1)),查询慢(O(n))。
4. HashMap 的底层实现?
- JDK 1.7:数组 + 链表。
- JDK 1.8:数组 + 链表 + 红黑树(链表长度 ≥ 8 时转为树)。
5. HashMap 1.7 和 1.8 的区别?
- 1.7:数组 + 链表,采用头插法,易出现并发死循环。
- 1.8:数组 + 链表/红黑树,尾插法,避免死循环,提高性能。
- 1.8 引入红黑树,查找效率从 O(n) 优化为 O(log n)。
6. HashMap 如何解决哈希冲突?
- 方法:拉链法(链表)+ 红黑树。
- 冲突时,将元素挂到链表/树上。
7. HashSet 的底层实现?
- 基于 HashMap 实现。
HashSet的元素存储在HashMap的 key 上,value 为一个固定对象PRESENT。
8. ConcurrentHashMap 的底层实现?
- JDK 1.7:分段锁(Segment + HashEntry)。
- JDK 1.8:CAS + synchronized,数据结构与 HashMap 类似,数组 + 链表 + 红黑树。
9. Hashtable 和 HashMap 的区别?
- 线程安全性:Hashtable 是同步的,HashMap 不是。
- null:Hashtable 不允许 key、value 为 null;HashMap 允许一个 null key 和多个 null value。
- 效率:HashMap 更高效。
10. TreeMap 和 HashMap 的区别?
- TreeMap:基于红黑树,有序(按 Key 排序)。
- HashMap:基于哈希表,无序。
11. WeakHashMap 的特点?
- Key 使用 弱引用,当没有强引用指向该 Key 时,会被 GC 回收。
- 常用于缓存。
12. CopyOnWriteArrayList 的应用场景?
- 写时复制:写操作时复制新数组,读操作不加锁。
- 适用于 读多写少 的并发场景,比如缓存、订阅列表。
13. LinkedHashMap 的底层原理?
- 基于 HashMap + 双向链表。
- 保证插入顺序(或 LRU 顺序)。
14. Map 的 key 是否可以为 null?
- HashMap:允许一个 null key。
- Hashtable / TreeMap:不允许 null key。
15. ArrayList 扩容机制?
- 初始容量 10,超过容量时扩容为原来的 1.5 倍。
- 通过
Arrays.copyOf()实现数组复制。
16. Vector 和 ArrayList 的区别?
- Vector:线程安全(方法加 synchronized),扩容为 2 倍。
- ArrayList:非线程安全,扩容为 1.5 倍。
17. PriorityQueue 的底层实现?
- 基于 二叉小顶堆,保证队头元素是最小值。
- 插入、删除操作时间复杂度 O(log n)。
18. BlockingQueue 的几种实现类?
- ArrayBlockingQueue:数组结构,有界。
- LinkedBlockingQueue:链表结构,可选容量。
- PriorityBlockingQueue:优先级队列。
- DelayQueue:延时队列。
- SynchronousQueue:不存储元素,直接移交。
19. HashMap 死循环问题出现在哪个版本?
- JDK 1.7 多线程扩容时,链表采用头插法,可能形成环,导致死循环。
- JDK 1.8 使用尾插法,解决该问题。
20. fail-fast 和 fail-safe 的区别?
- fail-fast:迭代过程中结构被修改,会抛出
ConcurrentModificationException。如:ArrayList、HashMap。 - fail-safe:迭代基于副本,不会抛异常。如:CopyOnWriteArrayList、ConcurrentHashMap。
五、并发编程(JUC)
1. Java 中的线程生命周期?
- NEW(新建):
new Thread()后,还没调用start()。 - RUNNABLE(就绪/运行中):调用
start(),等待 CPU 调度。 - BLOCKED(阻塞):等待锁。
- WAITING(无限等待):调用
wait()、join(),需其他线程唤醒。 - TIMED_WAITING(限时等待):调用
sleep()、wait(timeout)。 - TERMINATED(终止):线程执行完毕或异常退出。
2. Runnable 和 Callable 的区别?
- Runnable:无返回值,不能抛出受检异常。
- Callable:有返回值(Future 获取),可抛出异常。
3. ThreadPoolExecutor 的参数有哪些?
- corePoolSize:核心线程数。
- maximumPoolSize:最大线程数。
- keepAliveTime:非核心线程存活时间。
- unit:时间单位。
- workQueue:任务队列(BlockingQueue)。
- threadFactory:线程工厂(自定义线程名)。
- handler:拒绝策略。
4. 线程池的拒绝策略有哪些?
- AbortPolicy(默认):抛
RejectedExecutionException。 - CallerRunsPolicy:任务交给提交任务的线程执行。
- DiscardPolicy:直接丢弃任务。
- DiscardOldestPolicy:丢弃队列里最旧的任务,再尝试提交。
5. 创建线程的四种方式?
- 继承 Thread 类。
- 实现 Runnable 接口。
- 实现 Callable + FutureTask。
- 使用线程池(ExecutorService)。
6. synchronized 的底层实现原理?
- 基于 对象头 (Mark Word) 和 Monitor(管程)。
- 进入同步块时,尝试获取对象的 Monitor,获取不到则阻塞。
- 底层依赖 JVM 指令:
monitorenter、monitorexit。
7. synchronized 和 ReentrantLock 的区别?
- 锁类型:synchronized 是 JVM 级别,ReentrantLock 是 JUC 提供的 API。
- 功能:ReentrantLock 支持公平锁/非公平锁、可中断、尝试加锁、条件变量。
- 可重入性:两者都可重入。
- 性能:synchronized 在 JDK1.6 之后优化(偏向锁、轻量级锁)性能已提升。
8. 公平锁和非公平锁的区别?
- 公平锁:按照等待顺序获取锁。
- 非公平锁:允许“插队”,减少上下文切换,提高吞吐量。
- ReentrantLock 默认非公平。
9. AQS 的原理?
- AQS(AbstractQueuedSynchronizer) 是构建锁和同步器的框架。
- 内部维护一个 state(表示资源数量)和 FIFO 双向队列。
- 通过 CAS 修改 state,失败则将线程加入等待队列,挂起阻塞,等资源释放再唤醒。
10. CountDownLatch 的应用场景?
- 倒计数器:一个线程等待多个线程完成后再执行。
- 例子:主线程等待多个子任务执行完毕。
11. CyclicBarrier 的应用场景?
- 栅栏:一组线程互相等待,直到所有线程到达屏障点再继续。
- 例子:多人游戏,所有玩家都准备好再开始。
12. Semaphore 的应用场景?
- 信号量:控制并发访问的线程数量。
- 例子:停车场限制车位数、数据库连接池。
13. Exchanger 的应用场景?
- 线程间数据交换:两个线程配对交换数据。
- 例子:生产者和消费者交换缓冲区。
14. volatile 能保证原子性吗?
- 不能,只能保证可见性和禁止指令重排。
- 例子:
count++在多线程下仍然不安全。
15. CAS 的原理?
- Compare And Swap:比较内存值是否为预期值,如果是则更新,否则重试。
- 底层依赖 CPU 的 cmpxchg 指令,保证原子性。
16. ABA 问题是什么?怎么解决?
- 问题:CAS 只比较值,无法感知值被改过。例如 A→B→A。
- 解决:使用 版本号/时间戳(AtomicStampedReference、AtomicMarkableReference)。
17. ThreadLocal 的作用及原理?
- 作用:为每个线程提供独立变量副本,避免共享数据冲突。
- 原理:Thread 内部有个 ThreadLocalMap,以 ThreadLocal 为 key 存储变量。
18. ThreadLocal 内存泄漏的原因?
- ThreadLocalMap 的 key 是 弱引用,可能被回收,value 却还存在,导致泄漏。
- 解决:用完及时
remove()。
19. JUC 中的原子类有哪些?
- 基本类型原子类:AtomicInteger、AtomicLong、AtomicBoolean。
- 引用类型原子类:AtomicReference、AtomicStampedReference。
- 数组原子类:AtomicIntegerArray、AtomicLongArray。
- 对象属性更新器:AtomicIntegerFieldUpdater 等。
20. 乐观锁和悲观锁的区别?
- 悲观锁:假设会发生冲突,先加锁再操作(synchronized、ReentrantLock)。
- 乐观锁:假设不会冲突,先操作再检查(CAS)。
六、JVM & 内存模型
1. JVM 内存结构?
JVM 运行时主要分为:
- 堆(Heap):存放对象实例,垃圾回收的主要区域。
- 虚拟机栈(Stack):存放方法调用的局部变量、操作数栈、动态链接、返回地址。
- 本地方法栈(Native Method Stack):为 native 方法服务。
- 程序计数器(PC Register):记录线程当前执行的字节码位置。
- 方法区(Method Area / Metaspace):存放类元数据、常量池、静态变量、JIT 编译后的代码。
2. 堆和栈的区别?
- 堆:存放对象实例,线程共享,需垃圾回收。
- 栈:存放方法帧、局部变量,线程私有,生命周期随线程结束。
3. 堆内存的分代模型?
- 新生代(Young Generation):Eden + Survivor(S0/S1)。
- 老年代(Old Generation):存放长生命周期对象。
- 永久代/元空间:存放类元信息、常量池。
4. 新生代、老年代、永久代的区别?
- 新生代:对象新建时分配,大部分对象“朝生夕死”。
- 老年代:存活时间长或大对象直接进入。
- 永久代(JDK 7 及之前):存放类信息、静态变量。
- 元空间(JDK 8 之后):替代永久代,使用本地内存,避免 OOM: PermGen。
5. 元空间和永久代的区别?
- 永久代(PermGen):使用 JVM 内存,容易 OOM。
- 元空间(Metaspace):使用本地内存,理论上更大,更灵活。
6. 对象在堆中的分配过程?
- 在 Eden 区分配。
- Minor GC 时,幸存的对象进入 Survivor 区。
- 多次 Minor GC 仍存活 → 晋升老年代。
- 大对象(如大数组)可能直接进入老年代。
7. 对象什么时候进入老年代?
- 在 Survivor 区经过多次 Minor GC,达到晋升阈值(默认 15 次)。
- 大对象超过阈值,直接进入老年代。
- Survivor 区放不下,直接进入老年代。
8. JVM 的垃圾回收算法?
- 标记-清除:标记存活对象,清除未标记。缺点:碎片多。
- 标记-整理:标记存活对象并移动,整理内存,避免碎片。
- 复制算法:将活对象复制到新区域,清空旧区域,适合新生代。
9. 垃圾收集器有哪些?
- Serial(单线程,适合小内存)。
- ParNew(多线程版 Serial)。
- Parallel Scavenge(吞吐量优先)。
- CMS(低延迟,响应快)。
- G1(区域化,均衡停顿时间)。
- ZGC、Shenandoah(超低延迟,适合大内存场景)。
10. CMS 和 G1 的区别?
- CMS:并发收集,低延迟,但可能产生内存碎片。
- G1:基于分区(Region),可预测停顿时间,整理内存,适合大堆内存。
11. Minor GC 和 Full GC 的区别?
- Minor GC:回收新生代,速度快,频繁。
- Full GC:回收整个堆(新生代 + 老年代 + 元空间),耗时长,频率低。
12. 什么是 Stop The World?
- GC 期间,所有工作线程必须停下来等待,造成应用暂停(STW)。
- STW 是不可避免的,只能尽量减少时长。
13. JVM 常见的调优参数?
-Xms初始堆大小。-Xmx最大堆大小。-Xmn新生代大小。-XX:SurvivorRatioEden:Survivor 比例。-XX:+PrintGCDetails打印 GC 日志。-XX:+UseG1GC指定垃圾收集器。-XX:MaxMetaspaceSize元空间大小。
14. 类加载的双亲委派机制?
- 过程:加载请求先交给父加载器,如果父加载器无法加载,再由子加载器加载。
15. 双亲委派的好处?
- 避免类重复加载。
- 保证核心类(如 java.lang.String)不会被篡改。
16. 类加载器的种类?
- Bootstrap ClassLoader:加载核心类库。
- Extension ClassLoader:加载扩展类。
- Application ClassLoader:加载用户应用类。
- 自定义 ClassLoader:用户自己扩展。
17. OOM 的常见类型?
- Java heap space:堆溢出。
- GC overhead limit exceeded:GC 回收无效。
- Metaspace:元空间溢出。
- Direct buffer memory:直接内存溢出。
- Unable to create new native thread:线程数过多。
18. 内存泄漏和内存溢出的区别?
- 内存泄漏:不再使用的对象仍然被引用,不能被 GC 回收。
- 内存溢出:内存不足,申请不到更多空间。
19. finalize() 方法的作用?
- 对象被 GC 前执行一次清理逻辑。
- 缺点:执行不确定、性能差,JDK9 开始废弃,推荐使用 try-with-resources/AutoCloseable。
20. Java 内存模型(JMM)的核心内容?
- 定义了多线程如何共享内存。
- 关键点:
- 主内存:存放共享变量。
- 工作内存:每个线程的本地副本。
- happens-before 规则:保证可见性、有序性。
- volatile、synchronized、final 提供内存语义。
七、泛型、注解、反射
1. 什么是泛型?
定义:泛型(Generic)是 参数化类型,让类、接口、方法可以操作不同类型的数据,而不用编写重复代码。
好处:
- 类型安全(编译期检查,避免
ClassCastException)。 - 代码复用(相同逻辑可适配多种类型)。
- 可读性高(类型明确)。
- 类型安全(编译期检查,避免
例子:
1
2List<String> list = new ArrayList<>();
list.add("abc"); // 编译期检查类型
2. 泛型的类型擦除机制?
原理:Java 的泛型是 伪泛型,只在 编译期有效,编译后泛型信息被擦除(Type Erasure)。
结果:
List<String>和List<Integer>在运行时是同一个类型:List。- 泛型方法的类型参数会被擦除为 上界(extends)或 Object。
例子:
1
2
3List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
System.out.println(l1.getClass() == l2.getClass()); // true
3. 泛型通配符 ? extends 和 ? super 的区别?
? extends T:表示类型的 上界,接受T及其子类。- 适合 生产者(Producer),只读,不可写。
? super T:表示类型的 下界,接受T及其父类。- 适合 消费者(Consumer),可以写入 T 及其子类对象。
- 口诀:PECS(Producer Extends, Consumer Super)。
4. 注解的作用是什么?
- 作用:注解(Annotation)是 元数据,为代码提供说明信息。
- 分类:
- 编译期使用(如
@Override,@SuppressWarnings)。 - 运行时使用(结合反射,驱动框架逻辑,如
@Autowired)。
- 编译期使用(如
- 应用场景:框架开发(Spring、Hibernate)、编译器检查、文档生成等。
5. 元注解有哪些?
Java 提供了四个元注解(注解的注解):
@Target—— 指定注解可用的位置(类、方法、字段、参数等)。@Retention—— 指定注解的生命周期(源码/编译期/运行时)。@Documented—— 是否包含在 javadoc 中。@Inherited—— 子类是否能继承父类的注解。
6. 自定义注解如何实现?
定义:
1
2
3
4
5
public MyAnnotation {
String value();
}使用:
1
2
public void foo() {}解析(反射):
1
2
3Method m = clazz.getMethod("foo");
MyAnnotation ann = m.getAnnotation(MyAnnotation.class);
System.out.println(ann.value());
7. 反射的作用?
- 定义:反射(Reflection)允许程序在 运行时 获取类的信息,并操作其属性、方法、构造器。
- 作用:
- 框架(如 Spring、MyBatis)自动注入和配置。
- 动态代理。
- 动态加载类。
8. 反射的性能问题?
- 原因:
- 反射调用方法比直接调用多了安全检查、方法查找,性能差 10~20 倍。
- 优化:
- 使用
setAccessible(true)关闭安全检查。 - 使用
MethodHandle(JDK7+)。 - 使用
LambdaMetafactory(JDK8+)做动态代理。
- 使用
9. 反射能否访问私有字段?
能,需要
setAccessible(true):1
2
3Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(obj, "newValue");注意:JDK 9 模块化后,跨模块访问可能会有
InaccessibleObjectException。
10. 动态代理的两种实现方式?
- JDK 动态代理:基于
InvocationHandler + Proxy,只能代理 接口。 - CGLIB 动态代理:基于 ASM 字节码生成,继承目标类,能代理 类。
11. JDK 动态代理和 CGLIB 的区别?
- JDK 动态代理:
- 代理接口。
- JDK 自带,无需依赖。
- 性能略低。
- CGLIB:
- 代理类(生成子类)。
- 不能代理
final类/方法。 - 性能更高(字节码生成)。
12. 反射如何创建对象?
使用 Class 的
newInstance()(已废弃):1
Object obj = clazz.newInstance();
使用 Constructor:
1
2Constructor<?> ctor = clazz.getConstructor(String.class);
Object obj = ctor.newInstance("param");
13. Class.forName 和 ClassLoader.loadClass 的区别?
- Class.forName:加载并 初始化 类(会执行静态代码块)。
- ClassLoader.loadClass:仅加载,不初始化(延迟执行
<clinit>)。
14. 注解和反射结合能实现哪些功能?
- IOC / DI(依赖注入,Spring 的
@Autowired)。 - ORM 框架(MyBatis 的
@Mapper,JPA 的@Entity)。 - AOP(
@Aspect)。 - 配置驱动开发(
@Configuration,@Value)。
15. 注解处理器(APT)的作用?
- APT(Annotation Processing Tool):编译期扫描注解并生成代码。
- 典型应用:
- Dagger2(依赖注入)。
- ButterKnife(视图注入)。
- Lombok(自动生成 getter/setter/toString 等)。
八、Java 8+ 新特性
- Lambda 表达式的作用?
- 函数式接口有哪些?
- Stream 流的常见操作?
- Optional 的作用?
- CompletableFuture 的作用?
- Java 8 接口的默认方法和静态方法?
- forEach 和传统 for 的区别?
- parallelStream 的原理?
- Java 9 模块化系统(Jigsaw)是什么?
- Java 14 的 switch 表达式增强?
- Java 16 的 record 特性?
- Java 17 的 sealed class 特性?
- var 关键字是什么?
- ZGC 的特性?
- Project Loom 的虚拟线程是什么?
九、IO
一、Java IO 基础与流(1–20)
1. Java IO 的总体体系结构是什么?
- IO 的两大基类:
- 字节流:
InputStream、OutputStream(处理二进制数据)。 - 字符流:
Reader、Writer(处理文本字符)。
- 字节流:
- 按功能划分:
- 节点流(直接对接数据源,如
FileInputStream、FileReader)。 - 处理流(包装其他流,如
BufferedReader、DataInputStream)。
- 节点流(直接对接数据源,如
- 核心设计模式:装饰器模式(Decorator Pattern),通过层层包装增强功能。
2. InputStream / OutputStream 的常见子类有哪些?
- InputStream:
FileInputStream、BufferedInputStream、DataInputStream、ObjectInputStream、ByteArrayInputStream。 - OutputStream:
FileOutputStream、BufferedOutputStream、DataOutputStream、ObjectOutputStream、ByteArrayOutputStream。
3. Reader / Writer 的常见子类有哪些?
- Reader:
FileReader、BufferedReader、InputStreamReader、CharArrayReader、StringReader。 - Writer:
FileWriter、BufferedWriter、OutputStreamWriter、CharArrayWriter、StringWriter。
4. 字节流与字符流的本质区别和使用场景?
- 字节流(8bit):面向二进制数据,如图片、音视频、压缩文件。
- 字符流(16bit,基于 Unicode):面向文本,自动处理字符编码。
- 联系:字符流常常通过
InputStreamReader/OutputStreamWriter包装字节流并指定编码。
5. 为什么要有缓冲流?
- 原因:文件 IO 属于系统调用,每次读写代价高。
- 作用:减少磁盘交互次数,提高性能。
- 例子:
BufferedReader.readLine()能按行读取,效率远高于FileReader.read()。
6. flush() 与 close() 的区别?
- flush():将内存缓冲区数据立刻写入目标(但不关闭流)。
- close():先执行
flush(),然后释放底层资源。 - 注意点:
- 若忘记
flush(),可能导致部分数据未写入文件。 close()后再调用write()会抛异常。
- 若忘记
7. try-with-resources 的工作原理?
- 语法糖:
try (Resource r = ...) {},要求资源实现AutoCloseable。 - 原理:编译器自动生成
finally { r.close() }。 - suppressed exceptions:如果
try和close()都抛异常,关闭时的异常会被标记为 suppressed,可通过Throwable.getSuppressed()获取。
8. 装饰器模式在 Java IO 中如何体现?
例子:
1
2
3Reader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("a.txt"), StandardCharsets.UTF_8));本质:每层流只关心自己的功能(缓冲、解码、行读取),通过链式组合增强能力。
9. File 类的能力与限制?
- 能做的:文件/目录的元数据操作(路径、存在性、权限、大小、修改时间等)。
- 不能做的:不支持读写文件内容,需结合
InputStream/OutputStream。
10. RandomAccessFile 的用途?
- 功能:支持随机读写,内部有一个文件指针,可
seek()到任意位置读写。 - 典型场景:日志文件、数据库存储文件。
- 并发问题:不是线程安全的,多线程写需加锁。
11. FileInputStream 与 FileReader 的区别?
- FileInputStream:字节流,直接读二进制。
- FileReader:字符流,内部用
InputStreamReader并默认使用平台编码。 - 使用建议:
- 非文本 →
FileInputStream。 - 文本(需编码处理) →
FileReader(但最好用InputStreamReader显式指定编码)。
- 非文本 →
12. 常见文件拷贝方式性能对比
- 裸 read/write:逐字节拷贝,最慢。
- 带缓冲:
BufferedInputStream/BufferedOutputStream,性能提升 5~10 倍。 - NIO
FileChannel.transferTo():利用零拷贝,性能最高。
13. 如何安全处理字符编码?
方式:使用
InputStreamReader/OutputStreamWriter并显式指定编码。例子:
1
new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8);
14. 字符编码常见坑?
- UTF-8 vs GBK 混用:解码错误,出现乱码。
- BOM(Byte Order Mark):部分 UTF-8 文件前 3 字节 BOM 可能导致解析失败。
- 截断:写入时按字节截断 UTF-8 多字节字符 → 乱码。
- 解决:统一编码(UTF-8),避免隐式编码。
15. 文件追加(append)方式?
实现:
1
new FileOutputStream("a.txt", true);
原子性问题:多线程写入需加锁,否则可能交叉覆盖。
16. 如何判断并处理文件状态?
- 存在性:
file.exists()。 - 权限:
file.canRead(),file.canWrite()。 - 占用:只能通过打开尝试,若被占用会抛异常。
17. File.mkdirs() 与 mkdir() 区别?
- mkdir():只创建当前目录,父目录不存在则失败。
- mkdirs():会递归创建所有不存在的父目录。
- 返回值:成功返回
true,否则false。
18. 临时文件(createTempFile)的用途?
- 方法:
File.createTempFile("prefix", ".tmp")。 - 特点:自动生成唯一文件名。
- 删除策略:
file.deleteOnExit()JVM 退出时删除,但依赖于正常退出。
19. 文件的读写锁与并发访问?
- Java NIO:
FileChannel.lock()提供文件锁(独占锁、共享锁)。 - 注意:文件锁是 OS 级别,跨进程生效;性能开销较大,不适合频繁操作。
20. 流关闭异常处理最佳实践
- 传统写法:try-finally 手动关闭流。
- 推荐:try-with-resources,避免遗漏关闭。
- 关闭异常:只 log,不影响主要业务异常处理。
二、Java IO 设计与模式(21–40)
- InputStream/OutputStream 的装饰器链常见组合(Buffered -> GZIP -> Cipher 等)。
- PushbackInputStream、SequenceInputStream 的作用与应用场景。
- PipedInputStream / PipedOutputStream(管道流)的使用与线程注意点。
- FilterInputStream/FilterOutputStream 的设计与扩展。
- IO 与异常处理:IOException 的常见子类与处理策略。
- 流复制的常见实现模板(模板方法式代码)。
- NIO 引入前的 IO 局限性(可扩展性、线程模型)。
- 如何实现一个带超时的读操作(Socket/Channel)?
- 流式处理与内存友好型处理(流式处理大文件)实践。
- Base64 编解码在流中的高效集成方法。
- 加密/解密流(CipherInputStream/CipherOutputStream)如何正确关闭?
- GZIPInputStream/GZIPOutputStream 的压缩流使用注意事项。
- ObjectInputStream/ObjectOutputStream 的工作机制(类元数据、句柄表)。
- 如何实现跨语言的序列化兼容(JSON/Protobuf/Avro)?
- 实现自定义 InputStream 子类时需要注意什么(read 方法语义)?
- 设计用于日志写入的高吞吐 IO 模式(异步批量写)。
- 流复制时如何统计速率与进度(带进度回调)?
- 如何安全处理二进制文件(流边界、magic header)?
- 大对象/大数组写入流时的内存优化策略(分块、流化)。
- 如何实现可重入/可恢复的断点续传文件写入?
三、文件系统、锁与操作(41–60)
- Java 中文件锁 FileLock 的类型(共享/独占)及实现原理。
- FileLock 的局限性(跨 JVM、跨 OS 行为差异)。
- 文件描述符泄露的常见原因与定位方法。
- 文件句柄上限(ulimit)对 Java 服务的影响与排查。
- 硬链接与软链接在 Java 中如何区分与操作?
- MappedByteBuffer(内存映射文件)的优劣与风险(内存回收、文件锁)。
- 大文件分片读取与并发合并策略。
- 如何高效统计大日志文件中某条件的行数(分块 + 并行)?
- 文件系统缓存(PageCache)对读写性能的影响机制。
- fsync / FileDescriptor.sync 在持久化保障上的作用。
- 文件顺序写与随机写的性能差异与优化建议。
- 磁盘类型(SSD vs HDD)对 IO 策略的影响。
- 原子重命名(renameTo/Files.move)的跨平台差异。
- 文件监控(WatchService)的实现限制与替代方案。
- 处理日志切割(rotation)时的文件句柄管理策略。
- 如何实现零停机部署中对文件的平滑迁移?
- 软删除(标记删除)与物理删除的 IO 考量。
- 备份/快照策略对 IO 的影响(冷备 vs 热备)。
- 大文件校验(MD5/SHA)在流式处理中的实现。
- 文件系统一致性问题(写入后立即读取到不同步)如何诊断。
四、序列化与反序列化(61–75)
- Java 原生序列化(Serializable)的机制和对象写入格式。
- serialVersionUID 的作用、自动生成与兼容性策略。
- transient 字段、static 字段在序列化中的处理。
- Externalizable 与 Serializable 的区别与使用场景。
- Java 序列化的安全风险(反序列化漏洞)与防护措施。
- 高性能二进制序列化方案对比:Kryo、Protostuff、Protobuf、Avro。
- JSON 和二进制序列化的权衡(可读性 vs 性能/大小)。
- 如何实现可演化的序列化协议(向前/向后兼容)?
- 对象图序列化时循环引用的处理(句柄机制)。
- 对象序列化性能调优要点(缓冲、对象重用)。
- 在分布式系统中如何管理序列化策略(跨服务版本)?
- 自定义序列化(writeObject/readObject)常见陷阱。
- 序列化时如何处理类加载器问题?
- 大对象序列化时的内存与 GC 风险如何降低?
- 使用序列化作为缓存(Redis/Memcached)时的注意事项。
五、NIO 深入:Buffer / Channel / Selector(76–100)
1. NIO 三大核心(Buffer、Channel、Selector)
- Buffer:数据容器,所有读写操作都要通过它完成。
- Channel:通道,连接数据源与 Buffer,支持双向读写。
- Selector:多路复用器,检测多个 Channel 的事件(读、写、连接等),实现非阻塞 IO。
2. ByteBuffer 的 position / limit / capacity
- capacity:缓冲区的总容量,创建时确定。
- position:当前读写指针,表示下一个操作的位置。
- limit:可操作的边界,写模式下=capacity,读模式下=写入的数据量。
3. flip(), clear(), rewind(), compact()
- flip():切换写 → 读模式(limit=position,position=0)。
- clear():清空缓冲,准备写(position=0,limit=capacity)。
- rewind():重新读(position=0,limit 不变)。
- compact():未读数据前移,position 指向未读数据末尾。
4. DirectByteBuffer vs HeapByteBuffer
- HeapByteBuffer:数据在 JVM 堆内存,GC 管理,拷贝到内核态多一份。
- DirectByteBuffer:数据在堆外内存,IO 时零拷贝,性能更高,但分配释放代价大。
5. ByteBuffer slice() / asReadOnlyBuffer()
- slice():共享底层数据,新的缓冲区 position=0,limit=剩余。
- asReadOnlyBuffer():生成只读视图,修改会抛
ReadOnlyBufferException。
6. Scatter/Gather IO
- Scatter Read:一个 Channel → 多个 Buffer(适合固定头+体)。
- Gather Write:多个 Buffer → 一个 Channel(避免拼接)。
7. FileChannel 的关键方法
- position():获取/设置文件指针。
- size():返回文件大小。
- truncate(long size):截断文件。
- force(boolean metaData):强制写入磁盘。
8. MappedByteBuffer(内存映射)
- 创建:
FileChannel.map(mode, position, size) - 优点:直接操作内存映射,效率高。
- 释放:JDK 无直接 API,通常通过反射
Cleaner.clean(),需注意内存泄露。
9. Channel vs Stream
- Stream:单向、阻塞。
- Channel:双向、可非阻塞,可和 Selector 配合。
10. Selector 工作流程
- 注册:
channel.register(selector, ops) - 选择:
selector.select(timeout) - 遍历:
selectedKeys()遍历处理 - 取消:调用
key.cancel()
11. Selector 底层实现
- Linux:epoll
- Windows:select
- macOS:kqueue
影响性能和可扩展性(epoll 适合高并发)。
12. 非阻塞 Channel 的 read/write 返回值
read:
0:读取字节数
0:无数据
-1:EOF(关闭)
write:
0:写入字节数
0:内核缓冲区满
13. 粘包/拆包问题
原因:TCP 流式传输无边界。
解决策略:
- 固定长度(定长协议)
- 分隔符(如
\n) - 长度前置(常见)
14. 超时断开 & 心跳检测
- 超时断开:在
select(timeout)中检测长时间无读写的连接。 - 心跳检测:客户端/服务端定期发送
PING,无响应则断开。
15. Pipe 在 NIO 中的作用
- 单向通信(写端 → 读端),常用于线程间通信。
- 但性能不如 BlockingQueue。
16. 多 Selector / 多 Reactor
- 单 Reactor:一个线程负责 Selector。
- 多 Reactor:主 Reactor 处理 accept,子 Reactor 处理 read/write。
- 多 Selector:线程池分担 Channel 负载。
17. ByteBuffer 多线程问题
- 不是线程安全的。
- 解决:每个线程独立 Buffer,或外部加锁。
18. DirectByteBuffer 避免泄露
- 使用
ByteBuffer.allocateDirect()代价大。 - 建议:使用
-XX:+UseLargePages、池化管理(Netty 的PooledByteBufAllocator)。
19. Selector selectedKeys 正确用法
- 遍历
selectedKeys() - 处理完一个 key →
iterator.remove() - 否则下次循环仍会触发。
20. NIO 高效读写循环模板
1 | while (true) { |
21. transferTo/transferFrom(零拷贝)
- 直接在内核空间完成文件复制(减少用户态拷贝)。
- 局限:Windows 一次传输大小有限,需循环。
22. FileChannel 高效复制
1 | try (FileChannel in = new FileInputStream(src).getChannel(); |
23. 高并发服务器瓶颈
- 单线程 Selector 性能不足。
- Buffer/Direct 内存泄露。
- 大量小包(TCP 粘拆包)。
- epoll 空轮询 bug(CPU 飙高)。
24. Windows vs Linux
- Linux:epoll,可伸缩到百万连接。
- Windows:select,fd 数量受限,扩展性差。
25. 简单 Echo Server 思路
- 创建
Selector - 注册
ServerSocketChannel - 循环
select() - 处理
accept→ 注册 SocketChannel - 处理
read→ 回写数据
⚡总结:NIO 的核心是 缓冲区管理(ByteBuffer)、多路复用(Selector)、零拷贝优化(FileChannel.transferTo/MappedByteBuffer)。
在高并发系统里(如 Netty),这些 API 会结合线程模型和内存池进一步优化。
六、Socket / TCP / UDP / WebSocket(101–140)
- Socket 的基本概念:端点、三元组/四元组(IP:port + peer)。
- Java 中 Socket、ServerSocket、DatagramSocket 的主要 API 区别。
- TCP 与 UDP 的核心差异(可靠性、有序性、连接性)。
- TCP 三次握手(SYN、SYN-ACK、ACK)与四次挥手流程详解。
- TIME_WAIT、CLOSE_WAIT、FIN_WAIT1/2 等 TCP 状态含义与产生原因。
- 半开连接(half-open)是什么,如何检测与恢复?
- TCP 的流量控制(窗口)与拥塞控制(慢启动、拥塞避免、快重传、快恢复)基础。
- Nagle 算法(TCP_NODELAY)的原理和在延迟场景下的影响。
- TCP 延迟确认(delayed ACK)对交互型应用的影响。
- Socket 选项 SO_TIMEOUT、SO_KEEPALIVE、SO_REUSEADDR、SO_REUSEPORT、SO_LINGER 含义与使用场景。
- backlog 参数(ServerSocket 构造或 listen)与 accept 队列(syn, accept 队列)区别。
- ephemeral port(短暂端口)与端口耗尽问题及解决办法。
- SYN flood 攻击的原理和防护(SYN cookies、firewall)。
- TCP 快速打开(TCP Fast Open)是什么,有何优劣?(简述)
- MTU、MSS 与 IP 分片对传输的影响与诊断方法。
- UDP 的组播(Multicast)与广播(Broadcast)机制与 Java 支持(MulticastSocket)。
- UDP 丢包、乱序、包大小限制(最佳实践)。
- UDP 穿透 NAT(STUN/ICE)的基本原理。
- 如何在 Java 中实现高性能 UDP 服务器?(NIO + DatagramChannel)
- WebSocket 协议基础(握手、帧格式、ping/pong、close)。
- Java 实现 WebSocket 的常见库(javax.websocket、Netty websocket、Undertow)。
- TLS over TCP(HTTPS)的握手流程要点(证书验证、对称密钥协商)。
- 使用 SSLSocket / SSLServerSocket 和 SSLEngine 的差别及适用场景。
- SSL/TLS 握手的重用(session resumption)与性能优化。
- 中间人攻击(MITM)与证书链、CA、信任根的角色。
- 如何在 Socket 程序中实现心跳、超时与断线重连?
- TCP Keepalive 与应用层心跳的区别和协同使用。
- 如何通过抓包(tcpdump/wireshark)诊断 Socket 连接问题?
- Socket 的非阻塞 accept/read/write 实现注意点(资源/异步安全)。
- 如何避免 TCP 粘包/拆包在 Socket 原生编程中的影响(流式协议设计)?
- 实战:实现一个带长度前缀的 TCP 协议的 Java 服务端/客户端(思路)。
- 如何优雅处理大量短连接的场景?(连接池、HTTP keep-alive)
- Socket 端口复用(SO_REUSEADDR vs SO_REUSEPORT)在负载均衡中的用法。
- 多路复用(select/poll/epoll)与 socket 大连接数的处理。
- 网络字节序(big-endian)与数据编解码注意点。
- 使用 TCP_NODELAY(禁用 Nagle)时的 CPU/网络 trade-off。
- 如何在 Java 中做到零拷贝文件传输(Socket + FileChannel.transferTo)?
- Socket 关闭流程中遇到阻塞(SO_LINGER)的处理办法。
- 如何防止或检测 “socket half-closed” 的资源泄漏?
- 实战题:用 BIO、NIO、Netty 各实现一个简易的聊天服务器,比较性能与代码复杂度(思路要点)。
七、异步 IO、IO 模型与高阶模式(141–150)
- IO 模型分类:同步阻塞、同步非阻塞、IO 多路复用、信号驱动、异步(AIO)。
- Reactor 模式与 Proactor 模式的原理与区别。
- Java AIO(AsynchronousChannel、AsynchronousSocketChannel)API 介绍。
- CompletionHandler 与 Future 风格的异步回调比较。
- 高并发下选择 NIO 还是 AIO 的实际考量(实现复杂度、平台支持)。
- 事件驱动系统的 back-pressure 设计(流控、速率限制)。
- 线程模型设计:acceptors / io workers / business workers 的权衡。
- RCU、无锁队列在高并发 IO 框架中的应用场景。
- 如何在应用层实现请求队列与排队策略来缓解突发流量?
- 实战题:设计一个支持百万连接的服务器架构(核心组件与 IO 策略)。
八、零拷贝、内核与性能调优(151–160)
- 零拷贝(zero-copy)概念和常见实现(sendfile, mmap, splice)。
- FileChannel.transferTo/transferFrom 在 Linux 下如何利用 sendfile 实现零拷贝?
- mmap(内存映射文件)的实现原理与 GC /回收问题。
- DMA(Direct Memory Access)在零拷贝中的作用与限制。
- 内核态 / 用户态拷贝次数与零拷贝带来的减少效果。
- TCP 窗口、拥塞控制调优参数(net.ipv4.tcp_* 系列常见项)。
- 系统级调优:文件描述符上限(ulimit -n)、somaxconn、backlog、epoll_limits 等。
- 高并发网络服务的监控指标(fd 使用、accept latency、context switch、cpu steal)。
- 性能诊断工具与流程:iostat, vmstat, sar, perf, tcpdump, ss, strace。
- 实战题:如何定位并修复一个高并发服务器的 accept 阻塞 / 连接丢失 问题(诊断步骤)。
Java 基础面试题
一、语言特性 & 基础语法
1. Java 中的 == 和 equals() 有什么区别?如何正确重写 equals()?
- ==
- 对于基本数据类型:比较值是否相等。
- 对于引用类型:比较两个引用是否指向同一块内存地址(即是否是同一个对象)。
- equals()
- 定义在
Object类中,默认实现也是==。 - 一般会被类(如
String、Integer等)重写,用来比较“内容”是否相同,而不是内存地址。
- 定义在
👉 正确重写 equals() 要满足 自反性、对称性、传递性、一致性,并且和 null 比较时返回 false。
典型实现步骤:
- 判断
this == obj,若相等直接返回true。 - 判断
obj是否为同类实例。 - 强制类型转换后逐个比较关键字段。
2. 为什么重写 equals() 时必须重写 hashCode()?不重写会怎样?
- 原因:因为
HashMap、HashSet等基于哈希表的集合使用hashCode()来决定元素存放位置,然后再用equals()判断元素是否相同。 - 契约(JDK 文档规定):
- 如果两个对象
equals()相等,那么它们的hashCode()必须相等。 - 反之,
hashCode()相等并不一定意味着equals()相等。
- 如果两个对象
- 不重写会怎样:
- 两个内容相同的对象可能
equals()为 true,但hashCode()不同,导致放进HashSet时被当作两个对象存储,违背集合“不重复”的原则。
- 两个内容相同的对象可能
3. Object 类有哪些方法?哪些方法经常需要重写?
- 常用方法:
equals():对象内容比较。hashCode():哈希值,配合equals。toString():对象的字符串表示。clone():对象克隆(浅拷贝,需实现Cloneable)。finalize():对象被 GC 前调用(已不推荐使用)。
- 同步方法:
wait()/notify()/notifyAll():线程通信。
- 类相关方法:
getClass():获取对象的运行时类型。
👉 经常需要重写的:equals()、hashCode()、toString()。
4. final、finally、finalize() 有什么区别?
- final:关键字
- 修饰类:类不能被继承。
- 修饰方法:方法不能被重写。
- 修饰变量:变量成为常量,只能赋值一次。
- finally:关键字
- 用于
try-catch-finally,无论是否有异常,finally块中的代码一定会执行(除非System.exit(0))。
- 用于
- finalize():方法
Object类定义,在 GC 前调用,常用于释放非托管资源。- 已被标记为废弃(Java 9 开始),因为不可预测、不可靠。
5. Java 中的 instanceof 关键字是如何实现的?
- 作用:判断某个对象是否为某个类或其子类的实例。
- 实现原理:
- JVM 在对象头中存储了对象所属的类的元数据指针。
instanceof实际是检查该类元数据是否是指定类或其子类。- 编译器会将
obj instanceof A转换为字节码instanceof指令,由 JVM 在运行时判断。
6. 接口和抽象类的区别?接口能不能有构造函数?
- 接口:
- 只能定义方法(Java 8 之后可有
default和static方法)。 - 不能有构造函数(因为接口不能直接实例化)。
- 可以实现 多继承(一个类可以实现多个接口)。
- 只能定义方法(Java 8 之后可有
- 抽象类:
- 可以有普通方法、抽象方法、成员变量。
- 可以有构造函数(但不能直接 new,用于子类构造时调用)。
- 只能单继承。
7. 接口可以多继承吗?抽象类可以实现接口吗?
- 接口可以多继承:
interface A extends B, C {} - 抽象类可以实现接口:可以选择性实现接口的方法,未实现的方法仍保持
abstract,由子类实现。
8. default 方法和 static 方法在接口中的意义是什么?
- default 方法:
- Java 8 引入,接口中可以有默认实现的方法。
- 解决接口扩展的兼容性问题(给老接口新增方法不会导致所有实现类报错)。
- static 方法:
- 属于接口本身,而不是实现类。
- 用于提供工具方法或公共逻辑(类似
Collections的工具类方法)。
9. 什么是内部类?分为哪几种?
- 内部类:定义在类中的类,可以访问外部类的成员。
- 种类:
- 成员内部类(非静态内部类)
- 依赖外部类实例,不能有
static成员。
- 依赖外部类实例,不能有
- 静态内部类
- 类似普通类,但作用域在外部类内,可以有
static成员。
- 类似普通类,但作用域在外部类内,可以有
- 局部内部类
- 定义在方法体或代码块中,只能在该范围内使用。
- 匿名内部类
- 没有名字,通常在需要“临时实现接口或继承类”的场景使用。
- 成员内部类(非静态内部类)
10. 匿名内部类和 Lambda 表达式的区别?
- 匿名内部类:
- 实现接口/继承抽象类的 具体匿名类对象。
- 可以有多个方法,但通常只重写需要的方法。
- 生成
.class文件(额外开销)。
- Lambda 表达式:
- 仅能用于 函数式接口(只有一个抽象方法的接口)。
- 更简洁,本质是语法糖,编译后生成
invokedynamic字节码指令,调用运行时生成的函数对象。 - 不会创建新的
.class文件,更轻量。
二、数据类型 & 内存
1. Java 中的基本数据类型和包装类型有哪些区别?
- 基本数据类型(primitive):
byte, short, int, long, float, double, char, boolean- 存储在栈上(或寄存器),效率高,空间占用固定。
- 不能为
null,默认值固定(如int默认0,boolean默认false)。
- 包装类型(wrapper):
Byte, Short, Integer, Long, Float, Double, Character, Boolean- 是对象,存储在堆中。
- 可以为
null,有丰富的 API。 - 可以参与集合类(如
ArrayList只能存对象)。
- 自动装箱/拆箱:Java 会在基本类型和包装类之间自动转换。
2. 为什么 Integer 有缓存机制?范围是多少?
- 原因:
- 为了避免频繁创建对象,提高性能。
- 小整数使用频率高(如循环、下标),所以缓存常用的整数。
- 范围:
-128 ~ 127。 - 实现:
IntegerCache静态内部类在类加载时初始化缓存数组。Integer.valueOf(int i)方法会先查缓存,再决定是否 new 对象。
3. 为什么 new Integer(127) == new Integer(127) 为 false,而 Integer.valueOf(127) == Integer.valueOf(127) 为 true?
new Integer(127):每次都会创建新对象,内存地址不同,所以==为 false。Integer.valueOf(127):会查缓存,127在-128~127范围内,返回同一个对象引用,所以==为 true。
👉 总结:
==比较的是引用地址。equals()才是比较数值。
4. NaN 和 Infinity 在 Java 中如何表示?
- NaN(Not a Number):
- 如
0.0 / 0.0或Math.sqrt(-1)。 - 特点:
NaN != NaN,要用Double.isNaN()检测。
- 如
- Infinity(正/负无穷大):
- 如
1.0 / 0.0 = Infinity,-1.0 / 0.0 = -Infinity。 - 用
Double.isInfinite()检测。
- 如
5. 为什么浮点数计算有精度问题?如何避免?
- 原因:
- 浮点数采用 IEEE 754 二进制浮点表示,大多数十进制小数(如
0.1)不能精确表示,只能存储近似值。 - 计算时会累积误差。
- 浮点数采用 IEEE 754 二进制浮点表示,大多数十进制小数(如
- 避免方法:
- 使用
BigDecimal(推荐)。 - 或使用整数替代(如金额用分而不是元)。
- 使用
6. BigDecimal 为什么能解决浮点数精度问题?
- 原理:
- 内部使用 BigInteger + 精度标识 scale 表示任意精度的十进制数。
- 不依赖二进制浮点存储,而是基于字符串或整数进行精确计算。
- 注意:
- 不能用
new BigDecimal(double)(会带入二进制误差)。 - 推荐
BigDecimal.valueOf(double)或new BigDecimal(String)。
- 不能用
7. Java 中的字符集默认是什么?在不同平台上会不一样吗?
- Java 内部使用 UTF-16(Unicode) 存储
char和String。 - 默认字符集(
Charset.defaultCharset())会随平台不同而不同:- Windows(中文环境):
GBK或GB2312。 - Linux / macOS:通常是
UTF-8。
👉 这会影响文件读写、网络传输等。
- Windows(中文环境):
8. String、StringBuilder、StringBuffer 区别?线程安全性?
- String
- 不可变类(底层
final char[]),每次拼接都会生成新对象。
- 不可变类(底层
- StringBuilder
- 可变字符序列(底层
char[]),高效,非线程安全。
- 可变字符序列(底层
- StringBuffer
- 可变字符序列,方法加了
synchronized,线程安全,效率比StringBuilder低。
- 可变字符序列,方法加了
👉 单线程推荐 StringBuilder,多线程推荐 StringBuffer。
9. 为什么 String 是不可变的?背后实现细节?
- 原因:
- 安全性:如网络请求 URL、文件路径、ClassLoader 名称等,一旦创建不可修改,避免被篡改。
- 线程安全:不可变对象天然线程安全。
- 哈希值缓存:
String缓存了hashCode,不可变保证哈希值不会变化。 - 字符串常量池优化:不可变对象可被复用。
- 实现:
private final char[] value(JDK 9 以后改成byte[]+coder,节省内存)。
10. intern() 方法的作用是什么?
作用:
- 将字符串放入 字符串常量池,返回池中的引用。
- 若池中已有等值字符串,则返回已有对象引用。
意义:
- 减少内存占用,提高比较效率(常量池中的
String可以直接用==比较)。
- 减少内存占用,提高比较效率(常量池中的
例子:
1
2
3
4String s1 = new String("abc");
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s2 == s3); // true
三、集合框架
1. ArrayList 和 Vector 的区别
| 特性 | ArrayList | Vector |
|---|---|---|
| 线程安全 | 非线程安全 | 线程安全(方法加 synchronized) |
| 扩容机制 | 默认 1.5 倍 | 默认 2 倍 |
| 性能 | 高(无锁) | 低(同步开销) |
| 用途 | 多线程外部不安全 | 适合多线程共享场景,但现在较少使用 |
| 面试拓展 | 如何在多线程下安全使用 ArrayList?可用 Collections.synchronizedList() 或 CopyOnWriteArrayList |
原理:
- 都基于动态数组实现
- 添加元素时如果容量不够,会创建一个新的数组并复制原数组内容
2. 为什么 ArrayList 的扩容是 1.5 倍?
目的:平衡 内存消耗 和 扩容频率
原理:
- 过小扩容:频繁创建新数组,复制开销大
- 过大扩容:浪费内存
JDK 1.8 的扩容公式:
1
newCapacity = oldCapacity + (oldCapacity >> 1); // 相当于 1.5 倍
面试点:
- LinkedList 不需要扩容,因为链表动态增长
- 如何自定义扩容策略?
3. LinkedList 是双向链表还是单向链表?
Java 中 LinkedList 是双向链表
结构:
1
2
3
4
5Node<E> {
E item;
Node<E> next;
Node<E> prev;
}优点:
- 支持快速插入/删除(O(1))
- 支持从头尾遍历
缺点:
- 随机访问慢(O(n))
- 内存占用大(需要 prev 指针)
4. HashSet 底层实现
- 底层使用 HashMap 实现
- 原理:
- 每个元素作为
HashMap的 key - value 使用一个固定的 Object 对象
PRESENT
- 每个元素作为
- 特点:
- 不允许重复
- 无序存储
- 性能:
- 插入、删除、查找均 O(1) 平均
- 面试拓展:HashSet 和 LinkedHashSet 的区别?
- LinkedHashSet 维护了插入顺序
5. TreeSet 和 TreeMap 的底层实现
- TreeMap:基于 红黑树 实现
- TreeSet:内部使用 TreeMap 存储元素
- 特点:
- 自动排序(自然排序或 Comparator)
- 查找、插入、删除 O(log n)
- 红黑树特性:
- 自平衡二叉搜索树
- 插入/删除保持平衡,通过旋转和染色操作
6. PriorityQueue 的底层数据结构
基于堆实现(默认最小堆)
特性:
- 插入 O(log n)
- 取出最小值 O(log n)
内部实现:
1
private Object[] queue; // 动态数组存储堆
应用:
- 定时任务调度
- Dijkstra 算法
面试拓展:如何实现最大堆?
- 使用 Comparator 或覆写 compare 方法
7. ConcurrentSkipListMap 的底层原理
- 基于跳表实现
- 特点:
- 有序 Map,支持并发操作
- 读操作无锁(lock-free),写操作使用 CAS/锁粒度较小
- 跳表原理:
- 多层链表
- 顶层链表间隔大
- 查找、插入、删除 O(log n) 平均
8. CopyOnWriteArrayList 的写时复制
- 原理:
- 每次写操作(add、remove)都会复制一份底层数组
- 读操作不加锁,直接读取旧数组
- 优点:
- 高度并发读性能
- 避免 ConcurrentModificationException
- 缺点:
- 写操作成本高(数组复制)
- 内存开销大
- 应用场景:
- 读多写少(如监听器列表、配置缓存)
9. EnumMap 和 EnumSet
- EnumMap:
- Key 是枚举类型
- 底层使用数组存储,效率高
- EnumSet:
- 元素为枚举
- 底层使用 位向量(bit vector) 存储
- 特点:
- 高效,占用内存小
- 遍历速度快
- 应用:
- 状态机、标志位集合
10. WeakHashMap 和 HashMap 的区别
| 特性 | HashMap | WeakHashMap |
|---|---|---|
| Key 引用类型 | 强引用 | 弱引用 |
| GC 行为 | 不受垃圾回收影响 | Key 被回收时自动删除 Entry |
| 用途 | 普通 Map | 缓存、内存敏感对象的映射 |
| 面试点 | WeakHashMap 如何防止内存泄漏? |
原理:
- WeakHashMap 的 key 是
WeakReference - 当 key 不再被外部强引用时,垃圾回收器回收,并通过 ReferenceQueue 清理 Entry
四、异常 & 错误处理
1. Java 中的异常分为哪两大类?
- Error(错误):
- 系统级错误,通常无法恢复
- 例子:
OutOfMemoryError、StackOverflowError - 不建议捕获
- Exception(异常):
- 程序可以捕获、处理
- 子类:
- 受检异常(CheckedException):必须 try/catch 或 throws 抛出
- 例子:
IOException、SQLException
- 例子:
- 非受检异常(UncheckedException / RuntimeException):可选择捕获
- 例子:
NullPointerException、IllegalArgumentException
- 例子:
- 受检异常(CheckedException):必须 try/catch 或 throws 抛出
面试点:
- Checked vs Unchecked 异常的设计初衷:
- Checked 异常强制处理,提高程序健壮性
- RuntimeException 用于编程错误,不必强制捕获
2. Error 和 Exception 的区别
| 特性 | Error | Exception |
|---|---|---|
| 是否可恢复 | 不可恢复 | 可恢复(程序可处理) |
| 捕获建议 | 不建议捕获 | 建议捕获和处理 |
| 子类示例 | OutOfMemoryError | IOException, SQLException |
| 使用场景 | JVM 内部错误 | 应用程序异常处理 |
3. 受检异常和非受检异常区别
| 特性 | CheckedException | UncheckedException |
|---|---|---|
| 编译器检查 | 编译器强制要求处理 | 编译器不要求 |
| 继承关系 | Exception 除 RuntimeException | RuntimeException 子类 |
| 使用场景 | 可恢复异常,如文件不存在 | 编程错误,如空指针 |
面试点:
- Checked 异常在 API 设计中可提示调用者
- RuntimeException 多用于程序逻辑错误,防止过多 try/catch
4. throw 和 throws 的区别
| 特性 | throw | throws |
|---|---|---|
| 位置 | 方法体内 | 方法声明上 |
| 用途 | 抛出具体异常实例 | 声明方法可能抛出的异常类型 |
| 个数限制 | 一次抛出一个 | 可声明多个异常 |
| 面试示例 | throw new IOException() |
void read() throws IOException |
5. try-with-resources 的底层原理
语法:
1
2
3try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
// 使用资源
}底层实现:
自动调用资源的
close()方法资源必须实现 AutoCloseable 接口
编译器将其转换为:
1
2
3
4
5
6BufferedReader br = new BufferedReader(new FileReader("file.txt"));
try {
// 使用资源
} finally {
if (br != null) br.close();
}
优势:
- 自动关闭资源,避免资源泄漏
- 支持多个资源声明
面试拓展:多资源关闭的异常抑制机制 (
addSuppressed)
6. 自定义异常如何设计?继承哪个类
继承 RuntimeException:
- 用于非受检异常(编程错误、业务逻辑异常)
继承 Exception:
- 用于受检异常(需要调用者处理)
实现建议:
1
2
3
4
5
6
7
8public class MyBusinessException extends RuntimeException {
public MyBusinessException(String message) {
super(message);
}
public MyBusinessException(String message, Throwable cause) {
super(message, cause);
}
}面试点:
- 自定义异常加上异常链(cause)
- 规范异常信息,便于日志追踪
7. 为什么不建议捕获 Throwable?
- Throwable 包含 Error 和 Exception
- 捕获 Throwable 会:
- 捕获系统级错误(OutOfMemoryError)
- 破坏 JVM 正常工作
- 最佳实践:
- 只捕获可恢复的 Exception 或 RuntimeException
- 捕获 Throwable 仅用于日志收集或监控
8. 异常链(Exception Chaining)是什么
概念:
- 在捕获一个异常时,将原异常封装在新异常中抛出
实现:
1
2
3
4
5try {
method();
} catch (IOException e) {
throw new MyBusinessException("读取文件失败", e);
}优势:
- 保留原始异常信息
- 便于调试和日志追踪
9. Java 8 之后对异常处理的改进
Lambda 表达式:
- 对受检异常的处理更加简洁,但需要特殊处理
1
2
3
4list.forEach(item -> {
try { process(item); }
catch(IOException e) { e.printStackTrace(); }
});Optional & Streams:
- 避免空指针异常
- 可以在
map/filter中处理异常
面试拓展:
- 异常在并行 Stream 中如何传递
CompletableFuture异常处理
10. 多线程环境下如何正确传递异常
问题:
- 子线程异常不会直接抛给主线程
解决方案:
Future / Callable:
1
2Future<?> future = executor.submit(() -> { throw new RuntimeException("error"); });
future.get(); // 会抛 ExecutionExceptionThread.UncaughtExceptionHandler:
1
thread.setUncaughtExceptionHandler((t, e) -> log.error("线程异常", e));
CompletableFuture.exceptionally:
1
2CompletableFuture.runAsync(() -> { throw new RuntimeException(); })
.exceptionally(ex -> { log.error(ex); return null; });
面试拓展:
- 如何处理线程池中未捕获异常
- 异步任务异常链追踪
五、JVM & 内存管理
1. Java 内存模型(JMM)是什么?
- 定义:
- Java 内存模型(Java Memory Model,JMM)是 Java 虚拟机规范中定义的一套规范,用于描述 Java 程序中各个线程如何访问共享内存以及如何保证可见性、原子性、有序性。
- 核心作用:
- 解决多线程可见性问题
- 规定内存操作顺序(happens-before 原则)
- 定义变量访问原子性
- 关键概念:
- 主内存(Main Memory):共享变量存储位置
- 工作内存(Working Memory / Thread Local Cache):线程私有缓存
- volatile、synchronized、final 的语义保证不同层次的可见性和原子性
2. JVM 内存分为哪几个区域?每个区域的作用是什么?
- 程序计数器(PC Register)
- 每个线程私有
- 保存当前线程执行的字节码指令地址
- 支持线程切换和方法调用返回
- 虚拟机栈(Stack)
- 每个线程私有
- 存储局部变量、操作栈、动态链接信息、方法出口
- 栈帧:方法执行的最小单位
- 异常:StackOverflowError
- 本地方法栈(Native Method Stack)
- JVM 调用本地方法(C/C++实现)使用
- HotSpot 通常与虚拟机栈合并
- 堆(Heap)
- 所有线程共享
- 存储对象实例及数组
- 垃圾收集的主要区域
- 子区域:
- 年轻代(Young Generation):Eden + Survivor0 + Survivor1
- 老年代(Old Generation)
- 元空间(Metaspace / 方法区):存放类元信息
- 方法区(Metaspace / 以前称为 PermGen)
- 存储类信息、常量池、静态变量
- Metaspace 从本地内存分配(不再受 JVM 堆大小限制)
- 运行时常量池
- 类加载时生成
- 存放字面量、符号引用、字符串常量等
3. 为什么 String 常量池要放在堆里而不是方法区?
- 历史原因:
- JDK 1.6 及以前:字符串常量池在永久代(PermGen),易导致 OOM
- 现代实现:
- JDK 7+ 将常量池移到堆
- 优势:
- 垃圾回收可管理
- 内存分配灵活,避免方法区溢出
- 多线程访问安全性提升(与堆对象 GC 同步)
4. 什么是垃圾回收(GC)的可达性分析算法?
可达性分析(Reachability Analysis):
- 从 GC Roots 开始,通过引用链判断对象是否可达
- 不可达对象即为垃圾,可以回收
示意:
1
2
3
4GC Roots
|
v
Object A --> Object B- 如果 Object B 无法通过 GC Roots 访问,则被回收
5. 哪些对象可以作为 GC Roots?
常见 GC Roots:
- 虚拟机栈中的引用(局部变量、方法参数)
- 静态变量(类静态成员)
- 常量引用(常量池引用)
- JNI 引用(本地方法栈中的引用)
6. 强引用、软引用、弱引用、虚引用的区别
| 类型 | 特点 | GC 行为 | 面试应用 |
|---|---|---|---|
| 强引用(Strong Reference) | 普通对象引用 | 不可回收 | 普通对象 |
| 软引用(Soft Reference) | 通过 SoftReference 引用 | 内存不足时回收 | 缓存(Memory-sensitive cache) |
| 弱引用(Weak Reference) | WeakReference 引用 | GC 时立即回收 | ThreadLocal 清理、缓存 |
| 虚引用(Phantom Reference) | PhantomReference 引用 | 仅做引用队列通知 | 对象回收前做清理、资源释放 |
7. GC 算法
- 标记-清除(Mark-Sweep)
- 标记可达对象,清除未标记对象
- 优点:简单
- 缺点:产生碎片
- 标记-整理(Mark-Compact)
- 标记可达对象
- 将存活对象移动到一端
- 优点:解决碎片问题
- 缺点:移动对象成本高
- 复制算法(Copying)
- 将内存分为两块
- 将存活对象复制到另一块,清理旧块
- 优点:分配快,整理快
- 缺点:内存利用率低
8. 常见的垃圾收集器
| 垃圾收集器 | 适用场景 | 特点 |
|---|---|---|
| Serial GC | 单核 | STW 串行,简单、高吞吐量低 |
| Parallel GC | 多核 | 并行回收年轻代,高吞吐量 |
| CMS GC | 多核 | 并发回收老年代,减少 STW |
| G1 GC | 大堆 | 分区回收,低延迟,预测停顿 |
| ZGC | 超大堆 | 并发标记-清理,低延迟 |
| Shenandoah | 大堆 | 并发标记-整理,减少 STW |
9. 什么是 Stop-The-World (STW)?
- 定义:JVM 暂停所有应用线程执行,专门进行 GC
- 原因:
- 防止对象被修改导致回收错误
- 面试拓展:
- G1/ZGC/Shenandoah 尽量缩短 STW 时间
- 并发 GC 与 STW 的折中
10. JVM 调优常见参数
| 参数 | 作用 |
|---|---|
-Xmx |
最大堆大小 |
-Xms |
初始堆大小 |
-Xmn |
年轻代大小 |
-XX:SurvivorRatio |
Eden/Survivor 比例 |
-XX:+UseG1GC |
使用 G1 GC |
-XX:MaxGCPauseMillis |
最大停顿时间 |
-XX:+PrintGCDetails |
打印 GC 日志 |
-XX:+HeapDumpOnOutOfMemoryError |
OOM 时生成堆转储 |
面试拓展:
- 如何调优老年代和年轻代比例?
- GC 日志分析示例:年轻代满、老年代晋升、STW 停顿
六、并发编程基础
1. Java 中的 volatile 关键字的底层实现原理
- 作用:
- 保证可见性:一个线程修改变量后,其他线程立即可见
- 禁止指令重排:保证顺序性
- 原理:
- 底层通过 内存屏障(Memory Barrier / Fence) 实现
volatile读操作:- 加载屏障(LoadLoad + LoadStore)
volatile写操作:- 写屏障(StoreStore + StoreLoad)
- 注意:
- 不能保证 原子性
- 适合 状态标识、开关变量
2. synchronized 的实现原理
- 作用:
- 保证 互斥访问
- 保证 可见性
- 底层机制:
- Java 对象头包含 Mark Word
- 每个对象可以关联一个 Monitor
- Monitor 用于管理线程的加锁、阻塞、唤醒
- 锁升级流程:
- 偏向锁(默认轻量级线程无竞争)
- 轻量级锁(CAS 竞争,膨胀前)
- 重量级锁(锁膨胀,阻塞其他线程)
3. 偏向锁、轻量级锁、重量级锁
| 锁类型 | 适用场景 | 实现方式 | 优缺点 |
|---|---|---|---|
| 偏向锁 | 单线程 | 在对象头标记线程 ID | 减少无竞争开销 |
| 轻量级锁 | 多线程竞争轻量 | CAS 自旋 + 栈记录锁 | 高效,避免阻塞 |
| 重量级锁 | 多线程竞争严重 | Monitor + 阻塞 | 开销大,可能 STW |
面试拓展:
- 如何通过
-XX:+PrintFlagsFinal查看锁标志位 - 偏向锁默认延迟 4 秒启用
4. 什么是 CAS 操作?ABA 问题如何解决?
CAS(Compare-And-Swap):
- 原子操作
- 比较内存值是否为预期值,如果是就更新
1
CAS(V, A, B)
ABA 问题:
- 值先从 A 改成 B,又改回 A
- CAS 无法检测变化,可能出错
解决方案:
- 版本号机制(AtomicStampedReference)
- 加锁
5. 什么是线程安全?举例线程安全类
- 定义:多个线程访问同一个对象时,不会破坏对象的状态,并保证正确性
- 线程安全类示例:
- 集合类:
Vector,ConcurrentHashMap,CopyOnWriteArrayList - 原子类:
AtomicInteger,AtomicReference - 工具类:
ThreadLocal,BlockingQueue
- 集合类:
6. ThreadLocal 的底层原理与内存泄漏问题
- 原理:
- 每个线程维护一个
ThreadLocalMap - key 是 ThreadLocal 对象
- value 是对应的值
- 每个线程维护一个
- 内存泄漏原因:
- ThreadLocalMap 的 key 是 弱引用
- 如果 ThreadLocal 对象被回收,但线程仍存活,value 仍然存在
- 解决方法:
- 使用
remove()方法 - 避免长生命周期线程存储大量 ThreadLocal
- 使用
7. 线程池的核心参数
| 参数 | 作用 |
|---|---|
| corePoolSize | 核心线程数,常驻线程 |
| maximumPoolSize | 最大线程数 |
| keepAliveTime | 非核心线程空闲存活时间 |
| workQueue | 等待队列,用于存放任务 |
| threadFactory | 线程创建工厂 |
| RejectedExecutionHandler | 拒绝策略 |
8. 什么是拒绝策略
- 触发场景:线程池满了,队列满了
- 常见策略:
- AbortPolicy:抛异常,默认
- CallerRunsPolicy:由调用线程执行任务
- DiscardPolicy:直接丢弃任务
- DiscardOldestPolicy:丢弃队列中最旧任务
9. Executors 提供的常见线程池
| 线程池类型 | 特点 | 使用场景 |
|---|---|---|
| FixedThreadPool | 固定线程数 | CPU密集型任务 |
| CachedThreadPool | 可扩容线程池 | 短生命周期任务 |
| SingleThreadExecutor | 单线程顺序执行 | 顺序任务 |
| ScheduledThreadPool | 支持定时/周期执行 | 定时任务 |
面试拓展:
- Executors.newFixedThreadPool 会使用 LinkedBlockingQueue
- CachedThreadPool 使用 SynchronousQueue,任务直接交给线程
10. Future 和 CompletableFuture 区别
| 特性 | Future | CompletableFuture |
|---|---|---|
| 异步 | 提交任务后可阻塞获取结果 | 可链式异步操作 |
| API | get() 阻塞 | thenApply, thenAccept 等链式 |
| 异常处理 | 阻塞式,捕获 ExecutionException | 支持 exceptionally/handle |
| 组合任务 | 组合困难 | 支持 allOf / anyOf / thenCombine |
面试点:
- CompletableFuture 适合多异步任务组合
- FutureTask 可作为 Future 的实现类
七、编译 & 运行时
1. Java 的反射机制是什么?底层原理?
概念:
- 反射机制允许在运行时 获取类的信息、实例化对象、访问字段和方法,而不需要在编译期知道具体类型
常用 API:
1
2
3
4Class<?> clazz = Class.forName("com.example.User");
Object obj = clazz.newInstance();
Method method = clazz.getMethod("sayHello", String.class);
method.invoke(obj, "world");底层原理:
- JVM 中类对象
Class对应内存中的元数据(方法表、字段表、常量池) - 通过 Native 方法 调用,访问类的元数据和对象实例
- JVM 中类对象
面试拓展:
- 反射可访问私有字段/方法(
setAccessible(true)) - 反射适用于框架(Spring、MyBatis)
- 反射可访问私有字段/方法(
2. 为什么反射性能差?JDK 9 之后如何优化?
- 性能差原因:
- 每次访问方法/字段都要进行动态解析
- JVM 无法做内联优化(方法调用不是静态绑定)
- 需要检查权限、类型转换
- 优化措施:
- JDK 7 及以后:
MethodHandle和invokeDynamic - JDK 9+:增强模块化(模块边界检查优化)
- 热点编译器 JIT 可以对反射调用做内联
- JDK 7 及以后:
3. 什么是动态代理?JDK 动态代理和 CGLIB 区别?
动态代理:
- 在运行时生成代理对象,拦截方法调用,增强行为
JDK 动态代理:
- 基于接口
- 使用
Proxy.newProxyInstance - 通过
InvocationHandler.invoke调用
CGLIB:
- 基于子类继承(无接口也可代理)
- 使用 ASM 字节码生成子类
- 注意 final 类/方法不能被代理
对比:
特性 JDK 动态代理 CGLIB 是否需要接口 必须 不需要 原理 Proxy + InvocationHandler ASM 字节码生成子类 性能 略慢 较快 局限性 无 final 类/方法不可代理
4. 注解的底层原理是什么?运行时注解如何实现?
- 概念:
- 注解是一种 元数据,用于给类、方法、字段等提供信息
- 底层原理:
- 编译时生成
.class文件中RuntimeVisibleAnnotations属性 - JVM 通过
java.lang.reflect.AnnotatedElement获取
- 编译时生成
- 运行时注解:
@Retention(RetentionPolicy.RUNTIME)- 通过反射 API 读取注解并执行逻辑
- 应用场景:
- Spring 注解(
@Autowired、@Service) - JPA 注解(
@Entity、@Column)
- Spring 注解(
5. ClassLoader 的双亲委派模型是什么?
原理:
- 当类加载请求到来时,先委托父 ClassLoader 加载
- 如果父 ClassLoader 加载失败,再由子 ClassLoader 加载
优点:
- 避免重复加载
- 保证核心类(如
java.lang.*)唯一性
示意:
1
Bootstrap -> Extension -> App -> Custom
面试拓展:
- 双亲委派模型是保证 JVM 核心类安全的重要机制
6. 如何打破双亲委派?
- 自定义 ClassLoader
- 在
loadClass中先加载自己定义的类,再委派父类(逆向委派) - 注意:破坏双亲委派可能引发类冲突、内存泄漏
7. 什么是 SPI(Service Provider Interface)机制?
- 概念:
- Java 提供的一种服务发现机制
- 通过接口 + 配置文件,动态加载实现类
- 实现:
- 接口
MyService - 文件
META-INF/services/com.example.MyService- 内容:实现类全限定名
ServiceLoader.load(MyService.class)自动加载实现类
- 接口
- 应用:
- JDBC 驱动加载
- Logging 框架(SLF4J)
8. Java 是如何实现跨平台的?
- 原理:
- Java 代码 -> 编译成 字节码(.class)
- JVM 负责字节码解释或即时编译(JIT)执行
- JVM 将字节码映射到不同操作系统和 CPU
- 面试拓展:
- 字节码验证机制确保安全性
- HotSpot JIT 提供本地优化
9. JIT(即时编译器)优化了哪些东西?
- JIT(Just-In-Time Compiler):
- 将热点字节码动态编译成本地机器码
- 优化策略:
- 方法内联:减少方法调用开销
- 循环优化:消除冗余计算
- 逃逸分析:栈上分配对象
- 锁消除/锁粗化:减少锁开销
- 指令重排序优化
10. 什么是逃逸分析?
- 概念:
- 分析对象的引用范围
- 判断对象是否“逃逸”出方法或线程
- 优化应用:
- 栈上分配:对象不逃逸可以直接分配在栈上
- 标量替换:将对象拆解为基本类型,提高缓存命中
- 锁消除:对象不逃逸,synchronized 可被消除
- 面试拓展:
- JIT 结合逃逸分析可显著提升性能
- 适用于短生命周期对象
八、常见场景 & 设计
1. 单例模式在 Java 中有哪些实现方式?哪种最优?
常见实现方式:
饿汉式(静态常量)
1
2
3
4
5public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
}- 优点:线程安全,简单
- 缺点:类加载即初始化,不支持延迟加载
懒汉式(加锁)
1
2
3
4
5
6
7
8public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(instance == null) instance = new Singleton();
return instance;
}
}- 优点:支持延迟加载
- 缺点:每次访问都有同步开销
双重检查锁(Double-Checked Locking)
1
2
3
4
5
6
7
8
9
10
11
12public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) instance = new Singleton();
}
}
return instance;
}
}- 优点:线程安全 + 延迟加载 + 高性能
- 推荐
静态内部类
1
2
3
4
5public class Singleton {
private Singleton() {}
private static class Holder { private static final Singleton INSTANCE = new Singleton(); }
public static Singleton getInstance() { return Holder.INSTANCE; }
}- 原理:JVM 保证类加载线程安全
- 优点:懒加载 + 高性能
- 最佳实践之一
枚举单例
1
public enum Singleton { INSTANCE; }
- 原理:JVM 保证枚举类型单例
- 优点:线程安全、防止反射和序列化破坏
- 最佳实践
2. 懒汉模式和饿汉模式的区别
| 特性 | 懒汉模式 | 饿汉模式 |
|---|---|---|
| 加载时机 | 延迟加载,第一次使用 | 类加载即初始化 |
| 线程安全 | 需加锁/双重检查 | 天然线程安全 |
| 性能 | 多线程同步有开销 | 无额外开销 |
| 内存使用 | 按需分配 | 类加载时分配 |
3. 枚举单例为什么是最佳实践
- JVM 保证枚举实例唯一性
- 防止反射和序列化破坏
- 实现简洁,无需手动同步
- 面试常考点:相比双重检查锁更安全
4. 为什么要用工厂模式?
- 作用:
- 隐藏对象创建逻辑
- 解耦调用方和实现类
- 类型:
- 简单工厂:静态方法返回对象
- 工厂方法:子类决定创建哪种对象
- 抽象工厂:一组相关对象的创建
- 优势:
- 可扩展性高
- 符合开闭原则
5. 装饰器模式和代理模式区别
| 特性 | 装饰器模式 | 代理模式 |
|---|---|---|
| 目的 | 动态增强对象功能 | 控制对象访问 |
| 结构 | 包装原对象,继承接口 | 持有原对象,方法委托 |
| 场景 | IO 流(BufferedReader) | 远程调用、权限控制 |
| 动态性 | 高 | 可以通过动态代理实现 |
6. Java 中的事件监听模型是怎么实现的?
- 机制:
- 事件源(Event Source):产生事件
- 事件监听器(Listener):接口,定义回调方法
- 注册与触发:源对象维护监听器列表,事件发生时通知监听器
- 典型应用:
- Swing/AWT 事件
- Spring ApplicationEvent
7. 为什么 Java 不支持多继承?
- 原因:
- 避免 钻石问题(多继承产生方法冲突)
- 简化对象模型,保证 JVM 类型安全
- 替代方案:
- 接口多继承 + 默认方法(Java 8+)
- 组合设计模式(优先使用 has-a 而不是 is-a)
8. 如何用 Java 实现回调机制?
原理:
- 将函数封装成对象或接口传递给另一个对象
- 由被调用对象在合适时机调用接口方法
示例:
1
2interface Callback { void onDone(String result); }
void asyncProcess(Callback callback) { new Thread(() -> callback.onDone("ok")).start(); }
9. Java 中如何实现观察者模式?
机制:
- Subject(被观察者):维护观察者列表,提供注册/注销方法
- Observer(观察者):接口,定义 update 方法
- 触发事件:Subject 状态变化时调用所有 Observer 的 update
示例:
1
2
3
4
5
6
7
8interface Observer { void update(String msg); }
class Subject {
List<Observer> observers = new ArrayList<>();
void addObserver(Observer o) { observers.add(o); }
void notifyAllObservers(String msg) {
observers.forEach(o -> o.update(msg));
}
}
10. Optional 的设计初衷是什么?
目的:
- 避免 null 值导致的 NullPointerException
- 提供函数式 API,提高可读性
使用方式:
1
2
3Optional<String> optional = Optional.ofNullable(getValue());
optional.ifPresent(System.out::println);
String val = optional.orElse("default");优势:
- 显示处理缺失值
- 支持链式调用、函数式风格
面试拓展:
- Optional 不适合用于集合属性,避免滥用
二、Java 并发编程(30题)
1. 线程的生命周期
- 状态:
- NEW:创建后,未调用
start() - RUNNABLE:调用
start(),可运行(线程调度器决定何时执行) - BLOCKED:等待锁
- WAITING:无限期等待(
Object.wait()/LockSupport.park()) - TIMED_WAITING:超时等待(
sleep(),join(timeout)) - TERMINATED:执行完毕
- NEW:创建后,未调用
2. 创建线程的方式
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable + FutureTask
- 线程池(推荐)
3. Runnable 和 Callable 区别
| 特性 | Runnable | Callable |
|---|---|---|
| 返回值 | 无 | 可以有返回值 |
| 异常 | 不可抛出检查异常 | 可以抛出异常 |
| 执行方式 | new Thread(runnable) | ExecutorService.submit(callable) |
4. Future 和 CompletableFuture 区别
- Future:
- 阻塞式获取结果
- 不支持链式异步处理
- CompletableFuture:
- 支持链式异步处理(thenApply, thenCombine, exceptionally)
- 可组合多个异步任务
5. ThreadLocal 原理
- 每个线程持有一个 ThreadLocalMap
- key 是 ThreadLocal 对象,value 是线程私有变量
- 内存泄漏风险:
- ThreadLocal 弱引用被 GC,value 没有被回收
- 解决方法:
- 使用
remove()清理
- 使用
6. synchronized 的锁优化机制
- 偏向锁:减少无竞争开销
- 轻量级锁:CAS + 自旋
- 重量级锁:Monitor 阻塞线程
- 锁消除/锁粗化:JIT 优化
7. ReentrantLock 实现原理
- 基于 AQS(AbstractQueuedSynchronizer)
- 内部维护一个 state 表示锁状态
- 队列 FIFO 管理等待线程
- 支持公平锁/非公平锁
8. 公平锁和非公平锁区别
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 加锁顺序 | FIFO | 不保证 |
| 性能 | 低 | 高(线程争抢) |
| 场景 | 避免饥饿 | 高性能锁场景 |
9. 乐观锁和悲观锁区别
- 悲观锁:假设会发生冲突,直接加锁(如 synchronized / ReentrantLock)
- 乐观锁:假设冲突少,使用 CAS(如 Atomic 类)
10. AQS 原理
- 核心:state + CLH 队列
- 特性:
- 独占模式 / 共享模式
- 基于 FIFO 队列管理等待线程
- 应用:
- ReentrantLock、Semaphore、CountDownLatch、FutureTask
11. CountDownLatch 和 CyclicBarrier 区别
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 使用次数 | 一次性 | 可重复使用 |
| 功能 | 等待线程完成 | 等待线程到达同一点 |
| 场景 | 线程结束同步 | 多线程并行分段 |
12. Semaphore 的实现原理
- 控制并发线程数量
- 基于 AQS 的共享锁
- acquire() 获取许可,release() 释放许可
13. Exchanger 的使用场景
- 两个线程交换数据
- 典型场景:生产者-消费者线程交换缓冲区
14. ForkJoinPool 的原理
- 工作窃取算法:
- 空闲线程从其他线程队列中窃取任务
- 适合 递归任务
- 通过 ForkJoinTask 递归拆分计算
15. CAS 操作的底层实现
- 基于 CPU 原子指令(CMPXCHG)
- JVM
Unsafe提供支持 - 优点:无锁、线程安全
- 缺点:ABA 问题
16. ABA 问题及解决
- 问题:值先 A → B → A,CAS 无法判断变化
- 解决:
- 版本号机制(AtomicStampedReference)
- 双重检查
17. Atomic 原子类原理
- 基于 CAS + Unsafe + volatile
- 实现非阻塞线程安全
- 示例:
AtomicInteger使用compareAndSwapInt
18. BlockingQueue 实现原理
- 内部维护 锁 + 条件队列
- put 阻塞满队列,take 阻塞空队列
- 常用实现:
- ArrayBlockingQueue(数组 + ReentrantLock)
- LinkedBlockingQueue(链表 + ReentrantLock)
19. ConcurrentHashMap 的分段锁机制
- JDK 1.7:
- Segment[] + synchronized
- JDK 1.8:
- CAS + synchronized 链表 + 红黑树 + Node
- 支持高并发读写
20. ConcurrentHashMap 扩容机制
- 采用 分段扩容 + CAS
- 链表转红黑树提高查找性能
- 扩容过程中使用 迁移节点
21. CopyOnWriteArrayList 原理
- 写时复制:
- 写操作复制数组
- 修改后替换引用
- 适合读多写少场景
22. 线程池核心参数
| 参数 | 作用 |
|---|---|
| corePoolSize | 核心线程数 |
| maximumPoolSize | 最大线程数 |
| keepAliveTime | 非核心线程存活时间 |
| workQueue | 任务队列 |
| RejectedExecutionHandler | 拒绝策略 |
23. 线程池拒绝策略
- AbortPolicy(默认,抛异常)
- CallerRunsPolicy(调用线程执行)
- DiscardPolicy(丢弃任务)
- DiscardOldestPolicy(丢弃队列最老任务)
24. 线程池工作流程
- 提交任务 → 放入队列
- 核心线程未满 → 创建线程
- 队列满 → 创建非核心线程
- 达到最大线程 → 触发拒绝策略
25. ThreadPoolExecutor vs ScheduledThreadPoolExecutor
| 特性 | ThreadPoolExecutor | ScheduledThreadPoolExecutor |
|---|---|---|
| 功能 | 通用线程池 | 支持定时 / 周期任务 |
| 队列 | BlockingQueue | DelayQueue |
| 场景 | 并发任务 | 定时任务 |
26. 为什么不建议使用 Executors 创建线程池
- Executors 会产生 无限队列(如 newCachedThreadPool)
- 容易 OOM / 线程数过多
- 推荐:手动使用 ThreadPoolExecutor 配置核心参数
27. JMM 内存可见性问题
- 多线程访问共享变量,修改不立即可见
- 解决方法:
volatilesynchronized/ Lock
28. happens-before 规则
- 保证操作顺序与可见性
- 常见规则:
- 锁的解锁 → 加锁
- volatile 写 → 读
- 线程 start() → run()
- 线程 join() → 结束
29. 死锁产生条件及避免
- 四个必要条件:
- 互斥
- 占有且等待
- 不可抢占
- 循环等待
- 避免方法:
- 避免循环等待,按顺序加锁
- 使用 tryLock + 超时
- 使用单一锁
30. 常见并发容器及区别
| 容器 | 特性 |
|---|---|
| ConcurrentHashMap | 高并发读写,分段/链表+红黑树 |
| CopyOnWriteArrayList | 写时复制,读多写少 |
| ConcurrentLinkedQueue | 非阻塞 |
队列,基于 CAS |
| BlockingQueue(Array/Linked) | 支持阻塞 put/take |
| ConcurrentSkipListMap | 有序,基于跳表,线程安全 |
三、JVM 与性能优化(30题)
1. JVM 内存结构
- 方法区 / 元空间(Metaspace):存放类信息、常量、静态变量
- 堆(Heap):对象实例、GC 管理区
- 栈(Stack):方法调用、局部变量
- 程序计数器(PC):记录线程执行地址
- 本地方法栈(Native Stack):执行 native 方法
- 直接内存(Direct Memory):堆外内存,用于 NIO
2. 堆和栈的区别
| 特性 | 堆 | 栈 |
|---|---|---|
| 存储内容 | 对象实例 | 方法调用帧、局部变量 |
| 生命周期 | GC 管理 | 随方法结束释放 |
| 线程共享 | 是 | 否(线程私有) |
| 访问速度 | 较慢 | 快 |
| 内存大小 | 大 | 小 |
3. 新生代、老年代、永久代(元空间)的区别
- 新生代(Young Generation):
- 存放新创建对象
- 分为 Eden + 2 个 Survivor 区
- Minor GC 主要发生在此
- 老年代(Old/Tenured Generation):
- 长寿命对象
- Full GC 主要作用区域
- 永久代 / 元空间:
- 存放类元数据
- Java 8+ 使用 Metaspace(Native 内存)
4. 对象的创建过程
- 类加载检查
- 分配内存:
- 堆上分配对象空间(TLAB/大对象直接分配)
- 初始化零值
- 构造器初始化
- 引用赋值
5. 对象的内存分配策略
- 年轻代分配:TLAB(Thread Local Allocation Buffer)
- 大对象直接进入老年代(如数组 > 1MB)
- 长期存活对象晋升到老年代
- 栈上分配:通过逃逸分析优化
6. GC Roots
- 常见类型:
- 栈帧中的引用变量
- 方法区静态变量
- 常量引用
- 本地方法栈引用
7. 垃圾回收算法
- 引用计数(不可解决循环引用)
- 可达性分析(GC Roots)
- 具体回收算法:
- 标记-清除
- 标记-整理
- 复制算法
- 分代回收
8. CMS 垃圾回收器流程
- 初始标记(Stop-The-World)
- 并发标记(标记可回收对象)
- 并发预清理
- 重新标记(Stop-The-World)
- 并发清理(回收老年代)
- 特点:低停顿,但有浮动垃圾和内存碎片问题
9. G1 垃圾回收器原理
- Region(分区)管理堆
- 分代+并行+增量收集
- 垃圾回收策略:
- 优先回收回收成本低的 Region
- 并发标记 + 并行清理
- 目标:控制停顿时间
- 优势:无碎片、可预测停顿
10. ZGC 和 Shenandoah 特点
- 低延迟 GC,停顿时间 < 10ms
- 并发标记 + 并发压缩
- 内存可扩展性好
- 支持大内存 (>100GB)
- 面试常问区别:ZGC 使用 Load Barrier,Shenandoah 使用 Region Pinning
11. Minor GC 与 Full GC 区别
| 特性 | Minor GC | Full GC |
|---|---|---|
| 触发区域 | 新生代 | 整个堆(包括老年代) |
| 停顿时间 | 短 | 长 |
| 频率 | 高 | 低 |
| GC 类型 | 复制算法 | 标记-整理 / CMS / G1 |
12. OOM 常见类型及排查
- Java Heap Space:堆内存不足
- GC Overhead Limit:GC 花费过多时间
- Metaspace:类元信息过多
- Direct Memory:堆外内存溢出
- 排查工具:
- jmap、jstack、VisualVM、MAT
13. 类加载器分类
- Bootstrap(根类加载器)
- Extension(扩展类加载器)
- Application(系统类加载器)
- 自定义 ClassLoader
14. 双亲委派模型破坏场景
- 动态加载不同版本类
- 热部署、插件化框架(Tomcat、OSGi)
15. JVM 常见参数调优
- 堆大小:
-Xms -Xmx - 新生代比例:
-XX:NewRatio - GC 类型:
-XX:+UseG1GC - 线程栈大小:
-Xss - Metaspace:
-XX:MetaspaceSize
16. 内存泄漏 vs 内存溢出
- 内存泄漏:对象不再使用,但仍有引用
- 内存溢出(OOM):申请内存失败
17. 逃逸分析
- 作用:
- 分析对象作用域
- 栈上分配、锁消除、同步优化
- JVM 优化:
- 栈上分配(不入堆)
- 去掉无用锁(锁消除)
18. 方法内联
- JIT 优化
- 小方法直接替换调用,减少方法调用开销
- 增加 CPU 指令局部性,提高性能
19. JIT 编译器优化
- 方法内联
- 循环展开
- 逃逸分析
- 常量折叠
- 逃逸分析 + 栈上分配
20. Safepoint 机制
- 所有线程暂停,进行 GC 或其他全局操作
- JVM 可安全修改线程状态
21. Stop The World (STW)
- 定义:暂停所有用户线程进行 GC 或其他 JVM 内部操作
- 影响:应用停顿,低延迟场景需控制 STW 时间
22. Finalize 方法问题
- 不确定执行时间
- 性能开销大
- 可导致对象“复活”,增加内存压力
- 替代方案:try-with-resources / Cleaner
23. JVM 如何判断对象可回收
- 可达性分析:从 GC Roots 出发,未被可达的对象可回收
24. TLAB(Thread Local Allocation Buffer)作用
- 每个线程独享一块 Eden 内存
- 作用:
- 减少多线程竞争
- 提高对象分配效率
25. Direct Memory 为什么会 OOM
- 堆外内存不足
- NIO Buffer 使用
Unsafe分配 - 不受堆内存限制,但 JVM 无法直接管理
26. Metaspace 作用
- 存放类元数据
- Java 8+ 替代永久代
- 可以通过
-XX:MetaspaceSize限制
27. Full GC 触发条件
- 老年代空间不足
- 永久代 / 元空间不足
- System.gc() 调用
- 内存分配失败
28. 查看 JVM 内存使用情况
jstat -gc <pid>jmap -heap <pid>VisualVM/JConsoleMAT分析 heap dump
29. Arthas 常用命令
dashboard:系统指标thread:线程状态heapdump:堆转储monitor:方法耗时watch:方法参数/返回值jad:反编译类
30. JVM 调优常见面试题
- 堆大小设置策略
- 新生代/老年代比例
- GC 选择(G1 / CMS / Parallel)
- TLAB 与对象分配优化
- Full GC 触发条件
- 内存泄漏排查与 MAT 使用
四、Spring & Spring Boot(30题)
1. Spring 的 IoC 容器原理?
核心回答:
IoC(控制反转)是通过容器管理对象创建和依赖注入,开发者不需要手动 new 对象,而是由 Spring 容器统一管理。
深入解析:
- IoC 容器的核心是 BeanFactory 和 ApplicationContext。
- 工作流程:
- 配置解析:读取 XML / 注解 / JavaConfig;
- BeanDefinition:将配置转成 BeanDefinition;
- Bean 实例化:通过反射或 CGLIB 创建对象;
- 依赖注入:根据构造方法 / Setter / @Autowired 注入依赖;
- 初始化回调:执行 BeanPostProcessor / InitializingBean;
- 放入单例池:容器保存对象供全局使用。
延伸思考:
- IoC 是 依赖查找(DL) 和 依赖注入(DI) 的结合。
- 面试可能追问:Spring 如何实现延迟加载?(@Lazy + 单例池管理)
- 实战:配置文件少时用 XML,大项目推荐 JavaConfig + 注解。
2. Bean 的生命周期?
核心回答:
创建 → 属性注入 → 初始化 → 使用 → 销毁。
深入解析:
- 实例化:通过反射创建对象;
- 依赖注入:注入属性;
- Aware 接口回调:如 BeanNameAware、ApplicationContextAware;
- BeanPostProcessor 前置处理:比如 @Autowired 的处理;
- 初始化方法:@PostConstruct / InitializingBean.afterPropertiesSet() / init-method;
- BeanPostProcessor 后置处理;
- 就绪使用;
- 销毁回调:@PreDestroy / DisposableBean.destroy() / destroy-method。
延伸思考:
- 面试常追问:Spring 容器关闭时,单例 Bean 会被销毁吗?(会,调用 destroy 回调)。
- 原型作用域 Bean 不会被销毁,需要开发者手动管理。
3. BeanFactory 和 ApplicationContext 的区别?
核心回答:
- BeanFactory:最基础 IoC 容器,懒加载,轻量级;
- ApplicationContext:继承 BeanFactory,支持 AOP、国际化、事件机制、自动装配,默认预实例化。
深入解析:
- BeanFactory:面向低资源环境,功能有限;
- ApplicationContext:常用实现有 ClassPathXmlApplicationContext、AnnotationConfigApplicationContext。
延伸思考:
- 面试常问:为什么 ApplicationContext 默认是预加载?(减少运行时性能开销,启动时完成依赖检查)。
4. 单例 Bean 如何保证线程安全?
核心回答:
Spring 不保证单例 Bean 的线程安全,需要开发者在代码中保证。
深入解析:
- IoC 容器中的单例 Bean 是 多线程共享的;
- 常见的线程安全措施:
- 无状态设计(推荐);
- 使用 ThreadLocal 保存状态;
- 方法内部使用局部变量;
- 必要时加锁(synchronized / Lock)。
延伸思考:
- 面试陷阱:Spring 单例 Bean 是不是线程安全的?答:不是。
- 例子:Controller、Service 通常是无状态的,可以安全单例。
5. Spring AOP 的实现原理?
核心回答:
AOP(面向切面编程)通过 动态代理 实现,在方法执行前后插入增强逻辑。
深入解析:
- 实现方式:
- JDK 动态代理:基于接口;
- CGLIB 代理:基于子类字节码增强。
- AOP 核心组件:
- JoinPoint(连接点)、Pointcut(切点)、Advice(通知)、Weaving(织入)、Proxy(代理对象)。
- AOP 流程:调用目标方法 → 代理对象拦截 → 执行切面逻辑 → 继续执行原方法。
延伸思考:
- 面试官可能问:Spring 默认用哪种代理?
- 如果有接口 → JDK 动态代理;
- 没有接口 → CGLIB。
- JDK 代理只能基于接口,CGLIB 可以基于类。
6. JDK 动态代理和 CGLIB 的区别?
核心回答:
- JDK:基于接口,生成实现类;
- CGLIB:基于继承,生成子类字节码。
深入解析:
- JDK 动态代理:利用
Proxy.newProxyInstance,性能较高; - CGLIB:利用 ASM 字节码操作生成子类,性能稍慢,但支持类代理;
- Spring 默认优先使用 JDK,如果没有接口再用 CGLIB。
延伸思考:
- 限制:CGLIB 无法代理
final类 /final方法; - Spring Boot 可通过
spring.aop.proxy-target-class=true强制使用 CGLIB。
7. Spring 事务的传播机制?
核心回答:
事务传播机制定义了方法在调用时事务的边界,常见类型:
- REQUIRED(默认)、REQUIRES_NEW、NESTED、SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER。
深入解析:
- REQUIRED:有事务就加入,没有就新建;
- REQUIRES_NEW:挂起当前事务,新建一个;
- NESTED:嵌套事务(依赖保存点 rollback);
- SUPPORTS:有就用,没有就不用;
- NOT_SUPPORTED:挂起事务,以非事务运行;
- MANDATORY:必须在事务中调用;
- NEVER:必须在非事务中运行。
延伸思考:
- 面试陷阱:嵌套事务和新事务的区别?
- 嵌套事务依赖外层事务回滚;
- 新事务互相独立。
8. Spring 事务的隔离级别?
核心回答:
事务隔离级别解决并发问题,Spring 支持数据库的 5 种隔离级别:
- DEFAULT(数据库默认)
- READ_UNCOMMITTED(可能脏读)
- READ_COMMITTED(防脏读)
- REPEATABLE_READ(防脏读、不可重复读)
- SERIALIZABLE(防脏读、不可重复读、幻读)
深入解析:
- InnoDB 默认是 REPEATABLE_READ;
- Oracle 默认是 READ_COMMITTED。
延伸思考:
- 面试可能追问:MySQL 的 REPEATABLE_READ 如何避免幻读?(MVCC + 间隙锁)。
9. @Transactional 的实现原理?
核心回答:
@Transactional 通过 AOP 代理,在方法执行前后控制事务。
深入解析:
- Spring 容器解析 @Transactional → 生成代理 → 在代理方法里执行事务增强逻辑:
- 获取事务管理器;
- 开启事务;
- 执行目标方法;
- 异常回滚 / 正常提交;
- 清理资源。
延伸思考:
- 默认只回滚 RuntimeException 和 Error,检查异常需要
rollbackFor。 - 面试常问:为什么 @Transactional 方法用
private修饰无效?
→ 因为 AOP 代理无法拦截 private 方法。
10. 循环依赖如何解决?
核心回答:
Spring 通过 三级缓存 解决单例 Bean 的循环依赖。
深入解析:
- 三级缓存:
- singletonObjects(成品对象)
- earlySingletonObjects(提前暴露的对象,半成品)
- singletonFactories(对象工厂,提供代理对象)
- 流程:A → 依赖 B → B 依赖 A → 提前暴露 A 的引用给 B → 依赖注入成功。
延伸思考:
- 只能解决 单例 + setter 注入 的循环依赖;
- 构造器注入循环依赖 无法解决,会抛出异常。
11. Spring 的三级缓存解决了什么问题?
核心回答:
解决 单例 Bean 的循环依赖,保证在创建过程中能提前暴露代理对象。
深入解析:
- Spring 单例池维护了 三级缓存:
singletonObjects→ 一级缓存(完全初始化的单例);earlySingletonObjects→ 二级缓存(提前暴露的半成品对象);singletonFactories→ 三级缓存(对象工厂,通常生成代理对象)。
- 流程:
- 当 A 依赖 B,B 又依赖 A 时:
- 创建 A → 放入三级缓存;
- B 依赖 A 时,先从三级缓存拿到 A 的工厂,暴露提前代理对象 → 放入二级缓存;
- 等 A 完成初始化后,替换为一级缓存。
- 当 A 依赖 B,B 又依赖 A 时:
延伸思考:
- 面试官可能追问:为什么要三级缓存,不是二级就够了吗?
→ 因为 AOP 代理对象需要通过工厂提前暴露,不能只放半成品对象。 - 只支持 setter 注入的循环依赖,不支持 构造函数循环依赖。
12. Spring MVC 的工作流程?
核心回答:
请求 → DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → ViewResolver → 响应。
深入解析:
- 用户请求到达 DispatcherServlet;
- DispatcherServlet 调用 HandlerMapping 寻找处理器;
- 通过 HandlerAdapter 调用具体 Controller;
- Controller 执行业务逻辑,返回 ModelAndView;
- ViewResolver 解析视图;
- DispatcherServlet 渲染并返回响应给客户端。
延伸思考:
- 面试官常问:DispatcherServlet 是单例还是多例?(单例,线程安全依赖于无状态设计)。
- 实战:可以用
@RestController直接返回 JSON,省略 ViewResolver。
13. DispatcherServlet 的作用?
核心回答:
Spring MVC 的 前端控制器,负责请求分发和响应渲染。
深入解析:
- 职责:
- 拦截请求;
- 调用 HandlerMapping 找到 Controller;
- 调用 HandlerAdapter 执行 Controller;
- 调用 ViewResolver 渲染视图;
- 返回响应。
- 核心思想:统一入口,集中控制。
延伸思考:
- 面试追问:DispatcherServlet 在 Spring Boot 中怎么注册的?
→ 自动配置类DispatcherServletAutoConfiguration注册。
14. Spring Boot 的自动装配原理?
核心回答:
基于 @EnableAutoConfiguration + SpringFactoriesLoader 实现,根据 classpath 条件加载配置类。
深入解析:
@SpringBootApplication→ 启用@EnableAutoConfiguration;SpringFactoriesLoader读取META-INF/spring.factories文件;- 根据
@Conditional注解判断是否加载; - 把匹配的 Bean 注册到容器。
延伸思考:
- 面试官可能追问:怎么禁用某个自动配置?
→@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) - 或者在
application.yml中配置spring.autoconfigure.exclude。
15. Spring Boot Starter 的机制?
核心回答:
Starter 是一组 依赖封装 + 自动配置,简化第三方库集成。
深入解析:
- 命名规范:
spring-boot-starter-xxx; - 依赖 Starter → 自动引入必要依赖 + 自动装配类;
- 例子:
spring-boot-starter-web→ 引入 Spring MVC + Tomcat + Jackson,并自动配置 DispatcherServlet。
延伸思考:
- 面试官常问:怎么写一个自定义 Starter?
- 提供
AutoConfiguration类; - 在
META-INF/spring.factories注册; - 打包发布。
- 提供
16. @ConfigurationProperties 和 @Value 的区别?
核心回答:
@Value:单个属性注入;@ConfigurationProperties:批量绑定配置,支持类型安全。
深入解析:
@Value("${key}"):只能注入单个值;@ConfigurationProperties(prefix = "xxx"):把配置文件中xxx.*映射到 JavaBean。@ConfigurationProperties更适合复杂配置对象。
延伸思考:
- 面试官可能追问:推荐用哪个?
→@ConfigurationProperties,因为支持校验(@Validated)、IDE 提示。 - 实战:数据库配置常用
@ConfigurationProperties。
17. Spring Boot 如何实现热部署?
核心回答:
主要通过 Spring Boot DevTools 或 JRebel。
深入解析:
- DevTools:监控 classpath 文件变化,触发 SpringContext 重启;
- JRebel:字节码增强,做到无感知热替换;
- IDE(IntelliJ IDEA / Eclipse)也支持 Build 自动 reload。
延伸思考:
- DevTools 适合开发,JRebel 更强大但收费。
- 生产环境不建议热部署,而是灰度发布/滚动更新。
18. Spring Boot 如何处理配置文件?
核心回答:
Spring Boot 读取 application.yml/properties,并支持多环境和优先级加载。
深入解析:
- 默认加载顺序:
- 命令行参数;
application.properties/application.yml;- 外部配置文件(config 目录);
- 默认配置。
- 支持多环境:
application-dev.yml、application-prod.yml,通过spring.profiles.active=dev切换。
延伸思考:
- 面试官可能问:优先级谁最高?
→ 命令行参数。 - 实战:生产环境常用外部化配置(如
--spring.config.location)。
19. @Conditional 注解的作用?
核心回答:
用于 条件装配,根据环境 / 配置 / Bean 是否存在决定是否加载 Bean。
深入解析:
- 常见实现:
@ConditionalOnClass(类存在时装配);@ConditionalOnMissingBean(没有 Bean 时装配);@ConditionalOnProperty(配置项满足条件时装配);@ConditionalOnWebApplication。
- 广泛用于自动配置。
延伸思考:
- 面试追问:怎么写自定义条件?
→ 实现Condition接口,重写matches()方法。
20. Spring Boot 常见的优化点?
核心回答:
- 启动优化:懒加载、裁剪 Starter;
- 内存优化:减小 Bean 数量、引入轻量组件;
- 数据库优化:连接池、批量操作;
- 生产优化:Actuator 监控、外部化配置。
深入解析:
- 启动性能:
spring.main.lazy-initialization=true; - 数据库性能:HikariCP 连接池、批处理 SQL;
- 缓存优化:整合 Redis、Caffeine;
- 日志优化:异步日志(Logback AsyncAppender);
- 部署优化:分层构建(Layered Jar)、Docker 镜像加速。
延伸思考:
- 面试常问:Spring Boot 为什么启动比 Spring 慢?
→ 因为要做自动装配和环境扫描。 - 实战:生产中可用
spring-context-indexer加快启动。
21. Spring Boot 的自动配置原理
核心回答
Spring Boot 的自动配置基于 SpringFactoriesLoader + @EnableAutoConfiguration + 条件注解(@ConditionalXXX)。
它会根据 classpath 中依赖的 jar 包和已有 Bean 自动装配所需的配置。
深入解析
- 关键入口:
@SpringBootApplication→@EnableAutoConfiguration→AutoConfigurationImportSelector。
- 配置来源:
META-INF/spring.factories或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。
- 条件装配:
- 通过
@ConditionalOnClass、@ConditionalOnMissingBean等来避免重复配置。
- 通过
- 运行时流程:
- 启动时加载所有候选配置类 → 根据条件筛选 → 注册 BeanDefinition → 实例化 Bean。
延伸思考
- 面试追问:如何排查“为什么某个 Bean 没有生效”?
→ 使用--debug参数查看 自动配置报告。 - 实战:如果不想加载某个自动配置,可以用
@SpringBootApplication(exclude=XXXAutoConfiguration.class)。
22. Spring Boot 的启动流程
核心回答
启动流程:
- 创建
SpringApplication对象。 - 准备环境(Environment)。
- 加载 ApplicationContext。
- 调用自动配置。
- 启动内嵌容器(Tomcat/Jetty/Netty)。
- 执行
CommandLineRunner和ApplicationRunner。
深入解析
- 源码入口:
SpringApplication.run()。 - 事件监听:
ApplicationStartingEvent(最早事件),ApplicationReadyEvent(启动完成)。
- Bean 加载:
- 先加载主类所在包及子包的组件(
@ComponentScan)。 - 再结合自动配置。
- 先加载主类所在包及子包的组件(
延伸思考
- 面试官可能问:Spring Boot 如何支持外部配置?
→ 通过ConfigFileApplicationListener解析application.properties/application.yml并注入 Environment。
23. Spring Boot 的配置文件加载顺序
核心回答
Spring Boot 的配置优先级(从高到低):
- 命令行参数
application.properties/yml(在config/> 当前目录 > classpath 下)- 外部系统环境变量
- JNDI、系统属性
- 默认配置
深入解析
- 多 profile:
application-dev.yml会在--spring.profiles.active=dev时覆盖默认配置。 - 优先级控制:同一属性多次定义时,高优先级覆盖低优先级。
延伸思考
- 面试追问:如果配置冲突,如何快速定位?
→ 启动时加参数--debug,查看配置源和优先级。
24. Spring Boot 如何实现热部署
核心回答
通过 Spring Boot DevTools 或 JRebel 实现热部署。
深入解析
- DevTools 原理:
- 使用两个 ClassLoader:
- Base ClassLoader(第三方依赖不会变),
- Restart ClassLoader(自己代码,改动后只重新加载)。
- 使用两个 ClassLoader:
- 局限性:修改依赖库不会触发热部署,只能重启。
延伸思考
- 实际开发中推荐用 DevTools + IDEA 自动构建。
- 面试追问:生产环境如何实现热更新?
→ 使用 Spring Cloud Config + 动态刷新(/actuator/refresh)。
25. Spring Boot 中的监控和健康检查
核心回答
Spring Boot 提供 Actuator 模块,可监控应用运行状态。
深入解析
- 常见端点:
/actuator/health(健康检查)/actuator/metrics(性能指标)/actuator/env(环境变量)/actuator/beans(已加载 Bean)
- 健康检查可扩展:实现
HealthIndicator接口,自定义检查逻辑(如 Redis、MQ)。
延伸思考
- 面试追问:如何在 Kubernetes 中结合健康检查?
→ 配置 livenessProbe 和 readinessProbe 调用/actuator/health。
26. Spring Boot 如何集成数据库(JDBC、JPA、MyBatis)
核心回答
Spring Boot 提供 spring-boot-starter-jdbc、spring-boot-starter-data-jpa 等 starter,支持开箱即用。
深入解析
- JDBC:直接操作数据库,结合
JdbcTemplate。 - JPA:基于 Hibernate,支持声明式 ORM(
@Entity)。 - MyBatis:需引入第三方 starter(
mybatis-spring-boot-starter),用@MapperScan。
延伸思考
- 面试追问:Spring Data JPA 与 MyBatis 区别?
- JPA → ORM 自动化,适合快速开发。
- MyBatis → SQL 手写,灵活可控,适合复杂业务。
27. Spring Boot 如何实现日志管理
核心回答
Spring Boot 默认使用 SLF4J + Logback,可支持 Log4j2。
深入解析
- 配置方式:
application.yml或logback-spring.xml。
- 日志级别:
TRACE<DEBUG<INFO<WARN<ERROR。 - 多环境日志:
logback-spring.xml+springProfile标签可区分 dev / prod。
延伸思考
- 面试追问:如何将日志输出到 ELK(ElasticSearch + Logstash + Kibana)?
→ 使用logstash-logback-encoder。
28. Spring Boot 如何处理跨域问题(CORS)
核心回答
通过 @CrossOrigin 或全局配置 CorsFilter。
深入解析
方式1:注解
1
2
3
public String getData() { ... }方式2:全局配置
1
2
3
4
5
6
7
8
9
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
};
}
延伸思考
- 面试追问:CORS 和 Nginx 反向代理跨域的区别?
- CORS → Spring Boot 处理,适合单应用。
- Nginx → 网关层处理,适合微服务架构。
29. Spring Boot 中的安全机制(Spring Security)
核心回答
Spring Security 提供认证(Authentication)+ 授权(Authorization)。
深入解析
- 认证流程:
- 用户请求 →
UsernamePasswordAuthenticationFilter→ AuthenticationManager → UserDetailsService → 认证成功 → SecurityContext 保存信息。
- 用户请求 →
- 授权机制:
- 基于 URL(拦截路径)、方法级(
@PreAuthorize)、表达式。
- 基于 URL(拦截路径)、方法级(
- 默认用户:Spring Boot 2.x 默认生成随机密码(日志中输出)。
延伸思考
- 面试追问:如何在 Spring Security 中集成 JWT?
- 自定义过滤器,解析 JWT 并注入 SecurityContext。
30. 如何优化 Spring Boot 启动速度
核心回答
优化手段:减少自动配置、延迟初始化、引入轻量依赖。
深入解析
- 延迟加载:
spring.main.lazy-initialization=true。 - 移除不必要的 starter。
- 本地调试时跳过安全/监控配置。
- 使用 GraalVM Native Image 加速启动。
延伸思考
- 面试追问:Spring Boot 启动慢,如何排查?
- 使用
--debug和ApplicationStartup查看 Bean 加载耗时。
- 使用
五、Spring Cloud(30题)
1. Spring Cloud 是什么?
核心回答
Spring Cloud 是基于 Spring Boot 的微服务治理框架,提供了分布式系统所需的全套组件:服务注册发现、负载均衡、配置中心、网关、熔断限流、链路追踪等。
深入原理
- 定位:微服务的 “生态整合者”,不是单一技术,而是一套解决方案。
- 核心组件:
- Eureka(注册中心)
- Ribbon / LoadBalancer(客户端负载均衡)
- Feign(声明式远程调用)
- Hystrix / Resilience4j / Sentinel(熔断限流)
- Config Server(配置中心)
- Gateway(网关)
- Sleuth + Zipkin(链路追踪)
- 本质:解决分布式复杂性,降低开发门槛。
面试延伸
- Spring Cloud 与 Kubernetes Service Mesh 的区别:前者 SDK 模式,后者 Sidecar 模式。
2. Spring Cloud 和 Dubbo 的区别?
核心回答
- Dubbo:高性能 RPC 框架,核心是远程调用。
- Spring Cloud:全家桶式微服务生态,功能更全面。
深入原理
- 协议层:Dubbo 用 TCP/自定义协议,性能高;Spring Cloud 多用 REST/HTTP。
- 功能覆盖:Dubbo 主要做服务调用,Spring Cloud 包含注册、配置、熔断、网关等。
- 演进:Dubbo3.0 开始支持 gRPC/Triple 协议,与 Spring Cloud 越来越接近。
面试延伸
- 公司内网场景(追求性能) → Dubbo;
- 大规模分布式互联网场景(追求生态) → Spring Cloud。
3. Eureka 的工作原理?
核心回答
Eureka 是 Netflix 开源的注册中心,负责 服务注册与发现,遵循 AP 原则。
深入原理
- 注册:服务启动后将自身信息(IP、端口、状态)注册到 Eureka Server。
- 续约:服务定期发送心跳(默认 30s)。
- 下线:服务关闭时发送下线请求。
- 拉取:客户端定期从 Server 拉取注册表(默认 30s),本地缓存,提高可用性。
面试延伸
- 面试官可能问:Eureka 与 Zookeeper 区别?
- Eureka AP → 保证可用性(即使部分节点挂了仍能服务);
- Zookeeper CP → 强一致性(选举可能导致服务不可用)。
4. Eureka 的自我保护机制?
核心回答
Eureka 会在网络分区或心跳丢失时,不立即移除服务实例,保证可用性。
深入原理
- 触发条件:在 15 分钟内心跳失败比例 > 85%。
- 行为:
- 暂停剔除失效服务。
- 保留已有注册表,允许客户端继续获取服务。
- 好处:防止因网络抖动导致“服务雪崩”。
面试延伸
- 缺点:可能把已经宕机的实例继续暴露出去。
- 可配置
eureka.server.enable-self-preservation=false来关闭。
5. Ribbon 的负载均衡策略?
核心回答
Ribbon 是客户端负载均衡组件,常见策略有:
- 轮询(RoundRobin)
- 随机(Random)
- 权重(WeightedResponseTimeRule)
- 最小并发(BestAvailableRule)
深入原理
Ribbon 在客户端维护服务列表,从注册中心拉取后,根据策略选择目标服务发起请求。
面试延伸
- 与 Nginx 的区别:Ribbon 是 客户端负载均衡(调用方决定),Nginx 是 服务端负载均衡(请求先到代理)。
6. Ribbon 和 Nginx 的区别?
核心回答
- Ribbon:客户端负载均衡,本地决定调用哪台服务。
- Nginx:服务端负载均衡,所有流量先经过代理。
深入原理
- 部署位置:Ribbon 在调用方应用中,Nginx 独立部署。
- 扩展性:Ribbon 与 Eureka 配合,支持动态上下线;Nginx 需手动改配置或借助 Consul/Keepalived。
面试延伸
- 面试官追问:为什么微服务更倾向于 Ribbon?
- 因为客户端模式更灵活,可以无感知扩容/缩容。
7. Feign 的工作原理?
核心回答
Feign 是声明式 HTTP 客户端,通过接口 + 注解的方式调用远程服务。
深入原理
- 运行时基于 动态代理(JDK Proxy)。
- 结合 Ribbon 负载均衡,自动选择服务实例。
- 可集成 Hystrix/Sentinel,支持熔断。
面试延伸
- 面试官问:Feign 和 RestTemplate 的区别?
- RestTemplate → 代码侵入性强,需要手动拼接请求。
- Feign → 声明式,更优雅。
8. Feign 如何支持熔断?
核心回答
Feign 可集成 Hystrix 或 Sentinel,通过 Fallback 提供降级逻辑。
深入原理
- Hystrix:在请求失败/超时时调用 fallback 方法。
- Sentinel:通过注解 @SentinelResource + fallback 实现。
面试延伸
- 面试官追问:熔断和降级区别?
- 熔断:保护系统不被拖垮。
- 降级:提供兜底逻辑,保证用户体验。
9. Hystrix 的工作原理?
核心回答
Hystrix 通过 线程池隔离 + 熔断器机制 保护服务,防止雪崩效应。
深入原理
- 线程池隔离:不同服务调用用不同线程池,避免相互影响。
- 熔断机制:
- 统计请求失败率。
- 达到阈值 → 熔断打开,直接走 fallback。
- 一段时间后 → 尝试半开,成功则恢复。
面试延伸
- Hystrix 已停止维护,推荐用 Resilience4j 或 Sentinel。
10. Hystrix 的线程池隔离和信号量隔离?
核心回答
- 线程池隔离:每个依赖用独立线程池,适合耗时操作。
- 信号量隔离:限制并发数,不开线程池,适合低延迟操作。
深入原理
- 线程池隔离:开销大,但最安全。
- 信号量隔离:轻量,但不能防止调用阻塞线程。
面试延伸
- 实战经验:大多数 IO 调用用线程池隔离,本地计算或轻量调用用信号量隔离。
11. Resilience4j 和 Hystrix 区别?
- Resilience4j 基于 Java 8 函数式接口,更轻量,支持熔断、限流、重试、隔离等;
- Hystrix 已停止维护,功能单一(熔断+线程隔离)。
- Resilience4j 模块化,可按需引入。
12. Gateway 的核心功能?
- 路由转发(根据路径、Header、参数转发请求)。
- 过滤器机制(统一鉴权、日志、限流)。
- 负载均衡(结合注册中心)。
- 协议支持(WebSocket、HTTPS)。
13. Gateway 和 Zuul 的区别?
- Zuul1 基于 Servlet,阻塞 IO,性能差。
- Gateway 基于 Netty + Reactor,支持响应式,性能高。
- Zuul2 虽然改为异步,但生态上 Gateway 更推荐。
14. Config Server 的作用?
- 统一管理微服务配置,支持 Git、SVN 等存储。
- 动态刷新配置,保证环境一致性。
- 与 Bus 结合可广播刷新。
15. Config Server 如何保证配置实时刷新?
- 客户端通过
@RefreshScope实现动态刷新。 - 手动触发
/actuator/refresh。 - 结合 Spring Cloud Bus,可用消息队列广播刷新。
16. Spring Cloud Bus 的原理?
- 底层基于消息队列(Kafka、RabbitMQ)。
- 当配置变更时,Bus 广播事件,所有服务接收并刷新配置。
17. Sleuth 的作用?
- 在微服务调用链路中,自动生成 TraceId / SpanId。
- 实现请求链路追踪,定位性能瓶颈。
18. Sleuth 与 Zipkin 的关系?
- Sleuth:负责埋点、生成日志。
- Zipkin:负责收集、存储、展示调用链数据。
19. 分布式链路追踪的原理?
- 通过唯一 TraceId 贯穿整个调用链。
- 每次远程调用生成新的 SpanId。
- 数据上报到集中式系统(Zipkin/Jaeger)。
20. Spring Cloud Stream 的作用?
- 封装消息中间件(Kafka/RabbitMQ)。
- 提供统一编程模型,屏蔽底层差异。
- 支持消息驱动架构。
21. 消息驱动模型和事件驱动模型区别?
- 消息驱动:强调解耦,消息可靠投递。
- 事件驱动:强调系统对事件的响应。
- Spring Cloud Stream 更偏消息驱动,但也支持事件模式。
22. Nacos 和 Eureka 的区别?
- Nacos 除了服务注册发现,还提供配置中心功能。
- Nacos 支持 AP/CP 模式切换;Eureka 固定 AP。
- Nacos 提供 UI,更易运维。
23. Nacos 配置中心和注册中心的实现原理?
- 配置中心:基于推拉结合(长轮询 + Server 推送)。
- 注册中心:基于心跳机制维护服务健康状态。
24. Sentinel 的工作原理?
- 基于 滑动窗口统计 请求 QPS、响应时间。
- 支持多种限流、熔断、降级规则。
- 与 Dubbo/Spring Cloud/Feign 无缝集成。
25. 限流的常见算法(令牌桶、漏桶)?
- 令牌桶:系统以固定速率生成令牌,请求需拿到令牌才执行。
- 漏桶:请求先进入桶,系统以固定速率处理,超出则丢弃。
- 区别:令牌桶允许突发流量,漏桶更平滑。
26. 熔断和降级的区别?
- 熔断:系统保护机制,拒绝调用,防止雪崩。
- 降级:业务兜底方案,返回备用结果。
- 熔断是“硬防护”,降级是“软兜底”。
27. CAP 定理和 Spring Cloud 的关系?
- CAP:一致性(C)、可用性(A)、分区容错性(P)。
- Eureka → AP;Consul → CP;Nacos → AP/CP 切换。
28. Spring Cloud 与 Kubernetes 的关系?
- Spring Cloud:偏应用层,开发框架。
- Kubernetes:偏基础设施,容器编排。
- 可结合使用:Spring Cloud 负责应用治理,K8s 负责资源调度。
29. 微服务中的灰度发布?
- 指新版本只对部分用户开放。
- 实现方式:
- Nginx/Gateway 按用户 ID 或流量比例路由。
- Service Mesh(Istio)支持更灵活的流量控制。
30. Spring Cloud 如何实现服务网格?
- 原生 Spring Cloud 偏 SDK 模式,缺乏服务网格能力。
- 可结合 Spring Cloud + Istio/Envoy,实现流量治理、观测、熔断、灰度。
- 越来越多团队采用 Spring Cloud Alibaba + Service Mesh 的混合方案。
六、微服务架构 & 分布式系统(30题)
1. 什么是微服务?
核心回答
微服务是一种架构风格,将单体应用拆分为一组小型、独立部署的服务,每个服务聚焦某个业务能力,通过 API(HTTP/gRPC/MQ)通信。
深入原理
- 每个服务独立开发、部署、扩容。
- 服务自治:有自己的数据库、缓存、逻辑。
- 通常结合容器化、CI/CD、服务治理框架(如 Spring Cloud、Dubbo、K8s)。
延伸思考
- 面试追问:和单体应用区别? → 单体耦合度高,微服务解耦但更复杂。
2. 微服务与 SOA 的区别?
核心回答
- SOA:面向服务的架构,通常依赖 ESB(企业服务总线)。
- 微服务:轻量级、去中心化,强调小而独立,常用 REST/gRPC。
深入原理
- SOA → 偏企业内部,服务较大,依赖统一中间件(如 ESB)。
- 微服务 → 服务更小,避免单点中心,强调去中心化自治。
延伸思考
- 面试追问:为什么微服务比 SOA 更流行? → 云原生背景下,微服务更适应敏捷开发和快速迭代。
3. 微服务的优缺点?
核心回答
- 优点:独立部署、技术多样性、弹性伸缩、快速迭代。
- 缺点:分布式复杂性、运维难度高、数据一致性挑战、服务治理成本高。
延伸思考
- 面试官常问:什么时候不适合微服务? → 业务小、团队小、系统简单时。
4. 微服务的拆分原则?
核心回答
- 按业务边界(DDD 的领域驱动设计)。
- 高内聚、低耦合。
- 一个服务只聚焦一个核心业务能力。
深入原理
- 拆分方式:按业务(订单、用户、支付)、按场景(核心 vs 辅助)、按非功能需求(性能瓶颈点)。
延伸思考
- 常见错误:按数据库表拆分,导致过度依赖跨服务调用。
5. 如何保证微服务之间的数据一致性?
核心回答
- 避免强一致,采用 最终一致性。
- 方案:分布式事务、消息驱动、补偿机制。
深入原理
- 同步方案:XA(两阶段提交)。
- 异步方案:TCC、SAGA、可靠消息。
延伸思考
- 面试官追问:你在项目中怎么做? → 消息队列(事务消息/本地消息表)。
6. 分布式事务有哪些解决方案?
核心回答
- XA(两阶段提交)
- TCC(Try-Confirm-Cancel)
- SAGA(长事务补偿)
- 本地消息表 / 事务消息
深入原理
- XA:强一致性,但性能差。
- TCC:业务入侵高,但灵活。
- SAGA:长事务场景,失败时用补偿。
- 本地消息表:保证最终一致性。
7. TCC、SAGA、XA 的区别?
- XA:数据库层两阶段提交,强一致,性能差。
- TCC:应用层三步(预留资源、确认、回滚),侵入性高。
- SAGA:长事务,每步都有补偿动作,保证最终一致性。
8. 本地消息表和可靠消息最终一致性?
- 本地消息表:在本地事务中写业务表 + 消息表 → MQ 异步投递 → 消费端确认。
- 可靠消息:MQ 支持事务(如 RocketMQ 半消息),确保消息必达,保证最终一致性。
9. 分布式锁的实现方式?
- 数据库锁(悲观锁/唯一索引)。
- Redis 分布式锁(setnx + expire)。
- Zookeeper 分布式锁(临时顺序节点)。
10. Redis 分布式锁的缺陷?
- 单点问题(需 Redis Cluster 或 Redlock)。
- 锁过期可能导致误释放。
- 不能保证严格公平。
11. Zookeeper 分布式锁的实现?
- 利用 临时顺序节点:
- 客户端创建顺序节点。
- 判断自己是否是最小节点 → 获取锁。
- 不是则监听前一个节点的删除事件。
12. 数据库分库分表策略?
- 垂直分库(按业务拆分)。
- 水平分表(按用户 ID、时间做分片)。
- 常见路由方式:哈希取模、范围、时间。
13. 水平拆分与垂直拆分区别?
- 水平拆分:同一张表的数据分到多个库表(按范围/哈希)。
- 垂直拆分:不同业务表放到不同库(用户库、订单库)。
14. 分布式 ID 生成方案?
- UUID(无序,不适合数据库)。
- 数据库自增 ID(单点瓶颈)。
- Redis 自增。
- 雪花算法(Snowflake)。
- Leaf(美团开源)。
15. 雪花算法的原理?
- 64 位长整型:
- 时间戳(41 位)
- 机器 ID(10 位)
- 序列号(12 位)
- 保证全局唯一、有序。
16. API 网关的作用?
- 统一入口,做路由转发。
- 鉴权、安全控制。
- 流量控制、熔断、降级。
- 日志监控。
17. 服务注册与发现机制?
- 服务启动 → 注册中心注册(IP/端口/状态)。
- 调用方 → 从注册中心获取地址列表。
- 注册中心 → 通过心跳维持健康状态。
18. 微服务如何保证高可用?
- 服务冗余(多实例部署)。
- 负载均衡。
- 熔断、降级、限流。
- 自动故障转移(K8s + 服务治理框架)。
19. 微服务中的限流和熔断?
- 限流:控制流量,保护系统(令牌桶/漏桶)。
- 熔断:当下游服务异常时,直接失败并快速返回。
20. 微服务如何保证安全性?
- 认证授权(OAuth2、JWT)。
- HTTPS、加密通信。
- API 网关统一鉴权。
- 数据脱敏。
21. OAuth2 的原理?
- 基于令牌的认证授权协议。
- 四种模式:授权码、密码、客户端凭证、简化模式。
- 典型流程:用户 → 授权服务器 → 获取 Token → 调用资源服务器。
22. JWT 的作用?
- JSON Web Token,一种无状态令牌。
- 内容包含三部分:Header、Payload、Signature。
- 适合分布式系统做认证,无需存储 Session。
23. 单点登录 SSO 的实现?
- 用户只需登录一次,获取全局 Token/Session。
- 常见实现:CAS、OAuth2、JWT。
- 核心:统一认证中心。
24. 配置中心的作用?
- 集中化配置管理。
- 支持动态刷新。
- 环境隔离(dev/test/prod)。
25. 微服务中的监控体系?
- 指标监控:Prometheus + Grafana。
- 日志监控:ELK(Elasticsearch + Logstash + Kibana)。
- 链路追踪:Sleuth + Zipkin/Jaeger。
26. Prometheus 的原理?
- 基于 Pull 模型,定期从应用/Exporter 拉取指标。
- TSDB(时间序列数据库)存储数据。
- 结合 Grafana 做可视化。
27. ELK 在微服务中的应用?
- Logstash:日志采集。
- Elasticsearch:日志存储、搜索。
- Kibana:日志分析、可视化。
- 用于统一日志平台。
28. 服务调用链路追踪的意义?
- 解决“调用链不清楚”的问题。
- 快速定位性能瓶颈、错误位置。
- 提升运维可观测性。
29. 服务雪崩效应是什么?
- 一个服务故障 → 引发调用它的服务故障 → 最终导致整个系统崩溃。
- 原因:依赖调用无保护(无熔断/限流)。
30. 如何设计一个高并发的微服务系统?
核心思路
- 架构:微服务 + API 网关 + 服务治理。
- 高可用:负载均衡、多副本、自动故障转移。
- 高性能:缓存(Redis)、异步化、限流、降级。
- 数据层:分库分表、读写分离。
- 安全:OAuth2/JWT。
- 可观测性:监控 + 日志 + 链路追踪。
七、数据库 & 缓存(30题)
1. MySQL 的存储引擎区别
- 存储引擎是 MySQL 管理数据的底层组件,决定了数据存储方式、事务支持、锁机制、索引支持等。
- 主要存储引擎:
- InnoDB:事务型,支持行级锁、外键、MVCC、崩溃恢复。适合高并发、事务要求高的场景。
- MyISAM:非事务型,表级锁,支持全文索引。适合读多写少、日志或统计类表。
- Memory:数据存储在内存中,访问速度快,但断电数据丢失。适合临时表。
- Archive:归档引擎,只支持插入和查询,不支持更新,压缩存储。
- 面试重点:
- 选择存储引擎需根据业务需求:事务要求、并发量、数据量和恢复策略。
2. InnoDB 和 MyISAM 的区别
| 特性 | InnoDB | MyISAM |
|---|---|---|
| 事务 | 支持 ACID | 不支持 |
| 锁类型 | 行级锁+表锁 | 表级锁 |
| 外键 | 支持 | 不支持 |
| 崩溃恢复 | 自动恢复 | 需手动修复 |
| 全文索引 | 5.6+ 支持 | 支持 |
| 并发场景 | 高并发写+读 | 读多写少 |
| 适用案例 | 金融、电商订单 | 日志分析、统计表 |
3. MySQL 事务特性
- ACID 原则:
- 原子性(Atomic):事务全部执行或全部回滚。
- 一致性(Consistency):事务执行前后数据库状态合法。
- 隔离性(Isolation):事务互不干扰。
- 持久性(Durability):提交后数据不会丢失。
- 隔离级别:
- Read Uncommitted:允许脏读。
- Read Committed:防止脏读,但可能不可重复读。
- Repeatable Read(默认 InnoDB):防止不可重复读,间隙锁解决幻读。
- Serializable:完全串行执行,性能最低,但完全隔离。
- 面试追问:隔离级别与死锁、性能的权衡。
4. MVCC 的实现原理
- 原理:
- InnoDB 为每行增加隐藏列
trx_id和roll_pointer。 - 查询时根据事务 ID 判断哪一版本可见。
- 读操作不阻塞写操作,实现 非阻塞读。
- InnoDB 为每行增加隐藏列
- 应用:提高并发性能,避免读写互相阻塞。
- 面试追问:MVCC 如何实现 Repeatable Read,为什么不锁表。
5. MySQL 的索引类型
- B+ 树索引:范围查询、排序,叶子节点按顺序连接。
- 哈希索引:精确查找快(Memory 引擎)。
- 全文索引:文本搜索。
- 空间索引:地理信息查询(R-Tree)。
- 面试追问:什么时候选择组合索引,覆盖索引优化查询。
6. 聚簇索引 vs 非聚簇索引
- 聚簇索引:
- 数据行存储顺序与主键顺序一致。
- 查询范围、排序效率高。
- 非聚簇索引:
- 索引存 key + 主键,回表获取其他字段。
- 面试追问:聚簇索引插入顺序对性能影响,回表成本。
7. B+ 树索引原理
- 内节点:存 key + 子节点指针
- 叶子节点:存完整记录或指针,并链表连接
- 特点:
- 支持范围查询
- 磁盘 IO 高效
- 面试追问:为什么 MySQL 选择 B+ 树而不是 B 树或 Hash。
8. 覆盖索引 vs 回表
- 覆盖索引:索引包含所有查询字段,无需访问表。
- 回表:索引只包含部分字段,需要通过主键查表。
- 优化方法:尽量让热点查询使用覆盖索引,减少 IO。
9. 索引下推优化
- WHERE 条件在存储引擎层过滤,减少返回上层的数据量。
- 特别在组合索引或函数查询时有效。
- 面试追问:索引下推能否应用于非 InnoDB 存储引擎。
10. MySQL 锁机制
- 表锁:锁整张表,低并发。
- 行锁:锁单行,InnoDB 默认。
- 意向锁:表级锁意图,避免死锁。
- 间隙锁:防止幻读。
- 面试追问:行锁、表锁的实现原理,锁冲突如何处理。
11. 行锁 vs 表锁
- 行锁:粒度小,高并发,事务安全。
- 表锁:粒度大,适合批量操作。
- 应用场景:高并发写用行锁,批量更新可用表锁。
12. 间隙锁
- 锁定索引间隙,防止其他事务插入幻读。
- 与记录锁结合形成 Next-Key Lock。
- 默认在 Repeatable Read 下使用。
13. 死锁排查
SHOW ENGINE INNODB STATUS\G查看最新死锁。- 优化:
- 统一访问表顺序。
- 尽量缩短事务时间。
- 加索引减少扫描行数。
14. SQL 执行计划
- 使用
EXPLAIN分析:- type:访问类型(ALL < INDEX < REF < CONST)
- key:使用的索引
- rows:扫描行数
- Extra:如 Using index, Using temporary
15. 慢查询优化
- 建索引(覆盖索引、组合索引)
- 避免 SELECT *
- 避免在列上函数操作
- 分库分表
- 缓存(Redis/Caffeine)
16. 分库分表难点
- 跨库 join、分页
- 分布式事务
- 全局唯一 ID
- 数据路由、迁移
17. 分布式事务
- XA:两阶段提交,保证强一致性。
- TCC:Try → Confirm → Cancel,业务补偿操作。
- SAGA:长事务补偿。
- 本地消息表 + MQ:最终一致性。
18. Redis 数据结构
- String、Hash、List、Set、Sorted Set
- Bitmap、HyperLogLog、Geo
- 应用:排行榜、计数、集合操作、地理位置、布隆过滤器
19. Redis 为什么快
- 全内存存储
- 单线程 + epoll
- 高效数据结构
- 避免锁竞争
20. Redis 持久化
- RDB:定期快照
- AOF:写命令日志,append only
- 混合持久化:RDB + AOF,提高安全性与效率
21. Redis 过期策略
- 惰性删除:访问时检查 TTL
- 定期删除:周期性扫描
- 定时删除:每个 key 单独定时器
22. Redis 内存淘汰策略
- noeviction、allkeys-lru、volatile-lru、allkeys-lfu、volatile-ttl
- LRU:最近最少使用
- LFU:使用频率最低
- TTL:临期优先淘汰
23. Redis 主从复制
- 异步复制
- 全量复制 + 增量复制
- 读写分离,提升扩展性
24. Redis 哨兵机制
- 监控、通知、故障转移
- Sentinel 投票机制切换主节点
- 高可用架构核心组件
25. Redis 集群模式
- 16384 个哈希槽分片
- Master + Slave
- 跨槽操作有限制
- 高可用 + 水平扩展
26. Redis 分布式锁
- 单机:SETNX + EXPIRE
- Redisson:
- 自动续期(看门狗机制)
- 可重入锁
- 公平锁
- RedLock:多节点强一致性
27. Redis 常见问题
- 内存不足 / OOM
- 热点 key
- bigkey 导致阻塞
- RDB fork 阻塞
- 主从延迟
28. 缓存穿透、击穿、雪崩
- 穿透:不存在数据频繁访问 → 布隆过滤器
- 击穿:热点 key 瞬间失效 → 互斥锁/永不过期缓存
- 雪崩:大量 key 同时过期 → TTL + 随机值
29. 缓存与数据库一致性
- Cache Aside:先 DB,再删缓存
- 双写:DB + 缓存同步更新
- 异步消息:MQ + 延时队列,实现最终一致性
30. Redis vs Caffeine
| 特性 | Redis | Caffeine |
|---|---|---|
| 类型 | 分布式缓存 | 本地缓存 |
| 持久化 | RDB / AOF | 不支持 |
| 集群 | 支持 | 不支持 |
| 访问速度 | 毫秒级 | 纳秒级 |
| 场景 | 分布式共享,高并发 | 本地热点缓存 |
最佳实践:Caffeine(本地缓存)+ Redis(分布式缓存)+ DB → 多级缓存结构。
八、消息队列(30题)
1. 为什么要使用消息队列?
- 解耦系统:生产者与消费者不直接调用,通过队列传递消息。
- 异步处理:用户请求不等待后端业务完成,提高响应速度。
- 削峰填谷:缓冲高峰流量,避免数据库或服务压力过大。
- 可靠传输:通过持久化、ACK 机制保证消息不丢失。
- 可扩展性:增加消费者可水平扩展,提高系统吞吐量。
- 面试追问:为什么 MQ 可以解决微服务间耦合问题?
2. 消息队列的优缺点
- 优点:
- 异步处理提升吞吐量
- 系统解耦
- 支持削峰填谷
- 消息可靠性可控
- 缺点:
- 系统复杂度增加
- 消息延迟
- 顺序和重复消费需要额外处理
- 优化点:结合幂等设计、延迟队列和批量处理
3. RabbitMQ 核心组件
- Producer:发送消息
- Exchange:路由消息(Direct、Fanout、Topic、Headers)
- Queue:存储消息
- Binding:绑定 Exchange 和 Queue
- Consumer:接收消息
- 原理:Exchange 决定路由规则,Queue 负责缓冲和投递
4. RabbitMQ 消息确认机制
- 生产者端:
- Publisher Confirm,确保消息写入 Broker
- 消费者端:
- 自动 ack:消费即确认
- 手动 ack:成功处理后确认
- 应用实践:手动 ack 可处理异常重试,保证消息不丢失
5. Kafka 核心概念
- Producer/Consumer:消息发送和接收
- Topic/Partition:主题和分区
- Broker/Cluster:节点和集群
- Consumer Group:消费组实现负载均衡
- Offset:记录消费位置
- 原理:生产者写入 Partition,消费者根据 Offset 消费
6. Kafka 分区机制
- Partition 提高并行
- 分区内顺序保证
- 路由策略:默认轮询,或 Key 哈希分区
- 面试点:如何保证全局顺序?只能保证分区内顺序
7. Kafka 副本机制
- Leader/Follower 结构
- 同步机制:ISR 列表跟随 Leader
- 高可用:Leader 异常自动切换
- 面试拓展:副本同步模式对性能的影响
8. Kafka ISR 原理
- ISR(In-Sync Replicas):跟上 Leader 的副本集合
- 消息提交规则:只要 ISR 内副本写入才算成功
- 优化:副本数量与 ACK 策略可调节可靠性与吞吐量
9. Kafka 消费组原理
- 同一组内的每个 Partition 只被一个 Consumer 消费
- 多组可重复消费,实现广播
- 提高并行消费,减少重复消费
- 拓展:消费组重平衡、再均衡机制
10. Kafka Offset 提交方式
- 自动提交:消费完成后自动提交 Offset
- 手动提交:
- 同步提交:阻塞等待
- 异步提交:性能高,可能丢少量消息
- 实践:结合事务实现 Exactly Once
11. Kafka 高吞吐量原理
- 顺序写磁盘,避免随机 IO
- 批量发送消息,减少网络开销
- 零拷贝机制,减少 CPU 消耗
- 面试拓展:如何调整批量大小提高吞吐量
12. Kafka 零拷贝机制
- 直接从磁盘 buffer 传到网络 buffer,无用户态复制
- 利用 sendfile() 提升大流量传输效率
13. RocketMQ 核心特点
- 高可靠,高性能
- 顺序消息、事务消息原生支持
- Broker 无状态,易水平扩展
- 支持多语言客户端
- 消息存储:CommitLog + ConsumeQueue + IndexFile
14. RocketMQ 顺序消息
- 通过 Key 哈希映射到队列
- 队列内顺序严格保证
- 应用场景:订单支付、流水处理
15. RocketMQ 事务消息
- 半消息机制:
- 发送半消息到 Broker
- 执行本地事务
- 提交或回滚消息
- 保证分布式事务最终一致性
16. 如何保证消息不丢失
- 生产者端:确认机制
- Broker端:消息持久化
- 消费者端:手动 ack + 幂等消费
17. 如何保证消息不重复消费
- 消息幂等:唯一 ID + 数据库唯一约束 + Redis 去重
- Kafka Exactly Once + 事务处理
18. 如何保证消息有序
- Kafka:Partition 内顺序
- RocketMQ:队列内顺序
- 注意:多 Partition 并行可能导致整体无序
19. 消息堆积解决办法
- 增加消费者数量
- 扩展队列分区
- 流量削峰
- 消费端批量处理,提高吞吐量
20. 如何设计延迟队列
- RabbitMQ:TTL + Dead Letter Queue
- Kafka:延迟消息插件 / 时间轮
- Redis:SortedSet + 轮询
- 场景:订单超时、短信延迟发送
21. 死信队列作用
- 消费失败消息收集到死信队列
- 支持重试、告警和问题排查
- 提高系统可靠性
22. 幂等实现方式
- 消息唯一 ID
- 数据库唯一约束
- Redis 去重缓存
- 本地事务结合 MQ 保证幂等
23. 消息队列限流方式
- 消费者限流:处理速率限制
- 生产者限流:令牌桶/漏桶
- Broker 队列容量限制 + 拒绝策略
24. 消息队列监控
- 消息堆积量
- 消费速率
- Broker 健康状态
- 延迟时间
- 工具:Prometheus + Grafana、Kafka Manager、RocketMQ Console、RabbitMQ Management
25. Kafka vs RabbitMQ
| 特性 | Kafka | RabbitMQ |
|---|---|---|
| 模型 | Pub/Sub + Queue | AMQP |
| 消息存储 | 持久化日志 | 队列存储 |
| 顺序保证 | Partition 内顺序 | 队列顺序 |
| 高吞吐 | 高(批量 + 零拷贝) | 中等 |
| 消费模式 | Pull / Push | Push |
| 使用场景 | 大数据流、日志、事件驱动 | 任务队列、异步处理、可靠投递 |
26. Kafka vs RocketMQ
| 特性 | Kafka | RocketMQ |
|---|---|---|
| 顺序消息 | Partition 内顺序 | 队列内顺序全局可控 |
| 事务消息 | 限制较多 | 原生支持 |
| 存储方式 | CommitLog | CommitLog + ConsumeQueue + IndexFile |
| 高可用 | ISR + 副本 | Master-Slave + HA |
| 生态 | Flink、Spark 集成 | 分布式事务、支付系统应用 |
27. Pulsar 特点
- 多租户支持
- 持久化使用 BookKeeper
- Topic 可分 Partition
- 支持事务、延迟消息
- Push/Pull 消费模式灵活
28. 消息中间件如何保证高可用
- 多副本 + 自动切换
- Broker 集群
- 消息持久化
- 客户端重试
- Zookeeper/Controller 管理元数据
29. 消息队列水平扩展
- Kafka:增加 Partition / Broker
- RabbitMQ:集群 + Shovel/Federation
- RocketMQ:增加 Broker + Topic 分区
- 注意:分区扩展可能影响顺序,需要 Key 路由
30. 消息队列 vs 事件总线
- MQ:关注消息可靠性、顺序、确认
- EventBus:关注解耦、广播、轻量级
- MQ 偏向可靠持久化,EventBus 偏向系统内部事件触发
九、DevOps & 容器化(30题)
1. CI/CD 的流程
- CI(Continuous Integration,持续集成):
- 开发者提交代码 → 自动构建 → 自动单元测试 → 自动生成构建产物
- CD(Continuous Delivery/Deployment,持续交付/部署):
- 自动部署到测试/生产环境 → 自动化验证 → 生产发布
- 工具链:
- Jenkins、GitLab CI、Argo CD
- 实践要点:
- 流程自动化
- 保证构建产物可回滚
- 面试拓展:CI/CD 与 DevOps 的关系
2. Jenkins 的作用
- 自动化构建、测试、部署工具
- 核心功能:
- 支持多种 SCM(Git、SVN)
- Pipeline 管理
- 插件生态丰富(Docker、K8s 集成)
- 面试追问:Pipeline 如何保证构建可重复、可回滚
3. Docker 核心原理
- 容器化技术:
- Linux namespace 隔离(进程、网络、文件系统)
- Linux cgroups 限制资源(CPU、内存)
- **联合文件系统(UnionFS)**实现镜像分层
- 容器 ≈ 轻量级虚拟机,共享宿主 OS 内核
4. Docker 镜像 vs 容器
| 区别 | 镜像 | 容器 |
|---|---|---|
| 定义 | 只读模板 | 镜像运行实例 |
| 存储 | 分层只读文件系统 | 读写层 + 镜像 |
| 生命周期 | 永久 | 临时,可停止/删除 |
| 用途 | 构建应用 | 运行应用 |
5. Dockerfile 常用指令
FROM:基础镜像RUN:执行命令构建镜像COPY/ADD:复制文件到镜像CMD/ENTRYPOINT:容器启动命令EXPOSE:声明端口ENV:环境变量- 优化:减少镜像层,顺序合理缓存
6. 镜像分层机制
- 镜像每条指令生成一个只读层
- 联合文件系统(UnionFS)叠加这些层
- 容器启动时增加读写层
- 优势:
- 共享公共层,节省空间
- 支持增量构建
7. 容器 vs 虚拟机
| 区别 | 容器 | 虚拟机 |
|---|---|---|
| 内核 | 共享宿主 OS 内核 | 独立 OS |
| 启动速度 | 秒级 | 分钟级 |
| 资源占用 | 轻量 | 重量 |
| 隔离 | 进程级 | 硬件级 |
8. Kubernetes 核心组件
- Master:
- API Server:接收用户请求
- Scheduler:Pod 调度
- Controller Manager:状态管理
- etcd:分布式配置存储
- Node:
- Kubelet:Pod 生命周期管理
- Kube-proxy:服务网络代理
- Container Runtime(Docker/CRI-O)
9. Pod 生命周期
- Pending → Running → Succeeded/Failed
- 状态管理:
- initContainer → main container → postStop Hook
- 面试点:Pod 重启策略(Always/OnFailure/Never)
10. Deployment vs StatefulSet
| 特性 | Deployment | StatefulSet |
|---|---|---|
| Pod 标识 | 无固定名字 | 有固定名字 |
| 顺序更新 | 可并行更新 | 严格顺序更新 |
| 持久化存储 | 共享卷 | 独立持久卷 |
| 应用场景 | 无状态服务 | 数据库、Kafka Broker 等状态服务 |
11. K8s 服务发现机制
- ClusterIP:集群内部访问
- NodePort:节点端口访问
- LoadBalancer:外部访问
- DNS + kube-proxy:Pod 名称解析 + iptables 转发
12. ConfigMap 和 Secret
| 特性 | ConfigMap | Secret |
|---|---|---|
| 数据类型 | 明文配置 | Base64 编码敏感信息 |
| 使用方式 | Env/Volume | Env/Volume |
| 安全性 | 不加密 | 加密可集成 KMS |
13. Ingress 的作用
- 集群外部访问 Pod 的入口
- 功能:
- 路由到不同服务
- TLS 终端
- 负载均衡
- 面试点:Ingress Controller vs Service LB
14. K8s 水平扩缩容机制
- HPA(Horizontal Pod Autoscaler):根据 CPU、内存或自定义指标调整 Pod 数量
- VPA(Vertical Pod Autoscaler):自动调整 Pod 资源配置
- Cluster Autoscaler:节点自动扩缩容
15. K8s 滚动更新机制
- Deployment 滚动更新策略:
maxUnavailable:最大不可用 PodmaxSurge:最大新增 Pod
- 保证无中断升级
16. K8s 灰度发布
- 方法:
- 分流流量(Ingress/Service Mesh)
- 多版本 Deployment
- Canary Pod + 路由权重
17. K8s 调度策略
- 默认调度:基于资源(CPU/Memory)
- 亲和性/反亲和性:
- NodeAffinity
- PodAffinity / PodAntiAffinity
- Taints & Tolerations:节点污点控制 Pod 调度
18. K8s 网络模型
- 要求:
- Pod 可以直接互相通信
- Pod 与 Node 通信
- 方案:
- Flannel、Calico、Cilium 等
- 面试点:Overlay 网络 vs Underlay 网络
19. Service Mesh 作用
- 透明通信管理
- 流量控制、熔断、限流
- 可观察性(Tracing、Metrics)
- 安全(mTLS)
20. Istio 工作原理
- Sidecar Proxy:每个 Pod 注入 Envoy
- 控制平面(Pilot):下发路由规则
- Mixer / Telemetry:采集指标和日志
- 应用:
- 灰度发布、流量镜像、故障注入、服务间安全通信
21. Helm 的作用
- Kubernetes 包管理工具
- 功能:
- Chart 模板化部署
- 版本管理
- 一键升级/回滚
- 面试拓展:Helm 与 Kustomize 区别
22. K8s 存储方案
- 临时存储:emptyDir
- 持久卷:PersistentVolume + PersistentVolumeClaim
- 存储类型:
- 本地存储
- NFS
- Ceph / GlusterFS
- 云存储(EBS、OSS)
23. Prometheus 监控体系
- Pull 模型采集指标
- TSDB 存储时间序列数据
- AlertManager 告警
- 可监控 K8s 组件、Pod、应用指标
24. Grafana 的作用
- 可视化 Prometheus 数据
- 仪表盘定制
- 支持多数据源(InfluxDB、ElasticSearch)
- 面试点:报警可视化和多租户支持
25. K8s 安全机制
- 认证(Authentication):Token / X.509 / OIDC
- 授权(Authorization):RBAC
- 网络策略(NetworkPolicy)
- Secret 加密
26. 容器资源限制
- CPU / Memory:requests / limits
- QoS 分类:
- Guaranteed、Burstable、BestEffort
- 面试点:容器 OOM 及限流策略
27. Docker Compose 作用
- 多容器应用管理
- 本地开发、测试
- 声明式配置网络、依赖关系
28. DevOps 最佳实践
- 持续集成/持续交付
- 基础设施即代码(IaC)
- 自动化测试与部署
- 微服务 + 容器化 + 云原生
29. GitOps 理念
- 将 Git 作为单一真源
- K8s 自动同步 Git 状态
- 优点:
- 可追踪、可回滚
- 流程统一、可审计
30. K8s 如何保证高可用
- Master 节点多副本(HA)
- ETCD 集群存储
- Pod 多副本 + Deployment/StatefulSet
- Service + LoadBalancer 提供稳定访问
- Cluster Autoscaler 支持节点弹性扩展
十、场景设计题(30题)
- 如何设计一个秒杀系统?
- 如何设计一个短链系统?
- 如何设计一个分布式 ID 生成系统?
- 如何设计一个高可用缓存系统?
- 如何设计一个订单系统?
- 如何设计一个支付系统?
- 如何设计一个消息推送系统?
- 如何设计一个日志收集系统?
- 如何设计一个搜索引擎?
- 如何设计一个推荐系统?
- 如何设计一个分布式锁?
- 如何设计一个统一鉴权系统?
- 如何设计一个限流系统?
- 如何设计一个灰度发布系统?
- 如何设计一个电商购物车?
- 如何设计一个库存系统?
- 如何设计一个分布式文件存储系统?
- 如何设计一个微服务网关?
- 如何设计一个分布式任务调度系统?
- 如何设计一个监控报警系统?
- 如何设计一个高并发聊天室?
- 如何设计一个排行榜系统?
- 如何设计一个评论系统?
- 如何设计一个分布式事务系统?
- 如何设计一个高可用注册中心?
- 如何设计一个数据同步系统?
- 如何设计一个 API 限速系统?
- 如何设计一个高可用的支付网关?
- 如何设计一个跨境电商架构?
- 如何设计一个金融级别的微服务系统?
