目录
正在加载目录…
专栏文章
专栏文章
Java 专栏
1. Java:跨平台语言与生态全景 2. Java 并发:JMM、锁与线程池原理 3. JVM:类加载、内存、GC 与性能诊断 4. Java 性能调优:CPU、内存、锁与 IO 排查 5. Java 新特性速查:从 Lambda 到虚拟线程 6. Java 业务开发:高频陷阱与 Code Review 清单 7. Java 核心知识:集合、并发与 JVM 面试要点 8. AQS 原理:同步队列与加锁解锁全流程 9. JVM 与 Linux 内存:分区、分配与 GC 边界 10. AtomicLong 与 LongAdder:并发计数器选型 11. Java 引用与内存泄漏:六类场景与修复 12. Java 测试实践:JUnit、Mockito 与 Testcontainers 13. Java 并发入门:线程、锁与线程池全景

Java 核心知识:集合、并发与 JVM 面试要点

发布于 2026-06-17 14:40 · 最后编辑于 2026-07-31 15:51 · 字数 3,935 👁 174 次阅读

从面试视角系统梳理 Java 核心知识体系,覆盖集合框架底层原理、异常体系、I/O 模型、并发工具、JVM 内存与 GC 等高频考点,每个知识点给出"一句话核心结论"。

深度笔记Java 并发编程 · Java 虚拟机 · Java 性能调优 · AQS 原理深度解析 · Java 业务开发陷阱 · JVM 内存分区与 Linux 内存分配机制

../../assets/07 Java 核心知识体系/file 20260605123646439

目录

章节说明
异常体系Throwable / Exception / Error 分类与最佳实践
集合框架底层原理HashMap / LinkedHashMap / TreeMap / ConcurrentHashMap
IO 模型BIO / NIO / AIO 对比与多路复用原理
并发核心synchronized / ReentrantLock / CAS / 线程池
JVM 内存模型内存区域 / happen-before / OOM 类型
GC 与调优垃圾收集器对比 / GC 调优思路
反射与动态代理反射机制 / JDK 动态代理 / CGLIB
泛型与类型擦除泛型边界 / 类型擦除 / 通配符
类加载机制双亲委派模型 / 热部署实现

异常体系

类层次结构

Throwable
├── Error(不应捕获)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── NoClassDefFoundError
└── Exception
    ├── RuntimeException(unchecked,不强制捕获)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ClassCastException
    │   └── IllegalArgumentException
    └── 受检异常 checked(必须声明或捕获)
        ├── IOException
        ├── SQLException
        └── ClassNotFoundException

一句话结论Error 代表 JVM 级别不可恢复的错误,不要捕获;RuntimeException 是编程错误,应该通过代码修复;受检异常是可预期的业务异常,必须显式处理。

经典面试题:NoClassDefFoundError vs ClassNotFoundException

维度NoClassDefFoundErrorClassNotFoundException
类型Error(不应捕获)Exception(受检)
触发时机编译时存在,运行时找不到运行时动态加载失败(Class.forName
常见原因类路径配置错误、jar 包缺失反射加载不存在的类

异常处理最佳实践

// 1. 捕获具体异常,不要捕获 Exception/Throwable
// 错误
catch (Exception e) { log.error("failed", e); }
// 正确
catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // 恢复中断状态
    log.warn("interrupted", e);
}

// 2. try-with-resources 自动关闭资源(Java 7+)
try (InputStream in = new FileInputStream("/tmp/data");
     OutputStream out = new FileOutputStream("/tmp/out")) {
    // 自动调用 close(),即使发生异常
}

// 3. 多异常合并捕获(Java 7+)
try {
    doSomething();
} catch (IOException | SQLException e) {
    log.error("operation failed", e);
}

// 4. 自定义异常:继承 RuntimeException,提供充足信息
public class BusinessException extends RuntimeException {
    private final String errorCode;

    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() { return errorCode; }
}

异常性能注意事项

  • 每次实例化 Exception 都会对栈进行快照,开销较重
  • 不要用异常控制正常业务流程(比 if/else 慢很多)
  • 高频异常路径考虑复用异常对象(去掉栈快照)

集合框架底层原理

Map 家族对比

特性HashMapLinkedHashMapTreeMapHashtable
线程安全是(粗粒度)
null key/value支持支持key 不支持 null不支持
顺序无序插入顺序 / 访问顺序按 key 排序无序
时间复杂度O(1)O(1)O(log n)O(1)
底层结构数组 + 链表 / 红黑树数组 + 链表 + 双向链表红黑树数组 + 链表

HashMap 底层原理(Java 8+)

