Java面试八股文高级篇

1、HashMap底层源码 难度系数:⭐⭐⭐

HashMap 底层最概括的说就是:数组 + 链表 + 红黑树

具体核心逻辑如下:

  1. 底层结构 :一个 Node<K,V>[] 数组(即哈希桶)。
  2. 存值逻辑
    • 计算键的 hashCode,通过扰动处理(高16位异或低16位)后,与数组长度取模,确定在数组中的索引位置
    • 若该位置无元素,直接存入(数组)。
    • 若已有元素(哈希碰撞),则用链表连接在尾部(尾插法)。
    • 当链表长度超过 8 且数组长度达到 64 时,链表转换为红黑树,以优化查询效率。
  3. 取值逻辑 :根据键定位到数组索引,若为链表或树,则通过 equals 方法遍历比较,找到匹配的键值。
  4. 扩容机制 :当元素数量超过 负载因子(默认0.75) × 数组长度 时,数组翻倍扩容 (变为原来的2倍)。扩容后,所有元素需重新计算索引位置(rehash)。

2、JVM内存分哪几个区,每个区的作用是什么 难度系数:⭐⭐

JVM内存主要分为5个区:

  • 堆(Heap):线程共享,存储所有对象实例和数组,是垃圾回收的主要区域。
  • 方法区(Method Area):线程共享,存储类信息、常量、静态变量等(JDK 8后由元空间实现)。
  • 虚拟机栈(VM Stack):线程私有,存储局部变量、操作数栈、方法出口,每个方法执行时创建栈帧。
  • 本地方法栈(Native Method Stack):线程私有,为虚拟机执行本地(Native)方法服务。
  • 程序计数器(PC Register):线程私有,记录当前线程执行的字节码行号,用于线程切换后恢复执行。

3、Java中垃圾收集的方法有哪些 难度系数:⭐

Java中的垃圾收集(GC)方法,按算法思想收集器实现概括如下:

  1. 标记-清除 :最基本的算法。先标记出所有存活对象,再统一清除未被标记的对象。缺点是容易产生内存碎片。
  2. 标记-复制 :将内存分为两块,每次只使用一块,存活对象被复制到另一块后,清空原块。优点 是效率高且无碎片,缺点是内存利用率低(通常用于新生代)。
  3. 标记-整理 :标记存活对象后,让它们向一端移动(整理),然后清理边界外的内存。优点 是无碎片,缺点是移动对象有开销(通常用于老年代)。
  4. 分代收集 :当前主流策略。根据对象存活周期的不同,将堆内存划分为新生代 (使用复制算法)和老年代(使用标记-清除或标记-整理算法),分而治之以提高效率。
  5. 并发收集 :指垃圾回收线程与用户线程同时工作(如CMS、G1),目标是在不停止应用线程(或仅短暂停顿)的情况下完成回收,以降低停顿时间。

4、如何判断一个对象是否存活(或者GC对象的判定方法) 难度系数:⭐

判断对象是否存活,主要有两种方法:

  1. 引用计数法 :每个对象维护一个引用计数器,只要有引用指向它,计数器就加1。计数器为0时判定为死亡。缺点:无法解决循环引用问题。
  2. 可达性分析算法 (主流JVM使用):从一组称为 GC Roots 的根对象出发,通过引用链向下搜索,未被搜索到的对象判定为不可达,即可以回收。

5、什么情况下会产生StackOverflowError(栈溢出)和OutOfMemoryError(堆溢出)怎么排查 难度系数:⭐⭐

一、什么情况下产生

  • StackOverflowError(栈溢出)

    • 深度超限 :最常见于无限递归递归调用层级过深,线程请求的栈深度大于虚拟机允许的深度。
    • 帧数过多:单个线程中方法调用帧过多(如大量嵌套循环调用),或线程栈帧过大(如局部变量过多),导致无法容纳新帧。
  • OutOfMemoryError(堆溢出)

    • 内存泄漏:对象无意识被GC Roots持有引用无法回收,导致内存被逐渐耗尽(如未关闭的流、监听器、缓存)。
    • 内存溢出:对象确实存活且所需内存超过JVM分配的堆上限(如加载海量数据、大对象分配无连续空间)。

