JVM

内存结构 · 垃圾回收 · 类加载 · 排查

运行时内存区域

  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 线程共享:堆(对象实例)、方法区(类元信息、运行时常量池)
  • JDK 8 之后方法区由元空间实现,位于本地内存,受 -XX:MaxMetaspaceSize 约束
  • 字符串常量池在 JDK 7 就移到了堆里

垃圾回收

  • 判定存活用可达性分析,不是引用计数(引用计数解决不了循环引用)
  • GC Roots:栈帧中的局部变量、静态变量、常量、JNI 引用、活跃线程
  • 分代假设:绝大多数对象朝生夕死 → 新生代用复制算法,老年代用标记-整理
  • 对象进入老年代:熬过 15 次 Minor GC(-XX:MaxTenuringThreshold)、大对象直接进、动态年龄判定
  • G1 把堆切成等大 Region,按回收价值排序优先回收,可预测停顿
面试常问:为什么 Full GC 要 stop the world? 标记阶段需要保证对象引用关系不变,否则会误判存活/已死,所以必须暂停所有应用线程。

类加载

  • 过程:加载 → 验证 → 准备 → 解析 → 初始化
  • 双亲委派:先交给父加载器,父加载不了才自己加载 —— 保证核心类不被篡改
  • 打破双亲委派的场景:SPI(JDBC 驱动)、Tomcat 的 WebApp 隔离、OSGi
  • 准备阶段赋零值,初始化阶段才执行 <clinit> 赋真正的值

线上排查

jps -l                     # 找进程
jstat -gcutil <pid> 1000   # GC 频率与停顿时长
jmap -dump:live,format=b,file=heap.hprof <pid>
jstack <pid>               # 线程栈,查死锁
# 线上更常用 arthas / async-profiler,因为 dump 会 STW

集合

HashMap · ConcurrentHashMap · List

HashMap

  • 数组 + 链表 + 红黑树(链表长度 ≥ 8 且数组长度 ≥ 64 时转树)
  • 默认容量 16,负载因子 0.75,扩容翻倍
  • 下标计算:(n - 1) & hash,所以容量必须是 2 的幂
  • hash 扰动:h ^ (h >>> 16),让高位也参与运算,减少碰撞
  • JDK 8 尾插法(解决成环),JDK 7 是头插

ConcurrentHashMap

  • JDK 7:分段锁 Segment,锁粒度是段
  • JDK 8:CAS + synchronized,锁粒度细化到桶的第一个节点
  • get 不加锁,靠 volatile 保证可见性
  • size() 用 baseCount + CounterCell 累加,避免热点
Collections.synchronizedMap 是整表一把锁,并发度远低于 ConcurrentHashMap。

ArrayList vs LinkedList

  • ArrayList 底层数组,随机访问 O(1),中间插入 O(n)
  • LinkedList 双向链表,插入删除 O(1),但随机访问 O(n),且每个节点额外开销大
  • 实际项目里 ArrayList 几乎总是更优,LinkedList 只在头尾频繁操作且不访问时才有优势
  • ArrayList 扩容 1.5 倍(old + (old >> 1))

并发

线程池 · 锁 · AQS · 内存模型

线程池

  • 七参数:核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略
  • 执行顺序:核心线程 → 队列 → 非核心线程 → 拒绝策略
  • 拒绝策略四种:AbortPolicy(抛异常)、CallerRunsPolicy(调用线程执行)、DiscardPolicy、DiscardOldestPolicy
  • 禁止用 Executors 创建,因为 newFixedThreadPool 用无界队列会 OOM,newCachedThreadPool 线程数无上限

volatile 与 synchronized

  • volatile 保证可见性和有序性,不保证原子性
  • 底层靠内存屏障:写前 StoreStore,写后 StoreLoad
  • synchronized 锁升级:无锁 → 偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(monitor)

CAS 与 ABA

  • CAS 是 CPU 指令级别,无锁但会自旋消耗 CPU
  • ABA 问题用版本号解决,即 AtomicStampedReference

AQS

  • 核心:一个 volatile state + 一个 CLH 双向队列
  • 独占模式用 tryAcquire / tryRelease,共享模式用 tryAcquireShared
  • ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都基于 AQS

MySQL

索引 · 事务 · MVCC · 优化

索引

  • InnoDB 用 B+ 树:非叶子只存键,叶子存数据且用双向链表相连 —— 树矮、范围查询快
  • 聚簇索引:主键即数据,二级索引叶子存的是主键值,所以要回表
  • 覆盖索引:查询字段全在索引里就不用回表
  • 最左前缀:联合索引 (a,b,c) 能走 a、a+b、a+b+c,走不了 b、c
  • 索引失效:函数运算、隐式类型转换、like '%x'、or 两侧有非索引列、范围查询后的列