数组(桶)
├── bucket[0]: null
├── bucket[1]: Node(k1,v1) → Node(k2,v2)  ← 链表(哈希冲突)
├── bucket[2]: TreeNode(k3,v3)              ← 红黑树(链表长度 >= 8 且容量 >= 64)
└── ...

关键源码逻辑

// 1. hash 扰动函数:高位异或低位,减少哈希冲突
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 2. 定位桶位置:位运算替代取模(容量必须是 2 的幂)
int index = (n - 1) & hash; // 等价于 hash % n,但更快

// 3. 扩容:容量翻倍,重新散列
// 触发条件:size > threshold(threshold = capacity * loadFactor)
// 默认:capacity=16,loadFactor=0.75,threshold=12

关键参数

参数默认值说明
初始容量16必须是 2 的幂
负载因子0.75超过则扩容,建议不修改
树化阈值8链表长度 ≥ 8 且容量 ≥ 64 时树化
退树阈值6红黑树节点数 ≤ 6 时退化回链表

为什么树化:防止哈希碰撞攻击(恶意构造大量相同 hash 的 key,使链表退化为 O(n),导致 CPU 100%)。

预设容量的计算

// 已知存放 N 个元素,预设容量避免扩容
// 需要满足:capacity * 0.75 > N
// 所以:capacity > N / 0.75,向上取 2 的幂
// 存 1000 个元素:capacity = 2048(1000/0.75 ≈ 1334,向上取 2048)
Map<String, Object> map = new HashMap<>(2048);

ConcurrentHashMap 演进

Java 7:分段锁(Segment)

ConcurrentHashMap(默认 16 个 Segment)
├── Segment[0](ReentrantLock)→ HashEntry[]
├── Segment[1](ReentrantLock)→ HashEntry[]
└── ... (并发度 = Segment 数量 = 16)
  • 每个 Segment 独立加锁,并发度 = Segment 数量(默认 16)
  • size() 方法:先无锁尝试 2 次,若 modCount 变化则全段加锁统计

Java 8+:CAS + synchronized(细粒度)

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ...
    for (Node<K,V>[] tab = table;;) {
        if (tab == null) tab = initTable(); // CAS 初始化
        else if ((f = tabAt(tab, i)) == null) {
            if (casTabAt(tab, i, null, new Node<>(hash, key, value))) // 空桶:CAS 无锁写
                break;
        } else {
            synchronized (f) { // 非空桶:只锁当前桶的头节点(更细粒度)
                // ... 链表/红黑树操作
            }
        }
    }
}
维度Java 7Java 8+
锁粒度Segment(16 个)单个桶头节点
锁实现ReentrantLocksynchronized(JVM 优化后性能更好)
计数分段计数LongAdder(Striped64,空间换时间)
初始化构造时初始化lazy-load(首次 put 时初始化)

LinkedHashMap 实现 LRU 缓存

// LinkedHashMap 通过双向链表维护访问顺序,可实现 LRU
Map<String, String> lruCache = new LinkedHashMap<>(16, 0.75f, true) { // true = 按访问顺序
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
        return size() > 100; // 超过 100 个元素时删除最久未访问的
    }
};

IO 模型

BIO / NIO / AIO 对比

模型阻塞性适用场景Java API
BIO(同步阻塞)阻塞连接数少、并发低java.ioSocket
NIO(同步非阻塞)非阻塞高并发、IO 密集java.nio(Channel/Selector/Buffer)
AIO(异步非阻塞)非阻塞超高并发、长连接java.nio.2(AsynchronousChannel)

NIO 核心组件

// NIO 多路复用服务器示例
Selector selector = Selector.open();
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress(8888));
serverSocket.configureBlocking(false); // 非阻塞模式

serverSocket.register(selector, SelectionKey.OP_ACCEPT); // 注册感兴趣的事件

while (true) {
    selector.select(); // 阻塞直到有 Channel 就绪
    Set<SelectionKey> keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        if (key.isAcceptable()) {
            // 处理新连接
        } else if (key.isReadable()) {
            // 处理读事件
        }
    }
    keys.clear();
}

NIO 三大核心

  • Buffer:数据容器,支持 flip()(写转读)、clear()(重置)
  • Channel:双向数据通道,支持非阻塞模式
  • Selector:多路复用器,底层依赖 Linux epoll / Windows iocp

BIO 的扩展性问题:每个连接一个线程,线程是重量级资源,连接数增大时线程上下文切换开销急剧上升。NIO 用单线程管理多个 Channel,解决了这个问题。

并发核心

synchronized vs ReentrantLock

