快手也开这么高啊,我真惊了。

科技   2024-11-18 14:04   河南  

大家好,我是二哥呀。

快手也定薪了,我这里也给大家同步一波手头上已有的信息,方便对比做个参考,或者去 A 薪资,下一届的小伙伴也好对快手做个评估。

信息来源于二哥的 Java 面试指南专栏
  • 硕士 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 一面为例来看看快手的面试标准,好做到知彼知己百战不殆。

背八股就认准三分恶的面渣逆袭

快手同学 4 一面

解释下什么是IOC和AOP?分别解决了什么问题?

所谓的IoC,就是由容器来控制对象的生命周期和对象之间的关系。控制对象生命周期的不再是引用它的对象,而是容器,这就叫控制反转

三分恶面渣逆袭:控制反转示意图

Spring 倡导的开发方式就是这样,所有类的创建和销毁都通过 Spring 容器来,不再是开发者去 new,去 = null,这样就实现了对象的解耦。

AOP,也就是面向切面编程,简单点说,AOP 就是把一些业务逻辑中的相同代码抽取到一个独立的模块中,让业务逻辑更加清爽。

三分恶面渣逆袭:横向抽取

业务代码不再关心这些通用逻辑,只需要关心自己的业务实现,这样就实现了业务逻辑和通用逻辑的分离。

IOC和DI的区别?

IOC 是一种思想,DI 是实现 IOC 的具体方式,比如说利用注入机制(如构造器注入、Setter 注入)将依赖传递给目标对象。

Martin Fowler’s Definition

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:环绕切点,打印方法签名和执行时间
技术派项目:配置 AOP 切面

第三步,在使用的地方加上自定义注解

技术派项目:使用注解

第四步,当接口被调用时,就可以看到对应的执行日志。

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 的生命周期大致分为五个阶段:

三分恶面渣逆袭: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 销毁阶段调用,容器关闭时调用。

二哥的Java 进阶之路:init-method 和 destroy-method

循环依赖有了解过吗?出现循环依赖的原因?

A 依赖 B,B 依赖 A,或者 C 依赖 C,就成了循环依赖。

三分恶面渣逆袭:Spring循环依赖

原因很简单,AB 循环依赖,A 实例化的时候,发现依赖 B,创建 B 实例,创建 B 的时候发现需要 A,创建 A1 实例……无限套娃。。。。

如何解决循环依赖?三大缓存存储内容的区别?

通过三级缓存机制:

  1. 一级缓存:存放完全初始化好的单例 Bean。
  2. 二级缓存:存放正在创建但未完全初始化的 Bean 实例。
  3. 三级缓存:存放 Bean 工厂对象,用于提前暴露 Bean。
三分恶面渣逆袭:三级缓存

如果缺少第二级缓存会有什么问题?

三分恶面渣逆袭:二级缓存不行的原因

如果没有二级缓存,Spring 无法在未完成初始化的情况下暴露 Bean。会导致代理 Bean 的循环依赖问题,因为某些代理逻辑无法在三级缓存中提前暴露。最终可能抛出 BeanCurrentlyInCreationException。

为什么使用SpringBoot?

Spring Boot 提供了一套默认配置,它通过约定大于配置的理念,来帮助我们快速搭建 Spring 项目骨架。

SpringBoot图标

以前的 Spring 开发需要配置大量的 xml 文件,并且需要引入大量的第三方 jar 包,还需要手动放到 classpath 下。现在只需要引入一个 Starter,或者一个注解,就可以轻松搞定。

SpringBoot自动装配的原理及流程?@Import的作用?

在 Spring 中,自动装配是指容器利用反射技术,根据 Bean 的类型、名称等自动注入所需的依赖。

三分恶面渣逆袭:SpringBoot自动配置原理

在 Spring Boot 中,开启自动装配的注解是@EnableAutoConfiguration

Spring Boot 为了进一步简化,直接通过 @SpringBootApplication 注解一步搞定,该注解包含了 @EnableAutoConfiguration 注解。

二哥的 Java 进阶之路:@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 容器中。

二哥的 Java 进阶之路:AutoConfigurationImportSelector源码

获取注入类的方法是 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,就可以看到欢迎消息了。

二哥的 Java 进阶之路

Spring中使用了哪些设计模式,以其中一种模式举例说明?

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 操作。写操作可能触发扩容或链表转为红黑树。

三分恶面渣逆袭:Java 8 put 流程

②、get 查询

通过计算哈希值快速定位桶,在桶中查找目标节点,多个 key 值时链表遍历和红黑树查找。读操作是无锁的,依赖 volatile 保证线程可见性。

如何判断死亡对象?

