java-v2

JDK、JRE、JVM 区别

这三个是 Java 开发最基础的概念,层层包含、各司其职,一句话先总结:

JDK = JRE + 开发工具 JRE = JVM + 核心类库 JVM = Java 虚拟机(核心运行引擎)

  1. 逐一定义(最简单理解)

① JVM(Java Virtual Machine)Java 虚拟机

  • 作用真正运行 Java 代码的核心

  • 它是一个虚拟计算机,能把编译好的 .class 字节码翻译成你电脑(Windows/Mac/Linux)能看懂的机器指令

  • 特点 :跨平台的关键 → 一次编译,到处运行

  • 不包含:编译器、调试器等开发工具

  • 给谁用:只负责运行,不负责开发

② JRE(Java Runtime Environment)Java 运行环境

  • 作用让 Java 程序能跑起来

  • 包含:

    • JVM(运行引擎)

    • 核心类库(Java 自带的工具包、基础功能)

  • 不包含:编译器(javac)等开发工具

  • 给谁用只想运行 Java 程序,不写代码的普通用户

③ JDK(Java Development Kit)Java 开发工具包

  • 作用开发 + 运行 Java 程序

  • 包含:

    • JRE(能运行)

    • 开发工具(编译器 javac、调试器、打包工具等)

  • 给谁用写 Java 代码的开发者

  1. 包含关系(最直观)

    JDK (最大,开发者用)
    └── JRE (运行环境)
    └── JVM (最小,核心运行引擎)

  2. 核心区别对比表

表格

名称 中文 核心功能 包含内容 使用人群
JVM Java 虚拟机 运行 .class 字节码 仅虚拟机 底层运行
JRE 运行环境 让 Java 程序运行 JVM + 核心类库 普通用户
JDK 开发工具包 开发 + 运行 Java JRE + 编译器 / 工具 开发者
  1. 最通俗的比喻

Java 程序 比作 一辆车

  • JVM = 发动机(车能跑的核心)

  • JRE = 整车(能开,但不能造)

  • JDK = 汽车工厂 + 整车(能造车,也能开车)

  1. 实际使用场景
  • 写代码、做开发 → 必须装 JDK

  • 只运行别人写好的 Java 程序 → 装 JRE 就够

  • JVM 不会单独安装,它被包含在 JRE/JDK 里

总结

  1. JVM:运行代码的核心引擎

  2. JRE:运行 Java 程序的最小环境(JVM + 类库)

  3. JDK:Java 开发全套工具(JRE + 编译器等)

  4. 包含关系:JDK ⊃ JRE ⊃ JVM

== 和 equals 的区别

一句话总结:

== 比较地址,equals 默认也比较地址,但常用类(String、Integer 等)被重写后比较内容。

  1. == 是什么?
  • 运算符

  • 基本数据类型 :比较值是否相等

  • 引用数据类型 :比较内存地址是否相同(是不是同一个对象)

    int a = 10;
    int b = 10;
    System.out.println(a == b); // true,值相等

    String s1 = new String("abc");
    String s2 = new String("abc");
    System.out.println(s1 == s2); // false,两个不同对象,地址不同

  1. equals() 是什么?
  • Object 类里的方法

  • 默认实现 :和 == 完全一样,也是比较地址

  • 但很多类重写了 equals

    • String、Integer、Date、包装类等

    • 重写后:比较内容是否相同,不再比地址

      String s1 = new String("abc");
      String s2 = new String("abc");

      System.out.println(s1.equals(s2)); // true,内容相同

  1. 核心区别对比

表格

比较项 == equals()
类型 运算符 Object 中的方法
基本类型 比较 不能使用(基本类型没有方法)
引用类型 比较内存地址 默认比地址,重写后比内容
能否重写 不能 可以自己重写
  1. 经典面试题(String)

    String a = "hello";
    String b = "hello";
    String c = new String("hello");

    System.out.println(a == b); // true(常量池里同一个对象)
    System.out.println(a == c); // false(c 是 new 出来的新对象)
    System.out.println(a.equals(c)); // true(内容一样)

  2. 简单记法

  • == :看是不是同一个东西

  • equals :看长得一不一样

hashCode 与 equals 关系

一句话核心:

equals 相等的两个对象,hashCode 必须相等; hashCode 相等的两个对象,equals 不一定相等。 重写 equals 必须重写 hashCode,否则 HashMap、HashSet 会出 bug。

  1. 先搞清楚两个方法的作用

① equals()

  • 用来判断两个对象是否逻辑相等

  • 默认比较地址,重写后比较内容

② hashCode()

  • 返回一个int 类型的哈希值

  • 作用:快速定位对象在哈希表中的位置(HashMap / HashSet / HashTable)

  • 本质是个粗略的快速判断

  1. 官方规定的契约(必须遵守)

  2. 同一个对象,多次调用 hashCode,必须返回相同值

  3. 如果两个对象 equals 为 true ,它们的 hashCode 必须相同

  4. 如果两个对象 equals 为 false,hashCode 可以相同也可以不同(最好不同,减少冲突)

简单记:

  • equals 相等 → hashCode 一定相等

  • hashCode 相等 → equals 不一定相等(哈希冲突)

  1. 为什么重写 equals 必须重写 hashCode?

场景:把对象放进 HashSet / HashMap

哈希表的工作流程是:

  1. 先算 hashCode,确定存到哪个桶

  2. 再用 equals 对比桶里的对象,判断是否重复

如果你只重写 equals,不重写 hashCode:

  • 两个逻辑相等的对象(equals = true)

  • 但它们的 hashCode 不同

  • 会被放进哈希表的不同桶

  • 结果:HashSet 会认为它们是两个不同对象,允许重复存入

破坏了 Set 不重复的特性,HashMap 也会出现重复 key

举个例子

你自定义一个 User 类,只重写 equals,不重写 hashCode:

复制代码
User u1 = new User("张三");
User u2 = new User("张三");

u1.equals(u2) → true
u1.hashCode() → 1234
u2.hashCode() → 5678

放进 HashSet:

复制代码
set.add(u1);
set.add(u2);

结果:set 里会同时存在 u1 和 u2,这明显错误。

  1. 反过来:只重写 hashCode 不重写 equals 可以吗?

可以运行,但毫无意义

  • hashCode 相同

  • equals 还是默认比较地址 → false

  • 哈希表依然认为是两个不同对象

  1. 超简洁总结

  2. equals 相等 → hashCode 必须相等(契约)

  3. hashCode 是给哈希表快速查找用的

  4. 只改 equals 不改 hashCode → HashSet/HashMap 失效,出现重复对象

  5. 规范做法:要么都不重写,要么一起重写

final、finally、finalize 区别

这三个长得像,但用途、场景、生命周期完全不一样,是 Java 经典面试题。

一句话总结

  • final :修饰符,用来限制类、方法、变量,表示不可变、不可继承、不可重写。

  • finally :异常处理关键字,和 try/catch 一起用,无论是否异常,代码一定执行

  • finalize :Object 里的方法,对象被垃圾回收前会调用,现在基本废弃不用。

  1. final(关键字・修饰符)

用来表示 "最终、不可变"。

三种用法

  1. 修饰变量

    • 变量变成常量,只能赋值一次

    • 基本类型:值不可变

    • 引用类型:地址不可变(对象内容仍可变)

  2. 修饰方法

    • 方法不能被子类重写(override)
  3. 修饰类

    • 类不能被继承

    • 如 String、Integer 都是 final 类

  4. finally(关键字・异常处理)

配合 try...catch 使用:

复制代码
try {
    // 代码
} catch (Exception e) {
    // 异常
} finally {
    // 无论是否异常、是否 return,这里**一定执行**
}

作用:

  • 关闭资源(流、连接、锁)

  • 做必须执行的清理工作

  1. finalize(方法・已废弃)
  • Object 类中的方法:protected void finalize() throws Throwable

  • 作用:对象被 GC 回收前,会调用一次

  • 问题:

    • 调用时机不确定

    • 性能差

    • 可能导致对象复活、内存泄漏

  • 现状:Java 9 已标记为废弃,不建议使用

核心区别对比表

表格

关键字 类型 作用 使用场景
final 修饰符 不可变、不可继承、不可重写 常量、工具类、禁止重写方法
finally 流程控制关键字 异常中必须执行的代码块 关闭 IO、数据库连接、释放资源
finalize Object 方法 对象被垃圾回收前调用(已废弃) 几乎不用

最简单记忆口诀

  • final:关住(不让变、不让继承)

  • finally:兜底(一定执行)

  • finalize:临死前喊一声(GC 前调用,已废弃)

接口 vs 抽象类

先给你一句面试标准答案

接口侧重行为规范、能力扩展 ,支持多实现;抽象类侧重模板、共性属性与逻辑 ,只能单继承;Java 8 后接口可以有默认方法、静态方法,但依然不能有构造方法、不能维护普通成员变量。

一、核心区别(直接背)

  1. 继承 vs 实现

    • 抽象类:extends 继承,只能单继承

    • 接口:implements 实现,可以多实现

  2. 成员变量

    • 抽象类:可以有普通成员变量、常量、静态变量

    • 接口:只能是 public static final 常量(默认就是)

  3. 构造方法

    • 抽象类:有构造方法(子类初始化调用)

    • 接口:没有构造方法

  4. 方法权限

    • 抽象类:方法可以是 public/protected/default/private

    • 接口:默认 public,不能随便改权限

  5. 设计思想

    • 抽象类:is-a(是什么)

    • 接口:can-do(具备什么能力)

二、Java 8+ 接口新增(重点)

Java 8 之后接口不再只能是纯抽象方法,可以有:

  • 默认方法default void method() {}

  • 静态方法static void method() {}

默认方法作用

  • 给接口增量扩展功能,不强迫所有实现类重写

  • 解决接口升级时,大量实现类要改代码的问题

但接口依然不是抽象类

  • 接口不能维护状态(不能有普通成员变量)

  • 接口不能有构造方法

  • 接口依然是多实现,抽象类是单继承

三、一张表搞定

表格

对比项 抽象类 接口(Java 8+)
继承方式 extends 单继承 implements 多实现
成员变量 任意变量 只能 public static final 常量
构造方法
抽象方法 可以有 可以有
普通方法 可以有 可以有 default /static 方法
设计目的 模板、共性代码 定义规范、能力扩展

四、怎么选?

  • 定义一组行为规范 ,让多个无关类实现 → 用 接口

  • 抽取子类公共代码、属性、模板逻辑 → 用 抽象类

  • 给接口加新方法又不想改所有实现类 → 用 默认方法

重载 vs 重写

一句话记:

重载:同一个类里,方法名相同、参数不同 重写:子类继承父类,方法签名完全相同

  1. 重载(Overload)
  • 位置:同一个类中

  • 方法名:必须相同

  • 参数列表:必须不同

    • 个数不同

    • 类型不同

    • 顺序不同

  • 返回值:可以相同,也可以不同

  • 修饰符:可以随意

  • 异常:无限制

典型例子:

复制代码
public int add(int a, int b) { ... }
public double add(double a, double b) { ... }

口诀:同名不同参

  1. 重写(Override)
  • 位置:子类继承父类 / 实现接口

  • 方法名:必须相同

  • 参数列表:必须完全相同

  • 返回值:必须相同(或其子类型,协变返回)

  • 修饰符 :子类权限 不能更严格

    • 父类 public → 子类只能 public

    • 父类 protected → 子类可以 protected /public

  • 异常 :子类抛出异常 不能更大、更多

典型例子:

复制代码
class Father {
    public void say() {}
}

class Son extends Father {
    @Override
    public void say() {}
}

口诀:同名同参,子类改实现

  1. 一张表秒懂

表格

对比项 重载 Overload 重写 Override
位置 同一个类 子类 / 实现类
方法名 相同 相同
参数列表 必须不同 必须完全相同
返回值 无要求 必须相同(或协变)
修饰符 无要求 不能更严格
异常 无要求 不能更大 / 更多
注解 建议 @Override
核心思想 一个方法多种用法 子类重新实现父逻辑
  1. 最简单记忆
  • 重载:编译时多态

  • 重写:运行时多态

