面经整理

面经整理

钉钉-一面

项目拷打,遇到了哪些难点,是否遇到过上线后的bug(实习见文档)

Http 和 Tcp 的关系是什么

TCP 连接说一下

三次握手、长连接(http2,数据库)、短连接(开销大,早期http)

Java中的强引用、弱引用、软引用之间的区别

Java 的 引用决定了 GC(垃圾回收器)是否会回收对象。

强引用 (Strong Reference)

  • 最常见的引用方式,比如:

    1
    Object obj = new Object();
  • 只要有强引用指向对象,GC 永远不会回收它。

  • 哪怕内存不足,宁可抛 OutOfMemoryError,也不会主动回收强引用对象。


软引用 (SoftReference)

  • SoftReference 类包装:

    1
    SoftReference<Object> ref = new SoftReference<>(new Object());
  • 特性:内存够用时不会回收,内存紧张时才会回收。

  • 应用场景:做 缓存,例如图片缓存。


弱引用 (WeakReference)

  • WeakReference 包装:

    1
    WeakReference<Object> ref = new WeakReference<>(new Object());
  • 特性:只要发生 GC,不管内存是否够用,都会回收弱引用对象。

  • 应用场景:避免内存泄漏,比如 ThreadLocal 里的 key 就是弱引用。


虚引用 (PhantomReference)(没提,但补充一下)

  • 必须和 ReferenceQueue 联合使用。
  • 特性:对象几乎无法通过虚引用访问,主要用于跟踪对象何时被回收,做一些资源清理工作

jvm 中的gc,是否了解过

1. JVM 内存模型(先铺垫)

  • JVM 内存主要分为:
    • 堆 (Heap):存放对象实例,是 GC 主要管理的区域。
    • 方法区 (Metaspace, JDK8 以后):存放类元数据、常量池。
    • 栈、程序计数器、本地方法栈:生命周期随线程,GC 不涉及。

👉 这样先告诉面试官:你知道 GC 是在 堆和方法区 里工作的。


2. GC 的判断依据

  • JVM 主要通过 可达性分析 (Reachability Analysis) 来判断对象是否存活。
  • GC Roots 包括:
    • 栈中的局部变量
    • 方法区的静态变量、常量
    • JNI 引用
  • 从 GC Roots 出发不可达的对象,才会被判定为垃圾。

3. GC 算法

常见算法:

  1. 标记-清除 (Mark-Sweep)
    • 标记存活对象,清除未标记的对象 → 会产生内存碎片。
  2. 复制算法 (Copying)
    • 将存活对象复制到另一块内存 → 没有碎片,常用于新生代。
  3. 标记-整理 (Mark-Compact)
    • 存活对象往一端移动,清理边界外 → 避免碎片,常用于老年代。
  4. 分代收集 (Generational GC)
    • 新生代(对象朝生夕死,频繁回收,用复制算法)
    • 老年代(对象存活率高,用标记-整理)

4. 垃圾收集器

你可以提到一些:

  • Serial GC:单线程,适合小应用。
  • Parallel GC:多线程,吞吐量优先。
  • CMS (Concurrent Mark-Sweep):低停顿,但有碎片。
  • G1 (Garbage First, JDK9 默认):面向服务端,分区管理,既保证低停顿又有较高吞吐量。

6. 结尾总结

“总体来说,我理解 GC 的核心是 分代收集 + 可达性分析,不同的垃圾收集器在吞吐量和低延迟之间做权衡。实际工作中,我更关注如何通过选择合适的 GC 和参数配置来减少停顿,提升系统稳定性。”

美团 一面

什么是内核态和用户态?

1. 基本概念

  • 用户态(User Mode):程序正常运行时所在的状态,权限有限,只能访问受限的内存空间,不能直接操作硬件。
  • 内核态(Kernel Mode):操作系统内核运行的状态,拥有最高权限,可以执行所有指令,访问所有硬件资源。

2. 运行机制

程序在用户态运行,如果要做特权操作(如读写磁盘、网络、分配内存),需要通过 系统调用 切换到内核态,由内核代为执行。

常见的切换场景:

  • 系统调用(如 read/write 文件)
  • 硬件中断(如键盘、网卡中断)
  • 异常(如缺页中断、除零错误)

Java 线程在哪个态?

  • 大多数时候:Java 线程运行在 用户态
    因为执行的只是字节码指令 / JIT 编译后的机器码,属于应用程序逻辑。
  • 需要操作系统/硬件资源时:通过 系统调用 进入 内核态
    比如:
    • sleep() → 内核挂起线程
    • I/O 操作(磁盘、网络)
    • 线程调度 / 上下文切换
    • 内存页缺页时

所以,Java 线程既会在用户态运行,也会在内核态运行,两者之间会频繁切换。

什么是 JIT,优化方法有哪些?

  • JIT (Just-In-Time Compiler):即时编译器,是 JVM 的一部分。
  • 运行时会把热点方法的 字节码 编译成本地机器码,直接在 CPU 上执行,比解释执行快很多。
  • JVM 执行模式:
    1. 解释执行:逐条将字节码翻译成机器码执行,启动快但运行慢。
    2. JIT 编译:对热点代码做优化并缓存机器码,下次直接运行机器码,性能接近甚至超过 C/C++。

