
面试这关八股文绕不开。哪怕是干了三五年的老手跳槽前也得把JVM、集合、并发、Spring这些基础再过一遍更不用说刚入门的新人。网上各种面试题汇总遍地都是但大部分只是列问题、贴答案背完了也不知道面试官到底在问什么。这篇我把Java面试里最核心的几个模块拆开讲清楚每个考点不只告诉你“答案是什么”还会讲“为什么会有这个设计”“面试官为什么爱问这个”“回答的时候怎么延伸”。零基础的同学可以把它当入门地图有经验的可以拿来做体系梳理看完这篇再去刷题思路会顺很多。1. 面试必考的JVM先搞懂内存和类加载JVM是Java面试的压轴戏十场面试九场会问。很多新手一听到JVM就头大觉得抽象、离日常开发远。其实原理弄清楚之后它反而比业务代码好理解得多因为JVM的设计逻辑非常清晰一切都是为了让Java程序跑得更快、更稳。1.1 运行时数据区域哪些线程私有哪些线程共享JVM在运行Java程序时会把它管理的内存划分成几个区域这个划分是面试的第一道开胃菜。记住一个口诀两私有、三共享。程序计数器、虚拟机栈是线程私有的堆、方法区、运行时常量池是线程共享的。程序计数器是当前线程执行字节码的行号指示器它占用的空间极小而且是唯一不会出现OutOfMemoryError的区域。虚拟机栈存的是栈帧每一个方法调用对应一个栈帧的入栈和出栈栈帧里包含局部变量表、操作数栈、动态链接、方法出口等信息。这个区域对应我们平时说的栈内存递归调用太深就会抛StackOverflowError。堆几乎存了所有的对象实例也是垃圾回收的主战场。方法区存类信息、常量、静态变量、JIT编译后的代码等。面试官通常会往上加难度堆和栈有什么区别这是一个特别经典的追问。回答的时候要从两个维度说。管理方式上栈是自动分配和释放的方法执行完栈帧就销毁堆需要垃圾回收器来回收。空间大小上栈通常比堆小很多默认1MB左右堆可以设置到几个GB。存储内容上栈存基本类型变量、对象引用、方法调用上下文堆存真正的对象数据。再补一句栈是线程隔离的堆是线程共享的共享就会涉及并发安全所以new出来的对象在多线程环境下要小心处理这句话能把话题引向并发方向。1.2 类加载机制与双亲委派模型一个类从被加载到可以被使用要经过加载、验证、准备、解析、初始化五个阶段。加载阶段把Class文件读进内存生成Class对象验证阶段检查字节流是否符合规范准备阶段为静态变量分配内存并设置默认值解析阶段把符号引用替换为直接引用初始化阶段执行静态代码块和静态变量的赋值。面试官最喜欢在这个环节追加“双亲委派模型”。这个模型讲的是当一个类加载器收到加载请求时它不会自己先去加载而是先把这个请求委派给父类加载器处理每一层都往上抛最后到启动类加载器。只有父类加载器反馈自己无法加载时子类加载器才会尝试自己加载。为什么要这样设计两个原因。安全比如自己写一个java.lang.String它不会去加载你写的那个版本而是交给启动类加载器加载JDK自带的String防止核心API被篡改。一致性保证同一个类在整个JVM里只被加载一次避免出现同一个类被不同加载器加载导致类型不兼容的问题典型的就是ClassCastException。如果面试官继续追问“能不能打破双亲委派”回答思路是可以通过继承ClassLoader并重写loadClass方法来实现但loadClass默认会走双亲委派逻辑所以要打破它就不要用super.loadClass而是直接改findClass或重写整个loadClass流程。然后可以提一下Tomcat的类加载器就打破了双亲委派它为了给每个Web应用维护独立的类库版本先把请求交给Web应用自己的类加载器这算是一个加分回答。1.3 GC与分代收集面试最爱问的那几个问题垃圾回收这块首先得明白判断对象是否存活有两种方法。引用计数法思路简单给对象加一个计数器被引用加1失效减1减到0就判定死亡。但它解决不了循环引用问题A引B、B引A两边计数器都不归零对象永远回收不掉。所以主流JVM用的是可达性分析算法从GC Roots出发向下搜索走过的路径叫引用链没有引用链的对象就是不可达的可以被回收。GC Roots包括虚拟机栈中引用的对象、方法区中静态属性引用的对象、常量引用的对象、JNI引用的对象。接下来的重点是三种基础回收算法。标记-清除先标记出可回收对象再统一回收缺点是有内存碎片大对象可能找不到连续空间。复制算法把内存分成两块只使用一块回收时把存活对象复制到另一块然后把当前块整个清空不会有碎片但浪费了一半空间而且存活对象多时复制成本高。标记-整理类似标记-清除但回收后会把存活对象向一端移动解决碎片问题代价是移动开销。HotSpot把堆分成新生代和老年代新生代又细分成Eden区和两个Survivor区默认比例8:1:1。新生代对象“朝生夕灭”适合复制算法老年代对象存活率高用标记-清除或标记-整理。面试问到这里可以主动讲一下对象晋升的流程对象先分配在Eden区Minor GC后存活对象进入Survivor区每熬过一次GC年龄加1达到阈值默认15进入老年代。另外大对象会直接进入老年代避免在新生代反复复制。如果面试官再往深挖会问CMS和G1的差别。CMS基于标记-清除并发收集停顿短但会产生碎片G1把堆分成多个Region可预测停顿时间兼顾年轻代和老年代回收JDK9之后成了默认收集器。答到这里基本就能让面试官满意了因为这些内容已经涉及到你在实际项目里怎么调优、怎么选型了。2. 集合框架面的核心HashMap原理必须倒背如流集合这块问得最多的绝对是HashMap没有之一。从底层结构到put流程、扩容机制、线程安全性面试官能一连串问十几分钟。搞定HashMap集合这块就站住了一半。2.1 HashMap底层结构与put流程JDK1.8之后HashMap的底层结构是数组加链表加红黑树。数组叫Node桶每个桶上可以挂一个链表当链表长度超过阈值默认8且数组长度大于等于64时链表会转成红黑树目的是把查询时间复杂度从O(n)降到O(log n)。现在面试最常问的就是“描述一下put一个key-value的完整流程”。这个一定要说得顺利对key计算hash值(h key.hashCode()) ^ (h 16)。为什么高位异或低位因为数组长度是2的幂计算下标时用的是(n - 1) hash高位参与异或能让散列更均匀降低hash碰撞概率。通过(n - 1) hash算出数组下标。如果数组为空先触发resize初始化。如果下标位置上没有元素直接new一个Node放进去。如果有元素先判断key是否相等相等就直接覆盖value。如果不相等判断当前节点是否为红黑树节点是就走红黑树的插入逻辑否则遍历链表找到相同key就覆盖没找到就尾插新节点。插入后如果链表长度达到8调用treeifyBin尝试转红黑树转之前还会检查数组长度是否达到64没达到就优先扩容而不是树化。插入完成后如果size加1之后超过threshold容量乘负载因子触发扩容。这套流程背下来不难关键是要能回答追问。比如为什么链表转红黑树的阈值是8这是基于泊松分布得出的一个统计结果。源码注释里给了数据在负载因子0.75下链表长度达到8的概率只有约千万分之六所以正常情况下很少触发树化。8这个值是为了在“空间成本”和“时间成本”之间取得平衡。树化是防止恶意hash攻击的兜底手段而不是常态。2.2 扩容机制和2的幂次方设计HashMap扩容听起来简单数组不够了就扩大到原来的两倍。但真正问起来有很多细节。先看为什么容量总是2的幂次方。因为下标计算用的是(n - 1) hash如果n是2的幂n - 1的二进制全是低位1那么与运算就等价于hash对n取模但位运算比取模快得多。同时扩容后元素在新数组中的位置只有两种位置不变或者原位置加旧容量。这取决于新增的那一位二进制是0还是1。这样rehash的成本很低元素均匀分布的概率也高。扩容的流程要能说清楚先算新容量和新阈值然后用新容量new一个新数组把旧数组里的元素搬过去。搬的时候如果是链表会拆成低位链和高位链分别放进新数组的原位置和原位置加旧容量的位置如果是红黑树会先拆分如果拆分后节点数小于6退化成链表。这正好对应另一个高频问题红黑树什么时候退化成链表答案是扩容拆分后节点数小于等于6时。为什么不是8因为如果阈值也是8树化和退化会在边界反复震荡来回切换成本太高取6留出缓冲空间。2.3 并发下的HashMap与ConcurrentHashMap演进HashMap是线程不安全的这个结论要从多线程put时可能发生覆盖、扩容期间可能出现死循环这些角度去说明。JDK1.7的扩容采用头插法并发扩容时可能形成环形链表读取时死循环CPU飙满。JDK1.8改成尾插法修复了这个问题但并发覆盖的问题仍然存在所以并发场景应该用ConcurrentHashMap。ConcurrentHashMap是面试里的另一座大山重点说它怎么保证线程安全。JDK1.7的是分段锁设计把数据分成一段一段每段都有一把锁锁粒度是Segment默认16段。并发度就是段数理论上16个线程可以同时写不同段的数据。JDK1.8放弃了分段锁直接用Node数组加CAS加synchronizedput的时候如果对应槽位为空用CAS去插入不用加锁如果槽位不为空就对链表头节点或红黑树根节点加synchronized锁。锁粒度从“一段”细化到“一个桶”并发度大幅提高。回答到这儿可以补一句JDK1.8之所以不用分段锁是因为Segment本身继承ReentrantLock锁粒度太粗而且内存占用也大。CAS加synchronized既保留了细粒度锁的优势又减少了加锁的开销这也解释了为什么现在的版本性能更好。3. 并发编程不止会背八股还要能说清设计动机并发是Java面试的分水岭基础题人人都能答深入问下去就筛掉一批人。这部分内容要么不背要背就必须连设计动机一起背否则面试官换个问法就露馅了。3.1 volatile到底解决了什么问题又解决不了什么volatile有三个考点可见性、禁止指令重排、不保证原子性。先说可见性。Java内存模型规定每个线程有自己的工作内存操作变量时先拷贝主内存的副本到自己工作内存操作完再刷回主内存。普通变量的更新对其他线程不可见因为刷回时机不确定。volatile修饰的变量写操作会立即刷回主内存读操作会强制从主内存读取相当于告诉线程“这个变量别缓存每次都去看主内存”。注意可见性本质是缓存一致性协议在硬件层面提供的保证JMM再通过内存屏障把这个语义暴露给程序员。再说禁止指令重排。编译器和CPU为了优化性能可能会调整指令执行顺序单线程下没问题多线程下可能出问题。volatile通过在读写操作前后插入内存屏障限制编译器重排保证程序串行语义一致。有一个很经典的DCL双重检查锁单例问题由于创建对象不是原子操作极端情况下其他线程会拿到一个半初始化的对象给 instance 加上volatile就能解决因为禁止了“分配内存、调用构造器、赋值引用”这三步的重排序。最后是不保证原子性。volatile不能保证复合操作的原子性最典型的例子是count。这个操作包含读取、加1、写回三个步骤即使每一步都可见两个线程同时读到一个旧值再各自加1写回最后结果也会少一次递增。这也是为什么说“volatile适合做状态标记不适合做计数器”。3.2 synchronized与Lock的选择逻辑聊并发锁既要会对比也要会回答“你项目里怎么选”。synchronized是JVM内置的关键字Lock是JDK提供的接口区别集中在四点上锁的获取和释放synchronized由字节码指令monitorenter和monitorexit自动管理异常时JVM会自动释放锁Lock必须手动lock和unlock一般放在try-finally里保证释放。可中断性synchronized在获取锁时不可中断如果拿不到锁就只能一直阻塞Lock的lockInterruptibly可以响应中断。非阻塞获取锁synchronized只能阻塞等待Lock的tryLock可以尝试获取拿不到就返回false避免死等。公平性synchronized是非公平锁ReentrantLock构造器传true可以启用公平锁。但面试官真正想听的其实是synchronized的锁升级过程。无锁 → 偏向锁 → 轻量级锁 → 重量级锁这个路径就是“让大多数场景下不要真的有线程阻塞”。偏向锁适用于只有一个线程访问同步块的场景一旦有竞争升级为轻量级锁通过CAS自旋尝试获取避免线程切换自旋失败或竞争激烈时膨胀为重量级锁依靠操作系统互斥量阻塞线程。这个机制说明JVM在“减少锁开销”这件事上做了大量优化。JDK15之后偏向锁被废弃回答时如果提到这一点会显得你关注新版本变化。选型上我的建议是能用synchronized就用synchronized它简单可靠而且JVM后续版本还在持续优化需要超时控制、可中断或者有大量读少写场景需要读写锁时才考虑Lock体系。3.3 线程池参数与拒绝策略线程池是并发模块必考内容核心是七个参数和一个执行流程。七个参数分别是核心线程数corePoolSize、最大线程数maximumPoolSize、空闲线程存活时间keepAliveTime、时间单位unit、任务队列workQueue、线程工厂threadFactory、拒绝策略handler。执行流程要按顺序说清楚提交任务时当前工作线程数小于corePoolSize就创建新线程执行任务大于corePoolSize就放进工作队列排队队列满了线程数小于maximumPoolSize就创建临时线程执行任务队列满且线程数到达最大值就触发拒绝策略。拒绝策略有四种AbortPolicy直接抛异常这是默认的CallerRunsPolicy由提交任务的线程自己执行起到限流作用DiscardPolicy静默丢弃DiscardOldestPolicy丢弃队列里最老的任务再重新提交。线程池的核心线程数设置没有唯一答案但要能给出合理思路。CPU密集型任务核心线程数设置为CPU核数加1比较合适因为CPU密集型很少等待线程多了反而增加切换开销。IO密集型任务核心线程数可以设置为CPU核数的两倍因为IO等待期间CPU是空闲的可以多安排线程占用它。更精确的公式是CPU核数除以(1 - 阻塞系数)阻塞系数通常在0.8到0.9之间。回答时强调具体问题具体分析、压测验证更重要比背死数字更有说服力。4. Spring框架问到底IoC、AOP与Bean生命周期Spring几乎是Java后端岗位的标配面试问到Spring的深度直接决定你是“会用的”还是“懂原理的”。这一章把核心概念拆到最底层。4.1 IoC容器的本质控制权反转到底反转了什么IoC全称Inversion of Control控制反转。很多新手背了定义也不知道在说什么。用生活例子解释一下你开一家餐厅以前食材、厨师、菜单全自己管每天累死。现在你把餐厅交给一个中央厨房管理你只需要说自己要什么菜中央厨房统一调度厨师、备好食材、把菜端上来。中央厨房就是IoC容器你需要什么就注入什么。开发者不用自己new对象、不用管依赖关系容器负责创建对象、组装对象、管理对象生命周期。反转的是什么是对象创建和依赖管理的控制权。以前是代码主动new依赖现在是容器把依赖“注入”进来所以也叫DI依赖注入。用Spring的时候最直观的体验是你只需要在类上标注Component或在配置类里声明Bean容器启动时就会帮你创建这个类的实例。创建的时候如果发现这个Bean依赖其他Bean容器会递归地先把依赖的Bean创建好再注入进来。这就是控制反转的落地。面试官如果问“IoC和DI的关系”回答是IoC是设计思想DI是实现IoC的方式Spring通过依赖注入让容器和被管理的对象解耦。4.2 AOP底层JDK动态代理与CGLIBAOP面向切面编程核心用途是抽离日志、事务、权限这些横切逻辑避免它们侵入业务代码。面试必问的是它的底层实现JDK动态代理和CGLIB。JDK动态代理要求目标类实现接口它基于Java反射机制在运行时生成一个代理类代理类实现目标类同样的接口在调用目标方法前介入逻辑。CGLIB不要求实现接口它通过继承目标类生成子类子类重写父类方法来实现增强。Spring默认规则是目标类实现了接口就用JDK动态代理没有实现接口就用CGLIB。面试加分点是性能差异。JDK动态代理自Java8之后性能已经很接近CGLIB而且它生成代理对象的速度更快因为基于接口的代理类生成逻辑更简单。CGLIB因为是字节码增强生成子类创建代理对象时有额外开销但调用方法时性能也不差。Spring Boot 2.x之后默认强制使用CGLIB不管有没有接口这样做的原因是为了统一行为、避免代理类型不一致带来的排查困难。还有一个高频追问JDK动态代理为什么只能代理接口因为代理类本身是运行时生成的Proxy子类Java是单继承Proxy已经占掉了一个父类位置只能通过实现接口来代理目标。这个限制是语法层面决定的。4.3 Bean生命周期和循环依赖Bean生命周期这道题要是能把Spring源码里那几哥后处理器串起来回答质量会高不少。完整生命周期大致是实例化对应构造器或工厂方法→ 属性填充依赖注入→ 执行前置处理器方法BeanPostProcessor的postProcessBeforeInitialization→ 执行初始化方法InitializingBean的afterPropertiesSet或自定义init-method→ 执行后置处理器方法postProcessAfterInitialization→ 使用 → 销毁DisposableBean的destroy方法或自定义destroy-method。面试官让你配一张这样的表并不要求你一字不差背源码但至少要知道实例化不等于初始化。实例化只是把对象new出来此时属性还没填充初始化才是业务上“准备好了”的节点。AOP代理一般发生在后置处理器阶段所以拿到代理Bean是在postProcessAfterInitialization之后。Spring三级缓存解决循环依赖也是个必考考点。一级缓存存成品Bean二级缓存存提前暴露的早期Bean三级缓存存ObjectFactory工厂对象。A依赖B、B依赖A时A实例化后先把自己的ObjectFactory放进三级缓存然后填充属性时发现需要B就去创建BB实例化后在填充属性时发现需要A从三级缓存拿到A的ObjectFactory调用getEarlyBeanReference得到早期引用放进二级缓存然后完成B的创建B创建完A拿到B的引用继续完成初始化最后把成品A放进一级缓存。这中间有个关键限制Spring只能解决setter注入和字段注入的循环依赖不能解决构造器注入的循环依赖因为构造器注入在实例化阶段就必须拿到依赖根本没法提前暴露早期引用。回答时主动说出这个限制面试官会觉得你真懂而不是背概念。5. 八股文怎么背才算“入门到精通”背知识是一回事把知识变成面试得分能力又是一回事。这一章不讲具体知识点了聊聊准备面试的方法论。这部分是我在看了大量新人面试反馈后总结出来的比多背一百道题更有用。5.1 知识串联先建体系再记细节很多零基础的同学背八股文失败不是因为记性差是因为知识点是散的。今天背一个HashMap明天背一个JVM垃圾回收相互之间没有关联过一周全忘。正确的做法是先画知识地图再往里填细节。我当时准备面试时先列了一张“Java面试能力地图”分成六大块Java基础与集合、JVM、并发、Spring家族、MySQL、Redis。每一块先记住“这个模块在解决什么问题”再记“核心概念有哪些”最后才记具体细节和数字。比如JVM这块先记住“JVM是为Java程序提供运行时环境的虚拟机核心工作是加载类、分配内存、回收垃圾”然后才是“内存分哪几块、垃圾回收有哪几种算法”。这样即使面试时某个细节忘了也能根据大框架现场推导不会开口就是一句“我不会”。这里强烈建议用费曼学习法把每周学的知识用自己的话讲给同事、同学听或者写成一篇文章发出来。当你能把一个问题用大白话讲得让零基础的人也听懂时这个知识点就真正属于你了。写面试总结博客就是很有效的练习方式我现在回头看自己当年写的笔记很多内容正是这样一点点从“背下来”变成“理解透”的。5.2 用“是什么、为什么、怎么做”组织答案背会了知识还要会表达。面试官问一道题最怕听到两种回答一种是只背结论不会展开另一种是东扯西扯没有逻辑。正确的方式是按“是什么、为什么、怎么做”三段式来组织答案。拿“为什么HashMap是线程不安全的”来举例是什么HashMap在多线程环境下put操作可能丢失数据扩容可能导致死循环JDK1.7所以是线程不安全的。为什么多个线程同时put时如果计算出的下标一致并且这个位置上原本没有值两个线程可能同时插入成功后者覆盖前者数据就丢了。扩容时多个线程同时rehash可能导致链表成环。怎么做并发场景用ConcurrentHashMap它通过CAS加synchronized保证线程安全或者对HashMap做外部同步。这个结构的好处是就算你的结论不够深入面试官也能看到你有完整的思考框架。几乎所有八股题都可以套这个结构。JVM类的问题套“是什么内存结构是什么样子、为什么为什么这样设计、怎么做怎么调优”Spring类的问题套“是什么IoC是什么、为什么解决什么问题、怎么做容器怎么实现”逻辑一下子就通了。5.3 面试时答不出的处理方式最后聊一个实战技巧。面试中一定会遇到不会的题任何“精通”的人都会遇到关键看你怎么处理。千万不要直接说“我不会”就冷场也不要不懂装懂瞎编。正确的做法是把问题拆成自己知道的子问题先回答相关部分再诚实说明盲区在哪。面试官问“JVM的ZGC了解吗”如果不够熟悉可以这样回答“ZGC是基于Region的内存布局目标是极低停顿时间JDK11开始引入核心思路是不分代、使用彩色指针等技术实现并发收集。具体到染色指针的细节我研究得还不深但这部分内容我正在系统补有兴趣的话我可以再聊聊G1的设计思路。”这样既展示了知识边界又显示了学习能力和关联思考能力比硬编一个答案体面得多。6. 一道典型面试题的完整拆解演示光讲方法论容易空洞我拿一道面试出现频率极高的题做个完整演示大家感受一下前面的“什么是、为什么、怎么做”框架是怎么落地的。这道题是线程池是怎么保证核心线程不被销毁的看到这道题先不要急着背答案。用三层框架思考是什么、为什么、怎么做。是什么线程池里有一个Worker内部类每个Worker持有自己的线程它通过一个while循环不断从工作队列里获取新任务来执行。核心线程不被销毁关键就在于这个循环和阻塞队列。为什么因为核心线程的存活策略跟普通线程不同。线程池给Worker配置了allowCoreThreadTimeOut标志默认是false。这个标志为false时核心线程在从队列里拿任务时用的是take()方法而take()是阻塞方法——队列为空时线程会阻塞等待而不是退出循环。只有当这个标志为true时核心线程才会受keepAliveTime控制空闲超时后退出。非核心线程拿任务用的是poll(timeout)它设置了排队时间上限超时拿不到任务就返回null线程退出。怎么做线程池在runWorker方法里包了一个while循环循环条件就是task不为空或者通过getTask()拿到了新任务。Worker维护着状态只要还在线就不断执行新任务。整个机制的本质是“让线程没事干的时候不要退出而是阻塞等待新任务到来”用阻塞代替销毁节省了重复创建线程的开销。这道题答到这里已经包含了参数细节、源码逻辑、设计动机三个层次面试官再追问什么都能接住。你会发现八股文背得再多不如掌握一个通用框架。把每道高频题都按这个思路过一遍面试时的表现会有质的提升。Java面试考察的从来不是背题能力而是你对自己技术栈的理解深度。把JVM、集合、并发、Spring这些核心模块的原理吃透再用一套自己的逻辑串起来面试官反而会记住你是一个“有体系”的人。这套方法我指导过很多人实践效果都很明显。希望这篇整理能帮你在准备面试的路上少走点弯路。