恒生电子 Java 一面 面经
1. Spring Boot自动配置原理是什么?和Spring有什么区别?
Spring Boot自动配置原理:
核心机制:
@SpringBootApplication注解包含三个关键注解@EnableAutoConfiguration:启用自动配置@ComponentScan:扫描组件@SpringBootConfiguration:标识配置类
自动配置流程:
- Spring Boot启动时加载
META-INF/spring.factories - 读取所有
EnableAutoConfiguration配置类 - 根据
@Conditional条件判断是否生效 - 满足条件的配置类自动注入Bean
示例:
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
Spring vs Spring Boot:
配置方式 |
XML或Java配置 |
约定优于配置 |
依赖管理 |
手动管理版本 |
starter自动管理 |
内嵌服务器 |
需要外部容器 |
内嵌Tomcat/Jetty |
启动方式 |
部署war包 |
jar包直接运行 |
开发效率 |
配置繁琐 |
开箱即用 |
核心优势:
- 零配置快速开发
- 统一的依赖管理
- 生产级监控(Actuator)
- 简化部署流程
2. MySQL索引的底层实现,B+树和B树的区别
MySQL索引结构:
InnoDB使用B+树的原因:
- 所有数据存储在叶子节点
- 叶子节点通过指针连接,支持范围查询
- 非叶子节点只存索引,树高度低
- 磁盘IO次数少
B树 vs B+树:
数据位置 |
所有节点都存数据 |
只有叶子节点存数据 |
叶子节点 |
不连接 |
通过指针连接 |
范围查询 |
需要中序遍历 |
叶子节点顺序扫描 |
查询稳定性 |
不稳定(可能在非叶子节点找到) |
稳定(都要到叶子节点) |
磁盘IO |
相对多 |
相对少 |
B+树结构示例:
[10, 20]
/ | \
[5,8] [12,15] [25,30]
/ | \
叶子节点 → 叶子节点 → 叶子节点
(存储实际数据)
索引类型:
- 主键索引(聚簇索引):叶子节点存完整数据行
- 辅助索引(非聚簇索引):叶子节点存主键值
- 联合索引:多个字段组合,遵循最左前缀原则
索引优化建议:
- 选择区分度高的列建索引
- 避免在索引列上使用函数
- 使用覆盖索引减少回表
- 合理使用联合索引
3. JVM内存模型,说说各个区域的作用
JVM内存结构:
1. 程序计数器(Program Counter)
- 当前线程执行的字节码行号指示器
- 线程私有
- 唯一不会OOM的区域
2. 虚拟机栈(VM Stack)
- 存储局部变量表、操作数栈、动态链接、方法出口
- 线程私有
- 每个方法执行创建一个栈帧
- StackOverflowError:栈深度超限
- OutOfMemoryError:栈扩展失败
3. 本地方法栈(Native Method Stack)
- 为Native方法服务
- 线程私有
4. 堆(Heap)
- 存储对象实例和数组
- 线程共享
- GC主要区域
- 分代:新生代(Eden + Survivor)、老年代
5. 方法区(Method Area)
- 存储类信息、常量、静态变量、JIT编译代码
- 线程共享
- JDK8后改为元空间(Metaspace),使用本地内存
6. 运行时常量池
- 方法区的一部分
- 存储编译期生成的字面量和符号引用
内存分配示例:
public class Example {
private static int staticVar = 1; // 方法区
public void method() {
int localVar = 2; // 虚拟机栈
String str = new String("abc"); // str引用在栈,对象在堆
final int constant = 3; // 栈
}
}
常见内存问题:
- 堆溢出:对象过多,内存不足
- 栈溢出:递归调用过深
- 元空间溢出:加载类过多
4. 说说你对分布式事务的理解,有哪些解决方案?
分布式事务场景:
- 跨数据库操作
- 跨服务调用
- 微服务架构下的数据一致性
解决方案:
1. 两阶段提交(2PC)
- 准备阶段:协调者询问所有参与者是否可以提交
- 提交阶段:所有参与者都同意则提交,否则回滚
- 缺点:同步阻塞、单点故障、数据不一致风险
2. 三阶段提交(3PC)
- CanCommit、PreCommit、DoCommit
- 增加超时机制,减少阻塞
- 仍然存在数据不一致问题
3. TCC(Try-Confirm-Cancel)
// Try阶段:预留资源
public void tryDeduct(String userId, BigDecimal amount) {
// 冻结账户金额
accountService.freeze(userId, amount);
}
// Confirm阶段:确认提交
public void confirmDeduct(String userId, BigDecimal amount) {
// 扣减冻结金额
accountService.deduct(userId, amount);
}
// Cancel阶段:回滚
public void cancelDeduct(String userId, BigDecimal amount) {
// 解冻金额
accountService.unfreeze(userId, amount);
}
4. 本地消息表
- 业务操作和消息插入在同一事务
- 定时任务扫描消息表发送消息
- 消费方幂等处理
5. 消息队列(最终一致性)
- 使用RocketMQ的事务消息
- 半消息机制保证可靠投递
- 适合对一致性要求不高的场景
6. Saga模式
- 长事务拆分为多个本地事务
- 每个本地事务有对应的补偿操作
- 正向执行或反向补偿
实际选择:
- 强一致性要求:2PC/3PC(性能差)
- 最终一致性:消息队列、Saga(推荐)
- 业务补偿:TCC(开发成本高)
5. HashMap的底层实现,为什么线程不安全?ConcurrentHashMap如何保证线程安全?
HashMap底层结构(JDK 1.8):
- 数组 + 链表 + 红黑树
- 初始容量16,负载因子0.75
- 链表长度>8且数组长度≥64时转红黑树
- 红黑树节点<6时退化为链表
put操作流程:
1. 计算key的hash值 2. 根据hash值定位数组下标:(n-1) & hash 3. 如果位置为空,直接插入 4. 如果位置有元素: - key相同则覆盖 - 链表则遍历插入尾部 - 红黑树则按树规则插入 5. 判断是否需要扩容(size > threshold)
HashMap线程不安全的原因:
1. 并发put导致数据丢失
// 两个线程同时put到同一位置 Thread1: 判断位置为空,准备插入A Thread2: 判断位置为空,准备插入B 结果:只有一个元素被保存
2. 扩容时形成环形链表(JDK 1.7)
- 多线程同时扩容
- 链表rehash时形成环
- get操作死循环,CPU 100%
3. size计数不准确
- size++不是原子操作
- 并发修改导致计数错误
Concurr
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经 文章被收录于专栏
Java面试圣经,带你练透java圣经
查看10道真题和解析