2026年Android中高级面试题专栏(java基础与进阶)

6 阅读10分钟

欢迎来到 2026年Android中高级面试题专栏 的第三篇:java基础与进阶。

尽管现代 Android 框架层出不穷,甚至 Kotlin 已成为首选,但 Java 基础、JUC 并发编程与 JVM 虚拟机机制 依然是所有 Android 研发人员的根基。在面试中,考察 Java 底层与 JVM 能最直接地检验出候选人的代码基本功、内存把控力以及对多线程并发安全的理解深度。本文将带你一次性攻克集合源码、JUC 锁机制、线程池优化以及 JVM 垃圾回收等高频大厂必考点。

一、 Java 语言基础与面向对象

Q1: HashMap 的底层实现原理是什么?在 JDK 1.8 中有哪些关键演进?

核心回答:

  • 数据结构:JDK 1.8 采用了 数组 + 单向链表 + 红黑树 的结构。

  • 哈希冲突与树化

    • 当发生 Hash 碰撞时,以链表形式挂载在对应桶(Bucket)下。

    • 当单个桶中的链表长度 8\ge 8 数组总容量 64\ge 64 时,链表会转为红黑树,将查找复杂度从 O(n)O(n) 降低到 O(logn)O(\log n)。若数组容量未达到 64,则优先触发 resize() 扩容而非树化。

    • 当红黑树节点数退化到 6\le 6 时,重新转回链表。

  • 扩容机制(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)原理

      1. 计算 Hash 值定位桶位置。若桶为空,通过 CAS 无锁操作尝试插入头节点;

      2. 若桶不为空(发生 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 提供的轻量级同步机制,主要包含两个特性:

  1. 保证内存可见性

    当一个线程修改了被 volatile 修饰的变量时,JVM 会立即将该线程工作内存(CPU 缓存/寄存器)中的最新值强制刷新到主内存(JMM Main Memory),并使其他线程工作内存中该变量的缓存行失效,确保其他线程读取时必须重新从主内存加载。

  2. 禁止指令重排序

    编译器和 CPU 为了优化性能会对指令进行重排序。volatile 通过在编译生成的字节码中插入 内存屏障(Memory Barrier / Fence) ,阻止屏障两侧的指令重排序(典型应用:双重检查锁 DCL 单例模式)。

  • 原子性问题

    volatile 不能保证原子性(例如 i++ 自增操作包含读取、加1、写入三步,volatile 无法阻止多线程在中间步骤的并发交错)。保证原子性需使用 synchronizedReentrantLockAtomicInteger(CAS)。

Q2: 深度剖析 synchronized 锁升级过程(偏向锁 \rightarrow 轻量级锁 \rightarrow 重量级锁)

核心回答:

在 JVM(HotSpot)中,对象的对象头(Mark Word)根据锁状态存储不同的数据结构。锁只能升级不能降级(除 GC 倾斜外)。

无锁状态 ──(偏向 CAS 成功)──> 偏向锁 ──(出现竞争)──> 轻量级锁 (CAS 自旋) ──(自旋失败/重度竞争)──> 重量级锁 (Monitor)
  1. 偏向锁(Biased Locking)

    经过统计,大多数情况下锁总是由同一线程多次获得。当线程首次获取锁时,在 Mark Word 中记录该线程 ID。下次该线程再进入该锁时,无需任何同步操作,直接通关,开销极低。

  2. 轻量级锁(Lightweight Locking)

    当有另一个线程尝试竞争偏向锁时,偏向锁被撤销并升级为轻量级锁。竞争线程在各自的栈帧中创建锁记录(Displaced Mark Word),通过 CAS 自旋 尝试将 Mark Word 指向自己的锁记录。轻量级锁适用于线程交替执行、短时间持锁的场景。

  3. 重量级锁(Heavyweight Locking)

    当线程 CAS 自旋达到一定次数(或竞争过于剧烈)时,锁升级为重量级锁。 Mark Word 指向底层的 ObjectMonitor 对象。未抢到锁的线程会被阻塞(Park)并放入等待队列,涉及 Linux 用户态与内核态的上下文切换,开销最大。

Q3: 线程池 ThreadPoolExecutor 的 7 大核心参数与任务拒绝对策

核心回答:

1. 7 大核心参数说明

  • corePoolSize:核心线程数(常驻线程)。

  • maximumPoolSize:最大线程数(核心 + 非核心线程)。

  • keepAliveTime:非核心线程空闲存活时间。

  • unit:存活时间单位。

  • workQueue:阻塞队列(如 ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue)。

  • threadFactory:线程创建工厂(建议自定义给线程命名)。

  • handler:拒绝策略(默认 AbortPolicy 抛异常;CallerRunsPolicy 交由调用者线程处理;DiscardPolicy 直接丢弃;DiscardOldestPolicy 丢弃队列最老任务)。

2. 任务递交与处理流程(关键逻辑)

提交任务 ──> 核心线程满? ──No──> 创建核心线程执行
                 Yes
                
            队列满?      ──No──> 入队等待
                 Yes
                
          最大线程数满?  ──No──> 创建非核心线程执行
                 Yes
                
           触发拒绝策略 (RejectedExecutionHandler)
  • Android 避坑:绝不使用 Executors.newCachedThreadPool(),其 maximumPoolSizeInteger.MAX_VALUE,极易导致创建成百上千线程卡死 App 甚至引发 OOM。

三、 JVM 虚拟机原理

Q1: 简述 JVM 运行时数据区的划分及各自职责

核心回答:

┌─────────────────────────────────────────────────────────────┐
│                       JVM 运行时数据区                      │
│  ┌───────────────────────┐       ┌──────────────────────┐  │
│  │      程序计数器       │       │    Java 虚拟机栈     │  │
│  │ (Program Counter Reg) │       │ (JVM Stack / 栈帧)   │  │
│  └───────────────────────┘       └──────────────────────┘  │
│  ┌───────────────────────┐       ┌──────────────────────┐  │
│  │     本地方法栈        │       │       Java 堆        │  │
│  │    (Native Stack)     │       │     (Java Heap)      │  │
│  └───────────────────────┘       └──────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                元空间 / 方法区 (Metaspace)           │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
  1. 程序计数器(Program Counter Register) :线程私有。记录当前线程正在执行的字节码指令地址(Native 方法时为空)。

  2. Java 虚拟机栈(JVM Stack) :线程私有。每个方法执行时创建一个栈帧(Stack Frame) ,存放局部变量表、操作数栈、动态链接、方法出口信息。栈溢出抛出 StackOverflowError

  3. 本地方法栈(Native Method Stack) :线程私有。为 JVM 执行 Native(JNI/C/C++)方法服务。

  4. Java 堆(Heap)线程共享。存放所有创建的对象实例与数组,是垃圾回收(GC)的主战场。

  5. 元空间 / 方法区(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) \rightarrow 验证 (Verification) \rightarrow 准备 (Preparation) \rightarrow 解析 (Resolution) \rightarrow 初始化 (Initialization) \rightarrow 使用 \rightarrow 卸载

  • 双亲委派机制

    当一个类加载器收到类加载请求时,它首先不会自己尝试去加载这个类,而是将请求委派给父类加载器去完成;层层向上,只有当父类加载器反馈自己无法完成该加载请求(它的搜索范围内没有找到该类)时,子加载器才会尝试自己去加载。

Bootstrap ClassLoader (C++ 实现,加载 java.lang.* 等核心库)
       ▲
Extension / Platform ClassLoader (加载 ext 扩展库)
       ▲
Application / App ClassLoader (加载 ClassPath / 三方依赖)
       ▲
Custom ClassLoader (自定义类加载器)
  • 双亲委派的核心价值

    1. 安全性(防篡改) :防止核心 API 被篡改。例如用户自定义一个 java.lang.Object 类,双亲委派会将其向上委托至 Bootstrap ClassLoader,加载的是系统官方的 Object,确保核心代码安全。

    2. 避免重复加载:确保同一个类在 JVM 中只会被加载一次。

专栏目录导航

最后,祝愿每一位正在备战面试的 Android 开发者:

  • 能够在反复的梳理与思考中打破瓶颈,将知识真正融会贯通;
  • 在接下来的每一场面试中发挥出色、对答如流,展现出自己最出色的技术硬实力;
  • 拨云见日,顺利斩获心仪的大厂与中高级职位 Offer,在移动开发的技术道路上披荆斩棘、一路高歌!