Loading

【WebWiki】03 · Java 核心

01 Java 核心

WebWiki · 基础篇

面向有编程基础、需要夯实 Java 底层的工程师。本文把"面试常考 + 生产必懂"的 JVM、集合、并发、GC 串起来讲,所有代码块带中文注释。

1. 版本选择与 LTS

版本 状态 关键特性
JDK 8 存量最大 Lambda、Stream、Optional、新时间 API
JDK 11 (LTS) 主流基线 var 局部变量推断、HttpClient、ZGC(实验)
JDK 17 (LTS) 当前主流 sealed 密封类、record、switch 模式匹配、Text Block
JDK 21 (LTS) 新趋势 虚拟线程(Virtual Threads)、Record Patterns、分代 ZGC

新项目建议 JDK 17 起步,追求高并发吞吐可直接上 21 的虚拟线程。

// JDK 17 之后的现代写法示例
public record Point(int x, int y) {}          // record:一行定义不可变数据类(自动生成 getter/equals/hashCode/toString)

String desc = switch (shape) {                // switch 表达式 + 模式匹配
    case Circle c -> "圆,半径 " + c.radius();
    case Square s -> "方,边长 " + s.side();
    default -> "未知图形";
};

String sql = """                               // 文本块(Text Block),多行字符串免转义
    SELECT id, name FROM user
    WHERE age > %d
    """.formatted(18);

为什么企业还在用 8? 大量老框架(尤其某些商业中间件)只认证到 8;且 8 的 GC、lambda 已能满足多数业务。新系统若无历史包袱,直接 17/21。注意 JDK 8u 之后 Oracle 商业收费,生产建议用 Adoptium(Temurin)/Zulu 等 OpenJDK 发行版。

2. JVM 内存区域与类加载

2.1 运行时数据区

┌──────────────────────────────────────────────────────────────┐
│  线程私有(随线程生灭,不存在并发竞争)                          │
│   · 程序计数器:当前字节码行号,唯一不会 OOM 的区域              │
│   · 虚拟机栈:每个方法调用建一个栈帧(局部变量表/操作数栈/返回) │
│   · 本地方法栈:native 方法栈                                    │
├──────────────────────────────────────────────────────────────┤
│  线程共享                                                        │
│   · 堆:对象实例 + 数组,GC 主战场                              │
│       ├─ 新生代:Eden + S0 + S1(对象朝生夕死)                 │
│       └─ 老年代:存活久的对象                                    │
│   · 方法区/元空间(Metaspace):类元数据、常量池、静态变量        │
│       (JDK8 起永久代 PermGen 改为本地内存的 Metaspace,调参用  │
│        -XX:MaxMetaspaceSize,默认不限易撑爆本地内存)           │
├──────────────────────────────────────────────────────────────┤
│  直接内存:NIO 的 DirectByteBuffer,不受堆 GC 管理,受 -XX:MaxDirectMemorySize 限制 │
└──────────────────────────────────────────────────────────────┘

栈溢出 vs 堆溢出:无限递归 → StackOverflowError(栈);new 对象不释放 + 堆不够 → OutOfMemoryError: Java heap space。

2.2 类加载过程(双亲委派)

加载 → 链接(验证/准备/解析)→ 初始化。类加载器采用双亲委派模型:子加载器收到加载请求先委托父加载器,父找不到才自己加载。

  • 启动类加载器(Bootstrap,C++,加载 rt.jar)
  • 扩展类加载器(Platform)
  • 应用类加载器(AppClassLoader,加载 classpath)
  • 自定义加载器

好处:① 防止核心类被篡改(如自己写个 java.lang.String 不会被加载);② 避免重复加载。

破坏双亲委派的场景:SPI(如 JDBC 4.0 ServiceLoader)、Tomcat 的 WebAppClassLoader(隔离不同 webapp)、OSGi。Spring Boot 的 LaunchedURLClassLoader 也做了定制以支持 fat jar 内 classes 加载。

2.3 对象创建与内存布局

new 一个对象:类加载检查 → 分配内存(指针碰撞/空闲列表)→ 初始化零值 → 设置对象头(Mark Word + 类型指针)→ 执行 <init>。对象头里的 Mark Word 存哈希码、GC 分代年龄、锁状态标志——这正是后面 synchronized 锁升级的存储位置。