二、怎么排查

  • StackOverflowError

    • 看异常栈 :直接查看异常堆栈末尾的重复行,定位到递归或循环调用的代码行,重点检查终止条件。
    • 调参数 :若业务确实需要深调用,通过 -Xss 调整线程栈大小(但根治需优化代码逻辑)。
  • OutOfMemoryError

    • 抓转储 :添加 -XX:+HeapDumpOnOutOfMemoryError,自动获取内存快照(hprof文件)。

    • 分析工具 :使用 MAT(Memory Analyzer)JProfiler 分析堆转储,定位:

      • 大对象:查看占用内存最大的对象(Histogram)。
      • 泄漏嫌疑 :利用MAT的 Leak Suspects Report 快速定位GC Roots到泄漏对象的引用链。
      • 分场景:若为频繁Full GC但内存不降,通常是泄漏;若内存直线飙升后溢出,检查是否有超大集合或高并发下的瞬时流量。

6、什么是线程池,线程池有哪些(创建) 难度系数:⭐

线程池:一种管理一组可复用线程的技术,用于减少线程创建销毁的开销、控制并发数量。

创建方式(概括):

  • 通过 Executors 工具类:快速创建预定义类型(如固定数量、单线程、可缓存、定时执行等)
  • 通过 ThreadPoolExecutor:直接指定核心参数(核心数、最大数、队列、拒绝策略等)进行精细控制

7、为什么要使用线程池 难度系数:⭐

核心目的:管理资源,提升性能,保障稳定。

具体概括为三点:

  1. 降低开销:复用线程,避免频繁创建和销毁带来的性能损耗。
  2. 解耦任务:将任务的提交与执行分离,便于异步处理。
  3. 统一管理:限制并发数量,防止资源耗尽导致系统崩溃。

8、线程池底层工作原理 难度系数:⭐

线程池底层就两件事:用阻塞队列解耦任务和线程,用线程复用减少开销

核心逻辑是:

  1. 线程复用:内部线程创建后不会销毁,反复从阻塞队列取任务执行。

  2. 动态调控

    • 当前线程数 < 核心线程数 → 直接新建线程。
    • 当前线程数 ≥ 核心线程数 → 任务丢进阻塞队列排队。
    • 队列满了且线程数 < 最大线程数 → 紧急新建(非核心)线程。
    • 队列满了且线程数已达最大 → 执行拒绝策略。

9、ThreadPoolExecutor对象有哪些参数 怎么设定核心线程数和最大线程数 拒绝策略有哪些 难度系数:⭐

ThreadPoolExecutor 的核心参数有 7 个

  1. corePoolSize --- 核心线程数(常驻线程)
  2. maximumPoolSize --- 最大线程数(核心 + 临时)
  3. keepAliveTime --- 临时线程空闲存活时间
  4. unit --- 存活时间单位
  5. workQueue --- 阻塞队列(存放等待任务)
  6. threadFactory --- 线程工厂(用于创建线程)
  7. handler --- 拒绝策略(任务无法处理时的处理方式)

设定核心和最大线程数:直接通过构造函数的前两个参数指定,核心线程数 ≤ 最大线程数。

拒绝策略(4 种)

  • AbortPolicy(默认)--- 直接抛异常
  • CallerRunsPolicy --- 让调用者线程自己执行任务
  • DiscardPolicy --- 静默丢弃任务
  • DiscardOldestPolicy --- 丢弃队列中最老的任务,然后重试提交

10、常见线程安全的并发容器有哪些 难度系数:⭐

Java 中常见的线程安全并发容器,按场景概括如下:

  • ConcurrentHashMap :哈希表结构,通过分段锁 / CAS + synchronized 实现高并发读写,替代 HashtableCollections.synchronizedMap
  • CopyOnWriteArrayList / CopyOnWriteArraySet :写时复制,读操作无锁,适合读多写极少、容忍短暂数据不一致的场景。
  • ConcurrentLinkedQueue / ConcurrentLinkedDeque :基于 CAS 的无锁非阻塞队列,适用于高吞吐量的生产者消费者场景。
  • BlockingQueue 系列 (ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue 等):阻塞队列 ,提供 put/take 阻塞方法,常用于线程池与生产者消费者模型。
  • ConcurrentSkipListMap / ConcurrentSkipListSet :基于跳表实现,保持元素有序 ,并发度高于 Collections.synchronizedSortedMap