装箱、拆箱 & Integer 缓存

一、什么是装箱、拆箱?

  1. 基本概念
  • 基本数据类型byte、short、int、long、float、double、char、boolean

  • 包装类Byte、Short、Integer、Long、Float、Double、Character、Boolean

  • **装箱(Autoboxing)**基本类型 → 包装类对象

    复制代码
    Integer a = 10;  // 底层:Integer.valueOf(10)
  • **拆箱(Unboxing)**包装类对象 → 基本类型

    复制代码
    int b = a;       // 底层:a.intValue()

Java 5 之后自动完成,所以叫自动装箱 / 自动拆箱

二、Integer 缓存机制(重点)

Integer.valueOf(int i) 内部有缓存池,用来复用对象,提高性能。

  1. 缓存范围

默认缓存:-128 ~ 127

  • 这个范围内的数字,会复用同一个对象

  • 超出范围,会 new Integer(...) 新建对象

  1. 经典面试题

    Integer a = 100;
    Integer b = 100;
    Integer c = 200;
    Integer d = 200;

    System.out.println(a == b); // true
    System.out.println(c == d); // false

原因:

  • 100 在缓存范围内 → 同一个对象

  • 200 超出范围 → 两个不同对象

  • == 比较地址,所以结果不同

三、其他包装类缓存

  • Byte :全部缓存(-128~127)

  • Short :-128~127

  • Integer:-128~127

  • Long :-128~127

  • Character:0~127

  • Float / Double没有缓存

四、一句话总结

  • 装箱:基本类型 → 包装类

  • 拆箱:包装类 → 基本类型

  • Integer 缓存:-128~127 复用对象,超出新建对象

  • 比较包装类值是否相等,一定要用 equals ()

String、StringBuilder、StringBuffer 区别

一句话总结:

String 不可变,StringBuilder 可变且线程不安全最快,StringBuffer 可变且线程安全稍慢。

  1. 核心区别

① String

  • 不可变字符序列 ,底层 final char[](Java 9+ 是 byte [])

  • 每次修改(拼接、替换)都会生成新对象,效率低

  • 线程安全(不可变自然安全)

② StringBuilder

  • 可变字符序列

  • 线程不安全,没有 synchronized

  • 执行速度最快,适合单线程大量拼接

③ StringBuffer

  • 可变字符序列

  • 线程安全 ,方法带 synchronized

  • 速度比 StringBuilder 慢一点

  • 适合多线程环境拼接字符串

  1. 对比表

表格

特性 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 安全(不可变) 不安全 安全(加锁)
性能 最差(频繁生成对象) 最快 中等
适用场景 少量字符串、不修改 单线程大量拼接 多线程大量拼接
  1. 怎么选?

  2. 字符串很少修改、只赋值 → 用 String

  3. 单线程频繁拼接 (循环拼接)→ 用 StringBuilder(最常用)

  4. 多线程环境下拼接 → 用 StringBuffer

  5. 一句记忆口诀

  • String 不可变

  • StringBuilder 快但不安全

  • StringBuffer 安全但慢点

String 为什么不可变?有什么好处?

一、String 为什么不可变?

从源码设计上看,有 3 个关键点:

  1. 底层字符数组被 final 修饰

    • JDK 8 及以前:private final char value[]

    • JDK 9+ 优化为:private final byte[] valuefinal 数组 → 地址不能改,且数组是 private,没有对外修改方法。

  2. String 类本身是 final不能被继承,避免子类破坏不可变特性。

  3. 没有提供修改内部数组的方法 没有 set 方法,所有 substring、replace、concat 等操作,都返回新 String 对象,不修改原对象。

所以:

String 不可变,是设计出来的,不是天生的。

二、不可变有什么好处?

  1. 线程安全

不可变对象 = 只读,多线程随便用,不用加锁、不用同步

  1. 字符串常量池复用,节省内存

相同字符串只存一份,多个引用指向同一个对象。

复制代码
String s1 = "abc";
String s2 = "abc";

如果可变,s1 一改,s2 也跟着变,常量池就废了。

  1. 哈希值可以缓存

String 会缓存 hashCode,只计算一次。HashMap 里大量用 String 做 key,速度非常快。

  1. 安全可靠
  • 类加载器用 String 作为类名、路径

  • 网络连接、URL、文件名也常用 String如果 String 可变,容易被恶意篡改,引发安全漏洞。

  1. 避免副作用

方法里传入 String,不用担心被内部修改,代码更稳定。

三、一句话总结

  • String 不可变:因为底层 private final char [] + 类 final + 无修改方法。

  • 好处线程安全、常量池复用、缓存 hashCode、安全稳定、无副作用

反射是什么?应用场景

一句话总结:**反射就是在程序运行时,拿到类的完整信息(类名、字段、方法、构造器),并动态操作它们的机制。**不用提前 new 对象,不用硬编码调用方法。

一、反射是什么?

正常写代码:

java

运行

复制代码
User user = new User();
user.setName("张三");

编译时就确定了类、方法,叫编译期绑定

反射代码:

复制代码
// 运行时才加载类、创建对象、调用方法
Class<?> cls = Class.forName("com.example.User");
Object obj = cls.newInstance();
Method method = cls.getMethod("setName", String.class);
method.invoke(obj, "张三");

这就是运行期动态绑定,也就是反射。

反射能做什么:

  • 运行时获取 Class 对象

  • 创建对象

  • 获取 / 修改成员变量(包括 private)

  • 调用任意方法(包括 private)

  • 获取父类、接口、注解等

二、核心 API(简单记)

  • Class.forName("全类名"):获取 Class 对象

  • cls.newInstance() / getConstructor(...):创建对象

  • getField / getDeclaredField:获取字段

  • getMethod / getDeclaredMethod:获取方法

  • method.invoke(obj, args):执行方法

  • setAccessible(true):暴力访问私有成员

三、应用场景(面试常考)

  1. Spring IOC 容器(创建 Bean) 读取 xml / 注解里的类名,反射创建对象,完成依赖注入。

  2. MyBatis / ORM 框架数据库结果集 → 通过反射映射到实体类的字段。

  3. **动态代理(AOP 底层)**JDK 动态代理完全基于反射实现。

  4. 通用工具类比如通用 JSON 序列化、Bean 拷贝、对象转 Map 等。

  5. 框架注解解析 如 @Controller、@Autowired、@Test 等,都是反射读取注解实现的。

  6. JDBC 加载驱动

    复制代码
    Class.forName("com.mysql.cj.jdbc.Driver");
  7. 破坏封装、单元测试测试 private 方法、修改私有变量。

四、优点 & 缺点

优点:

  • 高度灵活,解耦,代码可配置化

  • 框架必备核心技术

缺点:

  • 性能比直接调用慢

  • 破坏封装,可以访问私有成员

  • 代码可读性差、复杂

五、极简记忆

  • 反射 = 运行时动态操作类

  • 核心作用:解耦 + 动态

  • 场景:Spring、MyBatis、动态代理、JSON、注解驱动

JDK 动态代理 vs CGLIB 动态代理

一句话总结:JDK 代理面向接口,用反射;CGLIB 面向类,继承子类,用字节码生成。

一、JDK 动态代理

  1. 基于接口

    • 被代理类必须实现接口

    • 没有接口 → 不能用 JDK 代理

  2. 原理

    • 运行时生成一个实现了相同接口的代理类

    • 通过反射调用目标方法

  3. 核心类

    • InvocationHandler

    • Proxy.newProxyInstance()

二、CGLIB 动态代理

  1. 基于类继承

    • 被代理类不需要接口

    • 直接生成目标类的子类作为代理

  2. 原理

    • 用 ASM 框架修改字节码,生成子类

    • 重写非 final 方法进行增强

  3. 核心类

    • MethodInterceptor

    • Enhancer

三、核心区别对比表

表格

对比项 JDK 动态代理 CGLIB
实现基础 接口 类继承
是否需要接口 必须有接口 不需要接口
代理方式 实现相同接口 生成目标类子类
方法限制 接口方法 不能代理 final 类、final 方法
性能 JDK 8 以前较慢JDK 8 后大幅优化,接近 CGLIB 旧版 JDK 下更快
底层机制 反射 字节码生成(ASM)

四、Spring 里的选择规则(必考)

  1. 目标类实现了接口 → 默认使用 JDK 动态代理

  2. 目标类没有接口 → 使用 CGLIB

  3. 可以强制 Spring 全局用 CGLIB:

    复制代码
    @EnableAspectJAutoProxy(proxyTargetClass = true)

五、一句话记忆

  • JDK 代理:有接口才能用,靠反射

  • CGLIB:没接口也能用,靠生成子类

  • Spring:有接口用 JDK,无接口用 CGLIB

深拷贝 vs 浅拷贝

一句话核心:浅拷贝只复制引用,深拷贝复制整个对象结构;浅拷贝共享成员对象,深拷贝完全独立。

  1. 浅拷贝(Shallow Copy)
  • 复制规则

    1. 基本类型:直接复制值

    2. 引用类型:只复制内存地址,不复制对象本身

  • 结果 :原对象和拷贝对象共用同一个成员对象,改一个,另一个也会变。

  • 实现方式 :实现 Cloneable 接口,重写 clone(),默认就是浅拷贝。

  1. 深拷贝(Deep Copy)
  • 复制规则

    1. 基本类型:复制值

    2. 引用类型:递归复制整个对象,创建新对象

  • 结果 :拷贝对象与原对象完全独立,互不影响。

  • 实现方式

    1. 重写 clone(),递归拷贝所有引用成员

    2. 序列化 / 反序列化(最简单通用)

    3. JSON 序列化

  1. 一张表看懂区别

表格

对比项 浅拷贝 深拷贝
基本类型 复制值 复制值
引用类型 复制地址,共享对象 复制整个对象,完全独立
修改引用成员 互相影响 互不影响
速度 慢(递归 / 序列化)
实现难度 简单 复杂
  1. 经典记忆口诀
  • 浅拷贝:复制外壳,共用内脏

  • 深拷贝:连壳带内脏,全部复制一份新的

  1. 典型场景
  • 对象只有基本类型 + 不可变对象(如 String)→ 浅拷贝足够

  • 对象包含可变引用类型(自定义类、集合)→ 必须用深拷贝

ArrayList 和 LinkedList 区别

一句话总结:ArrayList 是数组,查询快、增删慢;LinkedList 是链表,查询慢、增删快。

  1. 底层结构
  • ArrayList动态数组

  • LinkedList双向链表

  1. 访问效率
  • ArrayList :支持随机访问 ,通过下标直接定位,查询极快

  • LinkedList :不支持随机访问,查找要从头 / 尾遍历,查询慢

  1. 增删效率
  • ArrayList :中间 / 头部增删需要移动元素,尾部追加快

  • LinkedList :已知节点情况下增删只需改引用,极快适合频繁插入、删除

  1. 内存占用
  • ArrayList:内存连续,占用较少,有一定预留空间

  • LinkedList :每个节点存数据 + 前驱 + 后继,内存开销更大

  1. 线程安全

两者都线程不安全多线程用:

  • CopyOnWriteArrayList

  • 或自己加锁

  1. 对比表

表格

特性 ArrayList LinkedList
底层结构 动态数组 双向链表
随机访问 支持,快 不支持,慢
增删(中间) 慢(需移动元素) 快(改引用)
内存占用 较小 较大(节点开销)
适用场景 大量查询、遍历 频繁增删、队列 / 栈
  1. 最简单记忆
  • 查多用 ArrayList

  • 增删多用 LinkedList

  • 日常开发 90% 用 ArrayList

ArrayList 扩容机制

一句话总结

