资讯详情

280页Java后端面试知识整理:JVM、GC与并发集合

📅 2026/11/2 3:40:34 | 华诺云谱 👁 阅读
280页Java后端面试知识整理:JVM、GC与并发集合
简介面向Java后端开发与求职面试复习的核心知识梳理文档聚焦常考“八股文”与底层原理适合准备校招、社招技术面或希望系统查漏补缺的初中级开发者。内容以JVM为主线覆盖类加载器、执行引擎、运行时数据区程序计数器、虚拟机栈、本地方法区、堆与方法区等内存划分并延伸到新生代Eden、ServivorFrom、ServivorTo与老年代、永久代以及MinorGC“复制—清空—互换”的完整流程目录层级清晰便于按知识点快速定位。压缩包内含1个PDF文件约10.71MB适合在电脑或平板端翻阅、标注和打印。文档还提及Java语法、集合框架、多线程与设计模式等后续知识方向可作为搭建Java知识框架的索引。目前已有650人学习下载适合面试前短周期回顾与查漏补缺。1. JVM 到并发一份能吃透 Java 后端面试的 280 页知识整理面试前一天晚上翻收藏夹发现存了十几份“Java 八股”却一份都没读完——这种情况我见过太多次。这份 280 页的 Java 核心知识整理解决的就是这个问题它把后端面试里真正会追问的东西按主题串成了一条线从 JVM 内存区域一路排到集合与并发章节编号到四级检索起来像查手册。它适合两类人一是准备跳槽、需要在两三周内把知识体系过一遍的 Java 开发二是带新人、想找一份能直接发给对方对照复习的材料的人。目录结构本身就值一半价钱——JVM 占了 40 多页集合从接口继承关系讲到 HashMap 的 JDK7 与 JDK8 两套实现差异这种颗粒度说明整理者是按“会被追问到哪一层”来组织的不是简单堆名词。2. JVM 内存结构与垃圾回收把 40 页压缩成三条主线JVM 那部分看着厚其实就三条主线内存区域怎么划分、对象怎么死、垃圾怎么收。把这三条捋顺剩下全是细节填充。很多人背“程序计数器线程私有、堆线程共享”背得很熟一问“为什么程序计数器必须私有”就卡住这就是只记结论没记因果。2.1 运行时数据区五个区域的私有与共享判定整理里把程序计数器、虚拟机栈、本地方法栈划为线程私有堆和方法区划为线程共享这个分法要连原因一起记。程序计数器存的是当前线程执行的字节码指令地址线程切换后必须能回到自己的位置所以不能共享虚拟机栈存的是栈帧每个方法调用对应一帧方法是谁调的、局部变量归谁天然属于单一线程。堆和方法区存的是对象和类元数据多个线程都要访问共享是必然也就带来了同步问题。有一处容易翻车的细节整理里方法区标注了“永久代”同时在 JAVA8 与元数据那节提到元空间。这两者不是一回事。JDK7 及以前用永久代实现方法区受-XX:MaxPermSize限制JDK8 起换成元空间走本地内存参数变成-XX:MaxMetaspaceSize。面试里如果只说“方法区就是永久代”遇到追问 JDK8 变化的面试官会直接掉分。我一般会按这个顺序讲方法区是规范永久代是 JDK7 的实现元空间是 JDK8 的实现三者是“概念—旧实现—新实现”的关系。2.2 GC 判定与三种基础算法从引用计数到分代收集判断对象死活只有两条路引用计数和可达性分析。引用计数实现简单但解决不了循环引用——两个对象互相持有计数永远不为零。可达性分析从一组 GC Roots 出发做图遍历够不着的就判死这也是 HotSpot 实际采用的方式。GC Roots 常见的有虚拟机栈中引用的对象、方法区静态属性引用的对象、常量引用的对象、本地方法栈中 JNI 引用的对象。三种基础算法各有明确的适用边界整理里对标记清除、复制、标记整理都做了展开我把它们的对照关系重新排一下方便选型时快速定位算法核心动作缺点典型落点标记清除标记存活对象清除未标记的产生内存碎片CMS 的并发清除阶段复制存活对象复制到另一半空间浪费一半空间新生代 Eden/Survivor标记整理标记后把存活对象向一端移动移动成本高老年代收集器分代收集不是第四种算法而是策略新生代对象朝生夕死用复制算法只留少量 Survivor 就够老年代对象存活率高复制不划算改用标记整理。Minor GC 的三步在整理里写得很细Eden 和 SurvivorFrom 的存活对象复制到 SurvivorTo 并年龄加一清空 Eden 和 SurvivorFrom两个 Survivor 互换角色。默认 Eden 与单个 Survivor 的比例是 8:1:1这个比例能容忍的浪费控制在 10% 以内是空间与效率折中的结果。2.3 收集器选型Serial、CMS、G1 各自的使用场景收集器那节列了七种实际工作中真正要能说清区别的是三组。Serial 与 Serial Old 是单线程会 Stop The World适合客户端场景和内存小的环境Parallel Scavenge 与 Parallel Old 关注吞吐量适合后台批处理CMS 与 G1 关注停顿时间适合交互型服务。CMS 分四步初始标记、并发标记、重新标记、并发清除。只有初始标记和重新标记需要停顿其余并发执行所以停顿短。代价是并发清除阶段产生浮动垃圾且用标记清除算法留下碎片碎片多到无法分配大对象时会退化成 Serial Old 做一次 Full GC这一停顿可能比平时长一个量级是 CMS 最典型的翻车点。整理里把这四步拆成了独立的四级标题逐条对照看比一次性读完有效。G1 的思路是把堆切成大小相等的 Region按 Region 回收而不是按年代通过维护每个 Region 的回收价值做优先队列优先回收收益高的这种可预测停顿模型是它取代 CMS 的核心原因。要不要背参数-XX:MaxGCPauseMillis设期望停顿、-XX:ParallelGCThreads设并行线程数能把这两个参数和停顿模型对上号就够应付大多数追问了。3. 集合框架HashMap 与 ConcurrentHashMap 的两代实现差异集合是面试里追问最深的一块因为 HashMap 一个类就能从数据结构问到并发、从 JDK7 问到 JDK8。整理里把 ArrayList、Vector、LinkedList、HashSet、TreeSet、LinkedHashSet 和 Map 系全都过了一遍但真正需要逐行吃透的是 HashMap 和 ConcurrentHashMap。3.1 List 与 Set 的底层结构选型ArrayList 底层是数组随机访问 O(1)中间插入要移动元素LinkedList 底层是双向链表头尾操作 O(1)随机访问要遍历。这个对比人人会背但工程上还要多想一层LinkedList 每个节点额外存两个引用内存开销高且缓存局部性差实测中即使在中部插入ArrayList 因为 System.arraycopy 是连续内存拷贝性能经常反超。我的建议是除非明确需要队列语义默认用 ArrayList。Vector 是数组实现且方法加了 synchronized线程安全但锁粒度是整个对象现在基本被Collections.synchronizedList或CopyOnWriteArrayList取代。HashSet 底层就是 HashMap只用了 keyvalue 统一放一个占位对象TreeSet 底层是 TreeMap靠红黑树保证有序元素必须可比较或传入 ComparatorLinkedHashSet 在 HashSet 基础上用链表维护插入顺序适合既要去重又要保持顺序的场景。3.2 HashMap 的 JDK7 与 JDK8 实现对比JDK7 的 HashMap 是数组加链表插入用头插法扩容时把旧数组元素迁移到新数组链表顺序会反转。多线程同时扩容时两个线程可能把链表指针互相指向对方形成环形链表之后 get 操作就会死循环CPU 直接打满。这是 JDK7 最著名的血泪坑整理里在 JAVA7 实现那节点到但值得展开说清楚。JDK8 改了两处链表长度超过 8 且数组长度不小于 64 时链表转红黑树查询从 O(n) 降到 O(log n)插入改成尾插法扩容时保持相对顺序不会成环。但 JDK8 的 HashMap 依然不是线程安全的多线程 put 仍可能丢数据。// JDK8 HashMap 扩容时的关键判断伪代码便于理解阈值逻辑 final NodeK,V[] resize() { int oldCap table.length; int newCap oldCap 1; // 容量翻倍 // 链表节点迁移按 hash oldCap 是否为 0 拆成高低位两条链 // 为 0 留在原索引 i非 0 移到 i oldCap if ((e.hash oldCap) 0) { // 低位链位置不变 } else { // 高位链移动到 i oldCap } return newTable; }这段逻辑的意义在于扩容时不需要重新计算每个 key 的 hash只需看 hash 值与旧容量按位与的结果就能决定元素留在原位还是移到“原位加旧容量”。这是 JDK8 在扩容效率上的优化也是面试常问“为什么容量必须是 2 的幂”的答案——只有 2 的幂hash (cap - 1)才能等价于取模且扩容时能用一次位运算完成分流。3.3 ConcurrentHashMap从分段锁到 CAS 加 synchronizedJDK7 的 ConcurrentHashMap 用 Segment 分段锁默认并行度 16也就是把整个表分成 16 段每段独立加锁不同段的操作互不阻塞。Segment 继承 ReentrantLock所以本质是“锁分段降低冲突”。这个设计的局限是并行度在初始化后固定段数设多了浪费设少了并发上不去。JDK8 直接放弃了分段锁改用“CAS synchronized 锁单个桶头节点”。数组里每个位置桶就是一个锁粒度冲突只在同一个桶内发生时才需要竞争粒度比 Segment 细得多。核心流程可以概括为// JDK8 ConcurrentHashMap put 的核心骨架简化 for (NodeK,V[] tab table;;) { NodeK,V f tabAt(tab, i (n - 1) hash); if (f null) { // 桶为空CAS 直接放入新节点无锁 if (casTabAt(tab, i, null, new NodeK,V(hash, key, value))) break; } else if (f.hash MOVED) { // 正在扩容当前线程协助迁移 tab helpTransfer(tab, f); } else { // 桶非空synchronized 锁住头节点后插入 synchronized (f) { // 链表插入或红黑树插入 } } }参数上要理解sizeCtl它在不同状态下含义不同未初始化时存初始容量初始化或扩容时是一个负的控制值扩容完成后存下一次扩容的阈值。这个变量是理解 ConcurrentHashMap 并发控制的关键比记“分段锁换 CAS”这句话有用得多。4. 类加载与 IO 模型两个常被跳过但容易追问的模块JVM 和集合之外类加载机制和 IO/NIO 是两块很多人复习时草草翻过的内容。恰恰是这两块面试官喜欢在候选人自认为答得不错时抛一个追问用来区分“背过”和“懂了”。4.1 类加载五阶段与双亲委派类加载分加载、验证、准备、解析、初始化五个阶段。加载阶段通过类的全限定名获取二进制字节流转成方法区的运行时数据结构并在堆中生成 Class 对象验证确保字节流符合规范准备阶段为静态变量分配内存并设零值注意是零值不是代码里的初始值static int a 10在这一步 a 是 0赋 10 发生在初始化阶段解析把常量池的符号引用替换成直接引用初始化执行类构造器clinit按源码顺序收集静态变量赋值和静态代码块。双亲委派说的是加载请求先交给父加载器父加载器处理不了才自己加载。启动类加载器负责JAVA_HOME/lib下的核心类扩展类加载器负责lib/ext应用程序类加载器负责 classpath。这个机制保证核心类不被自定义类覆盖比如你写一个java.lang.String是加载不进来的。提示判断两个类是否“相等”前提是它们由同一个类加载器加载。同一个 class 文件被两个不同加载器加载实例化出来的对象instanceof会返回 false这是热部署、插件化框架里调试问题的常见根因。OSGi 那节讲的是打破双亲委派的动态模块系统支持模块热插拔和动态改变构造。实际工作中未必用 OSGi但“什么场景需要打破双亲委派”是个高频追问答案就是需要隔离同名类或实现热替换的框架场景。4.2 五种 IO 模型与 NIO 三件套五种 IO 模型按“等待数据”和“拷贝数据”两个阶段谁来阻塞来区分阻塞 IO 两个阶段都阻塞非阻塞 IO 数据没准备好立即返回需要轮询多路复用用 select/poll/epoll 统一监听多个描述符有事件才处理信号驱动靠信号通知数据就绪异步 IO 两个阶段都不阻塞数据拷贝完成才通知。日常说的“IO 多路复用”基本就是 NIO 的地基。NIO 三件套是 Channel、Buffer、Selector。Channel 是双向通道可读可写不同于流只能单向Buffer 是数据容器读写切换靠 flipSelector 让一个线程监听多个 Channel。整理里把 Buffer 的 position、limit、capacity 三个指针写清楚了这三个值的关系是 NIO 最容易绕晕的地方——写模式下 position 是写入位置、limit 等于 capacity调用 flip 后切读模式limit 变成原 positionposition 归零。// 典型的 NIO 服务端骨架一个线程管多个连接 ServerSocketChannel server ServerSocketChannel.open(); server.bind(new InetSocketAddress(8080)); server.configureBlocking(false); // 必须非阻塞否则 Selector 无法接管 Selector selector Selector.open(); server.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select(); // 阻塞直到有就绪事件 for (SelectionKey key : selector.selectedKeys()) { if (key.isAcceptable()) { SocketChannel client server.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel client (SocketChannel) key.channel(); ByteBuffer buf ByteBuffer.allocate(1024); int n client.read(buf); // 读到的字节数-1 表示断开 if (n -1) { client.close(); continue; } buf.flip(); // 切读模式limit 设为 positionposition 归零 // 处理数据 buf.clear(); // 复位准备下次写 } } selector.selectedKeys().clear(); // 必须清空否则事件重复处理 }参数说明configureBlocking(false)是注册到 Selector 的前提阻塞通道无法注册selector.select()返回就绪的通道数返回值大于 0 才会进入遍历selectedKeys()返回的是就绪集合的引用处理完必须 clear否则下一轮会把旧事件再处理一遍这是手写 NIO 最经典的翻车点之一。5. 避坑排查五条复习与实战里真会撞上的问题5.1 把方法区等同于永久代现象被问 JDK8 内存区域变化时答“方法区就是永久代”面试官追问元空间后答不上来。 原因整理里方法区标题带“永久代”字样容易只记标题不看正文的 JAVA8 与元数据小节。 解决按“方法区是规范、永久代是 JDK7 实现、元空间是 JDK8 实现”三层记忆参数对应-XX:MaxPermSize与-XX:MaxMetaspaceSize。5.2 认为 HashMap 在 JDK8 后已线程安全现象多线程写 HashMap 出现数据丢失或 size 不准排查半天以为是业务逻辑问题。 原因JDK8 改成尾插解决了成环但 put 过程依然没有同步并发写会互相覆盖。 解决并发场景用 ConcurrentHashMap或Collections.synchronizedMap加外部同步不要用“JDK8 修了 HashMap”这种模糊结论做技术决策。5.3 只背 CMS 四阶段不知道退化风险现象线上用了 CMS 仍出现长时间停顿监控里看到 Full GC 耗时异常。 原因CMS 并发清除产生碎片大对象分配失败触发 Serial Old 做兜底 Full GC这一停就是整个堆。 解决关注-XX:CMSInitiatingOccupancyFraction老年代占用达到阈值提前触发回收留出空间碎片严重时考虑换 G1 或加-XX:UseCMSCompactAtFullCollection。5.4 NIO 忘记清理 selectedKeys现象Selector 循环里同一批事件被反复处理CPU 飙高但业务日志重复。 原因selectedKeys()返回的集合不会自动清空处理完不 clear 就进入下一轮。 解决每次遍历结束调用clear()遍历中如需删除用迭代器的remove而不是直接操作集合。5.5 准备阶段与初始化阶段混淆现象被问static变量在类加载哪一步赋真实值答成准备阶段。 原因准备阶段分配内存并设零值真实赋值在初始化阶段执行clinit。 解决记住口诀“准备设零初始化赋真值”并注意clinit按源码顺序执行静态代码块和静态变量赋值谁在前谁先执行。6. 用目录当索引把 280 页拆成两周的复习节奏这份整理最容易被低估的地方是目录。它把 JVM 拆到 Minor GC 的三个步骤、把 ConcurrentHashMap 拆到 Segment 并行度这种颗粒度等于已经给了一份带层级的复习清单。我自己的用法是反着来不按顺序读先看目录里哪些四级标题我讲不出来只读那些。具体做法分三步。第一步花二十分钟过一遍 JVM 和集合两章的目录对每个四级标题做口头自述能连续讲满一分钟的标记跳过卡住的做记号。第二步按记号数量分配时间通常 JVM 内存区域、GC 算法、HashMap 两代实现、ConcurrentHashMap 这四块占掉七成记号集中啃。第三步每啃完一块用一段代码或一张表复述比如 GC 算法用前面那张对照表ConcurrentHashMap 用 put 骨架代码。几个能直接抄的复习动作讲 JVM 内存时强制带上“为什么私有/为什么共享”把结论和因果一起说避免被追问击穿。讲 HashMap 扩容时手写hash oldCap的分流逻辑这一个小动作能覆盖“为什么容量是 2 的幂”和“JDK8 扩容优化”两个追问。讲 CMS 必须主动提碎片和退化把风险说出来比等面试官问更有说服力。讲 NIO 就画 Channel-Buffer-Selector 三者的关系配一句“一个线程管多个连接”比背五种 IO 模型定义更落地。还有一点整理里的代码示例和参数名不要死记比如sizeCtl那种状态复用的变量记住它“不同阶段含义不同”这个设计意图就够了具体数值现场推。八股文的价值不在背答案在于它把知识点压缩成了可检索的索引真正的深度要自己往里填。从那以后我每次准备技术复习都强制先拿目录做一轮口头自述讲不出来的才翻开细读一本书的厚度能被砍掉三分之二。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