Java八股文核心:JVM内存、垃圾回收与集合框架面试要点

发布时间:2026/10/24 20:28:55
Java八股文核心:JVM内存、垃圾回收与集合框架面试要点

简介:《Java核心知识整理(常考八股文)》是一份面向Java后端开发者和求职面试者的PDF学习资料,采用常考八股文问答思路梳理核心考点,帮助读者快速抓住JVM、集合、多线程等高频重点,适合已有Java基础、正在准备面试的人群。包内为单个PDF文件,压缩包约10.71MB,目录标注页码,层级清晰,便于按章节定位复习。这份PDF当前重点围绕JVM专题展开,详细讲解JVM架构、线程、内存区域(程序计数器、虚拟机栈、本地方法区、堆、方法区)与运行时内存(新生代、老年代、永久代/元数据),其中新生代部分包含Eden区、ServivorFrom、ServivorTo划分及MinorGC过程;内容中也预告后续将补充Java语法、集合框架、多线程、设计模式等模块,可用于考前速查、体系化回顾与查漏补缺。整体结构以JVM为主线,兼顾面试题常见问法,适合按知识点快速翻查。已有646人学习下载,是备战Java岗位面试时值得参考的轻量级知识整理手册。

1. 什么是Java八股文:一份像题库一样的面试速查手册

如果你在后端开发这条路上走了一阵,八成见过这类资料:目录从JVM内存到垃圾回收,从Java集合到类加载机制,几百页按考点排得整整齐齐,被同行戏称为“Java八股文”。这名字带着自嘲,像古代科举考试有固定文章格式一样,Java岗面试也翻来覆去问那些固定题目。但别急着把它当成死记硬背的负担。八股文真正的价值,是把散落在教科书、源码和博客里的知识点,拧成一条面试现场能张口就说的逻辑线。懂原理是内功;能在紧张状态下把原理讲清楚,是外功。这份整理,就是给外功做的考前预习。

2. JVM内存区域:Java程序运行时,对象们都住在哪

JVM不是黑匣子。它会为每个Java程序搭一套固定的“宿舍楼”,把内存划成若干区域,每块区域各司其职。面试题问JVM内存,问的就是这栋楼怎么分房间。把这块理清楚,后面垃圾回收、类加载全部能串成一条线。

2.1 五个内存区:哪些是私人单间,哪些是公共大厅

整个JVM内存,按是否被线程共享,先分两大拨:线程私有和线程共享。

线程私有的有三个区。程序计数器是一块很小的空间,只管一件事:当前线程正执行到哪一条字节码。线程切换后还能接着原来的位置继续跑,全靠它记录“读到哪里了”。虚拟机栈存方法调用信息,每调用一个方法,就往栈里压入一个栈帧,方法的局部变量表、操作数栈、返回值全装在里面。本地方法区(也叫本地方法栈)是给native方法用的——这类方法通常用C或C++写好,通过JNI接口被Java调用,是Java程序和底层世界打交道的通道。

这三个区,每条线程各有一份,互不干扰,不需要加锁,这就是“线程私有=天然线程安全”的由来。

线程共享的也有两个。堆是最大的一块内存,new出来的对象和数组都躺在里面,也是垃圾回收重点关注的地方。方法区存的是类元信息、静态变量、运行时常量池,相当于整栋楼的建筑图纸和住户档案。堆和方法区所有线程共用,正因为共用,才容易出现并发安全问题。

这张表是面试里经常被要求当场默写的:

区域线程归属存什么会不会被GC
程序计数器私有当前字节码执行地址不会
虚拟机栈私有方法调用的栈帧不会
本地方法区私有native方法相关信息不会
堆共享对象、数组会
方法区共享类信息、静态变量、常量池可能

面试官爱问这几个区域,不是因为题目冷门,而是它们决定了后面一连串问题:为什么线程切换开销大、为什么静态变量不能被GC、为什么对象引用默认存在栈上。五个区域就像一张地图,后面所有考点都在图里找位置。

2.2 新生代的三块地:Eden、SurvivorFrom、SurvivorTo

堆内部不是一锅炖,而是按对象的“年龄”分成新生代和老年代。新生代是堆里最活跃的地带,大部分对象刚出生就住在这里。新生代又分成三块:Eden区、SurvivorFrom、SurvivorTo。

