【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、自动配置与事务等企业级框架机制。

浙公网安备 33010602011771号