欢迎来到 2026年Android中高级面试题专栏 的第三篇:java基础与进阶。
尽管现代 Android 框架层出不穷,甚至 Kotlin 已成为首选,但 Java 基础、JUC 并发编程与 JVM 虚拟机机制 依然是所有 Android 研发人员的根基。在面试中,考察 Java 底层与 JVM 能最直接地检验出候选人的代码基本功、内存把控力以及对多线程并发安全的理解深度。本文将带你一次性攻克集合源码、JUC 锁机制、线程池优化以及 JVM 垃圾回收等高频大厂必考点。
一、 Java 语言基础与面向对象
Q1: HashMap 的底层实现原理是什么?在 JDK 1.8 中有哪些关键演进?
核心回答:
-
数据结构:JDK 1.8 采用了 数组 + 单向链表 + 红黑树 的结构。
-
哈希冲突与树化:
-
当发生 Hash 碰撞时,以链表形式挂载在对应桶(Bucket)下。
-
当单个桶中的链表长度 且 数组总容量 时,链表会转为红黑树,将查找复杂度从 降低到 。若数组容量未达到 64,则优先触发
resize()扩容而非树化。 -
当红黑树节点数退化到 时,重新转回链表。
-
-
扩容机制(
resize) :-
默认初始容量为 16,加载因子(Load Factor)为 0.75。当元素个数超过
capacity * loadFactor时触发 2 倍扩容。 -
JDK 1.8 优化点:重新计算位置时,无需重新计算高阶 Hash 值,只需判断旧 Hash 值在高位新增的
bit是 0 还是 1。若是 0,位置不变;若是 1,新位置 = 原位置 + 旧数组容量。
-
-
高并发死链问题:JDK 1.7 在多线程扩容时由于采用头插法,极易形成环形链表导致死循环;JDK 1.8 改用尾插法,解决了死链问题,但多线程下仍存在数据覆盖的线程不安全问题。
Q2: ConcurrentHashMap 在 JDK 1.7 与 JDK 1.8 中是如何保障线程安全的?
核心回答:
-
JDK 1.7:Segment 分段锁
-
基于
Segment数组 +HashEntry数组结构。Segment继承自ReentrantLock。 -
默认并发度为 16(16 个 Segment)。对不同的 Segment 加锁,不同段之间的写操作互不干扰,但并发度受限于 Segment 数量。
-
-
JDK 1.8:CAS + synchronized(细粒度桶锁)
-
抛弃了
Segment,直接采用 Node 数组 + 链表 + 红黑树 结构。 -
插入节点(Put)原理:
-
计算 Hash 值定位桶位置。若桶为空,通过 CAS 无锁操作尝试插入头节点;
-
若桶不为空(发生 Hash 冲突),使用
synchronized关键字只锁住当前桶的头节点,然后进行链表/红黑树的插入。
-
-
锁粒度极致优化:锁粒度从“段”精细化到了“单个桶头节点”,最大并发度直接等同于数组的长度,极大地提升了高并发读写性能。
-
Q3: 什么是泛型擦除?PECS 原则(Producer Extends, Consumer Super)在实际开发中如何应用?
核心回答:
-
泛型擦除(Type Erasure) :
Java 的泛型是伪泛型,仅存在于编译期。编译成字节码后,所有泛型类型信息都会被擦除,替换为限定类型(未指定则替换为
Object),并在必要时插入强转代码。 -
PECS 原则(生产者用 extends,消费者用 super) :
-
Producer Extends(
? extends T/ 协变) :如果你需要从集合中读取/获取数据(生产者),使用? extends T。此时可以安全读取出T类型的对象,但不能向集合写入任何数据(除null外),因为编译器无法确定具体是哪种子类。 -
Consumer Super(
? super T/ 逆变) :如果你需要向集合中写入/添加数据(消费者),使用? super T。此时可以安全写入T及其子类对象,但读取出来的只能是Object类型的对象。
-
二、 Java 并发编程(JUC 核心专场)
Q1: volatile 关键字的底层原理是什么?它能保证原子性吗?
核心回答:
volatile 是 JVM 提供的轻量级同步机制,主要包含两个特性:
-
保证内存可见性:
当一个线程修改了被
volatile修饰的变量时,JVM 会立即将该线程工作内存(CPU 缓存/寄存器)中的最新值强制刷新到主内存(JMM Main Memory),并使其他线程工作内存中该变量的缓存行失效,确保其他线程读取时必须重新从主内存加载。 -
禁止指令重排序:
编译器和 CPU 为了优化性能会对指令进行重排序。
volatile通过在编译生成的字节码中插入 内存屏障(Memory Barrier / Fence) ,阻止屏障两侧的指令重排序(典型应用:双重检查锁 DCL 单例模式)。
-
原子性问题:
volatile不能保证原子性(例如i++自增操作包含读取、加1、写入三步,volatile无法阻止多线程在中间步骤的并发交错)。保证原子性需使用synchronized、ReentrantLock或AtomicInteger(CAS)。
Q2: 深度剖析 synchronized 锁升级过程(偏向锁 轻量级锁 重量级锁)
核心回答:
在 JVM(HotSpot)中,对象的对象头(Mark Word)根据锁状态存储不同的数据结构。锁只能升级不能降级(除 GC 倾斜外)。
无锁状态 ──(偏向 CAS 成功)──> 偏向锁 ──(出现竞争)──> 轻量级锁 (CAS 自旋) ──(自旋失败/重度竞争)──> 重量级锁 (Monitor)
-
偏向锁(Biased Locking) :
经过统计,大多数情况下锁总是由同一线程多次获得。当线程首次获取锁时,在 Mark Word 中记录该线程 ID。下次该线程再进入该锁时,无需任何同步操作,直接通关,开销极低。
-
轻量级锁(Lightweight Locking) :
当有另一个线程尝试竞争偏向锁时,偏向锁被撤销并升级为轻量级锁。竞争线程在各自的栈帧中创建锁记录(Displaced Mark Word),通过 CAS 自旋 尝试将 Mark Word 指向自己的锁记录。轻量级锁适用于线程交替执行、短时间持锁的场景。
-
重量级锁(Heavyweight Locking) :
当线程 CAS 自旋达到一定次数(或竞争过于剧烈)时,锁升级为重量级锁。 Mark Word 指向底层的 ObjectMonitor 对象。未抢到锁的线程会被阻塞(Park)并放入等待队列,涉及 Linux 用户态与内核态的上下文切换,开销最大。
Q3: 线程池 ThreadPoolExecutor 的 7 大核心参数与任务拒绝对策
核心回答:
1. 7 大核心参数说明:
-
corePoolSize:核心线程数(常驻线程)。 -
maximumPoolSize:最大线程数(核心 + 非核心线程)。 -
keepAliveTime:非核心线程空闲存活时间。 -
unit:存活时间单位。 -
workQueue:阻塞队列(如ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue)。 -
threadFactory:线程创建工厂(建议自定义给线程命名)。 -
handler:拒绝策略(默认AbortPolicy抛异常;CallerRunsPolicy交由调用者线程处理;DiscardPolicy直接丢弃;DiscardOldestPolicy丢弃队列最老任务)。
2. 任务递交与处理流程(关键逻辑) :
提交任务 ──> 核心线程满? ──No──> 创建核心线程执行
│ Yes
▼
队列满? ──No──> 入队等待
│ Yes
▼
最大线程数满? ──No──> 创建非核心线程执行
│ Yes
▼
触发拒绝策略 (RejectedExecutionHandler)
-
Android 避坑:绝不使用
Executors.newCachedThreadPool(),其maximumPoolSize为Integer.MAX_VALUE,极易导致创建成百上千线程卡死 App 甚至引发 OOM。
三、 JVM 虚拟机原理
Q1: 简述 JVM 运行时数据区的划分及各自职责
核心回答:
┌─────────────────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
│ ┌───────────────────────┐ ┌──────────────────────┐ │
│ │ 程序计数器 │ │ Java 虚拟机栈 │ │
│ │ (Program Counter Reg) │ │ (JVM Stack / 栈帧) │ │
│ └───────────────────────┘ └──────────────────────┘ │
│ ┌───────────────────────┐ ┌──────────────────────┐ │
│ │ 本地方法栈 │ │ Java 堆 │ │
│ │ (Native Stack) │ │ (Java Heap) │ │
│ └───────────────────────┘ └──────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 元空间 / 方法区 (Metaspace) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
-
程序计数器(Program Counter Register) :线程私有。记录当前线程正在执行的字节码指令地址(Native 方法时为空)。
-
Java 虚拟机栈(JVM Stack) :线程私有。每个方法执行时创建一个栈帧(Stack Frame) ,存放局部变量表、操作数栈、动态链接、方法出口信息。栈溢出抛出
StackOverflowError。 -
本地方法栈(Native Method Stack) :线程私有。为 JVM 执行 Native(JNI/C/C++)方法服务。
-
Java 堆(Heap) :线程共享。存放所有创建的对象实例与数组,是垃圾回收(GC)的主战场。
-
元空间 / 方法区(Metaspace / Method Area) :线程共享。JDK 1.8 以后使用物理内存的“元空间”替代了传统的永久代(PermGen),用于存放类信息、常量、静态变量、JIT 编译后的代码。
Q2: 垃圾回收机制:GC Roots 包含哪些?三大常用 GC 算法深度对比
核心回答:
1. 可达性分析算法(GC Roots) :
从 GC Roots 对象作为起点向下搜索,不可达的对象即可被判定为可回收。可作为 GC Roots 的对象包括:
-
虚拟机栈(栈帧中的局部变量表)引用的对象;
-
方法区中静态属性(
static)与常量(final)引用的对象; -
本地方法栈(JNI / Native)引用的对象;
-
正在运行的线程对象(Thread)。
2. 三大 GC 垃圾回收算法对比:
| 算法 | 原理 | 优点 | 缺点 | 适用区域 |
|---|---|---|---|---|
| 标记-清除 (Mark-Sweep) | 标记废弃对象,直接回收占用空间 | 速度快,无需移动对象 | 产生大量不连续的内存碎片 | 老年代(配合碎片整理) |
| 复制算法 (Copying) | 将内存分为两块,将存活对象复制到另一块 | 无碎片,按顺序分配内存 | 内存利用率低(浪费 50% 空间) | 新生代 (Eden / Survivor) |
| 标记-整理 (Mark-Compact) | 标记后,将所有存活对象移动到一端,清理边界外内存 | 无内存碎片,利用率 100% | 需要移动对象,Stop-The-World 时间稍长 | 老年代 |
Q3: 深度解析 JVM 类加载机制与双亲委派模型(Parents Delegation Model)
核心回答:
-
类加载的生命周期:
加载 (Loading)验证 (Verification)准备 (Preparation)解析 (Resolution)初始化 (Initialization)使用卸载。 -
双亲委派机制:
当一个类加载器收到类加载请求时,它首先不会自己尝试去加载这个类,而是将请求委派给父类加载器去完成;层层向上,只有当父类加载器反馈自己无法完成该加载请求(它的搜索范围内没有找到该类)时,子加载器才会尝试自己去加载。
Bootstrap ClassLoader (C++ 实现,加载 java.lang.* 等核心库)
▲
Extension / Platform ClassLoader (加载 ext 扩展库)
▲
Application / App ClassLoader (加载 ClassPath / 三方依赖)
▲
Custom ClassLoader (自定义类加载器)
-
双亲委派的核心价值:
-
安全性(防篡改) :防止核心 API 被篡改。例如用户自定义一个
java.lang.Object类,双亲委派会将其向上委托至Bootstrap ClassLoader,加载的是系统官方的Object,确保核心代码安全。 -
避免重复加载:确保同一个类在 JVM 中只会被加载一次。
-
专栏目录导航
-
[第 3 篇] 2026年Android中高级面试题专栏(java基础与进阶)
最后,祝愿每一位正在备战面试的 Android 开发者:
- 能够在反复的梳理与思考中打破瓶颈,将知识真正融会贯通;
- 在接下来的每一场面试中发挥出色、对答如流,展现出自己最出色的技术硬实力;
- 拨云见日,顺利斩获心仪的大厂与中高级职位 Offer,在移动开发的技术道路上披荆斩棘、一路高歌!