Administrator 13 阅读 面试

面试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。
  • 实现方式:
    1. 通过 XML 或注解(@Component、@Service、@Autowired 等)定义 Bean。
    2. Spring 启动时扫描、解析,使用反射创建对象。
    3. 维护一个 Bean 容器(单例池),把依赖注入进去(属性注入、构造器注入、Setter 注入)。
  • 底层用到了工厂模式 + 反射 + 容器管理。

AOP(面向切面编程):

  • 把日志、事务、权限等横切逻辑从业务代码中抽离出来。
  • 实现方式主要是动态代理:
    • 有接口时用 JDK 动态代理。
    • 没有接口时用 CGLIB(继承目标类生成子类)。
  • Spring 把切面逻辑织入到目标方法前后(前置、后置、环绕、异常通知等)。
  • 事务(@Transactional)、日志、权限校验都是基于 AOP 实现的。

3. @Transactional 事务注解底层是怎么实现的?

回答:

底层是基于 AOP + 动态代理 实现的。

流程简述:

  1. 开启事务管理后,Spring 会为加了 @Transactional 的类生成代理对象。
  2. 调用方法时,代理先拦截。
  3. 通过 PlatformTransactionManager(一般是 DataSourceTransactionManager)获取连接,关闭自动提交,开启事务。
  4. 执行业务方法。
  5. 方法正常结束 → 提交事务;抛出异常 → 回滚(默认只回滚 RuntimeException)。
  6. 最终释放连接。

注意点(面试常追问):

  • 方法必须是 public,且从外部调用才会走代理(同类内部自调用事务失效)。
  • 默认只对 RuntimeException 和 Error 回滚,受检异常需要加 rollbackFor = Exception.class。
  • 多数据源时要指定正确的事务管理器。

二、MySQL 相关

慢查询的优化方案?

回答:

慢查询优化一般按以下步骤:

  1. 开启并分析慢查询日志
    找到执行时间长、出现频率高的 SQL。

  2. 使用 EXPLAIN 分析执行计划
    重点看:

    • type:是否出现 ALL(全表扫描)
    • key:有没有用到索引
    • rows:扫描行数
    • Extra:有没有 Using filesort、Using temporary
  3. 常见优化手段:

    • 加合适索引(最有效),注意最左前缀原则,避免过多索引。
    • 避免在索引列上使用函数、运算、类型转换。
    • 只查询需要的字段,避免 SELECT *(方便覆盖索引)。
    • 优化 WHERE、ORDER BY、GROUP BY,减少回表和文件排序。
    • 深分页优化:用「延迟关联」或记录上次最大 ID 的方式。
    • 大表考虑分表、冷热数据分离。
    • 必要时用缓存(Redis)减轻数据库压力。
  4. 其他:

    • 检查是否有锁等待、主从延迟。
    • 表结构是否合理(字段类型、范式与反范式权衡)。

三、Java 基础相关

1. ArrayList 和 LinkedList 的区别?ArrayList 为什么查询比较快?

回答:

对比点 ArrayList LinkedList
底层结构 动态数组 双向链表
随机访问 O(1) O(n)
头部插入删除 慢(要搬移元素) 快
尾部插入 均摊 O(1) O(1)
内存 连续,更紧凑 每个节点额外存指针

ArrayList 查询快的原因:
底层是数组,内存连续,可以通过下标直接定位(首地址 + index × 元素大小),时间复杂度 O(1)。而 LinkedList 需要从头部或尾部开始遍历,所以慢。


2. 线程的实现方式?继承 Thread 和实现 Runnable 哪个更好?为什么?

回答:

实现方式主要有:

  1. 继承 Thread 类
  2. 实现 Runnable 接口
  3. 实现 Callable 接口(可返回值、可抛异常)
  4. 使用线程池提交任务(实际项目最常用)

更推荐实现 Runnable 接口,原因:

  • Java 单继承,继承 Thread 后不能再继承其他类,实现接口更灵活。
  • Runnable 让任务和线程本身解耦,多个线程可以共享同一个任务实例。
  • 线程池也是基于 Runnable / Callable 工作的。

实际项目中几乎都用线程池,而不是自己 new Thread。


3. HashMap 是线程安全的吗?线程安全的对应是什么类?ArrayList 呢?有什么安全的实现方式?

回答:

  • HashMap:线程不安全。多线程并发 put 可能导致数据覆盖,JDK 1.7 还可能出现死循环。

    • 线程安全对应类:ConcurrentHashMap(推荐),或用 Collections.synchronizedMap 包装(性能较差)。
  • ArrayList:线程不安全。

    • 安全实现方式:
      1. Collections.synchronizedList(new ArrayList<>())
      2. CopyOnWriteArrayList(读多写少场景更合适)
      3. 使用 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 模式):

  1. 先更新数据库。
  2. 再删除(或更新)对应的 Redis 缓存。

伪代码思路:

// 更新
updateDatabase(user);
redis.delete("user:" + userId);   // 删除缓存,而不是直接更新缓存

为什么先更新 DB 再删缓存?

  • 如果先删缓存再更新 DB,在并发情况下容易产生脏数据。
  • 先更新 DB,再删缓存,是目前业界更常用的做法。删除失败可以通过重试或消息队列补偿,并设置合理的过期时间作为兜底。

读的时候:先查 Redis,没有再查 DB,然后回写缓存。


总结建议

回答时尽量结合自己项目的真实情况(线程池具体数值、Redis 具体 key 设计、有没有遇到过慢 SQL 等),会更有说服力。
如果某个问题自己项目中没有实际经验,就按通用最佳实践答,并说明「如果是我设计会怎么做」。