Java后端面试题精选。
数据存储
1、MySQL VS HBase
- 数据模型:
- MySQL:是一个关系型数据库,遵循SQL标准,使用表格形式存储数据,支持复杂的事务处理和联接查询。
- HBase:是一个基于Hadoop的分布式、列式存储的NoSQL数据库,适合大规模数据存储,数据模型基于键值对,列族支持灵活的数据模式。
- 存储模式:
- MySQL:采用行存储模式,适合快速查找特定行的数据。
- HBase:采用列存储模式,特别适合查询只涉及部分列的场景,能高效处理稀疏数据。
- 底层存储:
- MySQL:数据存储在本地文件系统或网络存储设备上,依赖于文件系统自身的可靠性。
- HBase:底层使用HDFS(Hadoop分布式文件系统),提供了高可用性和数据冗余,适合大数据存储和处理。
- 事务支持:
- MySQL:支持ACID(原子性、一致性、隔离性、持久性)事务,适用于需要高度一致性的场景。
- HBase:提供弱一致性模型,不支持复杂的事务处理,但适合大规模数据的高速写入和读取。
- 扩展性:
- MySQL:虽然可以通过主从复制、分片等方式扩展,但扩展性和水平扩展能力有限。
- HBase:天生为分布式设计,易于水平扩展,可以轻松应对数据量的快速增长。
使用场景:
- MySQL:
- 适用于需要严格事务处理、复杂查询(JOIN操作)和高度一致性的应用场景,如金融交易系统、企业资源规划(ERP)系统、内容管理系统(CMS)等。
- 当数据结构相对固定,需要利用SQL的灵活性进行数据操作和分析时。
- HBase:
- 适合大规模数据存储和分析,特别是需要快速写入和读取大量数据的场景,如日志处理、实时分析、物联网(IoT)数据存储、社交网络数据、搜索引擎索引等。
- 当数据模型较为灵活,需要处理大量半结构化或非结构化数据,且对数据的一致性要求不如关系型数据库严格时。
2、MySQL的索引为什么使用B+树而不使用跳表?
- 磁盘I/O效率:B+树是专为磁盘I/O优化的数据结构,能够最大限度减少磁盘访问次数。在B+树中,所有叶子节点包含了指向实际数据行的指针,并且是顺序排列的,这使得范围查询和顺序遍历非常高效。由于B+树的分支因子较大,即使面对大量数据,树的高度也能保持较低,通常不超过3到4层,这意味着大多数查询只需要几次磁盘I/O操作。
- 空间利用率:B+树能够更高效地利用磁盘空间,每个节点可以存储多个键值对,这减少了树的高度,进而减少了磁盘I/O操作次数。而跳表虽然在内存中效率高且实现简单,但在磁盘存储上不如B+树紧凑,尤其是在处理大规模数据集时,可能导致较高的磁盘I/O开销。
- 范围查询:B+树天然支持高效的范围查询,因为所有实际数据都存储在叶子节点上,并且叶子节点之间通过指针相连,这使得扫描连续的数据范围变得直接而快速。相比之下,跳表虽然也可以进行范围查询,但是效率不如B+树,因为它需要沿着多个层级进行多次查找。
- 事务和数据持久性:MySQL作为关系型数据库,需要支持事务处理和数据的持久化存储。B+树的结构更有利于实现事务的ACID特性(原子性、一致性、隔离性、持久性),特别是在并发控制和恢复机制方面。跳表在实现这些特性时可能更为复杂,因为它涉及到的多级更新可能破坏事务的原子性和一致性。
JVM
1、CPU100%,怎么处理?
1.1、业务类问题
死循环
死循环无休止地运行,消耗过多的处理器时间,导致CPU100%
死锁
发生死锁后,就会存在忙等待或自旋锁等编程问题,从而导致 繁忙等待问题。即进程在不释放 CPU 的情况下反复检查条件是否满足,会导致 CPU 占用率居高不下。这种低效率的资源使用会妨碍 CPU 执行其他任务。
不必要的代码块
在不需要的地方使用
synchronized
块,会导致线程竞争和上下文切换解决方案: 尽量减少同步块的使用范围
1.2、并发类问题
大量计算密集型的任务
比如复杂的数学计算,图像处理,视频编码
计算密集型的任务需要大量的计算能力。在没有足够系统资源的情况下运行这些应用程序,可能会导致 CPU 占用率达到 100%,因为它们试图执行高要求的任务。
解决方案:优化算法,使用更高效的库,或者利用并行计算来分摊
大量并发线程
多个线程同时运行会导致对 CPU 资源的竞争,尤其是当其中许多线程都是资源密集型进程时。这会导致所有线程获得的 CPU 时间减少,当每个线程都试图完成自己的任务时,CPU 时间可能会被耗尽。
大量的上下文切换
创建过多的线程,导致频繁的上下文切换
解决方案: 使用线程池来管理线程的数量
1.3、内存类问题
内存不足
当系统内存不足时,就会将磁盘存储作为虚拟内存使用,而虚拟内存的运行速度要慢得多。
这种过度的分页和交换会导致 CPU 占用率居高不下,因为处理器需要花费更多时间来管理内存访问,而不是高效地执行进程。
频繁GC
创建大量的短生命周期的对象,频繁触发GC
解决方案: 优化代码, 减少对象的创建 ,或者调整JVM的参数来优化
内存泄漏
程序持续分配内存但不释放,会导致频繁的GC
解决方案: 使用内存分析工具VisualVM进行检测和修复
1.4、CPU100%定位神器
想要定位到具体是哪一行的代码导致, 一般都会使用下面的两大神器
- 通常使用的jvm自带的工具jstack,
- 还有一种就是开源神器arthas,
(1) 使用jstack 解决CPU 100%问题,在方法论上要用到两个命令,
- top 命令查看TOP N线程,
- jstack命令查看堆栈信息
(2) 使用arthas解决CPU 100%问题,在方法论上要用到两个命令,
- dashboard 命令查看TOP N线程,
- thread 命令查看堆栈信息
2、死锁优化实践
1. 维持一致的锁定顺序
确保所有事务都以相同的顺序获取锁。这可以减少锁定冲突的可能性,因为事务不会因为等待其他事务释放锁而相互阻塞。
例如,如果有多个表或资源需要锁定,总是按照相同的顺序(如字典顺序)锁定这些资源。
2. 使用最小的锁粒度
尽量使用行级锁而不是表级锁。
行级锁允许更高的并发,因为它仅锁定需要修改或查询的数据行,而不是整个表。这样,即使多个事务操作同一表,它们也可能操作不同的行,从而减少死锁的风险。
3. 减少事务持续时间
尽量缩短事务的执行时间。
长事务占用锁的时间越长,与其他事务发生冲突的可能性就越大。你可以通过优化查询语句和减少事务中的操作数来减少事务持续时间。
4. 使用锁超时
在某些数据库管理系统中,可以设置锁的超时时间。
这意味着事务在等待锁超过设定的时间后将自动回滚。这不仅可以防止死锁,还可以避免一个事务无限期地等待资源。
5. 死锁检测和回滚
启用数据库的死锁检测功能,让数据库管理系统能够自动检测死锁并回滚某个事务来解锁。
这通常是最后的手段,因为它可能导致数据不一致的问题。应当只在其他方法都无法实现时使用。
6. 避免不必要的锁
审查和优化事务逻辑,确保只锁定必要的资源。
例如,如果事务只读取数据而不进行修改,可以考虑使用非锁定读(例如,在MySQL中使用SELECT … WITH (NOLOCK))。
7. 使用乐观并发控制
在一些场景中,使用乐观并发控制(OCC)而不是悲观锁定可能更合适。OCC通过在事务提交时检查数据是否已被其他事务修改来避免锁定,适用于读多写少的场景。
8. 避免无索引行锁升级为表锁
尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
9. 监控和日志记录
实施监控和日志记录来跟踪死锁和性能瓶颈。这可以帮助识别导致死锁的具体事务和操作,从而进行针对性的优化。
3、OOM三大场景和解决方案
Java中的OutOfMemoryError
(OOM)是开发和运维中常见的问题,主要涉及三个核心场景:堆内存OOM、元空间(Metaspace)OOM、堆外内存OOM。以下是这些场景的概述和相应的解决方案:
3.1、堆内存OOM
场景描述:这是最典型的OOM场景,发生于JVM堆内存不足以分配新对象时。包括新生代和老年代的内存不足。
解决方案:
- 在线分析:
- 使用
jmap
工具分析进程内占用内存最大的对象,例如:jmap -histo:live <pid>
。 - 导出堆内存快照:
jmap -dump:live,format=b,file=<path_to_dump_file> <pid>
,然后使用MAT等工具分析。 - 使用
Arthas
工具在线分析,利用火焰图定位问题。
- 使用
- 离线分析:
- 通过JVM参数
-XX:+HeapDumpOnOutOfMemoryError
,自动在OOM时生成堆转储文件。 - 分析转储文件,定位内存泄漏对象和其来源代码。
- 通过JVM参数
3.2、元空间OOM
场景描述:在Java 8及以上版本中,永久代被元空间替代,用于存储类的元数据。当类加载过多或元数据过大时,会导致Metaspace耗尽。
解决方案:
- 设置元空间大小限制,如
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
。 - 定位并减少动态类的生成,例如Spring BeanUtils的反射使用和JSON序列化。
- 监控元空间使用情况,定期清理不再使用的类加载器。
3.3、堆外内存OOM
场景描述:堆外内存包括直接内存分配(如ByteBuffer.allocateDirect
)和JNI调用的本地内存。当这部分内存使用超出系统限制时,也会导致OOM。
解决方案:
- 监控和限制直接内存的使用,确保及时释放。
- 检查JNI代码,确保没有内存泄露,正确管理本地内存。
- 使用工具监控堆外内存使用,如
jcmd pid VM.native_memory summary
。 - 优化线程栈大小和线程池配置,避免线程过多导致的栈空间耗尽。
通用策略:
- 对于所有场景,都需要良好的监控和预警机制,以便在问题发生前及时发现。
- 在生产环境中,开启JVM参数
-XX:+HeapDumpOnOutOfMemoryError
和配置-XX:HeapDumpPath
,以自动化收集故障时的内存快照。 - 定期审查代码和依赖库,避免内存泄漏和不必要的内存占用。
- 在设计和开发阶段考虑内存效率,避免过度使用反射和动态类加载等内存密集型操作。
Spring+
1、Spring Boot启动流程
- 启动入口类加载:Spring Boot 应用通常从一个带有
@SpringBootApplication
注解的类开始启动,这个注解是一个组合注解,包含了@SpringBootConfiguration
,@EnableAutoConfiguration
, 和@ComponentScan
。它告诉Spring去自动配置应用、扫描相关的组件,并且提供一个配置类。 - 初始化Spring应用上下文:Spring Boot会创建一个
ApplicationContext
,这是Spring的核心容器,负责管理Bean的生命周期、依赖注入等。应用上下文会加载配置文件(如application.properties
或application.yml
),这些配置文件中定义的属性会作为环境变量供应用使用。 - 自动配置:
@EnableAutoConfiguration
注解触发Spring Boot的自动配置机制。它会根据类路径上存在的jar依赖、配置文件中的设置以及激活的 profiles 自动配置Spring应用。Spring Boot会检查META-INF/spring.factories
文件来找出候选的自动配置类,并根据条件选择性地启用它们。 - Bean的注册与初始化:Spring会扫描标记了
@Component
、@Service
、@Repository
、@Controller
等注解的类,并将它们作为Bean注册到应用上下文中。此外,也会初始化配置类中定义的Bean,通过构造器注入、字段注入等方式完成依赖注入。 - 配置绑定与验证:Spring Boot会将配置文件中的属性绑定到配置类的字段上,同时支持属性值的类型转换和验证。
- 初始化启动器(Starters):Spring Boot提供了众多启动器(Starter),这些启动器本质上是一组依赖的集合,简化了依赖管理。在应用启动时,这些启动器所包含的依赖会自动加入,进而激活相关的自动配置。
- 执行初始化回调与监听器:如果有Bean实现了
InitializingBean
接口、定义了@PostConstruct
方法或配置了BeanPostProcessor
,Spring会在适当的时机调用它们。此外,实现了ApplicationListener
的Bean可以监听并响应Spring应用事件,如应用上下文刷新、启动完成等。 - 启动内置Web服务器:如果应用包含Web组件(如使用了Spring MVC或Spring WebFlux),Spring Boot会自动配置并启动一个嵌入式的Web服务器(如Tomcat、Jetty或Undertow),使得应用可以直接对外提供HTTP服务。
- 执行启动任务:实现了
CommandLineRunner
或ApplicationRunner
接口的Bean,在所有Bean初始化完成后会自动执行其run
方法,通常用于执行一些启动时的任务。 - 准备就绪检查:Spring Boot Actuator模块提供了应用健康检查和就绪检查功能,确保应用已经完全启动并准备好接受流量。
Kafka
1、Kafka如何保证消息0丢失?
1.1、生产者阶段(Producer):
- 设置最高可靠性的发送确认机制:通过设置
acks=all
,要求消息被所有参与复制的节点保存后,生产者才会收到一个确认,这是确保消息可靠性的重要配置。 - 设置严格的消息重试机制:通过合理设置
retries
(重试次数)和retry.backoff.ms
(重试间隔时间)来确保在遇到可恢复的错误时,生产者能够重试消息发送,减少消息丢失的风险。 - 本地消息表+定时扫描:为了应对极端场景下可能的消息丢失(如,Broker端异步落盘后断电),可以通过本地消息表记录消息发送状态和定时扫描这些状态来确保消息至少被发送一次。
1.2、 Broker端(消息存储阶段):
- 设置严格的副本同步机制:通过配置
replication.factor
(副本因子)和min.insync.replicas
(最小同步副本数),以及关闭unclean.leader.election.enable
(不允许脏副本成为Leader),来确保至少有N个副本同步保存了消息。 - 设置严格的消息刷盘机制:通过配置
log.flush.interval.messages
和log.flush.interval.ms
来控制消息的刷盘时间,减少因系统崩溃导致的消息丢失。
1.3、消费者阶段(Consumer):
- 手动提交位移:关闭自动提交位移
enable.auto.commit=false
,采取手动确认位移的策略,在消息成功处理后再提交位移,从而确保消息至少被消费一次而不会被重复消费。