本文整理了字节跳动秋招面试中涉及的关键Java技术问题,涵盖了集合框架、JVM内存模型、多线程并发等核心知识点,旨在为后续的Java开发者面试准备提供清晰的复习脉络。
一、集合框架深度剖析
1. Java常用集合及其适用场景
Java集合框架提供了多种数据结构,各自有其最佳适用场景:
ArrayList:基于动态数组实现,支持快速随机访问(get/set),适用于读多写少、需要按索引频繁访问元素的场景。但中间位置的插入和删除效率较低。
LinkedList:基于双向链表实现,在头尾的插入和删除效率极高,适用于需要频繁进行此类操作的场景,如实现队列或栈。但随机访问性能差。
HashMap:基于哈希表实现的键值对集合,提供了近乎常数时间的get和put性能(在理想哈希情况下),是最常用的映射结构,适用于需要通过键快速查找值的场景。
TreeMap:基于红黑树实现,能够保持键的自然顺序或自定义顺序。在需要有序键值对,或进行范围查找时使用。
HashSet / TreeSet:分别是基于HashMap和TreeMap实现的集合,用于存储不重复元素。前者无序,后者有序。
2. HashMap的键为何一般要求不可变?
这主要出于数据一致性和哈希稳定性的考虑。如果一个可变对象作为键,当其内容被修改后,其hashCode()的返回值很可能发生变化。这会导致在HashMap中无法再通过该键找到之前存入的对应值(因为get操作会去新的哈希桶查找),造成数据“丢失”,同时也破坏了映射关系。
3. HashMap如何处理哈希冲突?
Java的HashMap采用链地址法(Separate Chaining)解决冲突。当多个键的哈希值映射到同一个数组索引(桶)时,这些键值对会以链表形式存储在该桶中。在JDK 1.8及以后,当链表长度超过阈值(默认为8)且当前数组长度大于等于64时,链表会转换为红黑树,以提升极端情况下的查询效率(从O(n)提升到O(log n))。
4. 为何不直接用红黑树,而是先链表后转树?
红黑树虽然查询效率高,但其节点结构比链表复杂(需要维护颜色、父节点、左右子节点指针),空间开销更大,构建和平衡操作也更为耗时。对于绝大多数情况,哈希冲突很少,链表足以高效处理。采用链表+红黑树的混合结构是一种在时间和空间开销上的折中优化策略,兼顾了普通场景和极端场景的性能。
5. ArrayList的扩容机制
ArrayList内部使用一个Object[]数组存储元素。当添加元素导致当前容量不足时,会触发扩容。新容量通常是旧容量的1.5倍(即 oldCapacity + (oldCapacity >> 1))。采用1.5倍扩容(而非2倍)是一种工程上的平衡,旨在减少空间浪费的同时,控制扩容的频率,避免每次扩容申请过大内存导致的内存碎片或分配失败问题。扩容时,会创建一个新的更大数组,并将旧数组元素复制过去。
二、JVM内存管理与GC机制
6. Java反射的理解与应用
反射(Reflection)允许程序在运行时检查、调用或修改类、方法、字段等元信息的能力。它打破了封装性,应谨慎使用。
- 在Spring中的应用:Spring框架的核心——IOC(控制反转)容器大量使用了反射。当我们在配置中定义了一个Bean,Spring容器在启动时,会通过反射:
- 解析配置,获取类的全限定名。
- 使用
Class.forName() 加载类。
- 调用
Class.newInstance() 或通过构造器 (Constructor.newInstance()) 来动态创建对象实例。
- 通过
Field.set() 或 Method.invoke() 来为对象的属性注入依赖(DI)。
反射使得Spring无需在编译时硬编码依赖关系,实现了配置和代码的解耦。
7. JVM内存结构概览
主要分为以下几个区域:
- 程序计数器:线程私有,指向当前线程正在执行的字节码指令地址。
- Java虚拟机栈:线程私有,存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法调用对应一个栈帧。
- 本地方法栈:为Native方法服务。
- Java堆:所有线程共享,是存放对象实例和数组的主要区域,也是垃圾收集器管理的主要区域。
- 方法区(元空间):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
8. new一个对象的内存分配过程
- 类加载检查:JVM首先检查
new指令的参数是否能在常量池中定位到一个类的符号引用,并检查该类是否已被加载、解析和初始化。
- 内存分配:在Java堆中为新生对象分配内存。分配方式有“指针碰撞”(堆内存规整)和“空闲列表”(堆内存不规整)两种。
- 内存空间初始化:将分配到的内存空间(不包括对象头)都初始化为零值。
- 设置对象头:设置对象的哈希码、GC分代年龄、锁状态标志、类元信息指针等。
- 执行
<init>方法:按照程序员的意愿进行初始化(即执行构造器中的代码)。
9. new对象时内存不足会发生什么?
如果创建对象时,在Java堆中无法找到足够大的连续内存空间,JVM将会抛出 OutOfMemoryError。在此之前,会触发一次垃圾收集(GC) 尝试释放空间。如果GC后仍然无法满足内存分配需求,才会抛出错误。
10. Minor GC如何引发Full GC?
Minor GC是发生在新生代(Young Generation)的垃圾收集。
在以下常见情况,Minor GC可能导致或直接升级为Full GC(收集整个堆,包括老年代和元空间):
- 空间分配担保失败:Minor GC前,JVM会检查老年代最大可用连续空间是否大于新生代所有对象总空间(或历次晋升的平均大小)。如果条件不成立,则会直接触发Full GC。
- 大对象直接进入老年代,但老年代空间不足。
- 老年代空间不足:Minor GC后,存活对象需要晋升到老年代,但老年代剩余空间不足。
- 元空间(方法区)不足。
三、多线程与并发编程
11. 线程安全与保障关键字
线程安全指在多线程环境下,某个函数、函数库或共享数据在被并发调用/访问时,能够正确地处理和管理状态,不会出现数据污染、逻辑错误等不确定问题。
synchronized:Java原生的互斥同步锁。它修饰代码块或方法,确保同一时刻只有一个线程能执行该段代码。它保证了原子性、可见性和有序性。
volatile:一种轻量级的同步机制。它确保变量的修改对所有线程立即可见(可见性),并禁止指令重排序(有序性),但不保证原子性。常用于状态标志位。
12. synchronized 与 ReentrantLock 的区别
- 实现层面:
synchronized是JVM层面的关键字,由JVM实现锁的获取和释放;ReentrantLock是JDK层面的API,通过AQS(AbstractQueuedSynchronizer)队列实现。
- 功能性:
ReentrantLock功能更丰富,支持尝试非阻塞获取锁、可中断锁、公平锁以及绑定多个条件变量,这些都是synchronized不具备的。
- 使用方式:
synchronized无需手动释放锁;ReentrantLock必须lock()和unlock()配对使用,通常放在try-finally块中确保释放。
- 性能:在高度竞争的情况下,
ReentrantLock性能可能更好;低竞争时synchronized经过优化后性能相当,且更简洁。
13. 公平锁与非公平锁的取舍
- 公平锁:按照线程申请锁的时间顺序来获取锁。优点是等待时间长的线程不会“饿死”,缺点是整体吞吐量较低,因为要维护一个队列并唤醒队首线程。
- 非公平锁:线程尝试获取锁时,可以“插队”,直接尝试获取。如果获取失败,再排队。优点是减少了线程切换的开销,整体吞吐率高,缺点是可能导致某些线程长时间等待。
选择哪种取决于对吞吐量和公平性的权衡。在并发量高、锁持有时间短的场景,非公平锁通常是更好的选择,这也是ReentrantLock和synchronized默认的实现策略。
14. 线程池的拒绝策略
当线程池的任务队列已满,且工作线程数达到最大线程数时,新提交的任务将触发拒绝策略。JDK内置了四种策略:
- AbortPolicy(默认):直接抛出
RejectedExecutionException异常。
- CallerRunsPolicy:由调用者线程(提交任务的线程)自己执行该任务。这是一种反馈调节机制,会降低新任务提交速度。
- DiscardPolicy:直接丢弃新任务,不抛异常。
- DiscardOldestPolicy:丢弃队列中最旧(最先进入队列)的一个任务,然后尝试重新提交当前任务。
15. 线上线程池参数设定与拒绝策略经验
- 核心参数设定:
- 核心线程数 (
corePoolSize):根据任务类型(CPU密集型、IO密集型)和机器CPU核数设定。CPU密集型任务可设为 Ncpu + 1;IO密集型任务可设为 2 * Ncpu 或更高,需结合压测。
- 最大线程数 (
maximumPoolSize):受系统资源限制。需要预估极端流量下所需线程数,并考虑内存和上下文切换开销。
- 任务队列:根据任务特性选择。有界队列(如
ArrayBlockingQueue)可以防止资源耗尽,但需要合理设置大小。
- 拒绝策略选择:
- 默认的
AbortPolicy适用于关键业务,通过快速失败暴露问题。
CallerRunsPolicy适用于不允许失败,但可以承受一定延迟的场景,它能有效平缓流量峰值。
- 对于可丢弃的非核心任务,可选择
DiscardPolicy或DiscardOldestPolicy。
最佳实践是自定义拒绝策略,如将拒绝的任务持久化到数据库或消息队列,后续进行补偿处理,并结合监控告警,这是构建健壮分布式系统的重要一环。
|