ArrayList 默认初始容量 0,第一次添加变为 10; 满了之后按 1.5 倍扩容,底层用 Arrays.copyOf 复制数组。

  1. 初始化(JDK 1.8+)
  • 无参构造:

    复制代码
    new ArrayList<>();

    初始底层数组为 空数组 {},容量 0

  • 指定容量:

    复制代码
    new ArrayList<>(20);

    直接创建长度为 20 的数组

  1. 第一次添加元素

第一次调用 add() 时,才真正初始化容量为 10

  1. 什么时候扩容?

元素个数 size == 数组长度 时,再添加就会触发扩容。

  1. 扩容多少?

新容量 = 旧容量 + 旧容量 >> 1 ≈ 旧容量 × 1.5

例如:

  • 10 → 15

  • 15 → 22

  • 22 → 33

  1. 扩容怎么做?

底层调用:

复制代码
Arrays.copyOf(oldArray, newCapacity);

创建一个新数组,把原数组内容复制过去,原数组被丢弃。

  1. 面试常考要点

  2. JDK 7 vs JDK 8

    • JDK 7:一开始就创建容量为 10 的数组

    • JDK 8:懒加载,第一次 add 才变成 10

  3. 为什么是 1.5 倍?

    • 比 2 倍更节省内存,减少空间浪费

    • 位移运算 oldCapacity >> 1 效率极高

  4. 频繁扩容会影响性能所以能预估大小时,最好指定初始容量:

    复制代码
    new ArrayList<>(1000);

极简记忆版

  • 初始空,第一次变 10

  • 满了就 1.5 倍扩容

  • 底层数组复制

  • 懒加载、高效、浪费少

HashMap 底层原理

一句话概括:JDK 1.7:数组 + 单链表,头插法,易死循环; JDK 1.8:数组 + 链表 + 红黑树,尾插法,查询更稳定。

一、通用结构(两者共同点)

  • 底层:数组 + 链表(哈希表)

  • 核心:hash 取模定位数组下标,链表解决哈希冲突

  • 扩容:容量 ×2 ,负载因子默认 0.75

  • 线程不安全,高并发会出现数据丢失、死循环

二、JDK 1.7 结构

  • 数组 + 单向链表

  • 冲突时:头插法(新节点插到链表头部)

  • 扩容重哈希:重新计算所有 hash,链表会倒置

  • 问题:

    • 链表过长,查询效率退化为 O (n)

    • 多线程扩容会形成环形链表,导致死循环(CPU 100%)

三、JDK 1.8 优化(重点)

  1. 结构变为:数组 + 链表 + 红黑树

    • 链表长度 ≥8 且数组长度 ≥64 → 转为红黑树(O(logn))

    • 树节点 ≤6 → 退化为链表

  2. 尾插法

    • 新节点插到链表尾部

    • 扩容不会倒置链表,避免了死循环

  3. hash 算法简化

    • 高 16 位异或低 16 位,更高效
  4. 扩容更高效

    • 不用重新全部计算 hash

    • hash & 旧容量 判断是留在原位置还是移到「原位置 + 旧容量」

四、1.7 vs 1.8 对比表

表格

对比项 JDK 1.7 JDK 1.8
底层结构 数组 + 单链表 数组 + 链表 + 红黑树
插入方式 头插法 尾插法
哈希冲突 纯链表 链表 / 红黑树自动切换
扩容重 hash 全部重新计算 按 bit 位判断,效率更高
查询效率 链表长则慢 O (n) 树结构稳定 O (logn)
并发问题 可能形成环形链表死循环 不会死循环,但仍线程不安全

五、极简记忆

  • 1.7:数组 + 链表,头插,会死循环,慢

  • 1.8:数组 + 链表 + 红黑树,尾插,更快更安全

  • 阈值:链表≥8 转树,≤6 退链

  • 扩容始终是 2 倍扩容

HashMap 为什么线程不安全

一句话总结:HashMap 没有任何锁保护,多线程同时 put、扩容时,会出现数据覆盖、链表成环、数据丢失,甚至 JDK 1.7 会导致死循环。

下面分版本讲最核心的 3 个问题:

一、JDK 1.7 下最危险:扩容导致环形链表 + 死循环

1.7 用头插法 ,多线程同时扩容时,链表容易变成。一旦形成环:

  • get() 遍历链表时会无限循环

  • CPU 瞬间 100%,程序卡死

这是 HashMap 最经典、最严重的线程安全问题。

二、JDK 1.8 虽然修复了死循环,但依然不安全

1.8 改成尾插法 ,不会死循环了,但还是线程不安全,主要两个问题:

  1. 数据覆盖(最常见)
  • 线程 A 和 B 同时计算出相同下标

  • 都判断该位置为空

  • 同时执行插入→ 后执行的会覆盖先插入的数据,导致数据丢失。

  1. size 计数不准

size++ 不是原子操作:

  • 读取 size

  • +1

  • 写回 size

多线程并发时会出现丢失更新,最终 size 比实际元素少。

三、总结:不安全的根本原因

  1. 没有加锁(synchronized / CAS 都没有)

  2. 多线程并发 put 会互相覆盖

  3. JDK 1.7 扩容会形成环形链表,导致死循环

  4. size 计算非原子,数量不准

四、面试一句话标准答案

HashMap 是非线程安全的,因为没有加锁同步机制。JDK 1.7 中多线程扩容会使用头插法,容易形成环形链表导致死循环 ;JDK 1.8 改用尾插法避免了死循环,但仍会出现数据覆盖、size 不准确 等问题。高并发场景应使用 ConcurrentHashMap

ConcurrentHashMap 原理

一句话总结:1.7 用分段锁(Segment),细粒度锁,并发度 16; 1.8 用 CAS + synchronized + 红黑树,锁粒度更细,性能更高。

一、JDK 1.7:分段锁 Segment

结构

  • 底层:Segment [] + HashEntry [] + 链表

  • Segment 继承自 ReentrantLock

  • 默认 16 个 Segment,并发度就是 16

原理

  1. 先根据 key 定位到 Segment

  2. 对整个 Segment 加锁

  3. 同一 Segment 内的操作串行,不同 Segment 并行

优点

  • 比 Hashtable 全局锁性能高很多

缺点

  • 锁粒度依然偏大(锁住一段)

  • 链表过长查询效率低

二、JDK 1.8:CAS + synchronized + 红黑树

结构

  • 底层:数组 + 链表 + 红黑树(结构同 HashMap 1.8)

  • 取消 Segment,直接用 Node 数组

加锁策略

  1. 无锁:CAS 尝试插入数组对应位置为空时,用 CAS 直接插入,不加锁,效率极高

  2. 有锁:synchronized 锁住链表头节点 / 树节点 位置已存在数据时,只锁当前数组槽位的头节点锁粒度极小,只锁一条链表 / 一棵树

核心优化

  • CAS 无锁操作

  • synchronized 只锁头节点

  • 链表长了转红黑树

  • 并发性能远超 1.7

三、1.7 vs 1.8 对比表

表格

对比项 JDK 1.7 JDK 1.8
结构 Segment + 数组 + 链表 数组 + 链表 + 红黑树
锁实现 ReentrantLock 分段锁 CAS + synchronized
锁粒度 整个 Segment 数组槽位头节点(更细)
并发度 默认 16 更高,理论等于数组长度
查询效率 链表 O (n) 红黑树 O (logn)
锁复杂度 较高 更简单高效

四、一句话记忆

  • 1.7:分段锁,锁一段,并发 16

  • 1.8:CAS 无锁 + 锁头节点,粒度更细,性能更强

HashSet 底层实现 & 如何保证不重复

  1. 底层是什么?

一句话:HashSet 底层就是 HashMap,只是只用了 key,value 是一个固定的空对象。

源码一眼看懂:

复制代码
// HashSet 内部持有一个 HashMap
private transient HashMap<E, Object> map;

// 所有 put 都用这个固定值当 value
private static final Object PRESENT = new Object();

public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}
  • 存元素 → 往 HashMap 的 key 里存

  • value 统一是一个静态空对象 PRESENT,没用

  1. 如何保证不重复?

依靠 HashMap 的 key 不重复 机制:

添加元素时流程:

  1. 计算元素的 hashCode()

  2. 找到数组对应位置

  3. equals() 对比链表 / 红黑树上的节点

    • 如果 hashCode 相同 equals 为 true → 视为重复,不添加

    • 否则 → 添加成功

所以:HashSet 不重复 = HashMap key 唯一性 = hashCode + equals

  1. 一句话总结
  • HashSet 底层 = HashMap

  • 存值只存 key,value 是固定空对象

  • 不重复靠:hashCode 定位 + equals 判断相等

HashSet、LinkedHashSet、TreeSet 对比

一句话总结:HashSet 无序最快;LinkedHashSet 保持插入顺序;TreeSet 有序可排序。

  1. 底层结构
  • HashSet :底层 HashMap,数组 + 链表 + 红黑树

  • LinkedHashSet :底层 LinkedHashMapHashSet + 双向链表

  • TreeSet :底层 TreeMap红黑树

  1. 顺序性
  • HashSet完全无序,遍历顺序不固定

  • LinkedHashSet保持插入顺序(先进先出)

  • TreeSet自然有序(按大小排序)或自定义比较器排序

  1. 能否排序
  • HashSet:❌ 不能排序

  • LinkedHashSet:❌ 不能排序,只保插入顺序

  • TreeSet:✅ 支持排序(自然排序 / Comparator 定制排序)

  1. 元素要求
  • HashSet / LinkedHashSet:元素必须重写 hashCode() + equals()

  • TreeSet:元素实现 Comparable 接口,或传入 Comparator 比较器

  1. 性能(速度)

HashSet > LinkedHashSet > TreeSet

  • TreeSet 要维护红黑树排序,最慢

  • LinkedHashSet 要维护链表,略慢于 HashSet

  • HashSet 无额外开销,最快

  1. 是否允许 null
  • HashSet:允许 1 个 null

  • LinkedHashSet:允许 1 个 null

  • TreeSet:不允许 null(排序会报空指针)

  1. 线程安全

三者都线程不安全 多线程用:Collections.synchronizedSet()ConcurrentSkipListSet

  1. 对比表

表格

特性 HashSet LinkedHashSet TreeSet
底层 HashMap LinkedHashMap TreeMap (红黑树)
顺序 无序 插入顺序 自然有序 / 定制排序
排序 不支持 不支持 支持
性能 最快 中等 最慢
null 允许 1 个 允许 1 个 不允许
元素要求 hashCode+equals hashCode+equals Comparable/Comparator
  1. 怎么选?
  • 只去重、不在乎顺序 → HashSet(最常用)

  • 去重 + 保持插入顺序 → LinkedHashSet

  • 去重 + 自动排序 → TreeSet

LinkedHashMap 原理与应用

一句话总结:LinkedHashMap = HashMap + 双向链表,可保持插入顺序 / 访问顺序,是实现 LRU 缓存的天然结构。

一、底层原理

  1. 继承自 HashMap 结构:数组 + 链表 / 红黑树 + 双向链表

  2. 每个节点多两个指针

    • before

    • after用来维护一条双向链表,记录顺序。

  3. 两种顺序模式

    • 插入顺序(默认):按 put 顺序排列

    • 访问顺序 :get /put 都会把节点移到链表尾部通过构造参数 accessOrder = true 开启

java

运行

复制代码
new LinkedHashMap<>(16, 0.75f, true);

二、核心特点

  • 遍历顺序稳定可预测

  • 底层复用 HashMap 大部分逻辑

  • 性能略低于 HashMap,但远好于 TreeMap

  • 线程不安全

三、实现 LRU 缓存(高频面试)

什么是 LRU?

Least Recently Used 最近最少使用

  • 缓存满了

  • 优先删除最久没被访问的元素

LinkedHashMap 为什么适合?

开启 accessOrder=true 后:

  • 每次 get/put ,节点都会被移到链表尾部

  • 链表头部 就是 最近最少使用 的节点

  • 自带方法:

    java

    运行

    复制代码
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest)

    返回 true 就自动删除头部最老数据。

四、手写 LRU 模板(面试直接写)

java

运行