3. 集合框架底层

3.1 HashMap(JDK 8 实现)

数据结构:数组(桶)+ 链表 + 红黑树。

  • 默认容量 16,负载因子 0.75 → 阈值 12 触发扩容;
  • 哈希定位:(n - 1) & hash,容量恒为 2 的幂才能用位运算代替取模;
  • 扰动函数:hash = h ^ (h >>> 16),高位也参与运算,减少哈希碰撞;
  • 树化条件:链表长度 > 8 且 数组长度 >= 64 才转红黑树;否则优先扩容(树化有结构成本)。长度 <= 6 时退化为链表;
  • 扩容 rehash:容量翻倍,元素要么留在原索引、要么迁移到 原索引 + 旧容量(用 e.hash & oldCap 判断高位,无需重新计算 hash)。
// 面试常写:手写简化版 get(体现位运算定位 + 链表/树遍历思路)
Node<K,V> getNode(int hash, K key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {     // 位运算定位桶
        if (first.hash == hash && eq(first.key, key)) return first;  // 桶头命中
        if ((e = first.next) != null)
            return first instanceof TreeNode ? ((TreeNode<K,V>)first).getTreeNode(hash, key)
                                             : traverseList(e, hash, key); // 链表遍历
    }
    return null;
}

为什么线程不安全? 多线程 put 可能数据覆盖、扩容时链表成环(JDK7 的死循环 bug,JDK8 改为尾插法已修复成环但仍有丢失/覆盖问题)。并发场景一律 ConcurrentHashMap 或 Collections.synchronizedMap。

3.2 ConcurrentHashMap

  • JDK 7:Segment 分段锁(继承 ReentrantLock),默认 16 段,并发度 16;
  • JDK 8:取消 Segment,改用 Node + synchronized + CAS(synchronized 经锁升级后并不慢,且省去锁对象内存开销),数组每个桶头用 synchronized 保护写,读几乎无锁(volatile 保证可见性);
  • 同样有链表转红黑树(阈值 8);size()/mappingCount() 用 baseCount + CounterCell[] 分段计数,避免全局锁。
// JDK8 put 核心思路(伪代码)
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException(); // 不允许 null(HashMap 允许)
    int hash = spread(key.hashCode());
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null) initTable();                       // 懒初始化
        else if ((f = tabAt(tab, i = (n-1)&hash)) == null) {
            if (casTabAt(tab, i, null, new Node<>(hash, key, value))) break; // 桶空:CAS 无锁插入
        }
        else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f);        // 正在扩容:协助迁移
        else {
            synchronized (f) {                              // 桶头加锁(只锁当前桶,粒度极细)
                if (tabAt(tab, i) == f) { /* 链表/树插入或覆盖 */ }
            }
        }
    }
    addCount(1L, binCount);                                // 计数 + 可能触发扩容
    return null;
}

3.3 List / Set / Queue 选型

接口 实现 特点 / 场景
List ArrayList / LinkedList / CopyOnWriteArrayList 数组随机读 / 双链表插删 / 并发读多写少(写时复制,读无锁,适合监听器列表)
Map HashMap / LinkedHashMap / TreeMap / ConcurrentHashMap 哈希 O(1) / 保插入序(LRU 可用) / 排序 / 并发安全
Set HashSet / TreeSet 去重 / 排序去重
Queue ArrayDeque / PriorityQueue / LinkedBlockingQueue 栈与队列 / 堆 / 线程池任务队列
// HashMap 扩容:默认容量 16,负载因子 0.75,链表长度 >8 且表长 >=64 时树化
Map<Integer, String> map = new ConcurrentHashMap<>(64);   // 指定初始容量减少扩容

// 不可变集合(JDK9+):安全发布、防御性拷贝利器
List<String> fixed = List.of("a", "b", "c");

// LinkedHashMap 实现 LRU:重写 removeEldestEntry
LinkedHashMap<String, Object> lru = new LinkedHashMap<>(16, 0.75f, true) {
    @Override protected boolean removeEldestEntry(Map.Entry<String, Object> e) {
        return size() > 100;   // 超过 100 个淘汰最久未访问
    }
};