11、Atomic原子类了解多少 原理是什么 难度系数:⭐

Atomic原子类 是利用CAS(比较并交换)Volatile 实现的无锁并发工具。

原理概括:

  1. 核心机制 :通过 Unsafe 类 直接调用 CPU 的 CAS 指令(比较并交换),在硬件层面保证"读-改-写"操作的原子性。
  2. 内存可见性 :内部使用 volatile 修饰成员变量,确保多线程间的变量修改立即可见。
  3. 失败重试:当 CAS 操作失败(检测到值已被其他线程修改)时,通常会在循环中不断重试(自旋),直到成功。

优势 :相比 synchronized 锁机制,它避免了线程上下文切换的开销,适用于计数器、累加器等低并发冲突场景。


12、synchronized底层实现是什么 lock底层是什么 有什么区别 难度系数:⭐⭐⭐

1. synchronized 底层

  • 实现 :基于 JVM 层面的监视器锁 ,通过 monitorenter / monitorexit 指令实现。
  • 锁状态 :依赖 对象头 (Mark Word),通过 CAS + 自旋 进行升级(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)。
  • 特性:JVM 内置关键字,自动加锁/解锁(异常时 JVM 自动释放),非公平锁。

2. Lock 底层

  • 实现 :基于 JDK 层面的 AQS ,利用 volatile + CAS 维护同步状态。
  • 机制 :通过 Unsafe 类进行线程阻塞与唤醒(park / unpark)。
  • 特性 :接口实现,需手动 lock / unlock,支持公平/非公平、可中断、超时、条件变量等扩展功能。

3. 核心区别

  • 层级synchronized 是 JVM 内置锁;Lock 是 Java 代码实现的 API。
  • 灵活性Lock 支持尝试获取、超时、中断;synchronized 不支持。
  • 锁机制synchronized 自动释放;Lockfinally 显式释放。
  • 性能 :低竞争下 synchronized 经过优化(偏向锁)与 Lock 相近;高竞争下 Lock 通常可控性更好。

13、了解ConcurrentHashMap吗 为什么性能比HashTable高,说下原理 难度系数:⭐⭐

核心原理:锁粒度的革命

HashTable全局锁(synchronized 修饰整个方法),任何时刻只有一个线程能操作整个 Map,多线程竞争时效率极低。

ConcurrentHashMap 性能高的原因在于锁分离/细粒度化

  • JDK 1.7 :采用分段锁,将数据分成多个 Segment,每个 Segment 独立加锁,允许多个线程同时操作不同 Segment。
  • JDK 1.8+ :采用CAS + synchronized 锁链表/红黑树的头节点。写操作只锁住当前桶的第一个节点,读操作几乎无锁(volatile 保证可见性)。

概括:从"一把锁锁全表"降级为"只锁正在操作的某个桶",并发度从 1 提升至桶的数量(默认 16 或更高),且读操作无锁,因此性能大幅提升。


14、ConcurrentHashMap底层原理 难度系数:⭐⭐⭐

ConcurrentHashMap 底层原理最概括地说:

JDK 1.7: 采用 分段锁 机制,将数据分成多个 Segment(继承 ReentrantLock),每个 Segment 独立加锁,写操作只锁住一个 Segment,多线程可同时操作不同 Segment,提升并发度。

JDK 1.8+: 摒弃分段锁,改用 CAS + synchronized 锁粒度更细的节点。底层采用 数组 + 链表 + 红黑树 ,通过 CAS 进行无锁插入,发生哈希冲突时使用 synchronized 锁定链表或树的头节点,大幅降低锁竞争,结合 volatile 保证可见性,实现高效并发。


15、了解volatile关键字不 难度系数:⭐

volatile 的核心作用:保证变量的"可见性"和"有序性",但不保证"原子性"。

  • 可见性:一个线程修改了变量,其他线程能立刻看到最新值(避免从本地缓存读脏数据)。
  • 有序性:防止指令重排序,代码执行顺序按写代码的顺序来(一定程度上)。
  • 不保证原子性count++ 这种操作在多线程下依然不安全。

16、synchronized和volatile有什么区别 难度系数:⭐⭐

synchronized :互斥锁,保证原子性、可见性、有序性。线程阻塞,重量级。

volatile :轻量级同步,保证可见性 、有序性(禁止指令重排),不保证原子性。无阻塞。


