小鹏汽车 Java开发一面 问题一个比一个深

上周面了小鹏汽车的Java开发岗,整场面试将近50分钟,自我介绍之后直接进入技术环节,节奏很快。前半段是Java基础和JVM,后半段转向数据库和分布式,最后还聊了几个场景设计题。整体难度中等偏上,基础问题答完会继续追问原理,想靠背答案糊弄过去比较难。

题目覆盖面比较广,Java、MySQL、Redis、消息队列都有涉及,贴合小鹏车联网和后台服务的业务特点。把题目和答案整理出来,供大家备战参考。

1. Java 中 HashMap 的底层结构是什么?1.7 和 1.8 有什么区别?

HashMap 底层是数组+链表的结构,通过 key 的 hashCode 计算数组下标,冲突时用链表挂在同一个桶上。

1.7 和 1.8 的核心区别:

  • 1.7 是数组+链表,1.8 引入了红黑树,当链表长度超过8且数组长度超过64时,链表转为红黑树,查找时间复杂度从 O(n) 降到 O(log n)。
  • 1.7 插入链表用头插法,1.8 改为尾插法。头插法在多线程扩容时会产生环形链表导致死循环,尾插法解决了这个问题(但多线程下仍不安全)。
  • 1.8 的 hash 扰动函数简化为一次异或,1.7 做了四次扰动。
  • 扩容时 1.8 通过高位bit判断元素新位置,不需要重新计算 hash,效率更高。

2. HashMap 在多线程下会出现什么问题?ConcurrentHashMap 是如何解决的?

HashMap 多线程问题:

  • 1.7 中并发扩容会产生环形链表,导致 get 时死循环
  • 1.8 中并发 put 可能导致数据丢失(两个线程同时写同一个桶)
  • size 计数不准确

ConcurrentHashMap 的解决方式:

  • 1.7 用分段锁(Segment),每个 Segment 是一个独立的 HashMap,锁粒度是 Segment 级别,支持最多16个线程并发写。
  • 1.8 放弃了分段锁,改用 CAS + synchronized,锁粒度细化到单个桶(数组槽位),只有写同一个桶时才竞争锁,并发度大幅提升。
  • 1.8 用 CAS 更新 size,用 CounterCell 数组分散计数竞争。
  • 读操作不加锁,通过 volatile 保证可见性。

3. Java 的 synchronized 和 ReentrantLock 有什么区别?各自适合什么场景?

synchronized:

  • JVM 内置关键字,自动加锁解锁,异常时自动释放
  • 1.6 之后引入偏向锁、轻量级锁、重量级锁的升级机制,性能已经很好
  • 不支持超时、不支持中断、不支持公平锁

ReentrantLock:

  • java.util.concurrent 包,需要手动 lock/unlock,必须放在 finally 里
  • 支持超时获取锁(tryLock)、支持可中断、支持公平锁
  • 支持多个条件变量(Condition),比 wait/notify 更灵活

选择原则:简单同步场景用 synchronized,代码更简洁;需要超时、中断、公平性或多条件等高级功能时用 ReentrantLock。

4. 什么是 Java 内存模型(JMM)?happens-before 原则是什么?

JMM 定义了多线程程序中,变量的读写操作在不同线程间的可见性规则。每个线程有自己的工作内存(类似CPU缓存),变量修改先写到工作内存,再刷回主内存,其他线程从主内存读取。这就导致了可见性问题。

happens-before 是 JMM 对程序员的承诺:如果操作A happens-before 操作B,那么A的结果对B可见,且A的执行顺序在B之前。

常见的 happens-before 规则:

  • 程序顺序规则:同一线程内,前面的操作 happens-before 后面的操作
  • volatile 规则:对 volatile 变量的写 happens-before 后续对该变量的读
  • 锁规则:unlock happens-before 后续对同一锁的 lock
  • 线程启动规则:Thread.start() happens-before 该线程的任何操作
  • 线程终止规则:线程所有操作 happens-before 其他线程检测到该线程终止

5. JVM 的垃圾回收算法有哪些?各自的优缺点是什么?

标记-清除:标记存活对象,清除未标记对象。简单,但会产生内存碎片,分配大对象时可能触发 GC。

标记-复制:将内存分两半,存活对象复制到另一半,清空当前半。无碎片,分配效率高,但内存利用率只有50%。适合新生代(Eden+Survivor,存活对象少)。

标记-整理:标记存活对象后,将其移动到内存一端,清理边界外的空间。无碎片,内存利用率高,但移动对象开销大。适合老年代。

分代收集:现代JVM的实际策略,新生代用复制算法(对象存活率低),老年代用标记-整理或标记-清除。

6. 什么是 Stop-The-World?哪些操作会触发它?如何减少影响?

Stop-The-World(STW)是指 JVM 在执行某些 GC 操作时,必须暂停所有用户线程,等待 GC 完成后再恢复。

触发场景:

  • Minor GC(新生代回收):STW 时间短,通常几毫秒
  • Full GC(全堆回收):STW 时间长,可能几百毫秒到几秒,是性能杀手
  • CMS 的初始标记和重新标记阶段
  • G1 的 Mixed GC

减少影响的方式:

  • 选择低延迟收集器:G1、ZGC、Shenandoah,ZGC 的 STW 时间可以控制在毫秒以内
  • 合理设置堆大小,避免频繁 Full GC
  • 减少大对象分配,避免直接进入老年代
  • 监控 GC 日志,针对性调优

7. Java 中的 ThreadLocal 是什么?内存泄漏是怎么产生的?

ThreadLocal 为每个线程提供独立的变量副本,线程间互不干扰,常用于存储用户上下文、数据库连接、日期格式化对象等线程级别的状态。

内存泄漏原因:ThreadLocal 的实现是每个 Thread 内部有一个 ThreadLocalMap,key 是 ThreadLocal 对象的弱引用,value 是实际存储的值(强引用)。

当 ThreadLocal 对象没有外部强引用时,key 会被 GC 回收变成 null,但 value 仍然被 ThreadLocalMap 强引用,无法回收。如果线程是线程池中的长生命周期线程,这个 value 就会一直存在,造成内存泄漏。

解决方式:使用完 ThreadLocal 后,显式调用 remove() 方法清理。

8. Spring Bean 的生命周期是怎样的?

Spring Bean 的生命周期大致分为以下阶段:

  1. 实例化:通过反射调用构造方法创建对象
  2. 属性填充:注入依赖(@Autowired、@Value 等)
  3. Aware 接口回调:如果实现了 BeanNameAware、ApplicationContextAware 等,调用对应的 set 方法
  4. BeanPostProcessor 前置处理:调用 postProcessBeforeInitialization
  5. 初始

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Java面试圣经 文章被收录于专栏

Java面试圣经,带你练透java圣经

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务