为什么这么分?因为绝大多数对象“朝生夕死”,统计下来,新生代里的对象大多数活不过第一轮GC。为了让清理效率足够高,JVM在这里采用了复制策略,需要有一块能随时“倒垃圾”的空地。

Minor GC的标准动作可以记忆成三步:

// 一次 Minor GC 的三步流程(伪代码) // 1. 把 Eden 区和 SurvivorFrom 区中还存活的对象,复制进 SurvivorTo, // 每复制一次,该对象的年龄计数器 +1 // 2. 清空 Eden 区和 SurvivorFrom 区 // 3. 交换 SurvivorTo 与 SurvivorFrom 的身份: // 本轮接收对象的 SurvivorTo,下一轮变成 SurvivorFrom

这段流程里最关键的是第3步。两个Survivor区没有固定身份,每轮GC结束都会互换。新生代里永远保证有一块完全空旷的区域,用来承接下一轮幸存下来的对象。

提示:SurvivorFrom 和 SurvivorTo 是角色名,不是物理固定房间。名字看着像“信息来源”和“信息去向”,其实它们轮班上岗。

为什么非要留一块空区?这就是复制的思想:一块收、一块倒,最后腾出的空间连续紧凑,不会像旧城区改造那样留下满地碎块。存活对象的年龄每经历一轮GC加1,达到默认阈值(通常是15)后,就不再在Survivor之间折腾,直接晋升到老年代。超大对象也会直接进老年代,省得在Eden和Survivor之间反复搬运。

我见过不少人在这个细节上翻车:把SurvivorFrom理解为“上一轮留下的旧数据”,把SurvivorTo理解为“这轮要清空的旧区”。实际上旧区是Eden和SurvivorFrom,SurvivorTo是接收方,接收完再互换身份。记住“复制→清空→互换”这六个字,比死记每个区叫什么管用。

2.3 老年代与永久代:从“高龄区”到“元空间”

老年代专收高龄对象。它在堆里占的份额通常比新生代大,GC频率也低得多。老年代回收主要用标记整理算法,否则每次回收完都会留下大量碎片,大对象想找个连续位置就难了。

“永久代”这个词,Java 8之前的人很熟悉。它是方法区的一种实现,存放类的元信息、静态变量、常量池。Java 8之后,永久代被“元空间”取代,方法区的地块从JVM虚拟内存搬到了本地内存。改动原因是永久代的容量特别难预估:设太小,类一多就报内存溢出;设太大,又白占虚拟内存。放到本地内存后,默认只受操作系统实际内存大小约束,类再努力加载也不容易把永久代撑爆。

面试题“永久代和元空间本质区别是什么”,抓住两点就够:物理位置不同(JVM内部 vs 本地内存),容量策略不同(固定参数 vs 系统自适应)。至于方法区,它是一个逻辑概念,永久代和元空间是它不同年代的两张皮。搞清这条线,“方法区在哪”的疑惑基本就消除了。

3. 垃圾回收:JVM的日常保洁是怎么排班的

对象在堆里住久了,自然会产生“垃圾”——那些再也用不上的对象。JVM不能等内存满了再去清理,于是有了垃圾回收机制。这一章回答三件事:怎么判定谁该扔、用什么姿势扔、让谁来扔。

3.1 判断“该扔”:引用计数法有个致命伤,所以用可达性分析

判断一个对象是不是垃圾,历史上出现过两种思路。

引用计数法最朴素:每个对象配一个计数器,被引用一次就加1,引用失效就减1,计数归零说明没人用它,可以回收。这思路看起来很完美,但有个绕不开的死角——循环引用。两个对象互相指着对方,外部谁也不再引用它们,两个计数器却都还是1,永远无法归零。这道坎,让引用计数法在主流JVM里没法独挑大梁。

于是JVM采用可达性分析。从一组叫“GC Roots”的根节点出发,顺着引用链往下走,能走到的对象算“活着”,走不到的一律视为垃圾。哪些对象能当GC Roots?虚拟机栈中引用的对象、静态变量引用的对象、仍在本地方法栈中的引用等。面试考这块时,最常追问的就是“什么是GC Roots”,能答出两三个就算入门了。

