哈喽~,大家好,我是千羽。
下面分享我认识的一位大佬华中科技大学985硕,网易一面,全程八股文。
- 1、自我介绍
- 2、项目经历(DJL)
- 3、JVM的内存分配有了解吗?
- 4、讲一下Java的垃圾回收几种算法
- 5、Spring 的Autowired自动装配原理
- 6、Spring Bean的生命周期
- 7、Redis有哪几种基本数据结构
- 8、说一下 Zset 的底层原理
- 9、讲一下redis哨兵模式与集群模式
- 10、redis的持久化策略AOF和RDB、容灾
- 11、讲一下Go语言的select,Goroutine
- 12、Raft协议的一致性问题解决方案
- 13、为什么集群的节点数量设置为奇数个,而不是偶数个?
- 14、Python的GIL
1、自我介绍
大家好,我叫XXX,是一名XXX学校研二,目前专注于Java后端开发领域。我拥有丰富的项目经验,从需求分析、设计、编码、测试到维护,我能够熟练地运用Java语言和相关技术,独立或与团队一起完成各种复杂的开发任务。
在大学期间,我就开始接触编程,通过自学和实践,我掌握了Java基础语法、面向对象编程、常用数据结构与算法等知识。在工作中,我进一步深入学习了Java Web开发、Spring框架、MyBatis框架等后端开发技术,并积累了丰富的实践经验。
除了Java本身,我还对数据库技术有深入的了解和实践经验,包括MySQL、Oracle等关系型数据库和Redis等NoSQL数据库。同时,我也熟悉Linux操作系统和Shell脚本编程,能够高效地进行系统管理和运维工作。
在团队合作方面,我注重沟通与协作,能够与不同背景的团队成员有效合作,共同解决问题。同时,我具备强烈的责任心和自我驱动能力,能够在压力下保持冷静并按时完成高质量的工作。期待可以加入贵公司。
2、项目经历(DJL)
略
3、JVM的内存分配有了解吗?
Java虚拟机(JVM)在执行Java程序时,需要在内存中分配空间以存储各种数据和指令。JVM的内存分配主要涉及以下几个部分:
- 堆(Heap):堆是JVM中最大的一部分,用于存储对象实例。它是所有线程共享的,并且在Java程序执行期间动态增长和缩小。堆内存的大小可以通过JVM的启动参数进行配置,如
-Xms
和-Xmx
参数可以设置堆内存的初始大小和最大大小。 - 方法区(Method Area):方法区用于存储已被加载的类信息、常量、静态变量等数据。它也是所有线程共享的,但在HotSpot虚拟机中,方法区被实现为永久代(PermGen space),而在Java 8之后,这部分内存被移除,改为使用元空间(Metaspace)。
- 栈(Stack):栈用于存储线程执行时的局部变量、操作数栈、动态链接等数据。每个线程都有自己的栈,其大小可以通过JVM的启动参数进行配置,如
-Xss
参数可以设置栈的大小。 - 程序计数器(Program Counter):程序计数器用于存储当前线程所执行的字节码指令的行号。每个线程都有自己的程序计数器。
- 本机存储器(Native Memory):本机存储器用于存储由Java虚拟机使用的本机库函数的代码和数据。
4、讲一下Java的垃圾回收几种算法
- 标记-清除(Mark-Sweep)算法:它分为两个阶段,首先标记出所有被引用的对象,然后清除没有被标记的对象。这个算法的优点是简单高效,缺点是会产生内存碎片,这样会导致在分配大对象时无法找到连续的内存空间。
- 标记-整理(Mark-Compact)算法:这个算法在标记阶段与标记-清除算法相同,但在清除阶段则会把所有未被标记的对象压缩到一起,并把连续的空闲内存区域合并成一个大的空闲区域。这个算法的优点是不会产生内存碎片,但缺点是移动对象会消耗一定的计算资源。
- 复制(Copying)算法:这种算法把可用的内存空间划分为两个区域,一部分被标记为正在使用,另一部分被标记为空闲。当当前正在使用的内存空间被填满时,所有还被引用的对象会被复制到另一块空闲内存区域中,然后清空当前的内存空间。这个算法的优点是不会产生内存碎片,但缺点是会浪费一部分内存空间。
- 分代(Generational)算法:JVM将堆内存划分为多个年轻代和一个老年代。新创建的对象会被分配到年轻代中,而经过一定次数的垃圾回收后,如果对象仍然存活,它们将被移动到老年代。这样,垃圾回收器就可以重点关注年轻代中的对象,从而提高了效率。
5、Spring 的Autowired自动装配原理
@Autowired注解会在Spring容器中查找与属性或构造器参数相匹配的Bean,并将其注入到目标属性或构造器参数中。如果没有找到匹配的Bean,则根据配置决定是否抛出异常。
在Spring容器中,Bean的装配方式可以通过XML文件或注解进行配置。对于XML文件,可以在bean标签中使用autowire属性指定需要注入的属性或构造器参数。对于注解,可以在属性或构造器参数前添加@Autowired注解即可。
6、Spring Bean的生命周期
Spring Bean的生命周期是指从Bean实例化之后,即通过反射创建出对象之后,到Bean成为一个完整对象,最终存储到单例池中,这个过程被称为Spring Bean的生命周期。具体步骤如下:
- 实例化:Spring框架会取出BeanDefinition的信息进行判断当前Bean的范围是否是singleton的是否不是延迟加载的,是否不是FactoryBean等,最终将一个普通的singleton的Bean通过反射进行实例化。这个阶段是Spring最具技术含量和复杂度的阶段。
- 属性赋值:在实例化Bean之后,Spring将使用依赖注入或属性设置方法来设置Bean的属性值。
- 初始化:BeanPostProcessor前置处理;检查是否是InitializingBean已决定调用afterPropertiesSet方法;检查是否配置自定义init-method;BeanPostProcessor后置处理。
- 销毁:当应用程序关闭时,Spring容器会销毁所有的Bean。在销毁Bean之前,Spring将调用Bean的销毁方法。如果Bean实现了DisposableBean接口,那么Spring将调用它的destroy()方法。
7、Redis有哪几种基本数据结构
Redis有5种基本数据结构,包括:
- String(字符串):这是Redis最简单的数据结构。
- List(列表):是一种双端队列,可以通过push和pop操作从两端添加或删除元素。
- Set(集合):是一种无序且不重复的数据结构,可以用于保存唯一元素。
- Hash(哈希):用于存储键值对,可以用于存储对象。
- Zset(有序集合):与Set类似,但每个元素都有一个关联的分数,根据这个分数进行排序。
8、说一下 Zset 的底层原理
Zset(有序集合)是Redis提供的一种数据结构,底层原理主要包含以下两个方面:
- 哈希表:Zset底层使用了哈希表来实现。哈希表是一种数据结构,它可以将键映射到对应的值上。在Zset中,哈希表的作用是关联元素的值和权重(score),保障元素值的唯一性。通过元素值可以找到对应的权重值。
- 跳跃表:跳跃表是一种数据结构,它按score从小到大保存所有集合元素。每个元素存储的都是<value,score>对。跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。Zset底层使用了跳跃表来实现有序集合的底层结构。
9、讲一下redis哨兵模式与集群模式
Redis哨兵模式与集群模式都是用于提高Redis可靠性和可扩展性的解决方案,但它们之间有一些区别。
- Redis哨兵模式:
- 哨兵模式是在主从模式的基础上添加了故障检测和自动故障转移的功能。
- 在哨兵模式中,一个或多个哨兵进程监视Redis节点的运行状况。如果主节点发生故障,哨兵会检测到这一情况并自动将其中一个从节点提升为新的主节点。这个过程是自动的,所以不需要人为干预。
- 哨兵模式提高了Redis集群的可靠性,确保即使主节点发生故障,Redis服务也能够继续运行。
- Redis集群模式:
- 集群模式是在多个Redis节点之间分配数据,提供更高的可扩展性和容错能力。
- 在集群模式中,数据被分配到多个Redis节点上,每个节点处理自己的数据。当一个节点失效时,集群会自动将这个节点的数据迁移到其他节点上。
- 集群模式在Redis大规模部署中非常有用,因为它可以轻松扩展和缩小Redis集群,而不会影响到整个系统的性能和可靠性。
总结一下,Redis哨兵模式和集群模式的主要区别如下:
- 监控方式:哨兵模式通过哨兵进程监控Redis节点的运行状况,而集群模式通过多个Redis节点之间的协作来监控数据状态。
- 数据处理方式:在哨兵模式中,如果主节点发生故障,会自动将其中一个从节点提升为新的主节点;而在集群模式中,数据被分配到多个Redis节点上,每个节点处理自己的数据,当一个节点失效时,数据会自动迁移到其他节点上。
- 可扩展性和容错能力:集群模式提供更高的可扩展性和容错能力,可以轻松扩展和缩小Redis集群,而不会影响到整个系统的性能和可靠性;而哨兵模式主要是在主从模式的基础上提高了可靠性和可扩展性。
10、redis的持久化策略AOF和RDB、容灾
Redis的持久化策略有两种,分别是RDB(Redis DataBase)和AOF(Append Only File)。此外,Redis还具有容灾能力。
- RDB持久化策略: RDB持久化策略是通过生成数据快照的方式来保存数据。在指定的时间间隔内,Redis将内存中的数据生成一个二进制文件,通常是一个名为dump.rdb的文件。这个文件是一个完整的数据快照,可以用来备份和数据恢复。
- 优点:
- RDB文件紧凑,节省磁盘空间。
- RDB恢复数据速度快,因为直接加载快照文件到内存。
- 缺点:
- RDB是定时持久化,可能会导致丢失最近一段时间的数据。
- 在大数据量的情况下,生成快照会占用较多的CPU和内存资源。
- AOF持久化策略: AOF持久化策略是通过记录Redis的所有写操作命令到一个追加日志文件(Append Only File)中来保存数据。当Redis重启时,会通过回放这些写操作命令来恢复数据。AOF持久化可以配置不同的同步策略,如每秒同步、每写操作同步或根据需要同步。
- 优点:
- AOF持久化可以配置同步策略,相对RDB来说,丢失数据的时间更短。
- AOF文件易于理解和解析,可以用于数据备份和恢复。
- 缺点:
- AOF文件通常比RDB文件更大,因为它记录了所有的写操作命令。
- AOF恢复数据速度相对较慢,因为需要回放所有的写操作命令。
- 容灾:
Redis的容灾能力主要通过主从复制(Replication)来实现。在主从复制模式下,一个Redis主节点可以连接多个从节点,实现数据备份和负载均衡。当主节点发生故障时,可以迅速切换到从节点,保证服务的可用性。此外,Redis还支持哨兵模式(Sentinel)和集群模式(Cluster),进一步提高容灾能力。
总结:
- RDB持久化策略适合对数据丢失容忍度较低的应用场景,如缓存、会话管理等,因为它可以更快地恢复数据。
- AOF持久化策略适合对数据丢失容忍度较高的应用场景,如日志、排行榜等,因为它可以更好地保证数据的完整性。
- Redis的主从复制、哨兵模式和集群模式提供了强大的容灾能力,确保在主节点故障时能够快速切换到从节点或其他节点,保证服务的可用性。
11、讲一下Go语言的select,Goroutine
在Go语言中,select
语句用于在多个通道操作之间进行选择。它允许你同时等待多个通道的操作,一旦有一个通道就绪(可以发送或接收数据),就会执行相应的操作。这种机制常用于实现并发控制和超时处理。
当使用select
语句时,每个通道操作都需要放在一个case
子句中。下面是select
语句的基本语法:
select {
case <-channel1:
// 执行channel1就绪时的操作
case data := <-channel2:
// 执行channel2就绪时的操作,并将接收到的数据赋值给data变量
case channel3 <- data:
// 执行channel3就绪时的操作,并将data变量发送到channel3
default:
// 如果没有任何通道就绪,执行default子句
}
在select
语句中,只有一个case
子句会被执行,具体取决于哪个通道首先就绪。如果多个通道都就绪了,Go语言会随机选择一个case
子句执行。
select
语句通常与goroutine
(轻量级线程)一起使用,以实现并发执行和通信。通过使用go
关键字启动的函数会在一个新的goroutine
中异步执行。这样,你可以同时运行多个函数,并且使用select
语句来等待它们之间的同步。
下面是一个简单的示例,演示了如何使用select
语句和goroutine
来实现并发控制:
package main
import (
"fmt"
"time"
)
func main() {
channel1 := make(chan string)
channel2 := make(chan string)
go func() {
time.Sleep(2 * time.Second)
channel1 <- "Channel 1"
}()
go func() {
time.Sleep(1 * time.Second)
channel2 <- "Channel 2"
}()
select {
case msg1 := <-channel1:
fmt.Println(msg1)
case msg2 := <-channel2:
fmt.Println(msg2)
}
}
创建了两个通道channel1
和channel2
,并使用两个匿名的goroutine
分别向这两个通道发送消息。主函数中使用select
语句等待哪个通道首先就绪,并打印接收到的消息。在这个例子中,会先等待1秒钟后,channel2
就绪并发送消息"Channel 2",然后主函数打印该消息。
12、Raft协议的一致性问题解决方案
Raft协议的一致性问题解决方案体现在以下方面:
- Raft协议通过规定节点有三种角色,即Leader(主节点)、Follower(从节点)和Candidate(候选节点),来实现数据同步和leader选举。
- Raft协议规定,当Follower节点在指定时间内没有接收到Leader的心跳包时,会变成Candidate节点,并会向其他节点发送投票请求,请求其他节点投票。获得超过集群节点数一半的票数的节点会成为新的Leader节点。
- Raft协议的安全性体现在多个方面。首先,通过强制Followers复制Leader的日志来处理日志的不一致,即Followers上的不一致的日志会被Leader的日志覆盖。其次,当Follower和Leader之间的日志相差过大时,领导者会直接发送快照来快速达到一致。
13、为什么集群的节点数量设置为奇数个,而不是偶数个?
将集群的节点数量设置为奇数个而不是偶数个,主要是出于以下原因:
- 容错性:在分布式系统中,如果节点数量是偶数个,当发生网络分区或节点故障时,可能无法形成多数派,从而无法达成一致性。例如,在由5个节点组成的集群中,如果两个节点发生故障,剩下的3个节点无法形成多数派,导致系统无法正常工作。而奇数个节点能够确保在发生分区或故障时仍然存在多数派,从而保证系统的可用性和一致性。
- 性能优化:在分布式系统中,如果节点数量是偶数个,可能会存在一些性能上的问题。例如,在由4个节点组成的集群中,如果一个节点发生故障,剩下的3个节点需要进行一次选举来选出新的Leader。这个选举过程可能会消耗一定的时间和资源,影响系统的性能。而奇数个节点能够避免这种情况的发生,从而优化系统的性能。
14、Python的GIL
Python的全局解释器锁(Global Interpreter Lock,GIL)是一种在CPython解释器(Python的默认和最广泛使用的实现)中使用的同步机制。它限制了同一时间只有一个线程可以执行Python字节码。这个机制防止了多个线程同时执行Python字节码,确保了Python的线程安全。
GIL的存在使得Python在某些多线程应用中表现出瓶颈,特别是在那些需要大量计算和I/O操作的场景中。由于GIL的限制,即使在拥有多个处理器核心的机器上,Python的多线程程序也只能在一个处理器核心上运行。
尽管如此,Python仍然提供了几种方式来处理这个问题:
- 使用多进程:Python的multiprocessing模块允许你创建多个进程,每个进程有自己的GIL,因此它们可以并行执行。
- 使用异步IO:Python的asyncio库允许你在一个线程中同时处理多个I/O操作,从而在一定程度上绕过GIL的限制。
- 使用更高级的并行计算库:例如Numba和Cython等库可以让你在Python中执行并行和矢量化操作,从而提高性能。
- JIT编译器:像PyPy这样的Python实现使用JIT编译器来提高性能,可以在一定程度上避免GIL的限制。
- 原文链接:https://github.com/warthecatalyst/What-to-in-Graduate-School/blob/main/秋招的面经/华科计科第二人的秋招报告.md