这是「Java 从入门到精通」系列的第 18 篇,也是收官篇。写到这里,语法、集合、并发、JVM 这些”知识点”其实都只是入场券——真正决定一个工程师上限的,是他能不能把代码稳定地、可复现地、可观测地交付出去。本篇按”构建 → 规范 → 测试 → 日志 → 调优 → 收官”的顺序,讲清工具背后的原理,而不是给你一份工具清单。建议配合前面的篇章一起读:知道了 HashMap 怎么扩容,才知道为什么要给集合预设初始容量;知道了 JIT 会消除死代码,才知道为什么 System.currentTimeMillis() 测出来的性能十有八九是假的。
一、工程结构与构建:从”能编译”到”可复现”1.1 坐标、依赖范围与依赖传递Maven 的一切都建立在坐标(GAV)之上。坐标不只是”下载地址”,它是构件的唯一身份;仓库(本地仓库、私服、中央仓库)本质是”坐标 → 构件文件”的映射表,而 Maven 的核心工作,就是把 POM 里声明的坐标解析成一棵依赖树,再按这棵树拼出 classpath。
真正容易被忽略、却天天在影响你的是依赖范围(scope):它决定依赖在哪些 classpath 生效、 ...
本文是「Java 从入门到精通」系列的第 17 篇,假定你已经掌握集合框架、泛型、异常与并发基础(对应本系列第(七)(十二)(十三)篇)。全文代码以 JDK 21 为准,涉及预览特性的部分会用 --enable-preview 标注。阅读建议:Lambda 与 Stream 两章请务必动手跑一遍代码,尤其是 peek 不执行、流一次性消费、toMap 键冲突这三个坑,只有亲眼见过异常栈才会真正记住。
一、版本演进全景:从 Java 8 到 Java 212014 年发布的 Java 8 是 Java 历史上最重要的分水岭:它把函数式编程正式引入 Java 语法,Lambda、Stream、默认方法、新的日期时间 API 四件套同时落地。此后 Oracle 改为六个月一个版本的发布节奏,每两年挑一个版本作为 LTS(长期支持版)。到今天,Java 8 已经全面停止免费商业支持,而 Java 21 成为新一代的主流基线。
1.1 关键特性时间线
版本
发布时间
LTS
关键特性(各取 2-3 个)
Java 8
2014-03
是
Lambda 表达式、Stream API ...
本文默认读者已掌握 Java 基础与多线程,JVM 以 HotSpot 为主,JDK 横跨 8 / 11 / 17 / 21,会明确标注版本差异——尤其是 GC 日志格式(JDK 9 前后完全两套)与默认收集器的变更。建议边读边跑第五节的日志实验与第七节的 CPU 飙高实验:调优从来不是背参数,而是”看到数据—提出假设—小步验证”的闭环。
一、GC 到底要解决什么问题1.1 为什么不是引用计数判断对象”还活着没有”,最朴素的想法是引用计数(Reference Counting):每个对象维护计数器,有人引用 +1,失效 -1,归零立刻回收。Python、PHP、Objective-C ARC 都用这套方案,实现简单、回收及时。但它有致命缺陷——无法处理循环引用,两个对象互相持有对方时计数永远不为 0;且每次赋值都要改计数器,多线程下还要保证原子性。HotSpot 因此放弃了它。
123456789101112131415161718192021/** * 演示循环引用:在引用计数方案下这两个对象永远不会被回收。 * HotSpot 使用可达性分析,因此 GC 后它们会被正常回收。 * ...
本篇属于 进阶向硬核内容。文中所有代码均在 JDK 17(HotSpot 64-Bit Server VM) 上验证过,涉及 JDK 8 / 11 / 21 差异的地方会单独标注。想真正吃透,请把示例代码敲一遍,尤其是对象布局与自定义类加载器那两段。
写在前面:为什么要学这一块很多同学写 Java 三五年,对 JVM 的认知停留在「堆放对象、栈放局部变量、方法区放类信息」。这三句话没错,但粗到无法支撑下面这些问题:8C16G 的容器 -Xmx 该设多大(设小了频繁 Full GC、设大了被 OOMKilled);一个 new Object() 在 64 位机器上占几个字节;为什么 -Xmx 还剩一大截却抛 Metaspace 的 OOM;Tomcat 里两个 WebApp 各引一个不同版本的 Spring 为什么不打架。答案全在运行时数据区和类加载子系统里。
一、JVM 整体架构:规范与实现的分野1.1 一个必须厘清的前提:规范不等于实现《Java Virtual Machine Specification》定义的是抽象行为契约:Class 文件格式、字节码指令集、运行时数据区、类加 ...
本文默认读者已掌握线程基础与 synchronized/volatile(可参考本系列第十二篇)。JDK 以 8/11 为主,第八节虚拟线程需 JDK 21+。文中源码片段是对 OpenJDK 真实实现的简化改写:保留了位运算、CAS、锁与循环的关键结构,删掉了异常分支与注释噪音,便于阅读但不建议直接拷贝编译。所有 Demo 均可 javac 运行,建议把第三节的位运算实验与第六节的聚合 Demo 亲手跑一遍。
并发能提升吞吐,但”每来一个请求就 new Thread()“是把并发用成了灾难。第十二篇解决了”多个线程如何正确共享数据”,本篇解决另一个维度的问题:线程本身作为一种昂贵资源,该如何被复用、被管控、被观测。从 ThreadPoolExecutor 的位运算源码,到 CompletableFuture 的回调链编排,再到 JDK 21 虚拟线程对这套模型的颠覆,是一条从”会用”到”懂原理”再到”知道何时不该用”的完整路径。
一、为什么要线程池:先把线程的成本算清楚1.1 一个线程到底有多贵JDK 21 之前,java.lang.Thread 采用的是 1:1 线程模型:一个 ...
本文默认读者已掌握线程生命周期、JMM 与 synchronized/volatile(系列第十二篇),JDK 以 8 为主,涉及 9 之后的变化(Unsafe 迁往 VarHandle)会单独标注。AQS 一节最硬,建议按”state 是什么 → 队列怎么排 → 线程怎么睡 → 谁把它叫醒”四步推演,读完再回头看 ReentrantLock、CountDownLatch、Semaphore,会发现它们只是同一套骨架上的不同血肉。
一、JUC 全景与定位靠 synchronized 与 volatile 能解决大部分并发问题,但它们有两个天然短板:一是不可中断——等监视器锁时无法响应 interrupt(),死锁只能重启;二是不够灵活——没有尝试加锁、没有超时、没有读写分离、也没有多个等待队列,想做”生产者只唤醒消费者”这种精细控制,wait/notify 只能 notifyAll 全量广播。
JUC(java.util.concurrent)正是为了补上这块拼图。它由 Doug Lea 主导设计,从 JDK 5 引入,核心思路是:把并发控制的公共骨架抽出来做成可复用组件,让业务代码 ...
本文默认读者已掌握 Java 基础语法与集合框架,JDK 以 8 为主,同时标注 JDK 15 之后偏向锁被废弃带来的变化。文中代码均可直接运行,建议边读边跑,尤其是第三节 i++ 复现实验与第七节死锁实验——只有亲眼看到”结果不等于 20000”和”jstack 打印出 deadlock”,抽象的内存模型概念才会落地。
一、为什么要并发:从摩尔定律失效说起过去二十年程序员享受过一段”免费的午餐”:CPU 主频从几百 MHz 涨到几 GHz,同样的代码不改一行,明年就自动变快。但 2005 年前后这条曲线断了——受制于功耗墙与散热极限,单核主频停滞在 4GHz 附近,厂商转而把晶体管堆成多核。于是”免费午餐”结束:想变快,就得自己写并发。
并发(Concurrency)与并行(Parallelism)经常被混用,但层次不同。并发指”逻辑上同时处理多件事”,强调结构与调度,单核上靠时间片轮转也能实现;并行指”物理上真正同时执行多条指令”,必须依赖多核。可以说并行是并发在硬件上的一种执行形态,是并发的子集。
维度
并发 Concurrency
并行 Parallelism
...
本篇属于「知其然更知其所以然」的一章。反射、注解、动态代理这三样东西,日常写业务代码几乎碰不到,但只要你点开 Spring 的 AutowiredAnnotationBeanPostProcessor、MyBatis 的 MapperProxy、Jackson 的 BeanDeserializer,就会发现它们无处不在。理解这三块基石,你读框架源码时就不会再迷路,也能在真正需要”通用能力”的场景里写出靠谱的底层工具,而不是把反射当成万能胶乱抹一气。阅读建议:跟着代码敲一遍,尤其是第六节的 DI 容器和第七节的代理切面,跑通之后很多面试题会自然解开。
写在前面:为什么这三个东西要一起讲很多教材把反射、注解、动态代理拆成三章,结果读者学完只记住了三套 API,串不起来。实际上它们是一条流水线上的三道工序:注解负责”打标记”(声明元数据),反射负责”读标记 + 执行动作”(运行期解析与调用),动态代理负责”把动作织入调用过程”(无侵入增强)。
Spring 里 @Transactional 就是一个典型闭环:你在方法上打一个注解(注解),Spring 容器启动时用反射扫描到它并判读事务属性 ...
很多人的 Java IO 学习止步于”复制粘贴一段 try-catch 模板”:能读文件、能写文件,但说不清 new BufferedReader(new InputStreamReader(new FileInputStream(f), UTF_8)) 为什么要套三层;遇到中文乱码只会全局替换成 UTF-8;听说 NIO 快却不知道快在哪;背过 Buffer 的 flip 却总在读写切换时踩坑。根本原因在于:IO 从来不是纯 Java 的语法问题,它是应用程序、JVM、操作系统内核、磁盘与网卡四者协同的结果。不把内核态/用户态、系统调用、数据拷贝这些底层环节打通,上层 API 就永远是玄学。本篇的目标就是把这条链路从硬件一路讲到 API。
一、IO 基础概念:流、阻塞模型与内核/用户态的数据旅程1.1 什么是”流”流(Stream)是对有序、连续、有方向的数据序列的抽象。它有三个关键特征:
有序:先写入的字节先被读出,不存在”跳到中间读第 100 个字节”这种随机访问语义(RandomAccessFile 与 Channel 是例外)。
连续:数据像水流一样从一端流向另一端,读写 ...
上一篇我们把 List 与 Set 的底层结构、扩容机制、fail-fast 迭代器讲透了。本篇进入 Map 家族——这是 Java 面试中出现频率最高、源码最值得逐行精读的一块。全文代码均在 JDK 8 与 JDK 17 上实测通过,涉及版本差异的地方会明确标注。建议把文中的源码片段对照你本地 rt.jar / src.zip 里的 java.util.HashMap 一起读,尤其是 putVal、resize、treeifyBin 这三段,读完你会明白”为什么容量必须是 2 的幂””为什么树化阈值是 8”这些结论根本不需要死记硬背。另外文中会出现大量位运算,看不懂的地方先把二进制写出来。
一、Map 家族全景1.1 八种 Map 实现的语义与适用场景Map 不是 Collection 的子接口,它是独立的顶层接口,存储的是”键到值”的映射(key-value mapping)。JDK 提供了八种常用实现,它们的核心差异体现在四个方面:是否有序、是否允许 null、底层数据结构、是否线程安全。
实现类
底层结构
是否有序
key/value 可否为 null
线程安全
时间 ...






