复制代码
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true); // 开启访问顺序
        this.capacity = capacity;
    }

    // 超过容量就删除最老的
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

五、一句话记忆

  • LinkedHashMap = HashMap + 双向链表

  • 支持插入顺序 / 访问顺序

  • accessOrder=true 实现按访问排序

  • 完美用于 LRU 缓存

TreeMap 排序原理 + Comparable vs Comparator

一、TreeMap 排序原理

一句话:TreeMap 底层是红黑树,按照 key 自动排序,排序规则由 Comparable 或 Comparator 决定。

  1. 底层结构
  • 红黑树(自平衡二叉查找树)

  • 保证查询、插入、删除都是 O(logn)

  • 遍历时是中序遍历,所以输出有序

  1. 排序规则

  2. 如果构造 TreeMap 时传入了 Comparator → 用比较器排序

  3. 否则要求 key 必须实现 Comparable 接口 → 用 compareTo 排序

  4. 两者都没有 → 运行时抛出 ClassCastException

  5. 特点

  • key 不允许为 null

  • key 必须可比较

  • 线程不安全

  • 按 key 有序遍历

二、Comparable 和 Comparator 区别

  1. Comparable(内部比较器)
  • 接口:java.lang.Comparable<T>

  • 方法:public int compareTo(T o)

  • 特点:

    • 类自己实现,称为 "自然排序"

    • 一个类只有一种 compareTo

    • 侵入式,修改原类

示例:

复制代码
class User implements Comparable<User> {
    int age;
    @Override
    public int compareTo(User o) {
        return this.age - o.age; // 按年龄升序
    }
}
  1. Comparator(外部比较器)
  • 接口:java.util.Comparator<T>

  • 方法:public int compare(T o1, T o2)

  • 特点:

    • 单独写比较器,不修改实体类

    • 一个类可以有多个比较器(按姓名、年龄、ID...)

    • 灵活、无侵入

示例:

复制代码
Comparator<User> comparator = new Comparator<>() {
    @Override
    public int compare(User o1, User o2) {
        return o1.getAge() - o2.getAge();
    }
};

三、核心区别对比表

表格

对比项 Comparable Comparator
包路径 java.lang java.util
方法 compareTo(T o) compare(T o1, T o2)
实现位置 实体类自身 外部类 / Lambda / 匿名类
排序名称 自然排序 定制排序
灵活性 单一排序 多种排序随意切换
侵入性 侵入式(改原类) 无侵入
TreeMap 使用 key 实现即可 构造时传入
优先级 (Comparator 会覆盖 Comparable)

四、记忆口诀

  • Comparable:自己跟别人比(自己实现)

  • Comparator:找个裁判比(外部指定)

  • TreeMap 优先用 Comparator,没有才用 Comparable

五、一句话面试标准答案

TreeMap 底层是红黑树 ,根据 key 进行排序。排序规则优先使用构造时传入的 Comparator ,若没有则要求 key 实现 Comparable 接口 。Comparable 是内部比较器 ,在实体类中实现;Comparator 是外部比较器,更灵活,优先级更高。

start () 和 run () 区别

一句话总结:start () 是启动线程,真正开启新线程;run () 只是普通方法调用,还是在当前线程执行。

  1. start()
  • 作用 :启动一个新线程,让线程进入就绪状态

  • 底层 :调用本地方法 start0(),由 JVM 新建线程,再自动调用 run()

  • 特点

    • 真正实现多线程,主线程和子线程交替执行

    • 一个线程只能调用一次 start () ,多次抛 IllegalThreadStateException

  • 结果:两条独立线程并行运行

  1. run()
  • 作用 :只是 Thread 里的一个普通方法

  • 特点

    • 直接调用 run()不会开启新线程

    • 代码还是运行在当前调用线程(比如主线程)

    • 可以多次调用

  • 结果:同步执行,没有多线程效果

  1. 代码对比一眼懂

java

运行

复制代码
Thread t = new Thread(() -> {
    System.out.println(Thread.currentThread().getName());
});

t.start();  // 输出:Thread-0 → 新线程
t.run();    // 输出:main      → 主线程里普通调用
  1. 核心区别表

表格

对比项 start() run()
本质 启动线程的方法 业务逻辑的普通方法
是否新建线程
执行效果 多线程并行 同步串行执行
调用次数限制 只能 1 次 无限制
底层 JVM 本地方法,自动调用 run () 直接调用,无线程切换
  1. 极简记忆
  • start ():开新线程,真正多线程

  • run ():普通方法,还是单线程

  • 想启动线程必须用 start(),不能直接调 run ()

sleep () 和 wait () 区别

一句话核心:sleep 是线程类方法,抱着锁睡觉;wait 是 Object 方法,释放锁等待。

  1. 所属类不同
  • sleep()Thread 类的静态方法

  • wait()Object 类的方法(所有对象都有)

  1. 是否释放锁(最核心区别)
  • sleep()不释放锁抱着锁睡觉,别的线程还是拿不到锁。

  • wait()释放锁进入等待时会把锁交出去,让别的线程可以进入同步块。

  1. 使用位置与前提
  • sleep() :任何地方都能用,不需要在同步代码块里

  • wait() / notify() / notifyAll()必须在 synchronized 同步代码块中,否则抛 IllegalMonitorStateException。

  1. 唤醒方式
  • sleep(long time) :时间到自动唤醒,不需要别人通知。

  • wait() :必须等别的线程调用 notify() / notifyAll() 才能唤醒;也可以用 wait(time) 超时自动醒。

  1. 线程状态
  • sleep → TIMED_WAITING

  • wait → WAITING 或 TIMED_WAITING

  1. 用法用途
  • sleep:暂停当前线程一段时间,用于延时、定时。

  • wait:线程间通信、等待条件满足,配合 notify 使用。

一张表秒懂

表格

对比项 sleep() wait()
所属类 Thread Object
不释放锁 释放锁
同步环境 不需要 必须在 synchronized 内
唤醒 时间到自动醒 需要 notify ()/ 超时
用途 线程休眠延时 线程间通信协作

极简记忆口诀

  • sleep 抱着锁睡觉,谁也别想进

  • wait 放开锁等待,等别人叫醒

  • sleep 是线程的,wait 是对象的

synchronized 底层原理 & 锁升级过程(Java 面试压轴题)

一句话总结:synchronized 是对象锁,底层靠 Mark Word 实现; 锁升级顺序:无锁 → 偏向锁 → 轻量级锁(自旋) → 重量级锁(阻塞),只会升级不会降级。

一、核心基础:对象头(Mark Word)

Java 里每个对象都有对象头,里面存了:

  1. Mark Word :存哈希码、分代年龄、锁状态(锁升级全靠它)

  2. 类型指针

synchronized 加锁,本质就是修改对象头里的 Mark Word

二、锁升级 4 个阶段(必考流程)

  1. 无锁状态
  • 没有线程竞争

  • 对象头正常存储哈希码

  1. 偏向锁(Biased Lock)

场景:一个线程反复加锁,无竞争

  • 锁会偏向第一个获取锁的线程

  • 下次该线程再进来,不需要 CAS,直接通过

  • 成本极低,几乎无开销

触发升级 :出现第二个线程竞争 → 偏向锁撤销 → 升级为轻量级锁

  1. 轻量级锁(Lightweight Lock)

场景:少量线程竞争,时间很短

  • 采用 CAS 自旋 尝试获取锁

  • 不阻塞线程,不进入内核态

  • 消耗 CPU,但响应快

触发升级

  • 自旋次数超限

  • 线程等待时间长→ 升级为重量级锁

  1. 重量级锁(Heavyweight Lock)

场景:高并发、竞争激烈、持有时间长

  • 底层用 OS 互斥量(Monitor)

  • 没拿到锁的线程 进入阻塞队列

  • CPU 开销低,但线程切换成本高(慢)

三、锁升级完整流程(背这个)

  1. 无锁

  2. 单线程反复用 → 偏向锁

  3. 出现竞争 → 撤销偏向锁 → 轻量级锁(CAS 自旋)

  4. 自旋失败 / 竞争激烈 → 重量级锁(阻塞等待)

四、三种锁对比(面试必背)

表格

优点 缺点 适用场景
偏向锁 加解锁无消耗,最快 多线程竞争时撤销开销大 单线程反复同步
轻量级锁 不阻塞,响应快 自旋消耗 CPU 低竞争、短持有
重量级锁 不消耗 CPU,线程阻塞 线程阻塞、上下文切换慢 高并发、长持有

五、底层原理:Monitor(监视器锁)

重量级锁底层依赖 Monitor

  • 每个对象有一个 Monitor

  • 包含:_owner(持有锁线程)、WaitSet、EntryList

  • synchronized 就是让线程成为 Monitor 的 owner

JVM 指令:

  • monitorenter 加锁

  • monitorexit 解锁

六、一句话面试标准答案

synchronized 基于对象头 Mark Word 实现,锁会自动升级: 无锁 → 偏向锁 → 轻量级锁(CAS 自旋) → 重量级锁(Monitor 阻塞)。 偏向锁适合单线程,轻量级锁适合短竞争,重量级锁适合高并发。

volatile 三大作用

一句话背下来:volatile 保证可见性、禁止指令重排序,但不保证原子性。

  1. 保证可见性
  • 多个线程操作同一个变量时,一个线程改了,其他线程能立刻看到最新值

  • 原理:

    • 写操作:直接刷新到主内存

    • 读操作:直接从主内存读取,不使用工作内存缓存

  • 解决:多线程下变量 "看不见" 导致的数据不一致

  1. 禁止指令重排序(有序性)
  • 编译器 / CPU 为了优化,可能会打乱指令执行顺序

  • volatile 会加内存屏障

    • 屏障前的代码不能跑到屏障后

    • 屏障后的代码不能跑到屏障前

  • 典型应用:DCL 单例必须加 volatile,防止半初始化对象问题

  1. 不保证原子性(重点坑点)

volatile 只保证单次读 / 写原子,但复合操作不原子:

  • i++ 本质是:读 → +1 → 写

  • 多线程下会出现丢失更新

  • 解决原子性:

    • synchronized

    • Lock

    • AtomicInteger 等原子类

总结(面试直接说)

  1. 可见性:一个线程改,其他线程立刻可见

  2. 有序性:禁止指令重排,保证执行顺序

  3. 不保证原子性:复合操作(如 i++)仍线程不安全

小对比

  • volatile:轻量级,解决可见性 + 有序性,无锁

  • synchronized :重量级别,保证原子性 + 可见性 + 有序性

线程池 7 大参数

ThreadPoolExecutor 构造方法的 7 个核心参数

  1. corePoolSize核心线程数(常驻线程,即使空闲也不销毁)

  2. maximumPoolSize最大线程数(核心线程 + 非核心线程总数)

  3. keepAliveTime非核心线程空闲超时时间(超时就被回收)

  4. TimeUnit unit超时时间单位(秒、毫秒等)

  5. BlockingQueue<Runnable> workQueue任务阻塞队列(任务进来先放这里排队)

  6. ThreadFactory threadFactory线程工厂(用来创建新线程,可自定义名称、优先级)

  7. RejectedExecutionHandler handler拒绝策略(队列满 + 线程数达到最大时的处理策略)

一句话串起来

线程池先开 corePoolSize 个常驻线程;任务多了放进 workQueue 排队;队列满了再开新线程直到 maximumPoolSize ;多余线程空闲 keepAliveTime 后销毁;全都满了执行 handler 拒绝策略。

常见拒绝策略(顺带记)

  • AbortPolicy:直接抛异常(默认)

  • CallerRunsPolicy:让提交任务的线程自己执行

  • DiscardPolicy:直接丢弃当前任务

  • DiscardOldestPolicy:丢弃队列最老任务,再尝试提交

死锁的四个必要条件 + 避免方案(面试必背)

一、死锁的四个必要条件