Java 使用的是可达性分析算法,通过一组名为 “GC Roots” 的根对象,进行递归扫描。那些无法从根对象到达的对象是不可达的,可以被回收;反之,是可达的,不会被回收。

三分恶面渣逆袭:GC Root

GC Roots有哪些?

所谓的 GC Roots,就是一组必须活跃的引用,不是对象,它们是程序运行时的起点,是一切引用链的源头。在 Java 中,GC Roots 包括以下几种:

  • 虚拟机栈中的引用(方法的参数、局部变量等)
  • 本地方法栈中 JNI 的引用
  • 类静态变量
  • 运行时常量池中的常量(String 或 Class 类型)

空间分配担保是什么?

空间分配担保是指在进行 Minor GC(新生代垃圾回收)前,JVM 会确保老年代有足够的空间存放从新生代晋升的对象。如果老年代空间不足,可能会触发 Full GC。

类装载的执行过程?

三分恶面渣逆袭:类的生命周期

类装载过程包括三个阶段:载入、链接(包括验证、准备、解析)、初始化。

①、载入:将类的二进制字节码加载到内存中。

②、链接可以细分为三个小的阶段:

  • 验证:检查类文件格式是否符合 JVM 规范
  • 准备:为类的静态变量分配内存并设置默认值。
  • 解析:将符号引用替换为直接引用。

③、初始化:执行静态代码块和静态变量初始化。

双亲委派模式是什么?

双亲委派模型要求类加载器在加载类时,先委托父加载器尝试加载,只有父加载器无法加载时,子加载器才会加载。

三分恶面渣逆袭:双亲委派模型

为什么使用这种模式?

①、避免类的重复加载:父加载器加载的类,子加载器无需重复加载。

②、保证核心类库的安全性:如 java.lang.* 只能由 Bootstrap ClassLoader 加载,防止被篡改。

服务器的CPU占用持续升高,有哪些排查问题的手段?

三分恶面渣逆袭:CPU飙高

首先,使用 top 命令查看 CPU 占用情况,找到占用 CPU 较高的进程 ID。

top
haikuotiankongdong:top 命令结果

接着,使用 jstack 命令查看对应进程的线程堆栈信息。

jstack -l <pid> > thread-dump.txt

然后再使用 top 命令查看进程中线程的占用情况,找到占用 CPU 较高的线程 ID。

top -H -p <pid>
haikuotiankongdong:Java 进程中的线程情况

在 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。

二哥的 Java 进阶之路:jps 查看技术派的进程 ID

第二步,使用top -p [pid] 查看进程使用 CPU 和内存占用情况。

二哥的 Java 进阶之路:top -p

第三步,使用 top -Hp [pid] 查看进程下的所有线程占用 CPU 和内存情况。

二哥的 Java 进阶之路:top -Hp

第四步,抓取线程栈:jstack -F 29452 > 29452.txt,可以多抓几次做个对比。

29452 为 pid,顺带作为文件名。

二哥的 Java 进阶之路:jstack

看看有没有线程死锁、死循环或长时间等待这些问题。

二哥的 Java 进阶之路:另外一组线程 id 的堆栈

第五步,可以使用jstat -gcutil [pid] 5000 10 每隔 5 秒输出 GC 信息,输出 10 次,查看 YGCFull GC 次数。

二哥的 Java 进阶之路:jstat

通常会出现 YGC 不增加或增加缓慢,而 Full GC 增加很快。

或使用 jstat -gccause [pid] 5000 输出 GC 摘要信息。

二哥的 Java 进阶之路:jstat

或使用 jmap -heap [pid] 查看堆的摘要信息,关注老年代内存使用是否达到阀值,若达到阀值就会执行 Full GC。

二哥的 Java 进阶之路:jmap

如果发现 Full GC 次数太多,就很大概率存在内存泄漏了。

第六步,生成 dump 文件,然后借助可视化工具分析哪个对象非常多,基本就能定位到问题根源了。

执行命令 jmap -dump:format=b,file=heap.hprof 10025 会输出进程 10025 的堆快照信息,保存到文件 heap.hprof 中。

二哥的 Java 进阶之路:jmap

第七步,可以使用图形化工具分析,如 JDK 自带的 VisualVM,从菜单 > 文件 > 装入 dump 文件。

VisualVM

然后在结果观察内存占用最多的对象,找到内存泄漏的源头。

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 刷题 + 简历精修 的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。

两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的学习资源,相信能帮助你走的更快、更稳、更远

欢迎点击左下角阅读原文了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。

最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。

沉默王二
技术文通俗易懂,吹水文风趣幽默。学 Java,认准二哥的网站 javabetter.cn
 最新文章