大家好,我是二哥呀。
快手也定薪了,我这里也给大家同步一波手头上已有的信息,方便对比做个参考,或者去 A 薪资,下一届的小伙伴也好对快手做个评估。
硕士 211,客户端岗位,开到了 25k 的 base,算是小 SP,不过网上都是劝退客户端的,有点担心。 西北工业大学,测开开到了 26k,没想到快手也能开这么高,有点惊,但对互联网有点小恐惧 上海某 top3,后端开发,给到了 26.5*16,政府还能补贴 2.5k,除此之外,房补每个月还有 2k,杭州地区 本科后端开发给了 23k,算是大白菜,但感觉已经很不错了 后端开发岗,给到了 28.5k,但没有签字费,八点下班有 30 能量卷,早10.30 晚 9.30,base 北京 本科 211,Java 岗,给到了 27k,本科这薪资真不少了呀,至少 SP 了
整体上,快手给的薪资比去年多一点。并且有很多本科学历的样本,不像之前的一些公司,样本集中在硕士上。
目前市面上还没有开的知名互联网公司还有小红书、得物、拼多多、阿里系,估计最迟这个月底,也就意味着秋招即将进入最后的尾声。
11 月底,应该是腰部选手的机会,12 月应该是尾部选手的机会,大家一定要注意时间节点。
这个时候,可能难度比之前要小很多。
那接下来,我们就以Java 面试指南中收录的快手同学 4 一面为例来看看快手的面试标准,好做到知彼知己百战不殆。
1、二哥的 Linux 速查备忘手册.pdf 下载 2、三分恶面渣逆袭在线版:https://javabetter.cn/sidebar/sanfene/nixi.html
快手同学 4 一面
解释下什么是IOC和AOP?分别解决了什么问题?
所谓的IoC,就是由容器来控制对象的生命周期和对象之间的关系。控制对象生命周期的不再是引用它的对象,而是容器,这就叫控制反转。
Spring 倡导的开发方式就是这样,所有类的创建和销毁都通过 Spring 容器来,不再是开发者去 new,去 = null
,这样就实现了对象的解耦。
AOP,也就是面向切面编程,简单点说,AOP 就是把一些业务逻辑中的相同代码抽取到一个独立的模块中,让业务逻辑更加清爽。
业务代码不再关心这些通用逻辑,只需要关心自己的业务实现,这样就实现了业务逻辑和通用逻辑的分离。
IOC和DI的区别?
IOC 是一种思想,DI 是实现 IOC 的具体方式,比如说利用注入机制(如构造器注入、Setter 注入)将依赖传递给目标对象。
Spring AOP的实现原理?JDK动态代理和CGLib动态代理的各自实现及其区别?
AOP 是通过动态代理实现的,代理方式有两种:JDK 动态代理和 CGLIB 代理。
①、JDK 动态代理是基于接口的代理,只能代理实现了接口的类。
使用 JDK 动态代理时,Spring AOP 会创建一个代理对象,该代理对象实现了目标对象所实现的接口,并在方法调用前后插入横切逻辑。
②、CGLIB 动态代理是基于继承的代理,可以代理没有实现接口的类。
使用 CGLIB 动态代理时,Spring AOP 会生成目标类的子类,并在方法调用前后插入横切逻辑。
现在需要统计方法的具体执行时间,说下如何使用AOP来实现?
我在技术派实战项目中有应用,比如说利用 AOP 打印接口的入参和出参日志、执行时间,方便后期 bug 溯源和性能调优。
第一步,自定义注解作为切点
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MdcDot {
String bizCode() default "";
}
第二步,配置 AOP 切面:
@Aspect
:标识切面@Pointcut
:设置切点,这里以自定义注解为切点@Around
:环绕切点,打印方法签名和执行时间
第三步,在使用的地方加上自定义注解
第四步,当接口被调用时,就可以看到对应的执行日志。
2023-06-16 11:06:13,008 [http-nio-8080-exec-3] INFO |00000000.1686884772947.468581113|101|c.g.p.forum.core.mdc.MdcAspect.handle(MdcAspect.java:47) - 方法执行耗时: com.github.paicoding.forum.web.front.article.rest.ArticleRestController#recommend = 47
介绍下Bean的生命周期?
Bean 的生命周期大致分为五个阶段:
实例化:Spring 首先使用构造方法或者工厂方法创建一个 Bean 的实例。在这个阶段,Bean 只是一个空的 Java 对象,还未设置任何属性。 属性赋值:Spring 将配置文件中的属性值或依赖的 Bean 注入到该 Bean 中。这个过程称为依赖注入,确保 Bean 所需的所有依赖都被注入。 初始化:Spring 调用 afterPropertiesSet 方法,或通过配置文件指定的 init-method 方法,完成初始化。 使用中:Bean 准备好可以使用了。 销毁:在容器关闭时,Spring 会调用 destroy 方法或 destroy-method 方法,完成 Bean 的清理工作。
Aware 类型的接口有什么作用?
通过实现 Aware 接口,Bean 可以获取 Spring 容器的相关信息,如 BeanFactory、ApplicationContext 等。
常见 Aware 接口有:
接口 | 作用 |
---|---|
BeanNameAware | 获取当前 Bean 的名称。 |
BeanFactoryAware | 获取当前 Bean 所在的 BeanFactory 实例,可以直接操作容器。 |
ApplicationContextAware | 获取当前 Bean 所在的 ApplicationContext 实例。 |
EnvironmentAware | 获取 Environment 对象,用于获取配置文件中的属性或环境变量。 |
如果配置了 init-method 和 destroy-method,Spring 会在什么时候调用其配置的方法?
init-method 在 Bean 初始化阶段调用,依赖注入完成后且 postProcessBeforeInitialization 调用之后执行。
destroy-method 在 Bean 销毁阶段调用,容器关闭时调用。
循环依赖有了解过吗?出现循环依赖的原因?
A 依赖 B,B 依赖 A,或者 C 依赖 C,就成了循环依赖。
原因很简单,AB 循环依赖,A 实例化的时候,发现依赖 B,创建 B 实例,创建 B 的时候发现需要 A,创建 A1 实例……无限套娃。。。。
如何解决循环依赖?三大缓存存储内容的区别?
通过三级缓存机制:
一级缓存:存放完全初始化好的单例 Bean。 二级缓存:存放正在创建但未完全初始化的 Bean 实例。 三级缓存:存放 Bean 工厂对象,用于提前暴露 Bean。
如果缺少第二级缓存会有什么问题?
如果没有二级缓存,Spring 无法在未完成初始化的情况下暴露 Bean。会导致代理 Bean 的循环依赖问题,因为某些代理逻辑无法在三级缓存中提前暴露。最终可能抛出 BeanCurrentlyInCreationException。
为什么使用SpringBoot?
Spring Boot 提供了一套默认配置,它通过约定大于配置的理念,来帮助我们快速搭建 Spring 项目骨架。
以前的 Spring 开发需要配置大量的 xml 文件,并且需要引入大量的第三方 jar 包,还需要手动放到 classpath 下。现在只需要引入一个 Starter,或者一个注解,就可以轻松搞定。
SpringBoot自动装配的原理及流程?@Import的作用?
在 Spring 中,自动装配是指容器利用反射技术,根据 Bean 的类型、名称等自动注入所需的依赖。
在 Spring Boot 中,开启自动装配的注解是@EnableAutoConfiguration
。
Spring Boot 为了进一步简化,直接通过 @SpringBootApplication
注解一步搞定,该注解包含了 @EnableAutoConfiguration
注解。
main 类启动的时候,Spring Boot 会通过底层的AutoConfigurationImportSelector
类加载自动装配类。
@AutoConfigurationPackage //将main同级的包下的所有组件注册到容器中
@Import({AutoConfigurationImportSelector.class}) //加载自动装配类 xxxAutoconfiguration
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
AutoConfigurationImportSelector
实现了ImportSelector
接口,该接口的作用是收集需要导入的配置类,配合 @Import()
将相应的类导入到 Spring 容器中。
获取注入类的方法是 selectImports()
,它实际调用的是getAutoConfigurationEntry()
,这个方法是获取自动装配类的关键。
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// 检查自动配置是否启用。如果@ConditionalOnClass等条件注解使得自动配置不适用于当前环境,则返回一个空的配置条目。
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获取启动类上的@EnableAutoConfiguration注解的属性,这可能包括对特定自动配置类的排除。
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 从spring.factories中获取所有候选的自动配置类。这是通过加载META-INF/spring.factories文件中对应的条目来实现的。
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 移除配置列表中的重复项,确保每个自动配置类只被考虑一次。
configurations = removeDuplicates(configurations);
// 根据注解属性解析出需要排除的自动配置类。
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 检查排除的类是否存在于候选配置中,如果存在,则抛出异常。
checkExcludedClasses(configurations, exclusions);
// 从候选配置中移除排除的类。
configurations.removeAll(exclusions);
// 应用过滤器进一步筛选自动配置类。过滤器可能基于条件注解如@ConditionalOnBean等来排除特定的配置类。
configurations = getConfigurationClassFilter().filter(configurations);
// 触发自动配置导入事件,允许监听器对自动配置过程进行干预。
fireAutoConfigurationImportEvents(configurations, exclusions);
// 创建并返回一个包含最终确定的自动配置类和排除的配置类的AutoConfigurationEntry对象。
return new AutoConfigurationEntry(configurations, exclusions);
}
如果想让SpringBoot对自定义的jar包进行自动配置的话,需要怎么做?
第一步,创建一个新的 Maven 项目,例如命名为 my-spring-boot-starter。在 pom.xml 文件中添加必要的依赖和配置:
<properties>
<spring.boot.version>2.3.1.RELEASE</spring.boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring.boot.version}</version>
</dependency>
</dependencies>
第二步,在 src/main/java
下创建一个自动配置类,比如 MyServiceAutoConfiguration.java:(通常是 autoconfigure 包下)。
@Configuration
@EnableConfigurationProperties(MyStarterProperties.class)
public class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyStarterProperties properties) {
return new MyService(properties.getMessage());
}
}
第三步,创建一个配置属性类 MyStarterProperties.java:
@ConfigurationProperties(prefix = "mystarter")
public class MyStarterProperties {
private String message = "二哥的 Java 进阶之路不错啊!";
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
第四步,创建一个简单的服务类 MyService.java:
public class MyService {
private final String message;
public MyService(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
第五步,配置 spring.factories,在 src/main/resources/META-INF
目录下创建 spring.factories 文件,并添加:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itwanger.mystarter.autoconfigure.MyServiceAutoConfiguration
第六步,使用 Maven 打包这个项目:
mvn clean install
第七步,在其他的 Spring Boot 项目中,通过 Maven 来添加这个自定义的 Starter 依赖,并通过 application.properties 配置欢迎消息:
mystarter.message=javabetter.cn
然后就可以在 Spring Boot 项目中注入 MyStarterProperties 来使用它。
启动项目,然后在浏览器中输入 localhost:8081/hello
,就可以看到欢迎消息了。
Spring中使用了哪些设计模式,以其中一种模式举例说明?
Spring 框架中用了蛮多设计模式的:
①、比如说工厂模式用于 BeanFactory 和 ApplicationContext,实现 Bean 的创建和管理。
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyBean myBean = context.getBean(MyBean.class);
②、比如说单例模式,这样可以保证 Bean 的唯一性,减少系统开销。
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService1 = context.getBean(MyService.class);
MyService myService2 = context.getBean(MyService.class);
// This will print "true" because both references point to the same instance
System.out.println(myService1 == myService2);
Spring如何实现单例模式?
Spring 通过 IOC 容器实现单例模式,具体步骤是:
单例 Bean 在容器初始化时创建并使用 DefaultSingletonBeanRegistry 提供的 singletonObjects 进行缓存。
// 单例缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
public Object getSingleton(String beanName) {
return this.singletonObjects.get(beanName);
}
protected void addSingleton(String beanName, Object singletonObject) {
this.singletonObjects.put(beanName, singletonObject);
}
在请求 Bean 时,Spring 会先从缓存中获取。
刚刚提到了Spring使用ConcurrentHashMap来实现单例模式,大致说下ConcurrentHashMap的put和get方法流程?
①、put 流程
一句话:通过计算键的哈希值确定存储位置,如果桶为空,使用 CAS 插入节点;如果存在冲突,通过链表或红黑树插入。在冲突时,如果 CAS 操作失败,会退化为 synchronized 操作。写操作可能触发扩容或链表转为红黑树。
②、get 查询
通过计算哈希值快速定位桶,在桶中查找目标节点,多个 key 值时链表遍历和红黑树查找。读操作是无锁的,依赖 volatile 保证线程可见性。
如何判断死亡对象?
Java 使用的是可达性分析算法,通过一组名为 “GC Roots” 的根对象,进行递归扫描。那些无法从根对象到达的对象是不可达的,可以被回收;反之,是可达的,不会被回收。
GC Roots有哪些?
所谓的 GC Roots,就是一组必须活跃的引用,不是对象,它们是程序运行时的起点,是一切引用链的源头。在 Java 中,GC Roots 包括以下几种:
虚拟机栈中的引用(方法的参数、局部变量等) 本地方法栈中 JNI 的引用 类静态变量 运行时常量池中的常量(String 或 Class 类型)
空间分配担保是什么?
空间分配担保是指在进行 Minor GC(新生代垃圾回收)前,JVM 会确保老年代有足够的空间存放从新生代晋升的对象。如果老年代空间不足,可能会触发 Full GC。
类装载的执行过程?
类装载过程包括三个阶段:载入、链接(包括验证、准备、解析)、初始化。
①、载入:将类的二进制字节码加载到内存中。
②、链接可以细分为三个小的阶段:
验证:检查类文件格式是否符合 JVM 规范 准备:为类的静态变量分配内存并设置默认值。 解析:将符号引用替换为直接引用。
③、初始化:执行静态代码块和静态变量初始化。
双亲委派模式是什么?
双亲委派模型要求类加载器在加载类时,先委托父加载器尝试加载,只有父加载器无法加载时,子加载器才会加载。
为什么使用这种模式?
①、避免类的重复加载:父加载器加载的类,子加载器无需重复加载。
②、保证核心类库的安全性:如 java.lang.*
只能由 Bootstrap ClassLoader 加载,防止被篡改。
服务器的CPU占用持续升高,有哪些排查问题的手段?
首先,使用 top 命令查看 CPU 占用情况,找到占用 CPU 较高的进程 ID。
top
接着,使用 jstack 命令查看对应进程的线程堆栈信息。
jstack -l <pid> > thread-dump.txt
然后再使用 top 命令查看进程中线程的占用情况,找到占用 CPU 较高的线程 ID。
top -H -p <pid>
在 jstack 的输出中搜索这个十六进制的线程 ID,找到对应的堆栈信息。
"Thread-5" #21 prio=5 os_prio=0 tid=0x00007f812c018800 nid=0x1a85 runnable [0x00007f811c000000]
java.lang.Thread.State: RUNNABLE
at com.example.MyClass.myMethod(MyClass.java:123)
at ...
最后,根据堆栈信息定位到具体的业务方法,查看是否有死循环、频繁的垃圾回收(GC)、资源竞争(如锁竞争)导致的上下文频繁切换等问题。
排查后发现是项目产生了内存泄露,如何确定问题出在哪里?
当时在做技术派项目的时候,由于 ThreadLocal 没有及时清理导致出现了内存泄漏问题。
常用的可视化监控工具有 JConsole、VisualVM、JProfiler、Eclipse Memory Analyzer (MAT)等。
也可以使用 JDK 自带的 jmap、jstack、jstat 等命令行工具来配合内存泄露问题的排查。
严重的内存泄漏往往伴随频繁的 Full GC,所以排查内存泄漏问题时,可以从 Full GC 入手。
第一步,使用 jps -l
查看运行的 Java 进程 ID。
第二步,使用top -p [pid]
查看进程使用 CPU 和内存占用情况。
第三步,使用 top -Hp [pid]
查看进程下的所有线程占用 CPU 和内存情况。
第四步,抓取线程栈:jstack -F 29452 > 29452.txt
,可以多抓几次做个对比。
29452 为 pid,顺带作为文件名。
看看有没有线程死锁、死循环或长时间等待这些问题。
第五步,可以使用jstat -gcutil [pid] 5000 10
每隔 5 秒输出 GC 信息,输出 10 次,查看 YGC 和 Full GC 次数。
通常会出现 YGC 不增加或增加缓慢,而 Full GC 增加很快。
或使用 jstat -gccause [pid] 5000
输出 GC 摘要信息。
或使用 jmap -heap [pid]
查看堆的摘要信息,关注老年代内存使用是否达到阀值,若达到阀值就会执行 Full GC。
如果发现 Full GC
次数太多,就很大概率存在内存泄漏了。
第六步,生成 dump
文件,然后借助可视化工具分析哪个对象非常多,基本就能定位到问题根源了。
执行命令 jmap -dump:format=b,file=heap.hprof 10025
会输出进程 10025 的堆快照信息,保存到文件 heap.hprof 中。
第七步,可以使用图形化工具分析,如 JDK 自带的 VisualVM,从菜单 > 文件 > 装入 dump 文件。
然后在结果观察内存占用最多的对象,找到内存泄漏的源头。
Redis事务满足原子性吗?要怎么改进?
Redis 的事务不具备强制性的原子性,但可以通过 Lua 脚本来增强 Redis 的原子能力。
在 Redis 中,Lua 脚本是以原子操作的方式执行的,也就是说,在脚本执行期间,不会插入其他命令,天然保证了事务性。
比如秒杀系统是一个经典场景,我们可以用 Lua 脚本来实现扣减 Redis 库存的功能。
-- 库存未预热
if (redis.call('exists', KEYS[2]) == 1) then
return -9;
end;
-- 秒杀商品库存存在
if (redis.call('exists', KEYS[1]) == 1) then
local stock = tonumber(redis.call('get', KEYS[1]));
local num = tonumber(ARGV[1]);
-- 剩余库存少于请求数量
if (stock < num) then
return -3
end;
-- 扣减库存
if (stock >= num) then
redis.call('incrby', KEYS[1], 0 - num);
-- 扣减成功
return 1
end;
return -2;
end;
-- 秒杀商品库存不存在
return -1;
IO多路复用中select/poll/epoll各自的实现原理和区别?
select 使用位图管理 fd,每次调用都需要将 fd 集合从用户态复制到内核态。最大支持 1024 个文件描述符。
poll 使用动态数组管理 fd,突破了 select 的数量限制。
epoll 使用红黑树和链表管理 fd,每次调用只需要将 fd 集合从用户态复制到内核态一次,不需要重复复制。
ending
一个人可以走得很快,但一群人才能走得更远。二哥的编程星球已经有 6500 多名球友加入了,如果你也需要一个良好的学习环境,戳链接 🔗 加入我们吧。这是一个 编程学习指南 + Java 项目实战 + LeetCode 刷题 + 简历精修 的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。
两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的学习资源,相信能帮助你走的更快、更稳、更远。
欢迎点击左下角阅读原文了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。
最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。