这四个条件必须同时满足,才会产生死锁,缺一不可。

  1. 互斥条件 资源同一时刻只能被一个线程占有,其他线程必须等待。

  2. 请求与保持条件 线程已经持有至少一个资源,又去请求新资源 ,且不释放已持有的资源

  3. 不可剥夺条件 线程获得的资源,只能自己用完释放,不能被其他线程强行抢走。

  4. 循环等待条件 多个线程形成环形资源等待链:A 等 B,B 等 C,C 等 A。

二、如何避免死锁

破坏任意一个条件即可避免死锁。

  1. 破坏 "请求与保持"

    • 线程一次性申请所有需要的资源,要么全拿到,要么全不拿。
  2. 破坏 "不可剥夺"

    • 申请不到新资源时,主动释放已持有的资源,之后再重试。
  3. 破坏 "循环等待"(最常用、最实用)

    • 统一资源获取顺序 所有线程都按固定顺序申请锁(比如先锁 A,再锁 B,不许反过来)。

    • 从根源上杜绝环路。

  4. 尽量避免 "互斥"

    • 使用无锁结构:volatile、原子类、ConcurrentHashMap、CAS 等。

三、一句话面试标准答案

死锁产生必须同时满足:互斥、请求与保持、不可剥夺、循环等待 四个条件。避免死锁最常用的方式是破坏循环等待,统一加锁顺序;也可以一次性申请所有资源,或主动释放已占资源。

JVM 内存结构(运行时数据区)面试标准答案

一句话总结:线程私有:程序计数器、虚拟机栈、本地方法栈; 线程共享:堆、方法区; 直接内存(堆外内存)不算运行时数据区,但常用。

一、线程私有区域(每个线程一份)

  1. 程序计数器(PC Register)
  • 记录当前线程执行的字节码行号

  • 线程切换后能恢复到正确位置

  • 唯一不会 OOM 的区域

  1. 虚拟机栈(VM Stack)
  • 存储栈帧:局部变量表、操作数栈、方法出口等

  • 执行一个方法就创建一个栈帧,方法结束出栈

  • 异常:

    • 栈深度超限 → StackOverflowError

    • 无法申请栈空间 → OutOfMemoryError

  1. 本地方法栈(Native Method Stack)
  • native 方法服务

  • 作用同虚拟机栈,只是针对本地方法

二、线程共享区域(所有线程共用)

  1. 堆(Heap)------ 最大一块
  • 存放几乎所有对象实例、数组

  • GC 主要管理区域

  • 分代:新生代(Eden + S0 + S1)+ 老年代

  • 异常:OutOfMemoryError: Java heap space

  1. 方法区(Method Area)
  • 存储类信息、常量、静态变量、即时编译代码

  • JDK8 以前:永久代(PermGen)

  • JDK8 及以后:元空间(Metaspace,使用本地内存)

  • 运行时常量池也在方法区

三、直接内存(Direct Memory)

  • 不属于 JVM 运行时数据区

  • NIO 使用,堆外内存,不受 GC 直接管理

  • 申请过多也会 OOM

四、一张表秒记

表格

区域 线程 存储内容 常见异常
程序计数器 私有 字节码行号
虚拟机栈 私有 栈帧、局部变量 SOE、OOM
本地方法栈 私有 Native 方法栈 SOE、OOM
共享 对象实例 OOM
方法区 共享 类元信息、常量、静态变量 OOM

极简记忆口诀

栈管运行,堆管对象,方法区存类信息; 计数器指路,本地栈管 native; 堆栈共享私有分清楚。

3 大垃圾回收算法(标记清除 / 复制 / 标记整理)

一句话总结:标记清除会碎片、复制算法空一半、标记整理无碎片但慢。

  1. 标记 - 清除算法(Mark-Sweep)

流程

  1. 标记:遍历所有对象,标记存活对象

  2. 清除:统一回收未标记的垃圾对象

优点

  • 简单,不需要额外大量空间

缺点

  • 产生大量内存碎片

  • 空间不连续,大对象无法分配

  • 效率一般,标记和清除都要遍历

适用

  • 存活对象较多、垃圾较少的区域

  • 老年代早期回收器(CMS 基础)

  1. 复制算法(Copying)

流程

  1. 内存分成大小相等两块:From 和 To

  2. 只使用 From 区

  3. 垃圾回收时,把存活对象复制到 To 区

  4. 清空 From,交换 From/To 角色

优点

  • 简单高效

  • 无内存碎片

  • 适合存活对象少的场景

缺点

  • 浪费一半内存

适用

  • 新生代(对象朝生夕死,存活少)

  • Eden + S0 + S1 就是用这种思路

  1. 标记 - 整理算法(Mark-Compact)

流程

  1. 标记:标记存活对象

  2. 整理 :把存活对象向一端移动压缩

  3. 清理端外所有垃圾

优点

  • 无内存碎片

  • 能充分利用内存

缺点

  • 效率最低(需要移动对象、更新引用)

适用

  • 老年代(存活对象多、垃圾少、不能浪费空间)

对比表(面试直接背)

表格

算法 优点 缺点 适用区域
标记 - 清除 简单、不浪费空间 内存碎片、效率一般 老年代(CMS)
复制 无碎片、速度快 浪费一半内存 新生代
标记 - 整理 无碎片、内存利用率高 效率低、要移动对象 老年代

极简记忆口诀

  • 标记清除:简单但碎

  • 复制算法:快但空一半

  • 标记整理:整齐但慢

GC 如何判断对象死亡:可达性分析(面试标准答案)

一句话总结:以 GC Roots 为起点,向下搜索引用链,对象不可达就判定为垃圾,可以回收。

  1. 什么是可达性分析
  • 从一系列 GC Roots 根对象开始

  • 沿着引用链(强引用、软引用、弱引用等)遍历

  • 能被遍历到 → 可达 → 存活

  • 遍历不到 → 不可达 → 死亡 → 可被 GC 回收

  1. GC Roots 有哪些(必须背)

可以作为 GC Roots 的对象主要有:

  1. 虚拟机栈中引用的对象(局部变量表)

  2. 本地方法栈中引用的对象(Native 方法)

  3. 方法区中静态属性引用的对象(static 变量)

  4. 方法区中常量引用的对象(字符串常量池)

  5. 被同步锁持有的对象(synchronized 锁住的对象)

简单记:栈里的、静态的、常量的、锁持有的,都是根。

  1. 对象死亡的完整过程(不是一次判死刑)

  2. 第一次可达性分析不可达

  3. 判断是否有必要执行 finalize()

    • 没覆盖 / 已执行过 → 直接判死

    • 有且未执行 → 放入 F-Queue 等待执行

  4. finalize() 中如果对象重新与 GC Roots 建立引用 → 可以自救,活下来

  5. 否则,真正被判定死亡,等待回收

注意:finalize () 不推荐用,不确定、性能差,Java 9 已废弃。

  1. 和引用计数法对比(顺带考点)
  • 引用计数法 :对象被引用一次 + 1,取消引用 - 1,为 0 回收→ 简单,但无法解决循环引用

  • 可达性分析:解决循环引用,是 Java 主流使用方式

一句话面试背诵版

JVM 使用可达性分析算法 判断对象是否存活,以 GC Roots 为起点遍历引用链,不可达对象判定为可回收。GC Roots 包括虚拟机栈、本地方法栈、静态变量、常量、锁对象等。对象可在 finalize() 中自救一次,但该方法不推荐使用。

CMS vs G1 垃圾收集器(面试必背)

一句话总结:CMS 是追求低延迟的并发回收器,但有碎片;G1 是平衡型,兼顾吞吐量与延迟,是目前主流。

一、CMS (Concurrent Mark Sweep)

全称

Concurrent Mark Sweep(并发标记清除)

核心特点

  • **口号最低停顿时间(Low Latency GC)

  • 年代 :主要用在 新生代(ParNew)+ 老年代

  • 算法 :老年代采用 标记 - 清除 算法(会产生内存碎片)

回收流程(四步走)

  1. 初始标记 (Stop The World):标记 GC Roots 能直接关联的对象,极快

  2. 并发标记 :与用户线程同时运行,遍历整个引用链。

  3. 重新标记 (STW):修正并发标记期间用户线程变动的引用,比初始标记慢,但比 Full GC 快

  4. 并发清除 :与用户线程同时运行,直接清除垃圾对象。

优缺点

  • 优点并发收集、低停顿(用户感觉不到卡顿)。

  • 缺点

    1. 产生大量内存碎片(纯清除算法)。

    2. 占用 CPU 资源(并发阶段占用一部分线程)。

    3. 无法处理浮动垃圾(并发清除时产生的新垃圾)。

    4. 老年代碎片过多导致大对象分配失败,需降级使用 Serial Old 收集器(会产生长时间 STW)。

适用场景

响应速度优先的应用(如电商交易、门户),但 JDK 9 已被标记为废弃,不再推荐使用。

二、G1 (Garbage-First)

全称

Garbage-First Garbage Collector

核心特点

  • 口号兼顾吞吐量与延迟,面向服务端应用。

  • 布局 :不再区分物理新生代 / 老年代,而是将堆划分为多个 Region(区域)

  • 算法 :结合了 复制算法 (Eden/Survivor)和 标记 - 整理算法(老年代)。

  • 核心逻辑 :维护一个 Remembered Set,记录其他 Region 对本 Region 的引用,避免全堆扫描。

回收流程(两步走)

  1. 年轻代收集(Young GC):回收 Eden + Survivor 区域。

  2. 混合收集(Mixed GC)

    • 不仅回收整个年轻代,还根据 预测停顿时间,回收价值最高(垃圾最多)的部分老年代 Region。

    • 不搞 Full GC,而是做 Full GC 单线程版本(效率较低)。

优缺点

  • 优点

    1. 空间整合 :整体看是标记 - 整理,局部看是复制,无内存碎片

    2. 可预测的停顿:可以指定最大停顿时间(MaxGCPauseMillis)。

    3. 平衡:兼顾高吞吐量和低延迟。

  • 缺点

    1. 实现复杂,负载较高。

    2. 起步阶段收集效率不如 CMS。

适用场景

大堆、高并发、服务端应用(如微服务、大数据处理)。JDK 9 后成为默认垃圾收集器。

三、核心对比表

表格