JVM里还有一种常考的分类:四种引用类型。强引用就是平时写的new出来的对象,JVM宁可抛异常也不回收它。软引用适合做内存敏感的缓存,内存不够时优先照顾它。弱引用更短命,GC发现它的存在就顺手清掉。虚引用本身保不住对象,它只用来感知对象即将被回收的那一刻。四者之间,软引用和弱引用的区别是高频考点,每年都有候选人栽在这里。

3.2 标记清除、复制、标记整理:三种基础清理手艺

垃圾回收不只有一套动作。主流的三种算法,各有各的划算和代价。

标记清除算法分两阶段:先标记所有要回收的对象,再统一回收。问题在于清理完之后,内存东一块西一块,不连续。像客厅里把家具搬走,留下满地空隙,以后想放一组大沙发都放不下。

复制算法是把存活对象统一搬进另一半空间,原有区域整体清空。它没有碎片,代价是可用内存被劈成两块,总容量缩水。新生代正好吃下这个亏——反正大多数对象活不长,幸存率低,用“Eden区 + 两个Survivor区”拼出一整块连续空间,以小代价换高吞吐。

标记整理算法是标记清除的改良版:先标记,然后把存活对象往同一端移动,最后清理边界以外的空间。老年代对象多、存活率高,适合这种“移动后再清理”的路子,既能解决碎片,又不像复制算法那样牺牲一半空间。

算法核心思路优点缺点典型用在哪
标记清除标记后直接清实现简单碎片多配合其他策略使用
复制存活对象搬到另一半无碎片空间利用率减半新生代
标记整理标记后移动拼接再清无碎片、空间省移动对象有开销老年代

三个算法的取舍一句话就能说清:复制算法用空间换时间,标记清除用时间换实现,标记整理是两者的折中。

3.3 垃圾收集器谱系:从Serial到CMS再到G1

有了算法,还需要具体执行的“保洁员”——垃圾收集器。

Serial是最早的单线程选手,垃圾回收时必须暂停所有业务线程,也就是“Stop The World”。它简单可靠,适合客户端小规模场景,像请保洁阿姨来家收拾,全家人都得暂时停下手里的活。

ParNew是Serial的多线程版,在新生代使用复制算法,配合老年代的CMS,是许多服务端JVM的经典组合。Parallel Scavenge更强调吞吐量,恨不得把每个CPU都拿来跑业务,它配老年代的Parallel Old,组成“吞吐量优先”组合。

CMS收集器主打低停顿:初始标记、并发标记、重新标记、并发清除四步。头尾两步需要暂停线程,中间两步和业务线程并发执行。代价是CMS对CPU资源相当敏感,还会留下浮动垃圾,甚至可能触发“Concurrent Mode Failure”,被迫回退到Serial Old。很多人以为CMS是“年轻代收集器”,其实它在老年代工作,通常搭配ParNew一起使用。

G1则把堆切成一个个Region,不再严格按新生代老年代整体划分,而是按需给每个Region分配角色。它既能整堆回收,又能精确控制预期停顿时间,是JVM从JDK 8往JDK 9过渡后走向默认的收集器。面试问G1,抓住“分区 + 可预测停顿”这个特征,比死记一长串参数有用得多。

4. 常见误区:JVM和Java八股里最容易被绕晕的四个点

背八股文最怕的就是背岔了。看着好像懂了,一追问就露馅。这一章整理四组高频误区,按“哪里易混 → 为什么混 → 怎么区分”说清楚。

4.1 方法区、永久代、元空间:一个概念的三副面孔

易混点:提到方法区,有人说它是永久代,有人说是元空间,甚至以为三者是三个完全不同的地方。

为什么混:JDK版本一直在变。Java 8之前,方法区用永久代实现;Java 8之后,永久代被移出虚拟机内存,换成元空间。于是老资料写“方法区在堆里”,新资料写“方法区在本地内存”。两种说法都没错,只是时间不同。

怎么区分:先把逻辑层面和物理层面分开。方法区永远只有一个,它是JVM规范里定义的逻辑区域,负责存类元信息、静态变量和常量池。永久代和元空间是它不同时期的两张皮,一个老住户,一个新户型。看到任何关于这块的资料,先判断它讲的是哪个JDK版本,再套这句话,基本不会乱。

4.2 双亲委派:不是“家长替子女决定”,是“子女先问长辈”

易混点:双亲委派听起来像父母替孩子做选择、发任务,Java类加载的流程正好相反。

