【Java面试】——Java 基础与并发编程
以下是针对你整理的 Java 面试复习路线的详细补充,按原大纲结构逐一展开。由于内容量较大,本篇先完成第一部分Java 基础/JVM/JUC的完整补充。
一、Java 基础与并发编程(核心能力 20%)
1.1 JVM 内存模型
JVM 在执行 Java 程序时,将其管理的内存划分为5 个核心运行时数据区:
1.1.1 线程私有区域
(1)程序计数器(Program Counter Register)
- 记录当前线程正在执行的字节码指令地址(行号)
- 字节码解释器通过改变计数器值来选取下一条要执行的指令
- 是实现线程上下文切换的关键——线程切换后能恢复到正确的执行位置
- 唯一不会抛出 OOM 异常的内存区域
(2)Java 虚拟机栈(Java Virtual Machine Stack)
- 为 Java 方法执行提供内存空间,每个方法执行时创建一个栈帧(Stack Frame)
- 栈帧包含:局部变量表、操作数栈、动态链接、方法返回地址
- 可通过
-Xss参数设置栈大小(如-Xss1m) - OOM 场景:
StackOverflowError:线程请求栈深度超过允许最大深度(如无限递归)OutOfMemoryError: unable to create native thread:无法为新线程分配栈内存
(3)本地方法栈(Native Method Stack)
- 为 Native 方法执行提供服务,作用与虚拟机栈类似
1.1.2 线程共享区域
(4)堆内存(Heap)—— 垃圾回收的主要区域
采用分代模型,基于弱分代假说:
- 绝大多数对象朝生夕灭(90%+ 对象存活时间极短)
- 熬过越多次 GC 的对象越难被回收
| 区域 | 占比 | 核心职责 |
|---|---|---|
| Eden 区 | 80% | 新对象主要分配区域 |
| Survivor 0 区 | 10% | 存活对象"中转站" |
| Survivor 1 区 | 10% | 存活对象"中转站" |
| 老年代 | 堆的 2/3 | 存储生命周期长的对象 |
关键参数:
-XX:NewRatio:年轻代:老年代比例,默认 1:2-XX:SurvivorRatio:Eden:Survivor 比例,默认 8:1:1
(5)元空间(Metaspace)—— JDK 8+ 替代永久代
- 直接使用本地内存(Native Memory)而非 JVM 堆内存
- 永久代被移除的原因:字符串常量池内存泄漏、方法区大小难以调优、HotSpot 与 JRockit 合并
1.1.3 堆内存工作机制
Young GC(Minor GC)执行流程:
- 新对象优先分配到 Eden 区
- Eden 区满时触发 Minor GC
- 标记 Eden 区和当前 From Survivor 区的存活对象
- 将存活对象复制到 To Survivor 区
- 清空 Eden 区和 From Survivor 区
- 交换 From 和 To Survivor 的角色(始终有一个 Survivor 区是空的)
对象晋升老年代的 4 种途径:
- 正常晋升:年龄达到
MaxTenuringThreshold(默认 15) - 动态年龄判定:Survivor 区中同龄对象总大小 > Survivor 一半时提前晋升
- 大对象直接进入:超过
PretenureSizeThreshold设置值 - 空间分配担保失败:Minor GC 后 Survivor 区无法容纳所有存活对象
为什么 Young GC 很快?
- 绝大多数对象在 Eden 区创建后快速死亡
- 仅扫描 Eden 和单个 Survivor,不扫描老年代
- 复制算法仅处理少量存活对象,效率高
1.1.4 常见 OOM 原因
| OOM 类型 | 原因 | 排查方向 |
|---|---|---|
| 堆 OOM | 对象过多/内存泄漏 | jmap dump + MAT 分析 |
| 元空间 OOM | 类加载过多(动态代理、热部署) | 检查类加载器泄漏 |
| 栈 OOM | 线程过多 | 检查线程池使用 |
| 直接内存 OOM | NIO 使用不当 | 检查 ByteBuffer 分配 |
1.1.5 Full GC 触发条件
- 老年代空间不足
- 元空间空间不足
- 调用
System.gc()(建议但非强制) - 晋升失败(Minor GC 后存活对象无法放入老年代)
- 大对象直接进入老年代导致空间不足
1.1.6 CMS Concurrent Mode Failure
- 原因:老年代在 CMS 并发清理阶段被填满,无法等到下一次 GC
- 本质:并发收集速度跟不上对象晋升速度
- 解决方案:
- 调大老年代大小或调小年轻代
- 提高 CMS 触发阈值(
-XX:CMSInitiatingOccupancyFraction) - 使用 G1 替代 CMS
1.1.7 G1 为什么能减少 STW
- Region 化内存布局:将堆划分为多个大小相等的 Region,不再有固定的年轻代/老年代物理边界
- 并发标记 + 增量回收:通过并发标记阶段和增量回收,避免全堆扫描
- 可预测的停顿时间模型:用户可设定目标停顿时间,G1 根据历史数据智能选择回收 Region
- 混合 GC:同时回收年轻代和部分老年代 Region,均衡回收效率
1.1.8 ZGC 为什么可以做到几毫秒
- 染色指针(Colored Pointers):在指针中编码 GC 元数据,无需额外的标记位
- 读屏障(Load Barrier):在对象访问时进行 GC 操作,实现并发移动
- 并发整理:所有阶段(标记、整理、重定位)几乎都并发执行
- Region 化:动态 Region 大小,支持 2MB 到 32TB 堆
- NUMA 感知:优化多 CPU 架构下的内存访问
1.1.9 线上分析工具
| 工具 | 用途 |
|---|---|
jmap | 生成堆转储(dump)、查看堆信息 |
jstack | 查看线程堆栈、定位死锁 |
jstat | 实时查看 GC 状态、类加载信息 |
jcmd | 综合诊断命令(JDK 7+) |
jconsole | 可视化监控 |
jvisualvm | 性能分析、堆 dump 分析 |
| Arthas | 阿里开源在线诊断工具,可在线查看方法调用、热更新 |
| MAT | Eclipse 堆转储分析工具,内存泄漏分析 |
| GC 日志 | -Xloggc:开启,分析 GC 频率和停顿 |
1.2 Java 并发编程
1.2.1 AQS(AbstractQueuedSynchronizer)
核心思想:CLH 双向队列 + CAS 改 state
state:同步状态(int),通过 CAS 修改- CLH 队列:FIFO 双向链表,管理等待线程
- 独占模式:只有一个线程能获取(如 ReentrantLock)
- 共享模式:多个线程可同时获取(如 Semaphore、CountDownLatch)
基于 AQS 实现的同步器:
- ReentrantLock
- Semaphore
- CountDownLatch
- CyclicBarrier
- ReentrantReadWriteLock
- FutureTask
1.2.2 ReentrantLock
核心特性:
- 可重入锁:
state表示重入次数(0 表示未被占用) - 支持公平锁(
new ReentrantLock(true))和非公平锁(默认) - 公平锁:按队列顺序获取;非公平锁:允许插队(性能更好)
与 synchronized 对比:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置(C++) | JDK 层面(Java + AQS) |
| 释放方式 | 自动释放 | 手动unlock() |
| 中断支持 | 不可中断 | lockInterruptibly()可中断 |
| 超时获取 | 不支持 | tryLock(timeout)支持 |
| 条件变量 | wait/notify | Condition多条件 |
| 性能 | 相近 | 相近 |
1.2.3 CAS(Compare And Swap)
核心原理:
- 硬件级别原子操作,通过 CPU 指令
CMPXCHG实现 - 三个参数:内存地址 V、期望值 A、新值 B
- 当 V 值 == A 时,将 V 更新为 B;否则失败
ABA 问题:
- 现象:V 值从 A → B → A,CAS 误认为未变化
- 解决方案:
AtomicStampedReference:带版本号(stamp)的原子引用AtomicMarkableReference:带标记位(mark)的原子引用
Unsafe 类:
- 提供底层 CAS 操作(
compareAndSwapInt、compareAndSwapObject等) - 可直接操作内存(
allocateMemory、putInt等) - 禁止直接使用(JDK 9+ 限制访问)
1.2.4 Volatile
两个语义:
- 可见性:写操作立即刷新到主内存,读操作从主内存读取
- 禁止指令重排序:内存屏障(LoadLoad、StoreStore、LoadStore、StoreLoad)
适用场景:
- 状态标志位(
volatile boolean running) - 单次读/写操作(非复合操作,如
count++不适用)
1.2.5 Atomic 包
| 类 | 说明 |
|---|---|
AtomicInteger | 原子 int |
AtomicLong | 原子 long |
AtomicBoolean | 原子 boolean |
AtomicReference | 原子引用 |
AtomicIntegerArray | 原子 int 数组 |
AtomicStampedReference | 带版本号引用(解决 ABA) |
AtomicMarkableReference | 带标记引用 |
1.2.6 LongAdder vs AtomicLong
- AtomicLong:CAS 自旋,高并发下冲突严重,性能下降
- LongAdder:分段累加(Cell 数组),各线程操作不同 Cell,最后 sum 汇总
- 适用场景:LongAdder 适合写多读少,AtomicLong 适合读多写少
1.2.7 ThreadPoolExecutor
核心参数:
corePoolSize:核心线程数maximumPoolSize:最大线程数keepAliveTime:空闲线程存活时间unit:时间单位workQueue:任务队列(ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue)threadFactory:线程工厂handler:拒绝策略
拒绝策略:
AbortPolicy(默认):抛出 RejectedExecutionExceptionCallerRunsPolicy:调用者线程执行DiscardPolicy:丢弃不抛异常DiscardOldestPolicy:丢弃队列头部任务
线程池满的原因与排查:
- 核心线程数过小
- 任务队列过小或过大
- 任务执行时间过长
- 线程阻塞(IO、锁等待)
1.2.8 ForkJoinPool
- 分治思想:大任务拆分为小任务,并行执行后合并结果
- 工作窃取(Work-Stealing):空闲线程从其他队列尾部窃取任务
- 适用场景:递归任务、大数组并行计算
1.2.9 CompletableFuture
核心方法:
supplyAsync/runAsync:异步执行thenApply/thenAccept/thenRun:串行依赖thenCombine/thenCompose:组合依赖allOf/anyOf:多任务聚合complete/completeExceptionally:手动完成
异步编排最佳实践:
- 使用自定义线程池,避免共用
ForkJoinPool.commonPool() - 合理设置超时(
completeOnTimeout/orTimeout)
1.2.10 StampedLock
- 优化 ReentrantReadWriteLock:乐观读锁不阻塞写操作
- 三种锁模式:
writeLock:写锁readLock:读锁(悲观)tryOptimisticRead:乐观读(不阻塞写,需验证 stamp)
1.2.11 CountDownLatch vs CyclicBarrier vs Semaphore vs Phaser
| 同步器 | 核心机制 | 可重用 | 典型场景 |
|---|---|---|---|
CountDownLatch | 计数器递减至 0 释放 | ❌ | 等待多个任务完成 |
CyclicBarrier | 等待所有线程到达屏障点 | ✅ | 多线程分段执行 |
Semaphore | 许可证控制并发数 | ✅ | 限流、资源池 |
Phaser | 动态注册/注销参与者 | ✅ | 多阶段并行任务 |
1.3 JUC 源码
1.3.1 ConcurrentHashMap(JDK 1.8+)
为什么 1.8 不用 Segment?
- Segment 继承 ReentrantLock,锁粒度粗(一个 Segment 锁一个 Hash 桶数组)
- 1.8 改用synchronized + CAS:锁粒度细化到单个桶(Node 头节点)
- 更细粒度 → 更高并发度
核心数据结构:
// 核心属性transientvolatileNode<K,V>[]table;privatetransientvolatileNode<K,V>[]nextTable;privatetransientvolatileintsizeCtl;privatetransientvolatileinttransferIndex;为什么 TreeBin?
- 当链表长度 > 8 时,链表转为红黑树(
TreeBin包装 TreeNode) - 红黑树查询复杂度 O(log n),避免长链表导致查询慢
- 树化阈值 8,退化阈值 6(防止频繁转换)
为什么 ForwardNode?
- 扩容时,已迁移的桶位置放置
ForwardingNode - 指向
nextTable,用于扩容期间转发读写请求 - 写操作遇到 ForwardNode:协助扩容(
helpTransfer)
为什么 sizeCtl?
- 多状态控制变量:
-1:正在初始化-(1 + nThreads):正在扩容0:默认值> 0:下次扩容阈值(容量 * 0.75)
为什么 helpTransfer?
- 扩容期间,写线程检测到 ForwardNode 时主动参与扩容
- 提升扩容速度,减少阻塞等待
- 体现“所有线程都是 Worker”的设计思想
1.3.2 其他 JUC 源码要点
- CopyOnWriteArrayList:写时复制,适合读多写少
- BlockingQueue 实现:
ArrayBlockingQueue(有界数组)、LinkedBlockingQueue(有界链表)、SynchronousQueue(无存储)、PriorityBlockingQueue(优先级)、DelayQueue(延迟) - ConcurrentLinkedQueue:无界非阻塞队列,CAS 实现
1.4 线上排查经验
1.4.1 CPU 100% 排查流程
top -H -p <pid>查看进程内各线程 CPU 占用printf "%x\n" <tid>将线程 ID 转为十六进制jstack <pid> | grep -A 20 <tid_hex>查看线程堆栈- 定位热点代码(死循环、频繁 GC、锁竞争、正则匹配)
- Arthas
thread命令一键分析 CPU 高的线程
1.4.2 Full GC 频繁排查
jstat -gcutil <pid> 1000查看 GC 频率jmap -heap <pid>查看各代内存使用jmap -dump:live,format=b,file=heap.hprof <pid>导出堆快照- MAT分析大对象、内存泄漏
- 常见原因:内存泄漏、大对象频繁进入老年代、元空间膨胀
1.4.3 OOM 排查流程
- 分析事故现场(CPU、内存、日志)
- 通过
top -p <pid>分析进程资源占用 - 判断内存增长类型(爆炸性增长 vs 缓慢增长)
jmap -dump导出堆快照- MAT 分析:Leak Suspects报告、Dominator Tree、GC Roots路径
1.4.4 死锁排查
jstack <pid>自动检测死锁(会打印 “Found one Java-level deadlock”)- 查看线程状态(BLOCKED、WAITING)
- 分析锁持有和等待关系
本篇内容覆盖了原大纲第一、二、三部分的 Java 基础、JVM 和 JUC 相关知识点。后续将继续补充 Spring 体系、数据库、Redis、MQ、微服务、分布式、高并发、架构能力、云原生、制造业、跨境电商、代码能力、线上排查等剩余部分。
