Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Java 与 JVM 基础

即便项目用 Kotlin,面试官仍常问 Java/JVM——因为字节码、并发、GC 是共通的底层。这一篇覆盖高频考点。

一、集合框架

HashMap(必考)

  • 结构:数组 + 链表 + 红黑树(JDK8)。链表长度 ≥ 8 且数组长度 ≥ 64 转红黑树;退化阈值 6。
  • 初始容量 16,负载因子 0.75,扩容翻倍(2 倍)。容量始终是 2 的幂,便于用 (n-1) & hash 取代取模。
  • 扰动函数:hash = h ^ (h >>> 16),让高位参与运算,减少碰撞。
  • JDK8 头插改尾插:JDK7 头插法在并发扩容时会形成环形链表导致死循环;JDK8 改尾插缓解,但 HashMap 仍非线程安全
  • 并发问题:多线程 put 可能丢数据、扩容期间读到 null。

ConcurrentHashMap

  • JDK7:分段锁 Segment;JDK8:CAS + synchronized 锁单个桶头节点,粒度更细,并发度更高。
  • size() 用 baseCount + CounterCell 分散统计。

其他

  • ArrayList(动态数组,扩容 1.5 倍)vs LinkedList(双向链表)。
  • HashSet 底层是 HashMap;LinkedHashMap 维护插入/访问顺序(可做 LRU)。

二、并发

synchronized

  • 对象头 Mark Word 记录锁状态、GC 年龄、hashCode 或指向锁记录/monitor 的指针。synchronized 进入临界区时会围绕 Mark Word 做 CAS 或 monitor 竞争,所以面试讲锁升级不能只背流程,要能说清“对象头里记录了什么“。
  • 无锁:对象未被线程持有;第一次进入同步块时,运行时会尝试在 Mark Word 中记录当前锁形态或把对象头复制到线程栈上的 Lock Record。
  • 偏向锁:面向“同一线程反复进入同一把锁“的场景,Mark Word 记录偏向线程 ID,后续同线程进入几乎不需要 CAS。出现其他线程竞争、调用 hashCode() 等需要占用 Mark Word 的信息时,可能触发偏向撤销或重偏向。版本边界要说清:JDK 15 起偏向锁被废弃并默认关闭,JDK 18 之后 HotSpot 已移除相关实现,新版本面试更应把它当历史优化理解。
  • 轻量级锁:多线程交替进入但竞争不激烈时,线程在栈帧创建 Lock Record,用 CAS 把对象 Mark Word 指向该记录;失败后可能自旋,希望持锁线程很快退出,避免立即阻塞到内核态。
  • 重量级锁:竞争持续、线程自旋失败或需要阻塞/唤醒时,锁膨胀为 ObjectMonitor,未抢到锁的线程进入阻塞队列,由操作系统互斥量参与调度,吞吐更稳定但上下文切换成本更高。
  • 升级边界:常见路径可概括为无锁 → 偏向锁 → 轻量级锁 → 重量级锁,但这主要描述 HotSpot 早期到 JDK 14 默认配置下的优化路径;锁可以膨胀,退出同步块后不等于立刻恢复到最轻状态,具体是否偏向、是否自旋和阈值会受 JDK 版本与 JVM 参数影响。
  • 修饰实例方法锁 this,静态方法锁 Class,代码块锁指定对象。

volatile

  • 保证可见性(写立即刷主存,读从主存)和有序性(禁止指令重排,插入内存屏障)。
  • 不保证原子性(如 i++ 仍不安全)。
  • 经典用途:双重检查锁单例的实例字段。

CAS 与 AQS

  • CAS(Compare-And-Swap):无锁原子操作,compareAndSwap(内存值, 期望值, 新值),失败自旋重试。底层是 CPU 指令。
  • ABA 问题:值从 A→B→A,CAS 误判没变。用版本号(AtomicStampedReference)解决。
  • AQS(AbstractQueuedSynchronizer):用 volatile int state + CLH 队列实现的同步框架,ReentrantLock、CountDownLatch、Semaphore 都基于它。

线程池 ThreadPoolExecutor

七个参数:corePoolSize(核心线程)、maximumPoolSize(最大)、keepAliveTimeunitworkQueue(任务队列)、threadFactoryhandler(拒绝策略)。

执行流程:核心线程未满 → 创建核心线程;满了 → 入队;队列满 → 创建非核心线程到 max;再满 → 触发拒绝策略(AbortPolicy 抛异常 / CallerRunsPolicy 调用者执行 / DiscardPolicy 丢弃 / DiscardOldestPolicy 丢最老)。