为什么混:名字里有“委派”两个字,直觉上像是从上往下派活。但类加载实际上干的是自下而上的询问:一个类要被加载时,先请求父加载器出手,一层层往上托,直到顶层都没有,才轮到自己用findClass去加载。

怎么区分:把加载顺序倒过来记——“先问上面,再问下面”。启动类加载器在最高层,应用程序类加载器在最低层。这样做有两个好处:同一个类不会因为加载器不同被重复加载;核心类库也不会被用户自定义的类冒充篡改。像OSGi这种动态模块系统,正是通过打破标准双亲委派的顺序,才实现了模块的热插拔,这恰好反证了双亲委派的存在价值。

// 双亲委派的核心逻辑(伪代码,以逻辑顺序为准) protected Class<?> loadClass(String name) { // 1. 先请求父加载器加载 if (parent != null) { parent.loadClass(name); } // 2. 父加载器表示无能为力,才轮到自己去查找类文件 return findClass(name); }

这段逻辑还解释了为什么有人说“双亲委派 = 有爹先靠爹,爹不行自己上”。顺序不能反过来,否则类加载的沙箱保护就失效了。

4.3 HashMap的Java 7与Java 8:差别不止“加了红黑树”这一句

易混点:很多人背了“Java 8的HashMap引入了红黑树”,觉得这就是全部答案。

为什么混:因为面试题经常这么问,人也就顺势只记最显眼的结论。实际Java 8的HashMap改动是一整套连锁升级:底层从“数组 + 链表”变成“数组 + 链表 + 红黑树”;插入方式从头插法改成尾插法;扩容时哈希重算的细节也变了。头插法在并发扩容时容易形成循环链表,尾插法配合红黑树把最坏情况下的查找从O(n)压到O(log n)。

怎么区分:别只记“加了红黑树”,把转换条件也过一遍。链表转红黑树的阈值默认是8,红黑树转回链表的阈值默认是6,中间留了两档缓冲,避免节点数在边界来回摆动,频繁转换损耗性能。面试能说出这两个数字,比单纯背结论要扎实得多。

4.4 IO模型和NIO:阻塞、非阻塞、多路复用、异步不是一回事

易混点:很多人把Java NIO直接等同于“非阻塞IO”,再往上把多路复用和异步IO搅成一锅粥。

为什么混:NIO的N确实有“Non-blocking”的意思,Java官方也确实讲了这套IO是非阻塞设计。但实际用起来,NIO里的Channel读写并不总是立刻返回,Selector只是把多个通道的IO事件集中在一起监听。这种模式在教科书里叫“多路复用”,和“异步IO”的“干完活再通知你”是两码事。

怎么区分:按递进顺序记。阻塞IO是“一去不回,没结果就干等”;非阻塞IO是“去了没结果,先回来干别的”;多路复用是“一个线程盯着好多路,哪一路有动静就处理哪一路”;异步IO是“活儿干完了系统再主动通知你”。面试问JAVA IO/NIO,从阻塞递进到异步这么答,层次一下子就能拉开。很多所谓“血泪经验”,其实是把这几个模型混在一起,导致排错时对着日志干瞪眼,看着像玄学,根源就在概念没拆开。

5. Java集合框架:面试题里的数据结构全家桶

集合框架是Java面试里出镜率最高的区域,几乎每轮必被翻牌子。这一章把List、Set、Map三条主线捋一遍,把“面试官到底想问什么”讲透。

5.1 List三兄弟:数组、链表和同步的取舍

ArrayList底层是Object数组,支持随机访问,读快写慢。往中间插一个元素,后面的元素都得挪位置,数据量大时开销立马上来。LinkedList底层是双向链表,插入删除只需改前后节点的指针,可按下标取元素就得从头或从尾逐个遍历,随机访问是短板。Vector和ArrayList结构几乎一样,区别在于Vector大部分方法用synchronized修饰,线程安全,但代价是整体性能受影响。

三者对比几乎是必背题:

集合底层结构线程安全最合适场景
ArrayListObject数组否读多写少,按索引访问
LinkedList双向链表否头尾频繁增删
VectorObject数组 + synchronized是老代码兼容场景

提示:别默认LinkedList插入就一定比ArrayList快。ArrayList在尾部追加有扩容优化,LinkedList要额外维护节点前后指针,数据量小的时候两者差距根本拉不开。

