面试0820
面试问题整理与回答
一、Spring 相关
1. 反射了解吗?项目中什么业务用到反射?反射的常用方法有哪些?
回答:
了解。反射是在运行时动态获取类的信息并操作类或对象的机制,可以在不知道具体类的情况下调用方法、访问字段、创建实例。
项目中用到反射的场景(举例):
- 框架底层:Spring 的 IOC 创建 Bean、依赖注入,本质上大量使用反射。
- 自己的业务:
- 根据配置或类型动态创建对象(策略模式、简单工厂)。
- 通用导出/导入功能,根据注解或字段名动态取值。
- 操作日志 / 数据对比,通过反射获取对象字段变化。
- MyBatis 的 Mapper 代理、结果集映射也会用到反射。
常用方法:
Class.forName("com.xxx.User") // 获取 Class 对象
clazz.newInstance() // 创建实例(已过时,推荐 getDeclaredConstructor().newInstance())
clazz.getMethod / getDeclaredMethod // 获取方法
clazz.getField / getDeclaredField // 获取字段
method.invoke(obj, args) // 调用方法
field.set(obj, value) / field.get(obj) // 设置/获取字段值
注意:getDeclaredXxx 可以获取私有成员,需要配合 setAccessible(true)。
2. IOC 和 AOP 是怎么实现的?
回答:
IOC(控制反转):
- 核心思想是把对象的创建和依赖关系的管理交给 Spring 容器,而不是自己 new。
- 实现方式:
- 通过 XML 或注解(
@Component、@Service、@Autowired等)定义 Bean。 - Spring 启动时扫描、解析,使用反射创建对象。
- 维护一个 Bean 容器(单例池),把依赖注入进去(属性注入、构造器注入、Setter 注入)。
- 通过 XML 或注解(
- 底层用到了工厂模式 + 反射 + 容器管理。
AOP(面向切面编程):
- 把日志、事务、权限等横切逻辑从业务代码中抽离出来。
- 实现方式主要是动态代理:
- 有接口时用 JDK 动态代理。
- 没有接口时用 CGLIB(继承目标类生成子类)。
- Spring 把切面逻辑织入到目标方法前后(前置、后置、环绕、异常通知等)。
- 事务(
@Transactional)、日志、权限校验都是基于 AOP 实现的。
3. @Transactional 事务注解底层是怎么实现的?
回答:
底层是基于 AOP + 动态代理 实现的。
流程简述:
- 开启事务管理后,Spring 会为加了
@Transactional的类生成代理对象。 - 调用方法时,代理先拦截。
- 通过
PlatformTransactionManager(一般是DataSourceTransactionManager)获取连接,关闭自动提交,开启事务。 - 执行业务方法。
- 方法正常结束 → 提交事务;抛出异常 → 回滚(默认只回滚 RuntimeException)。
- 最终释放连接。
注意点(面试常追问):
- 方法必须是
public,且从外部调用才会走代理(同类内部自调用事务失效)。 - 默认只对 RuntimeException 和 Error 回滚,受检异常需要加
rollbackFor = Exception.class。 - 多数据源时要指定正确的事务管理器。
二、MySQL 相关
慢查询的优化方案?
回答:
慢查询优化一般按以下步骤:
-
开启并分析慢查询日志
找到执行时间长、出现频率高的 SQL。 -
使用 EXPLAIN 分析执行计划
重点看:type:是否出现 ALL(全表扫描)key:有没有用到索引rows:扫描行数Extra:有没有 Using filesort、Using temporary
-
常见优化手段:
- 加合适索引(最有效),注意最左前缀原则,避免过多索引。
- 避免在索引列上使用函数、运算、类型转换。
- 只查询需要的字段,避免
SELECT *(方便覆盖索引)。 - 优化
WHERE、ORDER BY、GROUP BY,减少回表和文件排序。 - 深分页优化:用「延迟关联」或记录上次最大 ID 的方式。
- 大表考虑分表、冷热数据分离。
- 必要时用缓存(Redis)减轻数据库压力。
-
其他:
- 检查是否有锁等待、主从延迟。
- 表结构是否合理(字段类型、范式与反范式权衡)。
三、Java 基础相关
1. ArrayList 和 LinkedList 的区别?ArrayList 为什么查询比较快?
回答:
| 对比点 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 头部插入删除 | 慢(要搬移元素) | 快 |
| 尾部插入 | 均摊 O(1) | O(1) |
| 内存 | 连续,更紧凑 | 每个节点额外存指针 |
ArrayList 查询快的原因:
底层是数组,内存连续,可以通过下标直接定位(首地址 + index × 元素大小),时间复杂度 O(1)。而 LinkedList 需要从头部或尾部开始遍历,所以慢。
2. 线程的实现方式?继承 Thread 和实现 Runnable 哪个更好?为什么?
回答:
实现方式主要有:
- 继承
Thread类 - 实现
Runnable接口 - 实现
Callable接口(可返回值、可抛异常) - 使用线程池提交任务(实际项目最常用)
更推荐实现 Runnable 接口,原因:
- Java 单继承,继承 Thread 后不能再继承其他类,实现接口更灵活。
- Runnable 让任务和线程本身解耦,多个线程可以共享同一个任务实例。
- 线程池也是基于 Runnable / Callable 工作的。
实际项目中几乎都用线程池,而不是自己 new Thread。
3. HashMap 是线程安全的吗?线程安全的对应是什么类?ArrayList 呢?有什么安全的实现方式?
回答:
-
HashMap:线程不安全。多线程并发 put 可能导致数据覆盖,JDK 1.7 还可能出现死循环。
- 线程安全对应类:ConcurrentHashMap(推荐),或用
Collections.synchronizedMap包装(性能较差)。
- 线程安全对应类:ConcurrentHashMap(推荐),或用
-
ArrayList:线程不安全。
- 安全实现方式:
Collections.synchronizedList(new ArrayList<>())- CopyOnWriteArrayList(读多写少场景更合适)
- 使用 Vector(已过时,不推荐)
- 安全实现方式:
现在基本推荐用 JUC 包下的并发容器,而不是 Vector 和 Hashtable。
4. 线程池的参数项目中怎么设置的?核心线程数和最大线程数设置多少?为什么?
回答(结合典型业务给出合理示例):
项目中一般自己用 ThreadPoolExecutor 创建,不直接用 Executors。
示例配置(IO 密集型业务,如接口调用、文件处理、PDF 生成):
- 核心线程数(corePoolSize):
CPU 核数 * 2,或根据压测定,比如 8~16。 - 最大线程数(maximumPoolSize):核心数的 2~4 倍,比如 32~64。
- 队列:
LinkedBlockingQueue或ArrayBlockingQueue,容量设为 100~1000,防止无界队列导致 OOM。 - 拒绝策略:常用
CallerRunsPolicy(让提交任务的线程自己执行,起到降速作用)或自定义策略。 - 空闲时间:60 秒。
为什么这么设置:
- IO 密集型任务线程大部分时间在等 IO,可以多开一些线程提高吞吐。
- 核心线程数保证基本处理能力,最大线程数应对突发流量。
- 有界队列 + 合理拒绝策略,保证系统在高负载下可控,不会把内存打爆。
(如果是 CPU 密集型,核心线程数一般设为 CPU 核数 + 1。)
四、JVM 相关
1. JVM 内存结构是怎样的?Class 类、静态变量在哪个区?
回答:
JVM 运行时数据区主要分为:
线程私有:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享:
- 堆:存放对象实例
- 方法区(元空间 Metaspace):存放类信息、常量、静态变量、即时编译后的代码等
Class 类对象和静态变量的位置:
- 类的元数据(类结构信息)在方法区(元空间)。
- 静态变量(static)在 JDK 8 之后随着类的元数据一起放在元空间,但静态变量如果是对象类型,对象本身还是在堆上。
- Class 对象本身在堆上。
(补充:JDK 8 之前静态变量在永久代,JDK 8 后永久代被元空间取代,使用本地内存。)
2. JVM 调优参数有哪些?
回答(常用基础参数):
-Xms512m # 初始堆大小
-Xmx512m # 最大堆大小(建议与 Xms 相同)
-Xmn256m # 新生代大小
-XX:SurvivorRatio=8 # Eden : Survivor = 8:1:1
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
-XX:+UseG1GC # 使用 G1 收集器
-XX:MaxGCPauseMillis=200 # G1 期望最大停顿时间
-XX:+HeapDumpOnOutOfMemoryError # OOM 时生成 dump
-XX:HeapDumpPath=/path/to/dump
调优思路:先通过监控和 GC 日志定位问题(是新生代频繁 GC、老年代晋升失败,还是元空间不够),再针对性调整,而不是盲目改参数。
五、Redis 相关
项目中有没有用到 Redis?写操作的时候执行步骤是怎样的?
回答:
有用到。主要用作缓存(热点数据、用户信息、配置等),也有部分场景做分布式锁和计数。
写操作(更新数据)时常见的执行步骤(Cache Aside 模式):
- 先更新数据库。
- 再删除(或更新)对应的 Redis 缓存。
伪代码思路:
// 更新
updateDatabase(user);
redis.delete("user:" + userId); // 删除缓存,而不是直接更新缓存
为什么先更新 DB 再删缓存?
- 如果先删缓存再更新 DB,在并发情况下容易产生脏数据。
- 先更新 DB,再删缓存,是目前业界更常用的做法。删除失败可以通过重试或消息队列补偿,并设置合理的过期时间作为兜底。
读的时候:先查 Redis,没有再查 DB,然后回写缓存。
总结建议
回答时尽量结合自己项目的真实情况(线程池具体数值、Redis 具体 key 设计、有没有遇到过慢 SQL 等),会更有说服力。
如果某个问题自己项目中没有实际经验,就按通用最佳实践答,并说明「如果是我设计会怎么做」。