2. JIT 的优化手段

(1)方法内联(Method Inlining)
  • 把被频繁调用的小方法直接展开到调用处,减少方法调用开销。
  • 比如 getter/setter 内联后就像直接访问字段一样。
(2)逃逸分析(Escape Analysis)
  • 判断对象是否会被方法外部引用:
    • 不会逃逸 → 可以在栈上分配,甚至直接标量替换(把对象拆成基本类型)。
    • 结果:减少 GC 压力,提升性能。
(3)锁优化
  • 基于逃逸分析,如果对象锁不会被多线程共享:
    • 锁消除:去掉无意义的 synchronized
    • 锁粗化:把连续的加锁/解锁合并成一个大锁,减少切换。
    • 偏向锁 / 轻量级锁:降低加锁的开销。
(4)循环优化
  • 循环展开:把循环体展开,减少分支跳转。
  • 循环无关代码外提:把不随循环变化的计算提到循环外。
(5)动态分派优化
  • Java 多态通常需要虚方法表查找。
  • JIT 可通过 内联缓存(Inline Cache)类型预测,将虚调用优化为直接调用。
(6)死代码消除(Dead Code Elimination)
  • 移除永远不会执行或无意义的代码,提高运行效率。
(7)分支预测 & 常量折叠
  • 常量折叠:编译时直接算出常量表达式。
  • 分支预测:根据运行时统计,优化常走的分支路径。

mysql聚簇索引/非聚簇索引,联合索引,索引下推

对于联合索引abc,如果我用cba去查,优化器会给我优化为 a b c走索引吗?

不会自动优化成 a b c 走索引

联合索引 (a,b,c) 在 B+Tree 中的排序方式是:先按 a 排序,a 相同时再按 b,再按 c

如果你写查询条件 WHERE c=? AND b=? AND a=?,虽然逻辑上和 a=? AND b=? AND c=? 等价,但优化器并不会“自动重排”成符合索引的顺序。

乐观锁悲观锁是什么?

  • 悲观锁:假设冲突一定会发生,每次访问都要“锁住”,常见实现是数据库的 select ... for update,或者 Java 中的 synchronized。优点是安全,缺点是并发性能低。
  • 乐观锁:假设冲突少,不加锁,提交更新时用 版本号机制 / CAS 检查是否被修改过。如果冲突,再重试。Java 的 AtomicInteger 就是基于 CAS 的乐观锁。

Java中有几种锁?

