Java 基础
集合
- `Hashmap`:数组链表红黑树,8,64,0.75 Entry,扩容2倍新数组,
- `LinkedHashMap` 继承HashMap
- `Hashset` Hashmap key, value PRESENT,去重相同元素认定(hashcode + equals - true)
- `transient` 序列化排除(比如password)
- `ConcurrentHashMap` 7分段锁,8 cas+synchonized
- `TreeMap` 红黑树,key必须可比较,天然有序
- `TreeSet` 基于TreeMap,key value PRESENT,去重相同元素认定(compareTo返回0)
- `HashTable` 数组+链表,默认11,0.75,不允许null,hash未优化,冲突概率高
- `ConcurrentSkipListMap` 跳表,线程安全,效率高,key必须可比较,天然有序
- `ConcurrentSkipListSet` 跳表,线程安全,效率高,key value PRESENT,去重相同元素
- `LinkedHashSet` 继承HashSet,底层是LinkedHashMap,key value PRESENT,去重相同元素
---
- `synchronized` 对象包含markword jvm级别锁,支持无锁(初始),偏向(单线程,Mark Word 记录线程 ID),轻(多线程交替 CAS抢占) 重量级(多线程抢占 markword指向monitor,失败进entrylist) ;对象头markword(锁状态和线程id+类型指针)+Monitor(互斥锁 owner EEntryList Waitlist); 流程: 一个线程try 设置Monitor Owner为自己,失败进Waitlist,
- 线程是否 “同时争抢锁”(竞争重叠度):重量级(不是线程数决定)
- T1 synchronized(LOCK){.. LOCK.`wait`()...} ==> T1 Waitlist
- T2 synchronized(LOCK){.. LOCK.`notify`()...} ==> 唤醒 Waitlist中T1
- 互斥,部分代码同步(自定义锁 object lock, static 代码块),,需区分「实例级互斥锁this」和「全局级互斥 - 锁 类.class」
- 单例模式(volatile+双重检查锁,避免重复创建)
- ReentrantLock 可公平/非公平、 AQS「状态变量 + 双向链表队列」 、CAS+volatile、自定义同步状态 、Lock() 是非阻塞的、
- CountDownLatch produceLatch = new CountDownLatch(2);
-final:produceLatch.countDown();
- produceLatch.await(); 等待 变0
- produceLatch.await(2, TimeUnit.SECONDS); // 超时 2 秒,阻塞2s
- Semaphore 基于 AQS(「AQS 状态变量(state)+ 双向等待队列」,且 Semaphore 是 AQS 的「共享模式」实现(区别于 ReentrantLock 的独占模式)
- acquire 获取许可
- release 释放许可
- tryAcquire 尝试获取许可
- ❓ synchronized 和 ReentrantLock 的区别是什么?底层实现原理有何不同?
- 区别:实现原理(s是jvm内置锁,字节码指令实现;R API层面,Java代码实现),灵活性(s非公平锁,R支持非/公平锁,R可以中断锁等待)、功能(s 1.6后性能优化接近R,R更稳定) 、性能
- ❓ volatile ? 内存屏障,写操作加内存屏障,阻止后面指令,读操作加屏障保证读主内存最新。修改变量后主动刷新到主内存。可以保障可见性、有序性,不保障原子性。 无锁CAS。
- 场景:停止标记,
- CAS漏洞 ABA问题。大部分场景不影响,少部分影响。 比如链表A-B-C,T1想删除B(Anext修改为C),但是T2先删除B,然后在C增加了B(AnextCnextB);后面T1再做删除,就会把新加的B删除变成(AnextC)。 解决方案(1)版本号+值一起作为CAS值比较 (2)使用带版本号的原子类(Java 自带,首选)AtomicStampedReference 和 AtomicMarkableReference,专门解决 ABA 问题:AtomicStampedReference:给变量绑定一个 “int 类型的版本号”,每次修改变量时,版本号自动 +1; AtomicMarkableReference:给变量绑定一个 “boolean 类型的标记”(仅表示 “是否被修改过”,不记录修改次数)
- ❓HashMap原理? 数据结构,16, 0.75,12。64,8 。 key hash,冲突 加快查询效率,树化,扩容迁移
- ❓HshSet: Hashmap,key是值,value 常量PRESENT。 eques + hash
- ❓ConcurrentHashMap: synchronized+分段锁。 key不能为null(歧义)
- ❓AQS 抽象队列同步器:volatile state 同步状态、FIFO 双向同步队列(CLH 队列) ; acquire()(获取锁)、release()
- ❓ReentrantLock: 基于 AQS、state 重入计数、FIFO 同步队列、CAS+synchronized 辅助。tryLock()(非阻塞)、lock()(阻塞)、lockInterruptibly()(可中断) 公平锁
- ❓CopyOnWriteArrayList 读多写少。 volatile 数组(保障读可见性)、ReentrantLock 写锁
Redis
- String(json 缓存对象 限流计数器,分布式锁)、Hash(字典,kv,用户消息,配置)、List(消息队列,任务队列,「有序、可重复、支持两端插入 / 删除」)、Set(无序去重, 好友,抽奖)、ZSet (有序,排行榜,延迟队列),Bitmap 活跃用户 , 或者 签到
- Bitmap(SETBIT key offset value) 比如签到:用户 ID=1001,2025 年 11 月 5 号签到(5 号对应 offset=4) SETBIT user:sign:1001:202511 4 1
- 查询某一天是否签到:GETBIT user:sign:1001:202511 4
- 统计某月签到:BITCOUNT user:sign:1001:202511
- 查询当月第一次 / 最后一次签到日期BITPOS user:sign:1001:202511 1
- 最后一次签到(需结合当月天数,反向查找)# 思路:从当月最后一天的偏移量(如30天=29)反向查第一个1 BITPOS user:sign:1001:202511 1 29 29 # 从offset=29开始查,步长-1
- 统计连续签到天数(核心业务逻辑) java 从今天向前遍历,直到遇到0或offset<0
jstatck 分析死锁
jps 获取 列表 pid, jstatck pid
数据库连接池爆满
可能: 线程数不够了 并发请求太高了 有慢SQL 思路: 看流量监控,没有异常流量。 查下SQL 耗时,慢SQL日志。 更新工单 WHERE id = ? AND deleted = ? AND lock_version = ? 简单的一个更新语句,其中使用了乐观锁进行并发控制。 热点行更新
📚 频繁FullGC问题如何排查
回答要点关键字
打开详情
频繁FullGC是Java面试中的高频问题,很多开发者因平时不总结,面试时容易思路空白。视频旨在总结FullGC问题排查的通用方法。
排查步骤(以电商网站系统反应慢为例)
• 收集GC日志:通过JVM参数(如-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/path/to/gc.log)输出GC日志,这是排查的起点。
• 分析GC日志:不建议直接阅读原始日志,推荐使用GCViewer等工具,可清晰展示FullGC频率(如案例中每10分钟一次,远高于正常的几小时/几天一次)、每次执行时间(案例中平均3秒,导致前端用户卡顿)及老年代内存使用情况(案例中FullGC后仍占80%以上,暗示内存泄漏)。
• 监控JVM内存使用:可借助VisualVM等工具,观察年轻代(Eden、Survivor区)和老年代的内存变化,分析是否因年轻代频繁填满触发MinorGC,或老年代持续增长导致FullGC频繁。
• 分析堆内存(Dump文件):通过jmap命令导出堆Dump文件(注意:线上执行需谨慎,避免业务高峰,因大内存 Dump 可能导致系统卡顿),再用MAT(Memory Analyzer Tool)分析,定位大对象或内存泄漏点(如案例中静态订单列表未清理导致内存泄漏)。
• 检查代码中的内存使用:审查代码,查找内存泄漏(如静态集合未释放)、大对象创建等问题,并进行代码优化(如及时清理无用对象、控制集合大小)。
• 调整JVM参数:若代码无明显问题,可调整堆内存大小、年轻代与老年代比例等JVM参数,平衡内存分配以减少FullGC频率。
• 优化代码:从业务逻辑层面优化,如案例中对静态订单列表做容量控制或及时清理。
• 验证改进:通过压测等方式验证调整后的效果,观察FullGC频率和耗时是否改善。
频繁Full GC的原因主要有以下几类:
-
内存泄漏
• 对象被无意持有引用,无法被GC回收。例如:
◦ 静态集合类(如static List)不断添加对象却不清理,对象长期驻留内存,最终导致老年代溢出触发Full GC。
◦ 监听器、回调函数等未正确注销,引用的对象无法释放。
-
内存不足
• 堆内存配置过小,无法满足应用正常运行的内存需求,对象快速填满老年代,频繁触发Full GC。
• 新生代与老年代比例不合理,例如年轻代过小,导致对象提前进入老年代,加速老年代内存消耗。
-
大对象创建频繁
• 频繁创建大对象(如大数组、大字符串),这些对象直接进入老年代,短时间内填满老年代,引发Full GC。
-
GC算法或参数不合理
• 垃圾收集器选择不当,例如在高并发场景下使用串行收集器,GC效率低,导致Full GC频繁。
• JVM参数配置不合理,如-XX:MaxTenuringThreshold(对象晋升老年代的年龄阈值)设置过小,对象过早进入老年代;或-XX:NewRatio(新生代与老年代比例)设置不合理,导致老年代内存紧张。
-
系统负载过高
• 应用在高并发、大数据量场景下,对象创建速度远超GC回收速度,老年代持续增长,频繁触发Full GC。
📚 频繁FullGC问题如何排查 - 2
回答要点关键字
打开详情
问题现象
视频作者分享了一个线上案例,服务差不多3 - 4秒钟就发生一次Full GC,导致服务断断续续,收到用户投诉。
原因分析
• 特定租户与API问题:
◦ 某个租户调用特定API时易触发Full GC,该租户处理的数据量约为其他租户的10倍。
◦ 一方面是数据量大,创建大量瞬时对象,堆内存空间不足引发频繁垃圾回收;另一方面是API程序存在问题,包含大量低效的聚合计算,对象来不及释放,且大对象直接进入老年代,加剧了Full GC。
• 内存泄漏:约70% - 80%的Full GC由内存泄漏导致。内存泄漏时,堆内存中的对象无法释放,不断增长,最终触发Full GC来释放内存。
• JVM参数设置不合理:堆内存过小或内存分配策略不当,导致堆内存管理效率低下,容易触发Full GC。
解决方案
• 优化程序代码:对大对象进行拆分,优化聚合算法等。
• 提前处理大数据量运算:避免调用API时频繁创建对象,减少Full GC频次。
• 合理设置JVM参数:建议年轻代与老年代比例设置为1:2,具体大小需根据业务量和硬件环境测试确定。
GC调优建议
• 若系统参数设置合理,无超时日志,GC频率和耗时都不高,无需进行GC优化。
• 若GC时间超过1 - 3秒,或GC频繁发生,则必须进行优化。
📚 Java 线上 OOM(内存溢出)定位与解决
回答要点关键字
打开详情
这是一个关于 Java 线上 OOM(内存溢出)定位与解决 的技术视频,核心内容可总结为以下几点:
一、OOM 的外在表现与本质
• 外在表现:
◦ 应用长时间运行后性能严重下降;
◦ CPU 使用率飙升(甚至 100%);
◦ 频繁触发 Full GC,伴随接口超时等告警;
◦ 应用抛出 OutOfMemoryError 异常,或偶尔耗尽连接对象(如数据库连接)。
• 本质:内存资源耗尽,老年代频繁 Full GC 仍无法回收,最终导致内存溢出。
二、OOM 的三大成因及解决思路
1. 一次性申请对象过多
◦ 场景:例如数据查询时一次性加载千万级数据(如千万级数据直接加载到 List)。
◦ 解决:控制对象申请数量,如分页查询(一次查 10/100 条)。
2. 内存资源耗尽未释放
◦ 场景:高并发下创建线程、使用 JDBC 连接后未关闭,资源长期占用。
◦ 解决:及时释放资源(如关闭 Connection),或引入池化思想(如线程池、连接池)限制资源数。
3. 堆内存本身分配不足
◦ 场景:应用需处理大对象,但堆内存配置过小,无法支撑业务。
◦ 解决:通过 jmap -heap <进程ID> 查看堆内存使用情况,结合业务调整堆内存参数(如 -Xms、-Xmx)。
三、OOM 定位的核心方法:Dump 文件分析
通过 堆 Dump 文件 分析是定位 OOM 的关键手段,分为两种场景:
-
系统已 OOM 挂掉
• 提前配置 JVM 参数:启动时添加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<dump文件路径>,当 OOM 发生时自动生成 Dump 文件。
• 分析步骤:
◦ 将 Dump 文件下载到本地,用 jvisualvm 工具加载;
◦ 查看“类”列表,找到内存占比最高的业务对象;
◦ 追踪该对象的 GCRoot(垃圾回收根节点),结合线程栈定位具体代码行。
-
系统仍在运行(未挂掉)
• 手动导出 Dump 文件:执行命令 jmap -dump:format=b,file=<文件名>.hprof <进程ID>,会触发一次 Full GC(STW,Stop-The-World),但为了快速定位值得权衡。
• 分析步骤:同“系统已挂掉”的后续步骤,用 jvisualvm 分析 Dump 文件。
四、工具与命令补充
• 查看 Java 进程:jps
• 查看堆内存概览:jmap -heap <进程ID>
• 查看存活对象分布:jmap -histo:live <进程ID>
• 调试工具:jvisualvm(可视化分析 Dump 文件)、Arthas(在线诊断工具,可辅助定位内存问题)。
总结来说,视频提供了一套 “成因分析 + 工具实操 + 场景适配” 的 OOM 定位与解决方法论,适合 Java 开发者在面试或实际工作中参考。
📚 项目中如何排查线上 OOM 问题
回答要点关键字
打开详情
一、OOM本质与排查核心
• OOM本质:JVM内存分配超过上限,且无用对象(如内存泄露、大对象堆积)无法被GC回收。
• 排查核心:线上问题处理需“先保服务,再查根因”,这是优先级关键。
二、排查流程(以订单服务OOM为例)
1. 应急止损
◦ 先重启服务,临时恢复可用性。
◦ 用jmap导出内存dump文件(无此文件则无法后续分析),同时记录当时的JVM参数(如堆内存配置)。
2. 离线分析
◦ 将dump文件下载到本地,用工具分析:
◦ 用jhat启动服务,查看内存对象分布,发现ArrayList对象占70%内存。
◦ 用MAT工具进一步分析,发现该ArrayList是未清理的历史订单缓存,用户越多堆积越严重,最终撑爆内存。
3. 验证解决
◦ 代码层面:添加订单缓存定时清理逻辑(每小时清理过期数据)。
◦ JVM参数层面:将老年代内存从1G调整到1.5G。
◦ 上线后用jstat -gc [进程ID] 1000监控GC趋势,确认内存不再持续上涨,问题解决。
三、OOM预防机制(加分细节)
• 自动生成dump文件:在Java参数中配置OOM时自动生成dump文件的参数,无需手动导出。
• 内存监控告警:用Prometheus+Grafana监控JVM内存使用率,超过80%即告警,提前排查避免故障。
四、总结
OOM排查需记住三点:
• 应急:重启服务+导出dump文件;
• 分析:用jmap+jhat/MAT定位内存泄漏点;
• 预防:JVM参数配置+内存监控告警。
📚 如何处理RocketMQ消息积压问题
回答要点关键字
打开详情
在高并发项目中,RocketMQ作为常用的消息中间件,时常会面临消息积压的问题。当面试官发现你的项目是高并发且使用了RocketMQ时,很可能会问到如何处理消息积压。下面我们就来详细聊聊这个问题。
一、消息积压的产生原因
说白了,消息积压就是生产者生产消息的速度大于消费者消费的能力,也就是“供大于求”。就像仓库里商品生产速度远超销售速度,最终导致商品堆积,消息队列里的消息也是如此,生产者疯狂发消息,消费者消费不过来,就形成了积压。
二、如何定位消息积压
要定位消息积压很简单,我们可以打开RocketMQ控制台,通过控制台的相关数据,就能直观地看到有多少积压的消息。
三、消息积压的处理策略
处理消息积压不能一概而论,要分情况对待:
- 瞬时激增流量,消息时效性要求不高
如果是生产端出现了瞬时的峰值流量,短时间内产生大量消息导致积压,而消费者一时半会没缓过来,且消息对时效性要求不是特别严格,那我们可以不用管。
因为RocketMQ的消息最终会落到磁盘上,不会在瞬时对消息队列产生毁灭性打击。随着流量下降,激增的高峰过去,消息队列最终会趋于平稳,消费者慢慢消化即可。
- 消费者自身性能低下
如果是因为消费者自身性能原因,比如存在慢SQL、耗性能的逻辑代码、耗性能的第三方调用等,导致消费能力低下,那这种情况必须优化消费者。
比如优化慢SQL,让数据库查询更快;优化耗性能的逻辑代码,减少不必要的计算;优化第三方调用,采用异步、缓存等方式降低消耗。要是应用访问量本身就大,不优化的话消息会越积越多,最终可能让消息队列支撑不住。
- 消息实时性要求高
如果积压的消息对实时性要求比较高,那必须及时处理,主要有以下两种方法:
(1)利用消费者组,增加消费者数量
在RocketMQ中,一个Topic下可能有多个MessageQueue,如果messageQueue数量多于消费者数量,就容易很容易积压。
我们可以利用消费者组的概念,让messageQueue和消费者一一一一对应,也就是立马在当前消费者组下上线新的消费者,以此增加消费能力。RocketMQ规定一个messageQueue最多只能被一个消费者消费(同组内),这样可以避免重复消费等问题,最大程度提升消费能力。
比如一个Topic下有2个messageQueue,那最多可以增加到2个消费者,实现一一对应消费。
(2)紧急上线新Topic
如果在项目上线初期创建的messageQueue数量很少,即使一一对应增加消费者也无法满足需求,那我们可以紧急上线一个新的Topic,并在这个新Topic下创建更多的messageQueue。
然后将生产者的消息发送地址改为新Topic,让老Topic里的积压消息由原有消费者继续消费,等老Topic的消息消费完后,再将其下线即可。
📚 RocketMQ架构是什么样的?
回答要点关键字
打开详情
视频指出,面试中被问到SpringBoot时,启动流程是高频问题。很多人只是死记硬背步骤,导致回答逻辑混乱。
视频将SpringBoot启动流程总结为三件事:准备环境、创建上下文、启动业务逻辑,并拆解为5个关键步骤:
1. 准备环境:加载配置文件、系统变量、命令行参数,封装成ConfigurableEnvironment。
2. 创建ApplicationContext:即上下文容器,进行Bean装配、加载配置类(@ComponentScan在此处生效)。
3. 刷新上下文:完成所有bean初始化、事件监听器注册,Web项目还会启动内嵌Tomcat。
4. 调用CommandLineRunner和ApplicationRunner:执行启动后需运行的逻辑。
5. 发布ApplicationReadyEvent:启动流程结束,应用进入运行状态。
此外,视频还强调面试时要表现出对源码的理解,而非死记硬背,可通过“假装回忆”的方式展示自己的思考过程,提升面试表现。
📚 SpringBoot启动流程的
回答要点关键字
打开详情
面试被问起Spring Boot,十有八九都逃不过启动流程这题。很多同学一上来就是背诵main()方法启动、创建SpringApplication、name方法启动、准备环境、刷新上下文,听起来是都说到了,但全堆一块像是一锅粥,听完面试官都得皱眉头,你到底是理解的还是硬背的? 今天我用1分钟时间教你一套能讲清逻辑还能显得你是理解不是硬背的答法。先记住一句话,Spring Boot的启动流程其实是在干三件事:准备环境、创建上下文、启动业务逻辑。
• 从头捋一遍,第一步启动main方法,核心就是这行代码,看似就一行,实际背后干了两件大事。
• 一:new SpringApplication(),这里会推断应用类型是Web还是普通应用,设置初始化器、监听器、主类。这里假装不经意提一嘴,我记得推断应用类型是判断类路径中存在javax.servlet.Servlet相关的类就是Web应用,还有设置初始化器,这一步触发了Spring Boot自动配置的动作,就是从spring.factories加载并实例化指定的类。
• 二:调用run()方法,真正启动流程全在这里,是不是很多,你总不能全念一遍吧,别怕,我帮你拆成了5个关键步骤。
• 第一:准备环境,加载配置文件、系统变量、命令行参数,最终封装成ConfigurableEnvironment,这一步就是把配置全准备好,做饭前先把材料全摆好是一样的。
• 第二:创建ApplicationContext,也就是常说的上下文容器,这里会装配Bean、加载配置类,熟悉的@ComponentScan就是在这里发挥作用。
• 第三:刷新上下文,所有bean初始化完成,事件监听器注册完毕,如果是Web项目还会在这时启动内嵌的Tomcat。
• 第四:调用CommandLineRunner和ApplicationRunner,如果你写了启动后要跑一段逻辑,比如打印一句,看面试鸭的朋友都升职加薪、拿大offer,就在这时候触发。
• 第五:发布ApplicationReadyEvent,所有启动流程结束,应用进入运行状态,准备好接收请求。
这样讲下来流程是不是清晰多了?整体流程总结可以看一下我画的这幅图,把启动流程分为两大类,每个大类又总结了一些重点,这样给面试官说出来大概率会觉得你不是背八股是真正的懂源码细节的。 还有一点特别重要,面试官问这类题型不要一上来就像报菜名一样,叭叭的一顿输出,压中题的心里暗爽了也得演,你要假装回忆一下,小声BB道:我之前学Spring Boot的时候好像看过这段逻辑,我当时还debug了研究了一下,我简单回忆一下应该是分了两个阶段,第一步什么什么,懂了吧,记住了。 面试的本质就是一场表演,后面我会分享更多高频面试题,等我。
📚【面试题目】垃圾回收(GC)如何选择?
回答要点关键字
(1) 选型核心依据(堆大小、响应时间、吞吐量需求)
(2) 主流GC特性与场景匹配逻辑
(3) 高级优化方向(分代/分区策略、JDK版本适配)
(4) 项目实践选型经验与避坑原则
### 打开详情
- 🍺基础回答:日常开发先看项目场景呀!堆内存小(<4G)、要求响应快就选SerialGC,服务端追求高吞吐量用ParallelGC,互联网高并发低延迟优先G1GC,超大堆(百G级)场景直接上ZGC/Shenandoah。
- 🎉高级扩展版:
- 分代收集器(Serial/Parallel/CMS)依赖对象分代假说,老年代CMS用标记-清除算法,存在内存碎片和STW波动问题,JDK9后逐步废弃。
- 分区收集器(G1/ZGC/Shenandoah)按Region回收,G1通过优先级调度平衡吞吐量和延迟,ZGC用着色指针+读屏障实现毫秒级STW。
- 选型需联动JDK版本(JDK11+ZGC成熟,JDK8优先G1替代CMS)和硬件资源(CPU核心数、内存带宽)。
- 📌 加分项:
- 结合GC日志(如G1的CSet/RSet状态)和监控数据(CMS的并发失败次数)调整选型。
- 举例场景:电商秒杀用G1,大数据批处理用ParallelGC,金融核心交易用ZGC。
- 对比Shenandoah与ZGC的差异(写屏障实现、堆大小适配)。
- ⚠️注意事项:
- 不盲目追求“无停顿”,ZGC的线程开销可能降低吞吐量。
- 小内存场景用SerialGC比G1更高效,避免过度设计。
- 避免在JDK8中强行使用ZGC(不支持),优先G1优化。
📚【面试题目】垃圾回收算法?
回答要点关键字
(1) 四大核心算法(标记-清除/复制/标记-整理/分代)原理
(2) 算法优缺点与适用区域(新生代/老年代)
(3) 高级优化(并发标记、分区回收、屏障技术)
(4) 算法与GC收集器的关联逻辑
### 打开详情
- 🍺基础回答:最基础的是标记-清除算法,先标存活对象再清垃圾,快但有碎片;复制算法把内存分成两块,复制存活对象,无碎片但浪费空间,适合新生代;标记-整理算法标记后移动对象compact内存,无碎片但耗时,适合老年代;分代算法就是新生代用复制、老年代用标记-整理/清除的组合。
- 🎉高级扩展版:
- 增量收集算法:拆分GC任务穿插应用线程,减少STW时长(早期CMS采用)。
- 并发标记算法:标记阶段与应用线程并发,仅初始/重新标记短暂STW(CMS/G1/ZGC通用)。
- 分区算法:堆分成多个Region单独回收,提升效率(G1/ZGC核心)。
- 屏障技术:读屏障(ZGC)、写屏障(G1的RSet维护)保障并发回收准确性。
- 📌 加分项:
- 解释“分代假说”(对象朝生夕死、存活对象年龄增长)对算法设计的支撑。
- 分析G1的混合算法逻辑(Region内复制+全局标记-整理)。
- 对比算法时间/空间开销(如复制算法空间利用率低,标记-清除时间效率高)。
- ⚠️注意事项:
- 区分“算法”与“收集器”,算法是核心思想,收集器是具体实现。
- 标记-清除的内存碎片可能导致大对象分配失败,需配合压缩或替换算法。
- 新生代复制算法需合理设置Eden:S0:S1比例(默认8:1:1)。
📚【面试题目】线程池参数如何设置?
回答要点关键字
(1) 核心参数(核心线程数、最大线程数、队列、拒绝策略)含义
(2) 参数设置依据(任务类型、IO/CPU密集度、系统资源)
(3) 动态调优与监控方案
(4) 常见误区与最佳实践
### 打开详情
- 🍺基础回答:线程池核心参数有核心线程数、最大线程数、阻塞队列、拒绝策略。IO密集型(如HTTP调用、数据库操作)核心线程数设为CPU核心数*2,CPU密集型(如计算任务)设为CPU核心数+1,避免上下文切换浪费。
- 🎉高级扩展版:
- 任务细分:IO密集型需考虑等待/计算时间比,比例越高线程数可越多;混合型任务拆分到不同线程池。
- 队列选择:优先用有界队列(ArrayBlockingQueue)设合理容量,避免无界队列(LinkedBlockingQueue)导致OOM。
- 拒绝策略选型:核心业务用CallerRunsPolicy避免任务丢失,非核心用AbortPolicy抛异常。
- 动态调优:JDK1.8+支持setCorePoolSize/setMaximumPoolSize,配合监控系统根据负载调整。
- 📌 加分项:
- 结合Spring @Async自定义线程池(核心参数+空闲线程存活时间配置)。
- 说明线程池监控指标(活跃线程数、队列任务数、完成数)与暴露方式(Micrometer)。
- 解释“核心线程数+队列容量”组合逻辑,避免仅调大线程数忽略队列影响。
- ⚠️注意事项:
- 禁止无界队列+最大线程数无效(线程池不会创建超核心线程数的线程)。
- 核心线程数不建议设为0(空闲时销毁所有线程,增加重建开销)。
- 避免默认使用DiscardPolicy(静默丢弃任务),需结合业务选拒绝策略。
📚【面试题目】GC流程?哪些可以作为GC Root?
回答要点关键字
(1) GC核心流程(标记-清除/复制/整理执行步骤)
(2) GC Root核心类型与判断依据
(3) 并发GC特殊流程(重新标记、屏障处理)
(4) 跨代引用处理与Root可达性分析细节
### 打开详情
- 🍺基础回答:GC流程先标记存活对象,从GC Root出发遍历可达对象,标记完后清除/复制/整理垃圾。GC Root常见的有虚拟机栈局部变量、方法区类静态变量、本地方法栈JNI引用、活跃线程对象。
- 🎉高级扩展版:
- 分代GC流程:Minor GC(Eden区满触发,复制到Survivor)、Major GC(老年代不足触发)、Full GC(新生代+老年代同时回收)。
- 并发GC流程(G1为例):初始标记(STW)→并发标记→最终标记(STW)→筛选回收(STW)。
- GC Root详细类型:虚拟机栈本地变量表、本地方法栈JNI引用、方法区静态变量/常量引用、活跃Thread对象、synchronized持有的对象、JVM内部引用(Class/异常对象)。
- 📌 加分项:
- 解释跨代引用处理(卡表Card Table,避免Minor GC扫描整个老年代)。
- 说明G1的RSet作用(记录Region间引用,优化标记效率)。
- 区分强/软/弱/虚引用对可达性分析的影响(仅强引用关联对象视为存活)。
- ⚠️注意事项:
- 并非方法区所有对象都是GC Root,仅静态变量、常量引用的对象才算。
- 并发标记会产生浮动垃圾,需下次GC回收。
- 尽量避免Full GC,可通过调整代际比例、优化对象生命周期实现。
📚【面试题目】Dubbo SPI
回答要点关键字
(1) SPI核心概念(服务发现、接口与实现解耦)
(2) Dubbo SPI与Java SPI的区别
(3) 实现原理(配置加载、自适应扩展、激活机制)
(4) 扩展实践与应用场景
### 打开详情
- 🍺基础回答:SPI是服务提供者接口,定义接口后在配置文件指定实现类,框架自动加载。Dubbo SPI是Java SPI的增强,Java SPI要遍历所有实现,Dubbo SPI支持按需加载、自适应扩展,Dubbo的负载均衡、协议都是用SPI实现的。
- 🎉高级扩展版:
- Java SPI缺点:一次性加载所有实现(性能差)、无法按需加载、无自适应机制。
- Dubbo SPI核心特性:
- 配置文件路径:META-INF/dubbo/、META-INF/dubbo/internal/、META-INF/services/。
- 自适应扩展:@Adaptive注解生成代理类,运行时按URL参数选实现(如Protocol自适应dubbo/http)。
- 激活机制:@Activate注解按分组/URL参数激活扩展(如Filter激活)。
- 实现原理:ExtensionLoader加载扩展类,缓存单例实例,通过getExtension/getAdaptiveExtension获取实现。
- 📌 加分项:
- 能手写自定义SPI扩展(如自定义负载均衡,配置文件+实现类+注解)。
- 说明Dubbo SPI的依赖注入(通过setter方法注入其他扩展)。
- 讲解扩展类生命周期(初始化、销毁)与ExtensionFactory作用。
- ⚠️注意事项:
- 扩展类必须有默认构造方法,否则ExtensionLoader无法实例化。
- @Adaptive未指定URL参数key时,默认用接口名转小写作为key。
- 扩展类初始化避免耗时操作,可通过懒加载优化。
📚【面试题目】Dubbo服务调用负载均衡
回答要点关键字
(1) 默认负载均衡策略与适用场景
(2) 核心策略(随机、轮询、最少活跃数、一致性哈希)实现原理
(3) 扩展方式与动态调整机制
(4) 高可用配合(与容错机制联动)
### 打开详情
- 🍺基础回答:Dubbo消费者从注册中心拿到服务列表后,通过负载均衡选节点调用。默认是加权随机策略,按权重分配概率,还有加权轮询、最少活跃调用数(选处理请求最少的节点)、一致性哈希(相同参数路由到同一节点,适合缓存)这几种。
- 🎉高级扩展版:
- 各策略原理:
- 随机(RandomLoadBalance):按权重计算随机概率,权重默认100。
- 轮询(RoundRobinLoadBalance):加权轮询避免普通轮询的权重倾斜。
- 最少活跃数(LeastActiveLoadBalance):统计活跃调用数,解决节点性能不均。
- 一致性哈希(ConsistentHashLoadBalance):哈希映射到哈希环,容错性强,减少缓存失效。
- 高级特性:支持SPI自定义负载均衡策略,支持应用级/服务级权重动态调整(Dubbo Admin)。
- 各策略原理:
- 📌 加分项:
- 场景选型分析:普通服务用随机/轮询,有状态服务用一致性哈希,CPU密集型用最少活跃数。
- 说明负载均衡与容错机制配合(失败自动切换到其他节点)。
- 解释权重配置意义(按服务器性能调整,性能好的节点权重高)。
- ⚠️注意事项:
- 一致性哈希需合理设置虚拟节点数(默认160个),避免哈希环倾斜。
- 动态调整权重后,需关注负载均衡缓存更新(默认有缓存,可配置缓存时间)。
- 高并发场景避免普通轮询,可能导致节点负载过高。
📚【面试题目】如何写一个类似OpenFeign调用的Starter?
回答要点关键字
(1) Starter核心组成(自动配置类、SPI配置、依赖管理)
(2) 动态代理实现(接口代理、请求封装)
(3) 核心功能(注解解析、HTTP客户端集成、负载均衡)
(4) 可扩展性与易用性设计
### 打开详情
- 🍺基础回答:写类似OpenFeign的Starter,核心是让用户用注解定义接口,自动生成代理类发HTTP请求。步骤就是:定义@MyFeignClient注解指定服务名,写自动配置类,用BeanPostProcessor扫描带注解的接口,JDK动态代理生成代理对象,解析@RequestMapping等注解拼接URL,用RestTemplate发请求。
- 🎉高级扩展版:
- 核心组件设计:
- 注解定义:@MyFeignClient(服务名、超时时间、负载均衡策略)、@MyGetMapping/@MyPostMapping(请求方法、路径、参数)。
- 自动配置:Spring Boot 2.7+通过AutoConfiguration.imports注册配置类,用@ConditionalOnClass控制生效条件。
- 动态代理:JDK动态代理+InvocationHandler,解析@PathVariable/@RequestParam/@RequestBody,封装请求参数。
- 集成扩展:HTTP客户端(OkHttp/HttpClient)、负载均衡(Spring Cloud LoadBalancer)、注册中心服务发现。
- 高级功能:超时配置、重试机制、熔断降级(集成Sentinel)、日志打印、JSON/Form编解码。
- 核心组件设计:
- 📌 加分项:
- 遵循Spring Boot Starter“约定优于配置”思想,配置文件自动生效。
- 支持自定义HTTP客户端(SPI扩展切换OkHttp和HttpClient)。
- 实现接口方法参数绑定与编码处理(如中文参数、特殊字符编码)。
- ⚠️注意事项:
- 代理Bean需Spring容器管理,确保单例避免重复创建。
- 处理服务列表缓存与更新,避免调用不可用节点。
- 避免URL拼接错误,需规范参数解析与编码逻辑。