4. Stream 与函数式

// 常用流水线:过滤 → 映射 → 分组
Map<String, List<String>> byDept = employees.stream()
        .filter(e -> e.getAge() > 25)
        .sorted(Comparator.comparing(Employee::getSalary).reversed())
        .collect(Collectors.groupingBy(Employee::getDept,
                 Collectors.mapping(Employee::getName, toList())));

// 分区与统计
Map<Boolean, List<Employee>> parts = list.stream()
        .collect(Collectors.partitioningBy(e -> e.getSalary() > 20000));
DoubleSummaryStatistics st = list.stream()
        .mapToDouble(Employee::getSalary).summaryStatistics();

注意:Stream 中避免副作用(不要在 lambda 里改外部变量);parallelStream 只在无状态、CPU 密集型任务使用,Web 请求处理慎用(默认用 ForkJoinPool.commonPool,一个任务阻塞会拖垮全局)。

5. 并发编程

5.1 线程池(生产必用,禁用 Executors 快捷方法)

Executors.newFixedThreadPool / newCachedThreadPool 隐藏了队列/线程数上限,可能 OOM。一律手动 new ThreadPoolExecutor:

ThreadPoolExecutor pool = new ThreadPoolExecutor(
        8, 16, 60, TimeUnit.SECONDS,                 // 核心8 / 最大16 / 空闲回收60s
        new ArrayBlockingQueue<>(1000),              // 有界队列,防止无限堆积 OOM
        new ThreadFactoryBuilder().setNameFormat("biz-%d").build(), // 命名线程,排查利器
        new ThreadPoolExecutor.CallerRunsPolicy());  // 拒绝策略:调用者线程自己跑,保住不入队
参数 说明
corePoolSize / maximumPoolSize 核心 / 最大线程数(CPU 密集 ≈ N+1,IO 密集 ≈ 2N)
keepAliveTime 非核心线程空闲回收时间
workQueue 任务队列,有界队列防止 OOM
threadFactory 给线程命名,出问题时 jstack 一眼定位业务
RejectedExecutionHandler Abort(抛异常)/ CallerRuns(调用者执行)/ Discard(直接丢)/ DiscardOldest(丢最老)/ 自定义(落库重试)

执行流程:提交任务 → 核心线程未满则建核心线程 → 满了进队列 → 队列满且未达最大线程则建非核心线程 → 都满则触发拒绝策略。

5.2 锁:synchronized 锁升级

synchronized 是 JVM 内置锁,锁状态存在对象头 Mark Word,经历无锁 → 偏向锁 → 轻量级锁 → 重量级锁的升级:

  • 偏向锁:第一个线程进来打标记,之后该线程进出无需 CAS(无竞争时几乎零成本);
  • 轻量级锁:有第二个线程竞争,偏向锁撤销,用 CAS 自旋抢锁(不挂起线程,适合锁持有极短);
  • 重量级锁:自旋超时/竞争激烈,膨胀为操作系统互斥量(monitor),未抢到的线程阻塞(涉及用户态→内核态切换,开销大)。

JDK 15 起默认禁用偏向锁(-XX:-UseBiasedLocking),因为维护偏向的撤销成本高、现代多核竞争激烈时收益低。理解"锁升级"仍有助于读懂 monitor 与性能问题。

// synchronized 三种用法
public synchronized void method() { /* 锁 this */ }
public static synchronized void staticMethod() { /* 锁 Class 对象 */ }
void block() { synchronized (lockObj) { /* 锁指定对象,粒度更可控 */ } }

5.3 volatile:可见性 + 禁止重排

volatile 保证:① 一个线程写、其他线程立即可见(绕过工作内存);② 禁止指令重排序(通过内存屏障)。但不保证原子性(i++ 仍非线程安全,需 AtomicInteger 或锁)。

