面经整理
钉钉-一面
项目拷打,遇到了哪些难点,是否遇到过上线后的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 算法
常见算法:
- 标记-清除 (Mark-Sweep)
- 标记存活对象,清除未标记的对象 → 会产生内存碎片。
- 复制算法 (Copying)
- 将存活对象复制到另一块内存 → 没有碎片,常用于新生代。
- 标记-整理 (Mark-Compact)
- 存活对象往一端移动,清理边界外 → 避免碎片,常用于老年代。
- 分代收集 (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 执行模式:
- 解释执行:逐条将字节码翻译成机器码执行,启动快但运行慢。
- 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
也是可重入锁,还提供公平/非公平选择。
总结:常见的可重入锁有 两种:
synchronized
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 | -- 奖品表 |
二、核心抽奖逻辑(Java/Kotlin风格伪代码)
1 | class Prize { |
说明:
- 当某奖品库存为 0 时,自动剔除出候选集,然后重新按比例分配概率。
- 这样实现了“奖品用完后,其他奖品概率重新按比例放大”的规则。
三、功能相关 SQL 示例
- 插入抽奖记录
1 | INSERT INTO lottery_record (user_id, prize_id, prize_name) |
- 扣减奖品库存(保证库存不为负数)
1 | UPDATE prize |
- 查询当前奖池
1 | SELECT id, name, stock, probability |
阿里云一面
- 项目拷打(接近45分钟)
reenterant lock是cas+队列实现的,那队列头释放锁了,怎么通知别人?
简短说法:靠 LockSupport.unpark()
唤醒队首的等待线程。AQS(ReentrantLock 的底座)在释放锁时,会找到队列中“头结点的后继”并 unpark
它;被唤醒的线程再去 CAS 争抢锁。下面把 acquire/release 两条路径串起来看就清楚了。
获取失败 → 入队并“订阅唤醒”
- 线程尝试
tryAcquire()
(内部是 CAS 改state
),失败后调用acquireQueued(addWaiter(EXCLUSIVE), arg)
。 addWaiter
把当前线程包装成Node
入 CLH FIFO 队列尾部,并确保有一个哑元head
。- 在自旋里做两件事:
- 若 前驱就是 head,再尝试
tryAcquire()
(公平锁会先看hasQueuedPredecessors()
)。 - 否则把前驱的
waitStatus
设为SIGNAL(-1)
,表示“前驱释放时请唤醒我”,然后LockSupport.park(this)
挂起自己。
- 若 前驱就是 head,再尝试
关键点:不是“我记得你”,而是“我让前驱记得我”(把前驱标成
SIGNAL
)。这样前驱释放时知道要叫醒后继。
释放成功 → 唤醒后继
- 持有锁的线程在
unlock()
中调用sync.release(arg)
→tryRelease(arg)
:- 递减重入计数;降到 0 时清空
exclusiveOwnerThread
并返回true
。
- 递减重入计数;降到 0 时清空
release()
见到true
后调用unparkSuccessor(head)
:- 若
head.waitStatus < 0
先清 0; - 找到
head.next
(如果为空或已取消,就从尾往前找第一个waitStatus <= 0
的有效节点); - 对该节点的
thread
调用LockSupport.unpark(thread)
。
- 若
被 unpark
的线程从 park()
返回,继续自旋:若其前驱已是新的 head
且 tryAcquire()
成功,就把自己设为新的 head
(旧 head.next=null
以便 GC),否则再次 park()
等下一次唤醒。
为什么不是“广播”而是“单播”
AQS 保持严格的 FIFO/顺序性:只唤醒一个后继,避免惊群。其他节点依赖链式唤醒(各自的前驱在释放时再唤醒它们)。
公平/非公平的差别
- 非公平:被唤醒的队首还没成功前,新来的线程也能 CAS 抢到锁(“插队”),吞吐更高。
- 公平:
tryAcquire
会检查hasQueuedPredecessors()
,有前驱就不抢,保证先来先服务,但吞吐略降。
条件队列(Condition
)的补充
await()
把线程放进 条件队列,释放锁并 park()
;signal()
把条件队列的首节点转移到同步队列尾部,再由同步队列的释放流程 unparkSuccessor
去唤醒/竞争锁。也就是说,signal
不是直接获得锁,而是“搬运 + 参与排队”。
- Gc,g1 vs go gc