三、JVM 内存与 GC

内存区域

  • 线程私有:程序计数器、虚拟机栈、本地方法栈。
  • 线程共享:堆(对象实例,GC 主战场)、方法区/元空间(类信息,JDK8 后用本地内存)。
  • 注意 Android 用 ART/Dalvik 而非标准 JVM,但内存模型概念相通。

GC

  • 判活:引用计数(有循环引用问题)vs 可达性分析(GC Roots,主流)。
  • GC Roots:虚拟机栈引用、静态变量、常量、JNI 引用等。
  • 回收算法:标记-清除(碎片)、复制(浪费空间,适合新生代)、标记-整理(适合老年代)。
  • HotSpot 分代模型:服务端 JVM 常按新生代/老年代理解,Eden + Survivor、Minor GC、Major/Full GC 是典型面试语言;但比例(如 8:1:1)和具体算法不是 Java 语言规范,会受 JVM 版本、收集器和参数影响。
  • HotSpot 回收器边界:Serial 适合小堆/单核或客户端场景;Parallel 关注吞吐;CMS 关注低停顿但有碎片和浮动垃圾问题,已在 JDK 9 标记废弃、JDK 14 移除;G1 将堆划分为 Region,兼顾可预测停顿;ZGC/Shenandoah 面向大堆低停顿。回答时应说“这些是 HotSpot 收集器“,不要直接套到 Android。
  • Android ART 区分:Android App 运行在 ART(早期为 Dalvik)上,不是标准 HotSpot Server VM。ART 也有堆、线程栈、JNI 引用、可达性分析和并发/分代等 GC 思路;Android Runtime 文档里的 CMS/CC 是 ART 自己的 GC plan,不等同于 HotSpot CMS/G1/ZGC,普通应用也不是通过 HotSpot collector 参数来选择它们。Android 面试更关注内存泄漏、对象分配抖动、Bitmap/native 内存、GC pause 对掉帧的影响。
  • 安全表述:讲 JVM 基础时可用 HotSpot 解释 Serial/Parallel/G1/ZGC;讲 Android 性能时应切到 ART 语境,用“ART 的具体 GC 策略随 Android 版本和设备实现演进“这类条件措辞,避免把服务端 JVM 参数经验当作移动端结论。

类加载

  • 过程:加载 → 验证 → 准备 → 解析 → 初始化。
  • 双亲委派:类加载请求先委托父加载器,父无法加载才自己加载。保证核心类不被篡改(如自定义 String 不会覆盖系统的)。
  • 打破双亲委派:SPI、热修复、插件化、Tomcat 的 WebappClassLoader。

高频面试题

Q1:HashMap 为什么线程不安全?ConcurrentHashMap 怎么优化? HashMap 并发 put 会丢数据,JDK7 扩容还会成环死循环。ConcurrentHashMap JDK8 用 CAS + synchronized 锁桶头,只锁单个桶,并发度高。

Q2:volatile 能保证原子性吗? 不能。只保证可见性和有序性。i++ 是读-改-写三步,volatile 不能保证复合操作原子,需用 Atomic 类或锁。

Q3:synchronized 锁升级过程? 先讲对象头 Mark Word:它保存锁标志位、GC 年龄、hashCode 或指向 Lock Record/ObjectMonitor 的指针。典型 HotSpot 路径是无锁 → 偏向锁(同一线程反复进入,Mark Word 记录线程 ID)→ 轻量级锁(栈上 Lock Record + CAS,少量竞争时自旋)→ 重量级锁(ObjectMonitor,竞争线程阻塞/唤醒)。边界是:偏向锁在 JDK 15 起废弃并默认关闭,JDK 18 后 HotSpot 移除;新版本里不要把偏向锁当成必经阶段。锁膨胀后通常不会在退出同步块时立刻退回最轻状态,具体策略受 JVM 版本和参数影响。

Q4:线程池核心线程会被回收吗? 默认不会。设置 allowCoreThreadTimeOut(true) 后核心线程空闲超时也回收。

Q5:为什么用线程池?核心线程数怎么定? 复用线程降低创建销毁开销、控制并发数、统一管理。CPU 密集型设为核数+1;IO 密集型设为核数×2 或更高(经验值,需压测)。

Q6:双亲委派的作用?为什么 Android 热修复要打破它? 保证类的唯一性和安全性。热修复需要让补丁类优先于原类被加载,所以要把补丁 dex 插到 classloader 的 dexElements 数组前面,本质是绕过/利用加载顺序。