// 经典双重检查单例:volatile 防止指令重排导致拿到"半初始化"对象
class Singleton {
    private static volatile Singleton instance;     // 必须 volatile
    static Singleton get() {
        if (instance == null) {                       // 第一次检查(无锁,快)
            synchronized (Singleton.class) {
                if (instance == null)                 // 第二次检查(持锁,防重复创建)
                    instance = new Singleton();       // new 可能被重排:分配内存→赋值引用→初始化
            }                                         // 若没 volatile,其他线程可能读到未初始化的引用
        }
        return instance;
    }
}

5.4 AQS:并发工具基石

AbstractQueuedSynchronizer(AQS)是 ReentrantLock、Semaphore、CountDownLatch、ThreadPoolExecutor Worker 等的底层框架。核心:一个 state 同步状态(volatile)+ 一个 CLH 双向等待队列。

  • 独占模式(如 ReentrantLock):tryAcquire 改 state,失败则入队park;
  • 共享模式(如 Semaphore/CountDownLatch):tryAcquireShared 允许多个线程通过。
// ReentrantLock 加锁本质(伪代码)
lock() {
    if (!tryAcquire(1)) acquire(null);   // state 从 0→1 成功则拿到锁;失败入队并 park 挂起
}
unlock() {                               // state→0,唤醒队列后继
    if (tryRelease(1)) unparkSuccessor(head);
}

synchronized vs ReentrantLock 怎么选? 大多数场景 synchronized 够用且不用手动释放;需要 tryLock(timeout) 超时、公平锁、多 Condition 精细唤醒、可中断锁时再用 ReentrantLock。

5.5 JUC 常用工具

CountDownLatch latch = new CountDownLatch(3);        // 等待 N 个任务完成(计数到0放行)
Semaphore rate = new Semaphore(10);                  // 限流:最多 10 并发
CyclicBarrier barrier = new CyclicBarrier(3);        // 多线程互相等齐(可重复使用)

// CompletableFuture 异步编排(接口聚合场景)
CompletableFuture<User> userF = CompletableFuture.supplyAsync(() -> getUser(id), pool);
CompletableFuture<List<Order>> orderF = CompletableFuture.supplyAsync(() -> listOrders(id), pool);
UserDetail detail = userF.thenCombine(orderF, UserDetail::new).join();  // 两结果汇合

5.6 ThreadLocal 与内存泄漏

ThreadLocal 做线程隔离(用户上下文、TraceId)。但线程池环境下线程复用,用完必须 remove(),否则旧线程的 value 一直被强引用导致内存泄漏(key 是弱引用会被 GC,value 是强引用不会)。

private static final ThreadLocal<User> CTX = new ThreadLocal<>();
void handle(Request req) {
    CTX.set(req.getUser());
    try { doBiz(); } finally { CTX.remove(); }  // 务必清理,否则线程复用时串号/泄漏
}

5.7 虚拟线程(JDK 21)

// 百万级并发 IO 任务的银弹:由 JVM 调度,阻塞时不占用 OS 线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(1000);          // 模拟 HTTP/DB 调用,虚拟线程在此挂起而非占用内核线程
            return i;
        }));
}

虚拟线程适合"每请求一线程"的高 IO 并发;但 synchronized 内阻塞会让载体线程(carrier)无法让出,建议虚拟线程里用 ReentrantLock 替代 synchronized 做阻塞临界区。

6. JVM 内存与 GC(垃圾回收)

6.1 对象存活判定

  • 引用计数:对象被引用次数,为 0 可回收(循环引用无法回收,Java 不用);
  • 可达性分析:从 GC Roots(栈帧局部变量、静态变量、JNI 等)出发向下搜,不可达即死。GC Roots 不可达的对象先进入"缓刑",经两次标记(finalize 至多自救一次)才真正回收。

6.2 四种引用

引用 回收时机
强引用 Object o = new Object() 永不回收(OOM 也不回收)
软引用 SoftReference 内存不足时回收(缓存场景)
弱引用 WeakReference 下次 GC 必回收(ThreadLocal 的 key、WeakHashMap)
虚引用 PhantomReference 仅用于回收通知(堆外内存清理)

