1、HashMap底层源码 难度系数:⭐⭐⭐
HashMap 底层最概括的说就是:数组 + 链表 + 红黑树。
具体核心逻辑如下:
- 底层结构 :一个
Node<K,V>[]数组(即哈希桶)。 - 存值逻辑 :
- 计算键的
hashCode,通过扰动处理(高16位异或低16位)后,与数组长度取模,确定在数组中的索引位置。 - 若该位置无元素,直接存入(数组)。
- 若已有元素(哈希碰撞),则用链表连接在尾部(尾插法)。
- 当链表长度超过 8 且数组长度达到 64 时,链表转换为红黑树,以优化查询效率。
- 计算键的
- 取值逻辑 :根据键定位到数组索引,若为链表或树,则通过
equals方法遍历比较,找到匹配的键值。 - 扩容机制 :当元素数量超过 负载因子(默认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)方法,按算法思想 和收集器实现概括如下:
- 标记-清除 :最基本的算法。先标记出所有存活对象,再统一清除未被标记的对象。缺点是容易产生内存碎片。
- 标记-复制 :将内存分为两块,每次只使用一块,存活对象被复制到另一块后,清空原块。优点 是效率高且无碎片,缺点是内存利用率低(通常用于新生代)。
- 标记-整理 :标记存活对象后,让它们向一端移动(整理),然后清理边界外的内存。优点 是无碎片,缺点是移动对象有开销(通常用于老年代)。
- 分代收集 :当前主流策略。根据对象存活周期的不同,将堆内存划分为新生代 (使用复制算法)和老年代(使用标记-清除或标记-整理算法),分而治之以提高效率。
- 并发收集 :指垃圾回收线程与用户线程同时工作(如CMS、G1),目标是在不停止应用线程(或仅短暂停顿)的情况下完成回收,以降低停顿时间。
4、如何判断一个对象是否存活(或者GC对象的判定方法) 难度系数:⭐
判断对象是否存活,主要有两种方法:
- 引用计数法 :每个对象维护一个引用计数器,只要有引用指向它,计数器就加1。计数器为0时判定为死亡。缺点:无法解决循环引用问题。
- 可达性分析算法 (主流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、为什么要使用线程池 难度系数:⭐
核心目的:管理资源,提升性能,保障稳定。
具体概括为三点:
- 降低开销:复用线程,避免频繁创建和销毁带来的性能损耗。
- 解耦任务:将任务的提交与执行分离,便于异步处理。
- 统一管理:限制并发数量,防止资源耗尽导致系统崩溃。
8、线程池底层工作原理 难度系数:⭐
线程池底层就两件事:用阻塞队列解耦任务和线程,用线程复用减少开销。
核心逻辑是:
-
线程复用:内部线程创建后不会销毁,反复从阻塞队列取任务执行。
-
动态调控:
- 当前线程数 < 核心线程数 → 直接新建线程。
- 当前线程数 ≥ 核心线程数 → 任务丢进阻塞队列排队。
- 队列满了且线程数 < 最大线程数 → 紧急新建(非核心)线程。
- 队列满了且线程数已达最大 → 执行拒绝策略。
9、ThreadPoolExecutor对象有哪些参数 怎么设定核心线程数和最大线程数 拒绝策略有哪些 难度系数:⭐
ThreadPoolExecutor 的核心参数有 7 个:
corePoolSize--- 核心线程数(常驻线程)maximumPoolSize--- 最大线程数(核心 + 临时)keepAliveTime--- 临时线程空闲存活时间unit--- 存活时间单位workQueue--- 阻塞队列(存放等待任务)threadFactory--- 线程工厂(用于创建线程)handler--- 拒绝策略(任务无法处理时的处理方式)
设定核心和最大线程数:直接通过构造函数的前两个参数指定,核心线程数 ≤ 最大线程数。
拒绝策略(4 种):
AbortPolicy(默认)--- 直接抛异常CallerRunsPolicy--- 让调用者线程自己执行任务DiscardPolicy--- 静默丢弃任务DiscardOldestPolicy--- 丢弃队列中最老的任务,然后重试提交
10、常见线程安全的并发容器有哪些 难度系数:⭐
Java 中常见的线程安全并发容器,按场景概括如下:
- ConcurrentHashMap :哈希表结构,通过分段锁 / CAS + synchronized 实现高并发读写,替代
Hashtable和Collections.synchronizedMap。 - CopyOnWriteArrayList / CopyOnWriteArraySet :写时复制,读操作无锁,适合读多写极少、容忍短暂数据不一致的场景。
- ConcurrentLinkedQueue / ConcurrentLinkedDeque :基于 CAS 的无锁非阻塞队列,适用于高吞吐量的生产者消费者场景。
- BlockingQueue 系列 (ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue 等):阻塞队列 ,提供
put/take阻塞方法,常用于线程池与生产者消费者模型。 - ConcurrentSkipListMap / ConcurrentSkipListSet :基于跳表实现,保持元素有序 ,并发度高于
Collections.synchronizedSortedMap。
11、Atomic原子类了解多少 原理是什么 难度系数:⭐
Atomic原子类 是利用CAS(比较并交换) 与 Volatile 实现的无锁并发工具。
原理概括:
- 核心机制 :通过 Unsafe 类 直接调用 CPU 的 CAS 指令(比较并交换),在硬件层面保证"读-改-写"操作的原子性。
- 内存可见性 :内部使用 volatile 修饰成员变量,确保多线程间的变量修改立即可见。
- 失败重试:当 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自动释放;Lock需finally显式释放。 - 性能 :低竞争下
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类加载过程分为三步:
-
加载 :找到类的二进制字节流(如
.class文件),通过类名获取,将其静态结构转化为方法区的运行时数据结构,并在堆中生成对应的Class对象作为入口。 -
链接:
- 验证:确保字节码符合JVM规范,安全无虞。
- 准备:为静态变量分配内存并设置默认零值。
- 解析:将常量池中的符号引用替换为直接内存地址。
-
初始化:执行静态代码块和静态变量的赋值操作,按父类到子类的顺序真正初始化类变量。
整个过程遵循双亲委派模型,确保核心类库优先由启动类加载器加载,防止核心API被篡改。
18、什么是类加载器,类加载器有哪些 难度系数:⭐
类加载器 是 Java 虚拟机(JVM)中负责将类的字节码文件加载到内存 ,并生成对应 java.lang.Class 对象的组件。
主要类加载器:
- 启动类加载器 :加载核心类库(如
rt.jar)。 - 扩展类加载器 :加载扩展目录(如
lib/ext)中的类。 - 应用程序类加载器:加载用户类路径(Classpath)下的类。
19、简述java内存分配与回收策略以及Minor GC和Major GC(full GC) 难度系数:⭐⭐
Java内存分配与回收策略(基于分代模型)
-
内存分配
- 对象优先在Eden分配:新对象通常分配在新生代的Eden区。
- 大对象直接进入老年代:避免在Eden和Survivor之间大量复制。
- 长期存活的对象进入老年代:对象在Survivor区每熬过一次Minor GC,年龄+1,达到阈值则晋升。
-
回收策略
- 分代收集:新生代复制算法(效率高),老年代标记-清除或标记-整理算法(空间大、频率低)。
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"。 - 图形工具 :
jconsole、VisualVM、JMC等,在"线程"面板中自动检测并显示死锁线程及锁信息。
核心思路:获取线程堆栈 → 识别循环等待锁的线程 → 分析代码修复。
21、Java死锁如何避免 难度系数:⭐
按顺序加锁,统一超时。
- 破坏循环等待:对所有共享资源定义全局唯一的加锁顺序,所有线程严格按此顺序获取锁。
- 破坏持有并等待 :使用
tryLock设置超时时间,获取不到所需锁时释放已持有的锁,避免无限阻塞。