面试里的进阶追问通常是“什么时候选LinkedList”,能答出“对中间位置频繁增删、且对随机访问要求不高时”,比一句“链表快”要稳得多。

5.2 Set的三种形态:去重之外各管一件事

Set的核心是不重复。HashSet底层其实就是HashMap,利用key的唯一性来去重,存取复杂度为O(1),但迭代时元素顺序不保证。TreeSet底层是红黑树,元素会按自然顺序或自定义比较器排序,查找和插入的时间复杂度为O(log n)。LinkedHashSet在HashSet基础上额外维护一条双向链表,记录元素插入顺序,遍历时能按插入顺序输出。

三者的选择题经常这样问:“哪个能排序?哪个能保证顺序?”答案记住三条:能排序的只有TreeSet;能按插入顺序遍历的是LinkedHashSet;HashSet两个都不管,只管不重复和够快。问到这里,面试官想确认的是你对哈希和树两种基础结构的理解是否到位。

5.3 Map的演进:HashMap到ConcurrentHashMap,线程安全是补出来的

HashMap的底层在Java 8之后变成“数组 + 链表 + 红黑树”,扩容和哈希扰动都持续优化。问题是它只适合单线程,多个线程同时操作,轻则数据错乱,重则直接抛异常。要线程安全,一般有三条层次的路。

第一层是Hashtable,所有方法直接加锁,简单粗暴,但每次操作都锁整张表,并发量一高就拖后腿。第二层是用Collections工具包里的synchronizedMap包装,本质也是整表加锁,思路和Hashtable没区别。第三层是ConcurrentHashMap。

Java 7的ConcurrentHashMap把整张表切成默认16个Segment,每个Segment各有一把锁,不同Segment的操作可以并行执行。锁的粒度从“整张表”降到了“一段表”。Java 8又往前跨了一步,废弃Segment,改用CAS配合synchronized,只对单个桶上锁,粒度更细,并发能力更强,同时引入红黑树优化了长链表查询。

// Java 8 ConcurrentHashMap 写入流程的简化示意 final V putVal(K key, V value, boolean onlyIfAbsent) { // 1. 先用 CAS 尝试把新节点放进去,没有竞争就直接成功 // 2. 如果目标桶已经被占用,对桶的头节点加 synchronized // 3. 在锁内完成链表插入,必要时把链表树化为红黑树 }

这段代码背后的演进逻辑,是一个很典型的工程问题:锁范围越大,越安全,但并发就越差;锁范围越小,并发越好,但实现越复杂。面试官问到这里,能把“锁粒度从表级降到段级,再降到桶级”这条线说出来,就已经从“背结论”跨到“懂设计”了。

6. 八股文该背到哪一步:用它对面试,别被它框住

遇到这份整理,你先要分清它在什么场景下有救命的用处,什么场景下帮不上忙。

它的最佳用法是两件事:一是面试前两天的系统复盘,把JVM内存、垃圾回收、集合框架这些高频考点从头到尾串一遍;二是平时当字典查,遇到源码里看不懂的变量、参数,回头翻对应章节找定位。项目里排查问题时,这套底子能帮你快速缩小范围——比如看到内存溢出,先判断是堆不够还是元空间不够,顺着内存区域的划分就能找到入口。

但也有两种场合,请主动合上它。

第一,面试官问“线上突然CPU打满,你怎么排查”这类开放问题时,只有背来的术语、没有真实踩坑经验,几个追问下来就接不住了。八股文给的是地图,不是导航记录,它不能替你经历每一次事故。第二,你已经背得很顺的时候,不要拿八股术语硬套所有问题。新人最容易出的状况是:面试官问“HashMap在并发下有什么问题”,明明讲的是线程安全,却开始背红黑树转换阈值,答非所问。

我自己吃过这样的亏。有一次讲到G1收集器,脑子里全是“Region、可预测停顿”,面试官追问“那它的停顿预测模型是怎么实现的”,我当场愣住。从那以后,我给自己定了个习惯:每次面试前一晚,强制把JVM和集合这两章过一遍,但绝不当着镜子念答案,而是看着每个章节标题,用大白话把原理讲给自己听。能完整讲顺,这个知识点才算真正长在我身上了;讲不顺的,第二天就是重点准备对象。这份资料说到底是外功招式,真正的内功,还是得靠一次次写代码、排查问题去积累。希望帮到你。

本文还有配套的精品资源,点击获取