6.3 分代收集与 GC 算法

  • 年轻代:复制算法(Eden → S0/S1 来回拷,存活少的对象代价低);
  • 老年代:标记-整理 / 标记-清除(CMS 用清除、G1/ZGC 用整理);
  • 对象经历一次 Minor GC 年龄 +1,默认到 15(-XX:MaxTenuringThreshold)晋升老年代;大对象(如大数组)直接进老年代(-XX:PretenureSizeThreshold)。

6.4 主流收集器对比(CMS / G1 / ZGC)

收集器 算法 停顿 适用场景 现状
Serial / Parallel 标记-整理 Parallel 停顿长(吞吐优先) 批处理、后台任务 稳定但慢
CMS 标记-清除(并发) 较短,但易碎片化、CPU 敏感、Full GC 停顿久 JDK8 老系统 JDK9 废弃、JDK14 移除,新项目别用
G1 Region + 标记-整理 可预测停顿(-XX:MaxGCPauseMillis,默认 200ms) 大堆(数 G~数十 G)通用默认 JDK9+ 服务端默认
ZGC 着色指针 + 读屏障,并发 < 1ms,几乎与堆大小无关 低延迟大内存(TB 级)服务 JDK15 生产可用、JDK21 分代 ZGC

CMS 为什么被弃? CMS 并发标记清理会产生内存碎片,碎片多到无法分配大对象时触发代价极高的 Serial Old Full GC("Concurrent Mode Failure"),停顿不可控。G1 以 Region 为单位整理,规避碎片化;ZGC 用染色指针把几乎全部工作并发化,停顿不随堆增长。

# 常见 GC 调优参数示例(G1)
java -jar app.jar \
  -XX:+UseG1GC \
  -Xms4g -Xmx4g \
  -XX:MaxGCPauseMillis=200 \
  -XX:+HeapDumpOnOutOfMemoryError \        # OOM 自动留快照,排错必备
  -XX:HeapDumpPath=/data/dumps/ \
  -Xlog:gc*:file=/data/logs/gc.log:time     # JDK9+ 统一 GC 日志

6.5 排查工具链

工具 用途
jps 列出 Java 进程 PID
jstack 线程栈快照,查死锁/阻塞
jmap -histo / -dump 堆对象统计 / 导出堆转储
jstat -gc 实时看 GC 次数与耗时
arthas 在线诊断:watch/tt 观测方法入参返回值、热修复、trace 耗时
MAT / VisualVM 离线分析 hprof 堆转储,找内存泄漏对象
# 一行定位最耗 CPU 的线程(生产常用)
top -Hp <pid>            # 看哪个线程 CPU 高,记下 tid(十进制)
printf "%x\n" <tid>      # 转十六进制 nid
jstack <pid> | grep -A 20 <nid>   # 在栈里定位到业务代码行

7. 常用工具库

库 用途
Lombok @Data @Builder @Slf4j 消灭样板代码
Hutool 国产工具集:日期、加密、HTTP、Excel
Guava 缓存、限流 RateLimiter、集合扩展
MapStruct 高性能 Bean 拷贝(编译期生成,比 BeanUtils 反射快得多)
Jackson/Fastjson2 JSON 序列化(Fastjson2 修复了老版诸多安全/性能问题)
commons-lang3 / io 字符串、IO 工具

8. 小结

  • JVM 内存:堆是 GC 主战场,Metaspace 替代 PermGen 且受本地内存限制;
  • 集合:HashMap 数组+链表+红黑树(8/64 树化),并发用 ConcurrentHashMap(JDK8 是 synchronized+CAS 细粒度锁);
  • 并发:线程池必须手写参数(有界队列 + 命名线程 + 合理拒绝策略);synchronized 经历锁升级,volatile 管可见性/重排不管原子,AQS 是锁与同步器基石,ThreadLocal 用完 remove;
  • GC:新项目 G1 起步、低延迟大内存上 ZGC,杜绝 CMS;OOM 前务必留 HeapDump;
  • 工具:jstack/jmap/arthas 是线上排错三板斧,MapStruct 替代反射拷贝。

下一篇《04-Spring Boot》在 JVM 与并发之上,讲容器、AOP、自动配置与事务等企业级框架机制。

posted @ 2026-09-19 05:40  Stellan7  阅读(10)  评论(0)    收藏  举报