事务与隔离级别

  • 四种隔离级别:读未提交、读已提交、可重复读(默认)、串行化
  • RC 解决脏读,RR 解决不可重复读,串行化解决幻读
  • InnoDB 的 RR 用间隙锁在很大程度避免了幻读
  • MVCC:隐藏字段 + undo log 版本链 + ReadView
  • RC 每次快照读都新建 ReadView;RR 只在第一次快照读时创建

锁

  • 表锁、行锁、间隙锁、临键锁(Next-Key Lock = 行锁 + 间隙锁)
  • 行锁加在索引上,没有索引会退化成表锁
  • 死锁排查:SHOW ENGINE INNODB STATUS

慢查询

EXPLAIN SELECT ...;
-- 重点看
-- type: system > const > eq_ref > ref > range > index > ALL
-- key: 实际用到的索引
-- rows: 预估扫描行数
-- Extra: Using index(覆盖) / Using filesort(要排序) / Using temporary(要临时表)

Redis

缓存问题 · 持久化 · 分布式锁

缓存三兄弟

  • 穿透:查不存在的数据,请求全打到库 → 布隆过滤器 / 缓存空值
  • 击穿:热点 key 过期瞬间大量请求 → 互斥锁重建 / 逻辑过期
  • 雪崩:大量 key 同时失效 → 过期时间加随机值 / 多级缓存 / 限流降级

持久化

  • RDB:快照,恢复快、体积小,但可能丢数据
  • AOF:记命令,always / everysec / no 三档刷盘策略
  • 混合持久化(4.0+):RDB 做基准 + AOF 记增量

分布式锁

SET lock_key token NX PX 30000
-- 释放必须用 Lua 保证原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then
  return redis.call("del", KEYS[1])
else return 0 end
Redis 主从异步复制,主节点宕机可能丢锁。对一致性要求高的场景用 Redlock 或换 ZooKeeper / etcd。

其他高频

  • 单线程指命令执行单线程,网络 IO 在 6.0 之后是多线程
  • 大 key 危害:阻塞、网络拥塞、删除卡顿
  • 过期策略:惰性删除 + 定期删除;内存淘汰 8 种策略,常用 allkeys-lru

Spring

IoC · AOP · 循环依赖 · 事务

IoC 与循环依赖

  • 三级缓存:singletonObjects、earlySingletonObjects、singletonFactories
  • 二级能解决普通循环依赖,三级是为了AOP 代理——提前暴露的是工厂而不是实例
  • 循环依赖只能解决单例 + setter/字段注入;构造器注入和 prototype 解决不了

AOP

  • 动态代理:JDK(接口)或 CGLIB(子类)
  • Spring Boot 2.x 起默认 CGLIB

事务失效的常见场景

  • 方法不是 public
  • 同类内部调用——绕过了代理,这是最高频的坑
  • 异常被 catch 没抛出,或抛的是检查异常(默认只回滚 RuntimeException)
  • 类没被 Spring 管理
  • 数据库引擎不支持事务(MyISAM)
内部调用要生效,可以注入自己、用 AopContext.currentProxy(),或者干脆把方法拆到另一个 Service。

微服务

注册中心 · 幂等 · 限流 · 分布式事务

Nacos

  • 单机模式默认内嵌 Derby,集群默认外置 MySQL
  • 2.x 起客户端走 gRPC,端口是主端口 + 1000(8848 → 9848)
  • 3.x 把控制台拆出去,独立端口 8080

幂等

  • 唯一索引(最可靠)、token 机制、状态机、分布式锁
  • 关键:幂等键要能覆盖真实的重复请求,而不是简单按参数哈希

限流

  • 固定窗口、滑动窗口、漏桶、令牌桶
  • 单机用 Guava RateLimiter(令牌桶),分布式用 Redis + Lua
  • Sentinel 支持流控、熔断降级、热点参数限流

分布式事务

  • 2PC / XA:强一致,性能差
  • TCC:业务侵入大,要做三个方法
  • 本地消息表 / 事务消息:最终一致,工程上最常用
  • Saga:长事务补偿,适合流程型业务

系统设计

短链 · 秒杀 · 排行榜

短链服务

  • 发号:号段模式或雪花,避免每次查库
  • 编码:62 进制把数字转字符串,6 位可支撑 568 亿
  • 存储:MySQL 存映射 + Redis 缓存热点
  • 跳转用 302 而非 301,才能统计点击
  • 统计异步化:写消息队列再落库,别拖慢跳转

秒杀

  • 层层过滤:前端限流 → 网关限流 → Redis 预扣库存 → 消息队列削峰 → 异步落库
  • 库存扣减用 Lua 保证原子性
  • 超卖靠数据库乐观锁兜底:UPDATE ... SET stock = stock - 1 WHERE id = ? AND stock > 0

排行榜

  • Redis ZSET,ZADD 更新、ZREVRANGE 取 Top N、ZREVRANK 查排名
  • 超大规模要分片 ZSET 再归并