特性 CMS G1
全称 Concurrent Mark Sweep Garbage-First
设计目标 极致低延迟(响应快) 平衡(延迟 + 吞吐量)
堆结构 物理分代(Eden/S0/S1/Old) 逻辑分代(Region 集合)
老年代算法 标记 - 清除(有碎片 标记 - 整理(无碎片
内存碎片 严重(需 Full GC 整理) (混合回收整理)
可预测停顿 无(只能保证短,不可控) (可指定最大停顿时间)
底层算法 标记 - 清除 复制 + 标记 - 整理
状态 废弃(JDK9+) 主流(JDK9 + 默认)

四、极简记忆口诀

  • CMS并发低延迟,清除有碎片,CPU 也占线,废弃不推荐。

  • G1Region 分块,兼顾快与稳,整理无碎片,主流最常用。

类加载过程 + 双亲委派模型(Java 面试必背)

一、类加载过程(5 步)

加载 → 验证 → 准备 → 解析 → 初始化

  1. 加载(Loading)
  • 找到 .class 文件,读取二进制流

  • 在方法区生成 Class 对象

  • 在堆中生成对应 java.lang.Class 实例

  1. 验证(Verification)
  • 校验文件格式、元数据、字节码、符号引用

  • 保证类符合 JVM 规范,不会危害虚拟机安全

  1. 准备(Preparation)
  • 静态变量分配内存

  • 设置默认初始值int=0boolean=falsereference=null

  • 注意:这里不执行代码,只赋默认值

  1. 解析(Resolution)
  • 将符号引用转为直接引用

  • 主要针对:类、接口、字段、方法

  1. 初始化(Initialization)
  • 执行静态代码块

  • 给静态变量真正赋值

  • 只有主动使用时才触发(new、调用静态方法等)

二、双亲委派模型(Parents Delegation Model)

  1. 三类类加载器(从高到低)

  2. Bootstrap ClassLoader(启动类加载器)

    • 加载 JAVA_HOME/jre/lib 核心类(rt.jar 等)

    • C++ 实现,最顶层

  3. Extension ClassLoader(扩展类加载器)

    • 加载 jre/lib/ext 下的类
  4. Application ClassLoader(应用类加载器)

    • 加载 classpath 下我们自己写的类
  5. 工作流程

  6. 类加载器收到加载请求

  7. 先委托给父加载器,一直向上传到启动类加载器

  8. 父加载器无法加载,才由子加载器自己加载

一句话:儿子先找爹,爹不行儿子才干。

  1. 为什么要双亲委派?
  • 沙箱安全 :防止核心类被恶意替换(比如自己写一个 java.lang.String 不会被加载)

  • 保证类的唯一性:同一个类只会被加载一次,避免重复加载

  1. 如何打破?
  • 继承 ClassLoader

  • 重写 loadClass() 方法,不执行双亲委派逻辑

  • 典型:Tomcat、JDBC 等都打破了双亲委派

极简面试背诵版

  1. 类加载五步:加载 → 验证 → 准备 → 解析 → 初始化

  2. 双亲委派:类加载时优先交给父加载器,父加载器加载失败才自己加载

  3. 作用:保证核心类安全、类唯一、不被篡改

强引用、软引用、弱引用区别(面试必背)

一句话总结:强引用永不回收,软引用内存不够才回收,弱引用下次 GC 就回收。

  1. 强引用(Strong Reference)
  • 默认引用Object obj = new Object();

  • 特点 :只要可达,GC 永远不会回收即使 OOM 也不回收

  • 场景:99% 日常业务对象

  • 结果:内存不足直接抛 OOM

  1. 软引用(SoftReference)
  • 写法:SoftReference<Object> ref = new SoftReference<>(obj);

  • 特点 :内存充足 → 不回收 内存不足(即将 OOM)→ 才回收

  • 场景:缓存(图片缓存、网页缓存)

  • 结果:尽量保留,不到万不得已不回收

  1. 弱引用(WeakReference)
  • 写法:WeakReference<Object> ref = new WeakReference<>(obj);

  • 特点只要发生 GC,就会被回收,不管内存够不够

  • 场景 :容器里的辅助对象、避免内存泄漏典型:ThreadLocal 内部使用

  • 结果:生命周期很短,一 GC 就没

  1. 虚引用(PhantomReference,顺带记)
  • 必须配合引用队列使用

  • 等于没引用,任何时候都可能被回收

  • 作用:对象回收时收到通知,做资源释放

  • 几乎不用

对比表(直接背)

表格

引用类型 回收时机 用途
强引用 永不回收(OOM 也不) 普通对象
软引用 内存不足时回收 缓存
弱引用 每次 GC 必回收 防止内存泄漏、ThreadLocal
虚引用 随时回收 资源释放监听

极简口诀

  • 强引用:宁死不回收

  • 软引用:不够才回收

  • 弱引用:GC 就回收

OOM 常见原因(面试高频,直接背)

OOM 本质:内存不够用 + 回收不了,常见就这 6 大类:

  1. 堆溢出(Java heap space)最常见
  • 原因

    • 集合对象只加不删,无限增长(static Map/List 缓存爆炸)

    • 死循环创建对象

    • 大对象一次性加载(一次性查全表数据到 List)

    • 线程池任务堆积,对象引用无法释放

  • 典型场景:批量查询未分页、缓存未过期

  1. 栈溢出 StackOverflowError /unable to create new native thread
  • StackOverflowError

    • 递归太深 / 死递归
  • unable to create new native thread

    • 线程创建太多,栈内存耗尽

    • 线程池无限制、手动无限 new Thread

  1. 方法区 / 元空间溢出(Metaspace / PermGen space)
  • 原因

    • 大量动态生成类(CGLib 代理、反射、动态代理滥用)

    • 大量 JSP、自定义 ClassLoader 加载过多类

  • JDK8+ 表现:Metaspace OOM

  1. 直接内存溢出(Direct buffer memory)
  • NIO、Netty 等使用堆外内存

  • 申请未释放、分配过大

  • 不受堆大小控制,但受本机内存限制

  1. GC overhead limit exceeded
  • GC 回收效率极低

  • 花大量时间 GC,却只回收一点点内存

  • 典型:内存快耗尽,对象几乎都存活

  1. 内存泄漏导致的 OOM(隐蔽但高发)
  • ThreadLocal 用完没 remove

  • 静态集合持有长生命周期对象

  • IO / 连接未关闭(流、数据库连接、HttpClient)

  • 内部类持有外部类引用(匿名内部类 + 长生命周期容器)

  • 缓存没过期、没淘汰策略

一句话总结版

  1. 堆 OOM:对象太多、只增不减、大对象、死循环

  2. 栈 OOM:递归太深、线程开太多

  3. 元空间 OOM:动态类太多、CGLib 滥用

  4. 直接内存 OOM:NIO 堆外内存没释放

  5. GC 超限:内存几乎满了,GC 救不回来

  6. 内存泄漏:该释放的对象被一直拿着,GC 收不回

IOC、AOP 是什么(Spring 核心,面试极简标准答案)

一、IOC(Inversion of Control)控制反转

一句话:把创建对象、管理对象依赖的控制权,从代码交给 Spring 容器。

通俗理解

以前:

  • 自己 new UserService()

  • 自己 setUserDao()

  • 控制权在代码手里

现在:

  • 类上加 @Service@Component

  • Spring 自动创建对象、自动注入依赖

  • 控制权交给 Spring 容器

核心作用

  • 解耦:对象之间不用硬编码依赖

  • 统一管理对象生命周期

  • 方便替换实现类

关键词

控制反转、依赖注入(DI)、容器管理 Bean

二、AOP(Aspect Oriented Programming)面向切面编程

一句话:在不修改原有业务代码的前提下,统一增强横向逻辑(日志、事务、权限)。

通俗理解

很多方法都需要:

  • 日志打印

  • 事务开启 / 提交

  • 权限校验

  • 性能监控

如果每个方法都写一遍,代码重复、难维护。

AOP 把这些通用逻辑抽成 "切面",统一织入到目标方法前后 / 异常时。

核心术语(简单记)

  • 切面(Aspect):通用功能模块(日志、事务)

  • 通知(Advice):什么时候执行(前、后、异常、环绕)

  • 切点(Pointcut):对哪些方法生效

  • 连接点(JoinPoint):可以被拦截的方法

典型应用

  • @Transactional 声明式事务

  • 统一接口日志

  • 全局权限校验

  • 接口耗时监控

三、一句话总结

  • IOC:让 Spring 帮你管对象、注入依赖 → 解耦

  • AOP:不改业务代码,统一加通用功能 → 简化横向逻辑

JDK 动态代理 vs CGLIB 动态代理(面试必背)

一句话总结:JDK 代理面向接口,用反射;CGLIB 继承类,用字节码生成,性能更好。

  1. 核心区别

JDK 动态代理

  • 基于接口

  • 被代理类必须实现接口

  • 利用 java.lang.reflect.Proxy + InvocationHandler

  • 底层是反射调用

CGLIB 动态代理

  • 基于继承

  • 被代理类不需要接口

  • 通过继承目标类,生成子类重写方法

  • 底层是字节码生成(ASM 框架),直接调用方法

  1. 能否代理没有接口的类
  • JDK 代理:不能必须有接口,否则无法生成代理类。

  • CGLIB:能直接继承类就行,有无接口都可以。

  1. 性能
  • JDK 代理:早期版本反射较慢,JDK8 以后反射大幅优化,但仍略逊于 CGLIB。

  • CGLIB :生成字节码,直接方法调用执行速度更快

  1. 方法限制
  • JDK 代理:接口里的方法都能代理,无限制。

  • CGLIB :不能代理 final 类、final 方法、static 方法(因为要继承重写,final 不能被重写)

  1. Spring 中使用规则
  • 目标类实现了接口 → 默认用 JDK 代理

  • 目标类没有接口 → 自动用 CGLIB

  • 可以通过配置强制 Spring 全部使用 CGLIB:

    java

    运行

    复制代码
    @EnableAspectJAutoProxy(proxyTargetClass = true)

对比表(面试直接背)

表格

对比项 JDK 动态代理 CGLIB 动态代理
实现方式 反射 + 接口 继承 + 字节码生成
是否需要接口 必须有接口 不需要接口
代理原理 生成接口实现类 生成目标类的子类
能否代理 final 可以 不能
性能 一般(反射) 更快(直接调用)
Spring 默认 有接口时使用 无接口时使用

极简口诀

  • JDK 代理:靠接口,用反射,不能代理无接口类

  • CGLIB:靠继承,字节码,速度快,不能代理 final

Spring Bean 生命周期(面试标准版,一步不差)

一句话总结:实例化 → 属性填充 → 初始化 → 使用 → 销毁

  1. 实例化(Instantiation)
  • 加载 Bean 定义,通过构造方法创建对象

  • 此时对象还是空壳,属性未赋值

  1. 属性填充(Populate Properties)
  • 执行依赖注入@Autowired@Value、XML 注入等

  • 给 Bean 的成员变量赋值

  1. 初始化前置处理(BeanPostProcessor#postProcessBeforeInitialization)
  • 执行后置处理器的前置方法

  • 常见:@PostConstruct 就是在这里执行

  1. 初始化(Initialization)

  2. 执行 @PostConstruct 标注的方法

  3. 执行 InitializingBean#afterPropertiesSet()

  4. 执行 init-method@Bean(initMethod = ...)

  5. 初始化后置处理(BeanPostProcessor#postProcessAfterInitialization)

  • 执行后置处理器的后置方法

  • AOP 代理在这里生成

  1. Bean 正常使用
  • 业务调用、方法执行
  1. 销毁(Destruction)

容器关闭时执行:

  1. @PreDestroy 标注的方法

  2. DisposableBean#destroy()

  3. destroy-method@Bean(destroyMethod = ...)

极简流程(背诵版)

  1. 实例化(new)

  2. 属性注入(DI)

  3. 初始化前(前置处理器)

  4. 初始化(@PostConstruct → afterPropertiesSet → initMethod)

  5. 初始化后(后置处理器,生成 AOP 代理)

  6. 使用中

  7. 销毁(@PreDestroy → destroy → destroyMethod)

超短口诀(面试秒答)

实例化 → 赋值 → 初始化 → 用 → 销毁 前后各包一层 BeanPostProcessor

@Autowired 和 @Resource 区别(面试必背)

一句话总结:@Autowired 按类型自动装配;@Resource 默认按名称,名称找不到再按类型。

  1. 来源不同
  • @AutowiredSpring 提供的注解

  • @Resource:**Java 标准(JSR-250)** 注解,通用性更强

  1. 装配顺序(核心区别)

@Autowired

  • 默认按类型(byType)装配

  • 类型唯一 → 直接注入

  • 同一类型多个 Bean → 报错

  • 配合 @Qualifier("beanName") 指定名称

@Resource

  • 先按名称(byName)装配

  • 名称匹配不到 → 再按类型(byType)

  • 可以直接指定 nametype

    java

    运行

    复制代码
    @Resource(name = "userService")
  1. 是否支持 required
  • @Autowired :支持 required = false,允许为 null

  • @Resource不支持 required 属性

  1. 装配对象范围
  • @Autowired:可注入构造器、成员变量、setter、方法参数

  • @Resource :只能用在字段、setter 方法

  1. Spring 依赖注入优先级
  • @Resource 按名字匹配更精确,不容易冲突

  • @Autowired 按类型,配合 @Qualifier 也能精确匹配

对比表(直接背)

表格

对比项 @Autowired @Resource
来源 Spring Java JSR-250
装配顺序 先按类型 先按名称,再按类型
指定名称 需要 @Qualifier 直接用 name 属性
required 支持 不支持
通用性 仅限 Spring 通用

极简口诀

  • @Autowired:Spring 出身,按类型,要名字加 Qualifier

  • @Resource:Java 标准,先名后型,更省心

Spring 事务传播行为 + 隔离级别(面试必背完整版)

一、事务传播行为(7 种,重点记 4 个)

控制被调用方法的事务调用方事务的关系。

  1. REQUIRED(默认)
  • 有事务就加入,没有就新建

  • 最常用,业务层默认

  1. REQUIRES_NEW
  • 新建独立事务,挂起外部事务

  • 内部事务提交 / 回滚不影响外部

  • 适用于:日志、通知必须成功,不跟主业务一起回滚

  1. NESTED
  • 嵌套事务,是外部事务的子事务

  • 外部回滚 → 内部一定回滚

  • 内部回滚 → 只回滚到保存点,不影响外部

  • 仅支持 JDBC,不支持 JPA/Hibernate

  1. SUPPORTS
  • 有事务就支持,没事务就以非事务运行
  1. MANDATORY
  • 强制要求已有事务,否则抛异常
  1. NOT_SUPPORTED
  • 始终非事务运行,挂起当前事务
  1. NEVER
  • 强制非事务,有事务就抛异常

二、事务隔离级别(4 种标准 + 默认)

  1. DEFAULT(默认)
  • 使用数据库默认隔离级别

  • MySQL:REPEATABLE_READ

  • Oracle:READ_COMMITTED

  1. READ_UNCOMMITTED
  • 读未提交

  • 问题:脏读、不可重复读、幻读

  1. READ_COMMITTED
  • 读已提交

  • 解决:脏读

  • 仍有:不可重复读、幻读

  1. REPEATABLE_READ
  • 可重复读

  • 解决:脏读、不可重复读

  • 仍有:幻读(MySQL InnoDB 用间隙锁很大程度规避幻读

  1. SERIALIZABLE
  • 串行化

  • 解决所有问题,但性能极低

三、3 大读问题(一句话区分)

  • 脏读 :读到未提交的数据

  • 不可重复读 :同一事务内,两次查询结果不一样(被 update)

  • 幻读 :同一事务内,查询到新增 / 删除的行(insert/delete)

四、极简背诵版

传播行为(重点)

  • REQUIRED:有就加,没有就建(默认)

  • REQUIRES_NEW:新开独立事务

  • NESTED:嵌套子事务

  • SUPPORTS:有就用,没有就算

隔离级别

  • 读未提交:脏读、不可重复读、幻读都有

  • 读已提交:解决脏读

  • 可重复读:解决脏读 + 不可重复读(MySQL 默认)

  • 串行化:全解决,性能差

SpringMVC 核心执行流程(面试标准 8 步,背这版就够)

一句话总结:用户请求 → 前端控制器分发 → 找映射 → 调处理器 → 执行方法 → 渲染视图 → 响应返回

  1. 用户发送请求

请求到达 DispatcherServlet(前端控制器,核心总入口)

  1. DispatcherServlet 调用 HandlerMapping

根据请求 URL 找到对应的 Controller 方法 得到一个 HandlerExecutionChain(处理器 + 拦截器链)

  1. DispatcherServlet 调用 HandlerAdapter

根据 Handler 找到对应适配器,统一执行 Controller 方法

  1. 执行拦截器 preHandle

按顺序执行所有 HandlerInterceptor#preHandle()

  1. 执行 Controller 业务方法

调用真正的接口方法,返回 ModelAndView(或直接返回 JSON)

  1. 执行拦截器 postHandle

执行 HandlerInterceptor#postHandle()

  1. 处理视图渲染
  • 有视图:调用 ViewResolver 解析视图,渲染页面

  • 返回 JSON:直接通过消息转换器写出 JSON,不走视图解析

  1. 执行拦截器 afterCompletion

最后执行 afterCompletion(),并把响应返回给用户

极简背诵版(8 步浓缩)

  1. 请求 → DispatcherServlet

  2. 找映射:HandlerMapping 找 Controller

  3. 找适配器:HandlerAdapter

  4. 执行:拦截器 preHandle

  5. 执行:Controller 方法

  6. 执行:拦截器 postHandle

  7. 渲染:ViewResolver 视图解析

  8. 最终:拦截器 afterCompletion → 返回响应

超简口诀(面试秒答)

前端控制器接单 → 找映射 → 找适配器 → 过拦截器 → 执行业务 → 渲染视图 → 返回

SpringBoot 自动配置原理(面试标准答案,精简好背)

一句话总结:SpringBoot 通过 @EnableAutoConfiguration 加载 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 里的配置类,再按条件判断是否生效,实现自动装配。

  1. 核心注解:@SpringBootApplication

它是一个组合注解,本质包含 3 个:

  1. @SpringBootConfiguration标记当前类为配置类。

  2. @ComponentScan自动扫描包下的 Bean。

  3. @EnableAutoConfiguration 自动配置的核心开关

  4. @EnableAutoConfiguration 做了什么

  • 借助 @Import(AutoConfigurationImportSelector.class)

  • 读取类路径下:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

  • 里面列出了所有自动配置类(如 DataSourceAutoConfiguration、WebMvcAutoConfiguration...)

  1. 自动配置类按条件生效

每个配置类上都有条件注解,满足才生效:

  • @ConditionalOnClass:类路径存在某个类

  • @ConditionalOnBean:容器中有某个 Bean

  • @ConditionalOnMissingBean:容器没有这个 Bean 才创建

  • @ConditionalOnProperty:配置文件有对应属性

  • @ConditionalOnWebApplication:是 Web 环境

例子:你引入 spring-boot-starter-web → 有 DispatcherServlet → WebMvcAutoConfiguration 生效。

  1. 配置绑定 @ConfigurationProperties

自动配置类从 application.yml/application.properties 读取属性:

  • 通过 @ConfigurationProperties(prefix = "spring.datasource")

  • 把配置绑定到 Bean 中,完成自动装配。

  1. 完整流程(极简背诵版)

  2. 启动类 @SpringBootApplication 开启自动配置

  3. @EnableAutoConfiguration 读取自动配置类列表

  4. 加载所有 xxxAutoConfiguration

  5. 通过 @Conditional 条件判断是否生效

  6. 结合 @ConfigurationProperties 读取配置

  7. 向容器注册 Bean,完成自动配置

  8. 一句话超简版

SpringBoot 扫描约定位置的自动配置类,根据依赖和条件自动装配 Bean,无需手动配置。

事务 ACID(面试必背,一句话一个)

ACID 是事务的四大特性:原子性、一致性、隔离性、持久性。

  1. A --- Atomicity 原子性
  • 事务是最小执行单元,不可再分

  • 要么全部执行成功 ,要么全部回滚失败

  • 不会出现只执行一半的情况

  1. C --- Consistency 一致性
  • 事务执行前后,数据库的完整性约束不变

  • 数据从一个合法状态 转到另一个合法状态

  • 例如:转账前后总金额不变

  1. I --- Isolation 隔离性
  • 多个并发事务之间互相隔离、互不干扰

  • 一个事务的修改在提交前,对其他事务不可见

  • 对应数据库的事务隔离级别

  1. D --- Durability 持久性
  • 事务一旦提交成功 ,对数据的修改就是永久性

  • 即使宕机、重启,数据也不会丢失

极简口诀

A 原子不可切,C 一致总不变, I 隔离互不扰,D 持久永留存。

事务隔离级别 + 脏读 / 不可重复读 / 幻读(面试必背)

一、先搞懂 3 个问题

  1. 脏读 读到了别的事务未提交的数据,对方一回滚,数据就无效了。

  2. 不可重复读 同一个事务内,两次查询结果不一样(被别的事务 update 并提交了)。

  3. 幻读 同一个事务内,按条件查询,突然多了 / 少了行(被别的事务 insert/delete 并提交了)。

二、4 大隔离级别(从低到高)

  1. READ UNCOMMITTED(读未提交)
  • 问题:脏读 ✅、不可重复读 ✅、幻读 ✅

  • 性能最高,数据最不安全,基本不用

  1. READ COMMITTED(读已提交)
  • 解决:脏读 ❌

  • 仍有:不可重复读 ✅、幻读 ✅

  • Oracle 默认

  1. REPEATABLE READ(可重复读)
  • 解决:脏读 ❌、不可重复读 ❌

  • 仍有:幻读 ✅

  • MySQL InnoDB 默认(通过间隙锁很大程度缓解幻读)

  1. SERIALIZABLE(串行化)
  • 解决:脏读 ❌、不可重复读 ❌、幻读 ❌

  • 事务排队执行,性能极低

三、一张表背完

表格

隔离级别 脏读 不可重复读 幻读
读未提交
读已提交
可重复读
串行化

四、一句话记忆

  • 脏读:读到未提交数据

  • 不可重复读:同事务两次查询结果不同(update)

  • 幻读:同事务查询行数变了(insert/delete)

  • MySQL 默认可重复读,Oracle 默认读已提交

索引为什么用 B+ 树(面试标准答案,精简好背)

一句话总结:B+ 树层级少、磁盘 IO 少、范围查询快、适合磁盘存储,是数据库索引的最优结构。

  1. 相比二叉树:层级更少,IO 更少
  • 二叉树:一次只读 1 个节点,数据量大时层级极深

  • B+ 树:多路平衡查找树,一个节点存大量 key

  • 同样 1000 万数据

    • 二叉树:深度 ≈ 24 → 24 次 IO

    • B+ 树:深度 ≈ 3~4 → 仅几次 IOIO 越少,查询越快。

  1. 相比 B 树:只在叶子节点存数据,节点更 "胖"
  • B 树:每个节点都存 key + 数据

  • B+ 树:

    • 非叶子节点只存 key,不存完整数据

    • 同样大小磁盘页,能存更多 key

    • 树更矮更胖,IO 更少

  1. 叶子节点形成有序链表,范围查询极快
  • B 树:范围查询需要回溯,效率低

  • B+ 树:

    • 所有叶子节点用双向链表串联

    • 范围查询(>、<、between、like)只需遍历链表

    • 非常适合数据库 order by / group by

  1. 查询稳定,所有查询都走叶子节点
  • B+ 树任何数据查询,路径长度都一样

  • 查询效率稳定、可预测

  1. 适合磁盘预读,充分利用局部性原理
  • 磁盘顺序读写远快于随机 IO

  • B+ 树节点大小通常等于磁盘页(4KB/16KB)

  • 加载一个节点就预读一整页,充分利用缓存

对比总结(直接背)

  • 哈希索引 :等值查询快,但不支持范围查询

  • 二叉树:层级深、IO 多

  • B 树:节点存数据,范围查询慢

  • B+ 树

    • 多路矮胖 → IO 少

    • 叶子链表 → 范围查询快

    • 节点只存 key → 更省空间

    • 查询稳定 → 适合数据库索引

超简口诀

多路矮胖 IO 少,叶子链表范围好, 只存键值节点胖,磁盘友好索引王。

聚簇索引 vs 非聚簇索引(面试必背,一句话分清)

一句话:聚簇索引:叶子节点存整行数据,索引即数据; 非聚簇索引:叶子节点存主键 / 地址,需回表查数据。

一、聚簇索引(Clustered Index)

特点

  • 索引结构和数据存放在一起 ,叶子节点就是完整的行数据

  • 一个表只能有一个聚簇索引

  • 数据物理存储顺序与索引顺序一致

InnoDB 表现

  • 主键索引就是聚簇索引

  • 没有主键时,用唯一键

  • 都没有,自动生成隐藏 rowid

优点

  • 按主键范围查询、排序非常快

  • 查整行数据不需要回表,一次找到

缺点

  • 插入、更新(尤其随机主键)会页分裂,影响性能

  • 主键不宜过长、不宜随机(如 UUID)

二、非聚簇索引(Secondary Index / 二级索引)

特点

  • 索引和数据分开存放

  • 叶子节点不存整行数据,只存:

    • InnoDB:主键值

    • MyISAM:数据行的物理地址

  • 一个表可以建多个非聚簇索引

查询过程

  1. 走二级索引找到主键

  2. 再通过主键走聚簇索引查完整数据→ 这一步叫 回表

优点

  • 适合多条件查询,灵活建索引

  • 不影响数据物理存储

缺点

  • 可能需要回表,多一次 IO

  • 查询比聚簇索引稍慢

三、一张表对比(直接背)

表格

聚簇索引 非聚簇索引(二级索引)
叶子节点 整行数据 主键 / 物理地址
数量 一个表只能一个 可以多个
回表 不需要 通常需要回表
范围查询 极快 一般
代表 InnoDB 主键索引 InnoDB 普通索引、MyISAM 索引

四、超简口诀

聚簇索引叶存行,主键索引就是它; 二级索引存主键,查到主键再回表。

最左前缀原则 & 索引失效场景(面试高频,直接背)

一、最左前缀原则

联合索引必须从左到右依次使用,跳过前面的列,后面的索引失效。

举例

索引:idx(a, b, c)

  • where a=? → 命中索引

  • where a=? and b=? → 命中

  • where a=? and b=? and c=? → 命中

  • where b=? → 失效(跳过 a)

  • where a=? and c=? → 只命中 a,b 断了,c 失效

  • where a=? and b like 'x%' and c=? → a、b 命中,c 失效

一句话:断了中间的列,后面全都用不上。

二、索引失效场景(高频 10 条)

  1. 使用函数 / 运算

    • where abs(age) = 18

    • where age + 1 = 20

  2. 隐式类型转换

    • varchar 字段传数字:where phone = 13800138000
  3. 模糊查询以 % 开头

    • like '%abc'

    • like '%abc%'

    • like 'abc%' 可以走索引

  4. 使用!= / <> /is not null

    • 会导致索引失效
  5. or 连接条件

    • 一边有索引,一边没有 → 整个索引失效
  6. 违反最左前缀

    • 联合索引跳过前面字段
  7. order by /group by 违反最左前缀

    • 索引 (a,b,c),order by c → 无法用索引排序
  8. not in / not exists

    • 通常导致全表扫描
  9. 数据分布不均,优化器选择不走索引

    • 如查询 80% 数据,MySQL 直接全表扫
  10. 使用 select *

  • 不一定失效,但容易无法使用覆盖索引

三、极简总结版

  • 最左前缀:联合索引必须从左往右用,断一个后面全废

  • 索引失效:函数运算、隐式转换、首百分号、不等值违反最左、or 连接、not in、数据太分散

MVCC 简单通俗理解(好记不绕)

一句话:MVCC = 多版本并发控制,让读不加锁、写不加锁,读写不阻塞,实现高并发。

  1. MVCC 是干嘛的?

解决两个问题:

  • 读数据时,别人在修改,不阻塞、也不乱

  • 写数据时,别人在读,也不阻塞

实现:读不加锁、写不加锁,并发极高

  1. 核心思想:多版本
  • 一条数据被更新时,不直接覆盖旧数据

  • 而是生成一个新版本,旧版本保留

  • 不同事务看到的是不同版本的数据

就像:同一行数据有好几个 "快照",每个人看自己那一版。

  1. 靠什么实现?(极简版)

每行数据隐藏 3 个字段:

  1. trx_id:创建这条版本的事务 ID

  2. roll_pointer:指向旧版本(undo log 里)

  3. deleted_flag:标记是否删除

再配合两个东西:

  • Read View:事务启动时的 "快照",决定能看见哪些版本

  • undo log:存放历史版本,用来回滚 & 读旧数据

  1. 读怎么工作?

事务启动时生成一个 Read View,根据规则判断:

  • 这个版本能不能看见

  • 看不见就顺着 roll_pointer 找旧版本

  • 直到找到可见版本为止

这就是快照读 (普通 select),完全无锁

  1. 写怎么工作?
  • 更新时生成新版本

  • 对记录加行锁(只锁这一行)

  • 旧版本保留在 undo log 里

  • 其他事务读的是旧版本,不被阻塞

  1. 最终一句话总结

MVCC 就是给数据存多个历史版本,让读操作读旧版本、写操作新版本,读写互不阻塞,实现高并发且安全。

它是 MySQL InnoDB 实现 RC、RR 隔离级别的底层原理。

Redis 常用数据结构(面试高频,极简版)

Redis 常用 5 种基础结构 + 3 种高级结构,一句话记用途:

一、5 大基础数据结构

  1. String(字符串)
  • 最简单:key=value

  • 场景:缓存、计数器、分布式锁、session

  1. Hash(哈希)
  • 类似 Map<String, Map<String, Object>>

  • 场景:用户信息、商品详情、对象缓存

  1. List(列表)
  • 有序可重复,双向链表

  • 场景:消息队列、排行榜、时间线、栈 / 队列

  1. Set(集合)
  • 无序不重复

  • 场景:去重、共同好友、点赞、抽奖

  1. ZSet(有序集合)
  • 按 score 排序,不重复

  • 场景:排行榜、延时任务、带权重排序

二、高级常用结构

  1. Bitmap(位图)
  • 用 bit 存状态,极省空间

  • 场景:签到、日活统计、布隆过滤器

  1. HyperLogLog
  • 基数统计(不精确但极省内存)

  • 场景:UV、独立访客统计

  1. GEO(地理坐标)
  • 存储经纬度

  • 场景:附近的人、附近门店

超简口诀

String 存值,Hash 存对象, List 做队列,Set 去重,ZSet 排序。

缓存穿透、击穿、雪崩(最简单易懂版)

一句话区分:

  • 穿透 :查不存在的数据,缓存没有,直接打库

  • 击穿一个热点 Key 过期,瞬间大量请求打库

  • 雪崩大量 Key 同时过期,或 Redis 宕机,数据库被打垮

  1. 缓存穿透

现象

  • 查询根本不存在的数据

  • 缓存不命中 → 每次都查数据库

  • 恶意攻击:id=-1、id=999999999 疯狂请求

解决方案

  1. 缓存空值 :查不到也存 null,设置短过期

  2. 布隆过滤器:过滤不存在的 key,直接返回

  3. 参数校验、接口限流

  4. 缓存击穿

现象

  • 某个热点 Key 过期

  • 同一瞬间大量并发请求进来

  • 缓存不命中 → 全部请求数据库

解决方案

  1. 互斥锁(mutex lock):只让一个线程去查库重建缓存

  2. 热点 Key 永不过期

  3. 加随机过期时间,避免扎堆失效

  4. 缓存雪崩

现象

  • 大量缓存同一时间过期Redis 宕机

  • 所有请求全部打到数据库

  • 数据库压力暴增 → 宕机 → 连锁崩溃

解决方案

  1. 过期时间加随机值,避免批量同时过期

  2. 多级缓存:本地缓存 + Redis

  3. Redis 集群、高可用(主从 + 哨兵)

  4. 限流、降级、熔断

  5. 缓存预热

极简口诀(面试秒答)

  • 穿透:查不存在数据 → 布隆过滤器、缓存空值

  • 击穿:热点 Key 过期 → 加锁、永不过期

  • 雪崩:大量 Key 同时挂 → 随机过期、集群、限流

缓存与数据库一致性(面试最实用版本)

核心一句话:保证缓存与数据库一致,本质是:先更数据库,再删缓存;并且要避免并发错乱。

  1. 为什么不能 "更新缓存"?
  • 线程 A 更新数据库

  • 线程 B 又更新数据库

  • B 先更新缓存

  • A 后更新缓存→ 缓存变成旧数据,永久不一致

所以:不做更新缓存,只做删除缓存

  1. 标准方案:先更数据库,再删缓存

流程:

  1. 更新数据库

  2. 删除缓存(而不是更新)

  3. 下次查询时,重新查询数据库并回填缓存

这是业界最常用、最推荐的方案。

  1. 极端并发不一致场景(面试必说)
  • 读请求:缓存未命中 → 查数据库得到旧值

  • 写请求:更新数据库 → 删除缓存

  • 读请求:把旧值回填到缓存

缓存是旧值,数据库是新值

概率极低,但存在。

解决方案:

  • 延时双删:更库 → 删缓存 → 延迟几百 ms 再删一次

  • 或使用 消息队列异步删缓存

  1. 强一致性方案(分布式事务)

要求极高一致性时用:

  • Canal 订阅 binlog

  • 数据库变更后自动异步更新 / 删除缓存

  • 最终一致性,业务大多足够用

  1. 一句话总结(面试背诵版)
  • 优先方案:先更新数据库,再删除缓存

  • 并发不一致问题用延时双删异步删缓存解决

  • 不推荐更新缓存,避免脏数据

  • 绝大多数业务保证最终一致性即可

说说JVM

  • 首先是 JVM 内存结构主要分成五块:

    • :存放对象实例,是 GC 主要区域
    • 虚拟机栈:每个线程一个,存局部变量、方法栈帧
    • 方法区(元空间):存类信息、常量、静态变量
    • 本地方法栈:给 native 方法用
    • 程序计数器:记录当前线程执行到哪行指令
  • 然后是垃圾回收 GC

    • 判断对象死活用 可达性分析,GC Roots 做根节点
    • 回收算法主要有:标记清除、标记复制、标记整理
    • 堆分代:新生代 Eden、S0、S1,对象熬过多次 GC 进老年代
    • 常见垃圾收集器:CMS、G1 现在用得比较多
  • 类加载机制

    • 过程:加载 → 验证 → 准备 → 解析 → 初始化
    • 双亲委派模型,防止类重复加载、保证安全
    • 类加载器:启动类加载器、扩展、应用类加载器
  • 最后是常见问题 比如 OOM 异常 ,可能是堆溢出、栈溢出;还有 GC 频繁、Full GC 太多 这些线上调优问题。

JVM 是 Java 虚拟机,核心作用就是加载并执行字节码,实现 Java 跨平台。我一般从四个维度理解:

第一是内存模型 。JVM 把内存划分为堆、虚拟机栈、本地方法栈、方法区、程序计数器。其中堆是最大的区域,存放对象实例,也是 GC 主要工作的地方;栈是线程私有的,每个方法对应一个栈帧,存局部变量和返回地址。

第二是垃圾回收机制 。JVM 不用手动管理内存,通过可达性分析判断对象是否存活,再用标记复制、标记清除、标记整理算法回收垃圾。堆一般分为新生代和老年代,对象先在 Eden 区分配,熬过多次 Minor GC 会进入老年代,老年代满了触发 Full GC。常用收集器比如 CMS、G1,现在线上基本都用 G1 了。

第三是类加载机制 。流程是加载、验证、准备、解析、初始化。采用双亲委派模型 ,优先让父类加载器加载,好处是安全、防止类重复、保证核心类不被篡改

第四是常见问题与调优。比如 OOM 一般是堆内存不足、内存泄漏或者创建对象太多没释放;GC 频繁多半是新生代设置不合理、大对象太多。平时排查主要看 GC 日志、堆 dump,定位是内存泄漏还是参数问题。

相关推荐
competes2 小时前
慈善基金投资底层逻辑应用 顶层代码低代码配置平台开发结构方式数据存储模块
java·开发语言·数据库·windows·sql
Ulyanov3 小时前
用Pyglet打造AI数字猎人:从零开始的Python游戏开发与强化学习实践
开发语言·人工智能·python
2501_913061343 小时前
网络原理知识
java·网络
独自归家的兔3 小时前
OCPP 1.6 协议详解:StatusNotification 状态通知指令
开发语言·数据库·spring boot·物联网
希望永不加班3 小时前
Spring AOP 代理模式:CGLIB 与 JDK 动态代理区别
java·开发语言·后端·spring·代理模式
RNEA ESIO3 小时前
PHP进阶-在Ubuntu上搭建LAMP环境教程
开发语言·ubuntu·php
23471021273 小时前
4.15 学习笔记
开发语言·软件测试·python
flushmeteor4 小时前
java的动态代理和字节码生成技术
java·动态代理·代理·字节码生成
eggwyw4 小时前
基于SpringBoot和PostGIS的云南与缅甸的千里边境线实战
java·spring boot·spring