从不同维度可以分很多类:

  • 按实现方式
    • synchronized(JVM 层面,基于对象头 + Monitor)
    • ReentrantLock(JUC,基于 AQS)
  • 按锁的粒度
    • 对象锁、类锁
  • 按获取公平性
    • 公平锁 vs 非公平锁
  • 按是否可重入
    • 可重入锁 vs 不可重入锁
  • 按共享方式
    • 互斥锁(exclusive lock)
    • 读写锁(shared lock, ReentrantReadWriteLock
  • 按使用方式
    • 自旋锁、偏向锁、轻量级锁、重量级锁(这些是 JVM 对 synchronized 的优化阶段)

你说可重入锁,有几种?

JVM 级别synchronized 本身就是可重入锁。

JUC 包ReentrantLock 也是可重入锁,还提供公平/非公平选择。

总结:常见的可重入锁有 两种

  1. synchronized
  2. ReentrantLock

公平锁和非公平锁分别会造成什么后果?

公平锁(Fair Lock)

  • 定义:严格按照线程请求锁的时间顺序(FIFO)来分配。
  • 优点
    • 公平性好,避免线程饥饿(Starvation)。
    • 每个线程都有机会获得锁。
  • 缺点
    • 需要维护等待队列,增加调度和上下文切换开销。
    • 吞吐量降低,整体性能比非公平锁差。

非公平锁(Non-Fair Lock)

  • 定义:新来的线程可以“插队”,直接尝试抢锁,如果抢到就跳过等待队列。
  • 优点
    • 吞吐量高,性能更好。
    • 减少线程挂起和唤醒的次数。
  • 缺点
    • 可能导致某些线程一直排不到锁 → 线程饥饿
    • 公平性差。

美团二面

项目拷打

  • 每天的订单量
  • 底层pg数据库如何优化?

AI了解情况

手撕

实现一个大转盘抽奖的功能。核心要素如下:
1.维护一个奖池,奖池中有n种奖品,例如特等奖、一等奖、二等奖、三等奖等
2.每个奖品有奖品名称、库存个数、中奖概率
3.当用户参与抽奖时,按照中奖概率随机地给用户进行抽奖,抽中后的奖品库存进行扣减。
4.要求奖品不能超发,若某个奖品已耗尽,则未耗尽奖品按照奖品配置概率重新计算比例进行抽奖。例如特等奖10%、一等奖20%、二等奖30%、三等奖40%,若特等奖已抽完,则一等奖的中奖概率为20%/(20%+30%+40%)
要求:
1.设计一套可以表述上述业务关系的实体关系,写出详细的数据表建表语句;
2.写出核心的抽奖算法功能的逻辑,代码要能体现面向对象的思想。
3.写出功能中涉及的SQL语句,如用户中奖记录插入、奖品库存扣减等。

应该具备面向对象的思维,先建立类一、数据库表设计

这里需要两个核心表:奖品表、抽奖记录表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 奖品表
CREATE TABLE prize (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL, -- 奖品名称
stock INT NOT NULL, -- 库存数量
probability DECIMAL(5,2) NOT NULL -- 配置的中奖概率(百分比,比如 10.00 表示10%)
);

-- 抽奖记录表
CREATE TABLE lottery_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL, -- 用户ID
prize_id BIGINT NULL, -- 抽中的奖品(可能为空,表示未中奖)
prize_name VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

二、核心抽奖逻辑(Java/Kotlin风格伪代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Prize {
Long id;
String name;
int stock;
double probability; // 配置概率
}

class LotteryService {
// 抽奖逻辑
Prize draw(List<Prize> prizePool) {
// 过滤掉库存为0的奖品
List<Prize> available = prizePool.stream()
.filter(p -> p.stock > 0)
.collect(Collectors.toList());

// 计算剩余奖品的总概率
double totalProb = available.stream()
.mapToDouble(p -> p.probability)
.sum();

// 归一化概率
double rand = Math.random() * totalProb;
double curr = 0;
for (Prize p : available) {
curr += p.probability;
if (rand <= curr) {
// 命中
p.stock--; // 内存中减库存,持久化时执行 UPDATE
return p;
}
}
return null; // 理论上不会到这里
}
}

说明:

  • 当某奖品库存为 0 时,自动剔除出候选集,然后重新按比例分配概率。
  • 这样实现了“奖品用完后,其他奖品概率重新按比例放大”的规则。

三、功能相关 SQL 示例

  1. 插入抽奖记录
1
2
INSERT INTO lottery_record (user_id, prize_id, prize_name)
VALUES (?, ?, ?);
  1. 扣减奖品库存(保证库存不为负数)
1
2
3
UPDATE prize
SET stock = stock - 1
WHERE id = ? AND stock > 0;
  1. 查询当前奖池
1
2
SELECT id, name, stock, probability
FROM prize;

阿里云一面

  1. 项目拷打(接近45分钟)

reenterant lock是cas+队列实现的,那队列头释放锁了,怎么通知别人?

简短说法:LockSupport.unpark() 唤醒队首的等待线程。AQS(ReentrantLock 的底座)在释放锁时,会找到队列中“头结点的后继”并 unpark 它;被唤醒的线程再去 CAS 争抢锁。下面把 acquire/release 两条路径串起来看就清楚了。

获取失败 → 入队并“订阅唤醒”

  1. 线程尝试 tryAcquire()(内部是 CAS 改 state),失败后调用 acquireQueued(addWaiter(EXCLUSIVE), arg)
  2. addWaiter 把当前线程包装成 NodeCLH FIFO 队列尾部,并确保有一个哑元 head
  3. 在自旋里做两件事:
    • 前驱就是 head,再尝试 tryAcquire()(公平锁会先看 hasQueuedPredecessors())。
    • 否则把前驱的 waitStatus 设为 SIGNAL(-1),表示“前驱释放时请唤醒我”,然后 LockSupport.park(this) 挂起自己。

关键点:不是“我记得你”,而是“我让前驱记得我”(把前驱标成 SIGNAL)。这样前驱释放时知道要叫醒后继。

释放成功 → 唤醒后继

  1. 持有锁的线程在 unlock() 中调用 sync.release(arg)tryRelease(arg)
    • 递减重入计数;降到 0 时清空 exclusiveOwnerThread 并返回 true
  2. release() 见到 true 后调用 unparkSuccessor(head)
    • head.waitStatus < 0 先清 0;
    • 找到 head.next(如果为空或已取消,就从尾往前找第一个 waitStatus <= 0 的有效节点);
    • 对该节点的 thread 调用 LockSupport.unpark(thread)

unpark 的线程从 park() 返回,继续自旋:若其前驱已是新的 headtryAcquire() 成功,就把自己设为新的 head(旧 head.next=null 以便 GC),否则再次 park() 等下一次唤醒。

为什么不是“广播”而是“单播”

AQS 保持严格的 FIFO/顺序性:只唤醒一个后继,避免惊群。其他节点依赖链式唤醒(各自的前驱在释放时再唤醒它们)。

公平/非公平的差别

  • 非公平:被唤醒的队首还没成功前,新来的线程也能 CAS 抢到锁(“插队”),吞吐更高。
  • 公平tryAcquire 会检查 hasQueuedPredecessors(),有前驱就不抢,保证先来先服务,但吞吐略降。

条件队列(Condition)的补充

await() 把线程放进 条件队列,释放锁并 park()signal() 把条件队列的首节点转移到同步队列尾部,再由同步队列的释放流程 unparkSuccessor 去唤醒/竞争锁。也就是说,signal 不是直接获得锁,而是“搬运 + 参与排队”。

  1. Gc,g1 vs go gc
-------------本文结束,感谢您的阅读-------------