维度synchronizedReentrantLock
实现层次JVM 内置,字节码 monitorenter/monitorexitJava 代码,基于 AQS
可中断不支持lockInterruptibly() 支持
超时获取不支持tryLock(timeout) 支持
公平锁不支持new ReentrantLock(true)
条件变量一个(wait/notify多个(Condition
性能Java 6+ 优化后与 Lock 相当高并发下略优
// ReentrantLock 使用规范:必须在 finally 中 unlock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock(); // 必须在 finally,防止异常时死锁
}

// 读写锁:读多写少场景
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();   // 读锁(共享)
rwLock.writeLock().lock();  // 写锁(排他)

synchronized 锁升级

无锁 → 偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(OS Mutex)
  • 偏向锁:只有一个线程访问时,记录线程 ID,无需 CAS
  • 轻量级锁:多线程交替访问,CAS 自旋,不阻塞
  • 重量级锁:多线程竞争激烈,挂起等待,OS 介入

CAS 与原子类

// AtomicInteger 底层:Unsafe.compareAndSwapInt(CAS 操作)
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();                     // 原子自增
count.compareAndSet(1, 2);                   // CAS:期望值 1,更新为 2

// 高并发计数推荐:LongAdder(分段累加,比 AtomicLong 更高效)
LongAdder adder = new LongAdder();
adder.increment();
long total = adder.sum();

CAS 的 ABA 问题:值从 A 变 B 再变回 A,CAS 无法感知中间变化。解决:使用 AtomicStampedReference 带版本号的 CAS。

线程池核心参数

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                                    // corePoolSize:核心线程数
    8,                                    // maximumPoolSize:最大线程数
    60, TimeUnit.SECONDS,                 // keepAliveTime:空闲线程存活时间
    new LinkedBlockingQueue<>(1000),      // workQueue:任务队列
    new ThreadPoolExecutor.CallerRunsPolicy() // rejectedHandler:拒绝策略
);

拒绝策略对比

策略行为
AbortPolicy(默认)抛出 RejectedExecutionException
CallerRunsPolicy由调用者线程执行(降速,不丢任务)
DiscardPolicy静默丢弃
DiscardOldestPolicy丢弃队列最老的任务

⚠️ 不要用 Executors.newCachedThreadPool():最大线程数为 Integer.MAX_VALUE,高并发时会创建大量线程导致 OOM。

JVM 内存模型

内存区域划分

JVM 内存
├── 线程私有
│   ├── 程序计数器(PC Register):当前字节码行号,唯一不会 OOM 的区域
│   ├── 虚拟机栈(VM Stack):栈帧(局部变量表、操作数栈、返回地址)
│   └── 本地方法栈(Native Method Stack):native 方法
└── 线程共享
    ├── 堆(Heap):对象实例、数组,GC 主要区域
    │   ├── 年轻代(Eden + Survivor)
    │   └── 老年代(Old Generation)
    └── 方法区(Metaspace,Java 8+):类信息、常量、静态变量
        └── 运行时常量池

OOM 类型对应区域

OOM 类型对应区域常见原因
Java heap space对象过多、内存泄漏
GC overhead limit exceededGC 时间超过 98%,回收不足 2%
Metaspace方法区动态生成大量类(如 Groovy、CGLIB)
StackOverflowError虚拟机栈无限递归
Direct buffer memory直接内存NIO 直接内存分配过多

happen-before 规则

happen-before 是 JMM(Java Memory Model)的核心概念:如果操作 A happen-before 操作 B,则 A 的结果对 B 可见。

8 条规则(记住前 4 条最重要)

  1. 程序顺序规则:同一线程内,前面的操作 happen-before 后面的操作
  2. 监视器锁规则unlock happen-before 后续的 lock
  3. volatile 规则:对 volatile 变量的写 happen-before 后续的读
  4. 线程启动规则Thread.start() happen-before 线程内的任何操作
  5. 线程终止规则:线程内所有操作 happen-before Thread.join() 返回
  6. 线程中断规则:interrupt() happen-before 检测到中断
  7. 对象终结规则:构造函数结束 happen-before finalize()
  8. 传递性:A happen-before B,B happen-before C,则 A happen-before C

volatile 的两个保证

  • 可见性:写操作立即刷新到主内存,读操作从主内存读取
  • 禁止指令重排:通过内存屏障实现

GC 与调优

垃圾收集器对比

收集器适用代算法特点
Serial年轻代复制单线程,适合小堆、客户端
ParNew年轻代复制Serial 多线程版,配合 CMS
Parallel Scavenge年轻代复制吞吐量优先
Serial Old老年代标记-整理Serial 老年代版
CMS老年代标记-清除低停顿,但有碎片
G1全堆标记-整理可预测停顿,Java 9 默认
ZGC全堆着色指针停顿 < 10ms,Java 15+ 生产就绪

GC 调优思路

1. 确认问题
   ├── jstat -gcutil <pid> 1000:观察各代 GC 频率和耗时
   └── 开启 GC 日志:-Xloggc:gc.log -XX:+PrintGCDetails