17、Java类加载过程 难度系数:⭐

Java类加载过程分为三步:

  1. 加载 :找到类的二进制字节流(如.class文件),通过类名获取,将其静态结构转化为方法区的运行时数据结构,并在堆中生成对应的Class对象作为入口。

  2. 链接

    • 验证:确保字节码符合JVM规范,安全无虞。
    • 准备:为静态变量分配内存并设置默认零值。
    • 解析:将常量池中的符号引用替换为直接内存地址。
  3. 初始化:执行静态代码块和静态变量的赋值操作,按父类到子类的顺序真正初始化类变量。

整个过程遵循双亲委派模型,确保核心类库优先由启动类加载器加载,防止核心API被篡改。


18、什么是类加载器,类加载器有哪些 难度系数:⭐

类加载器 是 Java 虚拟机(JVM)中负责将类的字节码文件加载到内存 ,并生成对应 java.lang.Class 对象的组件。

主要类加载器:

  1. 启动类加载器 :加载核心类库(如 rt.jar)。
  2. 扩展类加载器 :加载扩展目录(如 lib/ext)中的类。
  3. 应用程序类加载器:加载用户类路径(Classpath)下的类。

19、简述java内存分配与回收策略以及Minor GC和Major GC(full GC) 难度系数:⭐⭐

Java内存分配与回收策略(基于分代模型)

  1. 内存分配

    • 对象优先在Eden分配:新对象通常分配在新生代的Eden区。
    • 大对象直接进入老年代:避免在Eden和Survivor之间大量复制。
    • 长期存活的对象进入老年代:对象在Survivor区每熬过一次Minor GC,年龄+1,达到阈值则晋升。
  2. 回收策略

    • 分代收集:新生代复制算法(效率高),老年代标记-清除或标记-整理算法(空间大、频率低)。

Minor GC vs Major GC(Full GC)

  • Minor GC

    • 发生区域:新生代(Eden + Survivor)。
    • 触发条件:Eden区空间不足时触发。
    • 特点:频繁,回收速度快(大部分对象朝生夕灭)。
  • Major GC / Full GC

    • 发生区域:老年代,通常连带新生代一起回收(即Full GC)。
    • 触发条件 :老年代空间不足、晋升时担保失败、显式调用System.gc()等。
    • 特点:频率低,但耗时显著高于Minor GC,应尽量避免频繁触发。

20、如何查看java死锁 难度系数:⭐

查看Java死锁最概括的方式是:使用JDK自带的工具或命令捕获线程快照,定位处于BLOCKED状态且互相等待锁的线程

  • 命令行jstack <pid> 直接输出线程堆栈,末尾会明确提示"Found one Java-level deadlock"。
  • 图形工具jconsoleVisualVMJMC 等,在"线程"面板中自动检测并显示死锁线程及锁信息。

核心思路:获取线程堆栈 → 识别循环等待锁的线程 → 分析代码修复。


21、Java死锁如何避免 难度系数:⭐

按顺序加锁,统一超时。

  1. 破坏循环等待:对所有共享资源定义全局唯一的加锁顺序,所有线程严格按此顺序获取锁。
  2. 破坏持有并等待 :使用 tryLock 设置超时时间,获取不到所需锁时释放已持有的锁,避免无限阻塞。
相关推荐
写代码的小阿帆2 小时前
JVM基础——类加载与内存区域
jvm
m0_518019482 小时前
使用Kivy开发跨平台的移动应用
jvm·数据库·python
yc_xym2 小时前
SpringAI快速入门
java·springai·deepseek
没有bug.的程序员2 小时前
S 级 SaaS 平台的物理雪崩:Spring Cloud Gateway 多租户动态路由与 UserID 极限分片
java·gateway·springboot·saas·springcloud·多租户、·userid
你不是我我2 小时前
【Java 开发日记】我们来说一下 b+ 树与 b 树的区别
java·开发语言
左左右右左右摇晃2 小时前
Java笔记——反射
java·tomcat
左左右右左右摇晃2 小时前
Java笔记——IO
java·开发语言·笔记
萍萍学习2 小时前
蓝桥杯JAVA-3
java·职场和发展·蓝桥杯
weixin_421922692 小时前
机器学习模型部署:将模型转化为Web API
jvm·数据库·python