2. 分析原因
   ├── Full GC 频繁 → 老年代空间不足 / 大对象直接进老年代
   ├── Young GC 频繁 → Eden 区太小 / 对象存活率高
   └── GC 停顿长 → 堆太大 / 碎片化 / 收集器选择不当

3. 调整策略
   ├── 增大堆:-Xmx / -Xms(避免动态扩容)
   ├── 调整代比例:-XX:NewRatio / -XX:SurvivorRatio
   ├── 切换收集器:G1(-XX:+UseG1GC)/ ZGC(-XX:+UseZGC)
   └── 设置停顿目标:-XX:MaxGCPauseMillis=200(G1)

4. 生产必备参数
# 生产环境推荐 JVM 参数
java -Xmx4g -Xms4g \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/app/ \
     -XX:+PrintGCDateStamps \
     -XX:+PrintGCDetails \
     -Xloggc:/var/log/app/gc.log \
     -XX:+UseGCLogFileRotation \
     -XX:NumberOfGCLogFiles=10 \
     -XX:GCLogFileSize=100M \
     -jar app.jar

反射与动态代理

反射基础

// 获取 Class 对象的三种方式
Class<?> clazz1 = String.class;                    // 类字面量(编译期)
Class<?> clazz2 = "hello".getClass();              // 实例方法(运行时)
Class<?> clazz3 = Class.forName("java.lang.String"); // 动态加载(运行时)

// 反射操作
Class<?> clazz = UserService.class;
Method method = clazz.getDeclaredMethod("findById", Long.class);
method.setAccessible(true); // 绕过访问控制
Object result = method.invoke(userServiceInstance, 1L);

JDK 动态代理 vs CGLIB

// JDK 动态代理:基于接口,被代理类必须实现接口
public interface UserService { User findById(Long id); }

UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class},
    (proxyObj, method, args) -> {
        System.out.println("Before: " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("After: " + method.getName());
        return result;
    }
);
// CGLIB:基于继承,生成子类,无需接口
// Spring 默认使用 CGLIB 代理(对于没有接口的类)
// 限制:final 类和 final 方法无法被代理
维度JDK 动态代理CGLIB
要求目标类必须实现接口无接口要求
实现基于接口,反射调用继承目标类,字节码增强
性能反射调用有开销字节码直接调用,更快
限制只能代理接口方法final 类/方法不可代理
Spring 使用有接口时默认使用无接口或 proxyTargetClass=true

Spring AOP 原理@Transactional@Async 等注解通过 AOP 实现,本质是动态代理。这就是为什么 this.xxx() 调用无法触发事务——this 是原始对象,不是代理对象。

泛型与类型擦除

类型擦除

Java 泛型在编译期实现,运行时类型信息被擦除(Type Erasure)。

// 编译前
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);

// 编译后(类型擦除)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 编译器自动插入强制转换

类型擦除的影响

// 以下代码编译报错:泛型擦除后两个方法签名相同
public void process(List<String> list) { }
public void process(List<Integer> list) { } // 编译错误!

// 运行时无法获取泛型类型
List<String> list = new ArrayList<>();
list.getClass() == List.class; // true,不是 List<String>.class

// 绕过类型检查(不推荐)
List rawList = list;
rawList.add(42); // 编译通过,运行时 ClassCastException

泛型通配符

// ? extends T(上界):只读,不可写(Producer Extends)
List<? extends Number> nums = new ArrayList<Integer>();
Number n = nums.get(0); // 可以读
nums.add(1); // 编译错误,不可写(不知道具体类型)

// ? super T(下界):可写,读出来是 Object(Consumer Super)
List<? super Integer> list = new ArrayList<Number>();
list.add(1); // 可以写 Integer 或其子类
Object obj = list.get(0); // 读出来只能是 Object

// PECS 原则:Producer Extends, Consumer Super
// 从集合读数据 → extends;向集合写数据 → super

类加载机制

双亲委派模型

Bootstrap ClassLoader(启动类加载器)
    ↑ 委派
Extension ClassLoader(扩展类加载器)
    ↑ 委派
Application ClassLoader(应用类加载器)
    ↑ 委派
自定义 ClassLoader

加载流程

  1. 收到加载请求,先委派给父加载器
  2. 父加载器无法加载时,自己才尝试加载
  3. 确保核心类(如 java.lang.String)只由 Bootstrap 加载,防止被篡改

打破双亲委派的场景

  • Thread.setContextClassLoader():SPI 机制(如 JDBC Driver)
  • OSGi 模块化框架:每个模块有独立类加载器
  • 热部署:Tomcat 每个 WebApp 有独立类加载器

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论