在国内,贝壳是为数不多泛 955 的公司,薪资待遇也还不错。这两天,贝壳校招也陆续开奖了(薪资数据来源于 offershow+社区讨论贴+读者分享)。
Java 开发:23*16,北京,海归硕士 Java 开发:25*16,北京,硕士 211 Java 开发:23*16,北京,硕士 985 Go 开发:23*16,北京,硕士 985 前端:20*16,北京,本科双一流,大白菜 前端:22*16,北京,海归本科,SP
其他福利待遇:包三餐、实习头三个月满勤给工资的 60%、年终大部分拿满。
可以看到,薪资待遇还是非常不错的,而且贝壳还是泛 955 WLB 的公司,性价比还是非常高的。
下面,给大家分享一位读者面试贝壳 Java 岗位的凉经,大家感受一下难度。
面试题
面试问了快一个小时,问的问题还挺多,我对面试中提到的部分有代表性的面试问题进行了整理,并添加了参加答案供大家复习参考。
1、自我介绍
一个好的自我介绍应该包含这几点要素:
用简单的话说清楚自己主要的技术栈于擅长的领域,例如 Java 后端开发、分布式系统开发; 把重点放在自己的优势上,重点突出自己的能力比如自己的定位的 bug 的能力特别厉害; 避免避实就虚,适当举例体现自己的能力,例如过往的比赛经历、实习经历; 自我介绍的时间不宜过长,一般是 1~2 分钟之间。
2、介绍自己的实习经历,做了什么,学到了什么
如果你有实习经历的话,自我介绍之后,第二个问题一般就是聊你的实习经历。面试之前,一定要提前准备好对应的话术,突出介绍自己实习期间的贡献。
很多同学实习期间可能接触不到什么实际的开发任务,大部分时间可能都是在熟悉和维护项目。对于这种情况,你可以适当润色这段实习经历,找一些简单的功能研究透,包装成自己参与做的,大部分同学都是这么做的。不用担心面试的时候会露馅,只要不挑选那种明显不会交给实习生做的任务,你自己也能讲明白就行了。不过,还是更建议你在实习期间尽量尝试主动去承担一些开发任务,这样整个实习经历对个人提升也会更大一些。
3、项目的 TPS、QPS 多少,怎么测的?
校招简历,绝大部分同学的项目都不是真实的项目经验,那我们应该如何回答项目的 TPS、QPS 等性能指标这类问题呢?
开始回答这个问题之前,先分享几个常见的性能指标:
TPS (Transactions Per Second): 每秒处理的事务数。一个事务可以包含多个请求。例如,一个电商下单事务可能包含添加商品到购物车、填写收货地址、支付等多个操作。 QPS (Queries Per Second): 每秒处理的请求数。例如,一个用户访问网站的首页算作一个请求。 并发数: 并发数可以简单理解为系统能够同时供多少人访问使用也就是说系统同时能处理的请求数量。 吞吐量: 吞吐量指的是系统单位时间内系统处理的请求数量。
下面分享一下我对回答这类问题的建议:
多找一些相关的技术博客看看,关键词是性能优化,例如 Redis 性能优化、数据库优化等等。另外,一些开源项目会提供一些示例配置和性能测试结果,可以作为参考。 自己使用压测工具(例如 JMeter、Apache Bench、Gatling 等)模拟真实场景下的并发访问,例如模拟用户登录、下单等操作,并通过调整并发用户数、Ramp-up 时间等参数来观察不同配置下的 TPS、QPS、响应时间等指标的变化。 不要试图记住具体的数字,因为不同业务场景下的性能指标差异很大。更重要的是理解这些指标的含义,以及如何根据实际情况进行调整。 将你学到的知识和你的项目经验结合起来,例如 "我在之前的项目中使用了 Redis 作为缓存,为了提高可用性和性能,我们采用了 Redis Cluster 方案..."。即使你的项目经验有限,也可以尝试将这些知识应用到你的项目中,并进行一些模拟测试。 不要夸大其词,不要试图将其强行包装成大厂高并发项目的访问量,意义不大,反而容易露馅。
一些常见软件的理论 QPS,例如单机 Nginx 可以达到 30w +、单机 Redis QPS 可以达到 8w+、 单机 MySQL QPS 大概在 4k 左右、单机 Tomcat 的 QPS 在 2w 左右。
不过,QPS 会受到多种因素的影响,几乎不可能达到这些理论值。需要根据具体硬件、软件配置、以及测试方法的不同而变化。单纯记住这些数字意义不大,更重要的是理解影响 QPS 的因素以及如何优化:
Nginx (30w+ QPS): Nginx 作为反向代理和负载均衡器,其性能非常出色。30w+ QPS 的说法主要针对静态文件服务,并且在优化配置和高性能硬件的条件下才能达到。如果涉及到动态内容处理或 SSL 加解密,QPS 会显著下降。 Redis (8w+ QPS): Redis 以其高性能内存数据库著称。8w+ QPS 通常指简单的 GET/SET 操作,并且在客户端和服务器网络连接良好的情况下才能实现。复杂操作或大量数据传输会降低 QPS。 MySQL (4k QPS): MySQL 的 QPS 受到很多因素的影响,包括硬件配置、数据库设计、查询语句的复杂度、索引优化等等。4k QPS 是一个比较常见的数值,但实际情况可能远低于或高于这个数字。简单的查询可以达到更高的 QPS,而复杂的查询可能会降低到几百甚至几十。 Tomcat (2w QPS): Tomcat 的 QPS 同样取决于许多因素,例如硬件配置、应用代码的效率、JVM 参数调优等等。2w QPS 是一个可能的数值,但实际情况可能会有很大差异。复杂的业务逻辑或高并发访问都会降低 QPS。
4、JIT 了解么?和 JVM 什么关系?
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
Java 程序从源代码到运行的过程如下图所示:
我们需要格外注意的是 .class->机器码
这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言 。
JDK、JRE、JVM、JIT 这四者的关系如下图所示。
通过上图可以看出,JIT 编译器是 JVM 的一个组件。
5、为什么说 Java 语言“编译与解释并存”?
这个问题我们上面已经提到过,因为比较重要,所以我们这里再提一下。
我们可以将高级编程语言按照程序的执行方式分为两种:
编译型:编译型语言[1] 会通过编译器[2]将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。 解释型:解释型语言[3]会通过解释器[4]一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
根据维基百科介绍:
为了改善解释语言的效率而发展出的即时编译[5]技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码[6]。到执行期时,再将字节码直译,之后执行。Java[7]与LLVM[8]是这种技术的代表产物。
相关阅读:基本功 | Java 即时编译器原理解析及实践[9]
为什么说 Java 语言“编译与解释并存”?
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
6、JDK 1.8 的默认垃圾回收器是?JDK1.9 之后呢?
JDK 1.8 默认垃圾回收器:Parallel Scanvenge(新生代)+ Parallel Old(老年代)。这个组合也被称为 Parallel GC 或 Throughput GC,侧重于吞吐量。 JDK 1.9 及以后默认垃圾回收器:G1 GC (Garbage-First Garbage Collector)。G1 GC 是一个更现代化的垃圾回收器,旨在平衡吞吐量和停顿时间,尤其适用于堆内存较大的应用。
7、G1 垃圾回收的过程
G1(Garbage-First)垃圾收集器在 JDK 7 中首次引入,作为一种试验性的垃圾收集器。到了 JDK 8,G1 得到了进一步的完善和改进,功能基本已经完全实现,成为一个稳定、可用于生产环境的垃圾收集器。
G1 收集器的运作大致分为以下几个步骤:
初始标记:短暂停顿(Stop-The-World,STW),标记从 GC Roots 可直接引用的对象,即标记所有直接可达的活跃对象 并发标记:与应用并发运行,标记所有可达对象。这一阶段可能持续较长时间,取决于堆的大小和对象的数量。 最终标记:短暂停顿(STW),处理并发标记阶段结束后残留的少量未处理的引用变更。 筛选回收:根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。这一阶段包含一个或多个停顿(STW),具体取决于回收的复杂度。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
8、ZGC 有了解吗?
ZGC (Z Garbage Collector) 是一种低延迟垃圾回收器,它基于标记-复制算法的变种,并进行了重大改进。ZGC 的目标是将暂停时间控制在 10 毫秒以内,并且暂停时间受堆大小的影响,支持超大规模的堆内存(数百 GB 甚至 TB 级别)。ZGC 牺牲了一定的吞吐量来换取低延迟。
ZGC 在 Java 11 中引入,并在 Java 15 之后被标记为 Production Ready,可以在生产环境中使用。
启用 ZGC:
java -XX:+UseZGC <className>
Java 21 引入了分代 ZGC,进一步将暂停时间降低到亚毫秒级别。可以使用 -XX:+UseZGC -XX:+ZGenerational
选项启用分代 ZGC。虽然 ZGC 和分代 ZGC 都大大减少了停顿时间,但仍然会有 STW 暂停,只是时间非常短。
启用分代 ZGC (Java 21+):
java -XX:+UseZGC -XX:+ZGenerational <className>
关于 ZGC 收集器的详细介绍推荐看看这几篇文章:
从历代 GC 算法角度剖析 ZGC - 京东技术 新一代垃圾回收器 ZGC 的探索与实践 - 美团技术团队[10] 极致八股文之 JVM 垃圾回收器 G1&ZGC 详解 - 阿里云开发者
9、项目为什么要用 Redis?不用行吗?
下面我们主要从“高性能”和“高并发”这两点来回答这个问题。
1、高性能
假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢? 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
2、高并发
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 Redis 的情况,Redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。
10、Redis 内存不够用怎么办?
扩大内存容量: 最简单粗暴的方法是增加服务器的物理内存。 优化数据存储: 选择合适的数据类型,例如使用 HyperLogLog 统计页面 UV。 选择合适的淘汰策略:Redis 提供了 8 种内存淘汰策略。选择合适的淘汰策略可以有效地控制内存使用,并保证重要的数据不被删除。 使用 Redis 集群: Redis 3.0 官方推出了分片集群解决方案 Redis Cluster 。经过多个版本的持续完善,Redis Cluster 成为 Redis 切片集群的首选方案,满足绝大部分高并发业务场景需求。如果项目使用的是 Redis 3.0 之前的版本,可以使用 Twemproxy、Codis 这类开源分片集群方案。Twemproxy、Codis 就相当于是 Proxy 层,负责维护路由规则,实现负载均衡。 监控 Redis: 使用 Prometheus 的 Redis-exporter[11]、redis-stat[12] 等工具监控 Redis,查看 Redis 实时运行信息。其中一些监控工具还可以辅助找到内存占用高的 key,例如 Redis-exporter 就支持。
11、数据库和缓存一致性如何保证?
细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。
下面单独对 Cache Aside Pattern(旁路缓存模式) 来聊聊。
Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:
缓存失效时间变短(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 增加 cache 更新重试机制(常用):如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。
详细介绍可以参考这篇文章:缓存和数据库一致性问题,看这篇就够了 - 水滴与银弹。
12、MQ 知道哪些?
我知道以下几种消息队列(MQ):
Kafka: Kafka 最初由 LinkedIn 开发,现在是 Apache 的顶级项目。它是一个分布式流处理平台,提供消息队列功能以及强大的流处理能力和容错持久化。Kafka 以其高吞吐量而闻名,适用于微服务和大数据场景。早期版本依赖 ZooKeeper,但较新版本引入了 KRaft 模式,从而不再依赖 ZooKeeper。 RocketMQ: RocketMQ 由阿里巴巴开发,也是 Apache 的顶级项目。它是一个云原生的消息、事件和流处理平台,具有高吞吐量、金融级稳定性以及轻量级架构。RocketMQ 被广泛应用于各种业务场景,包括交易核心链路。 RabbitMQ: RabbitMQ 是用 Erlang 语言实现的高级消息队列协议(AMQP)的消息中间件。它以可靠性、灵活的路由和易用性著称,并支持多种协议和多语言客户端。RabbitMQ 适用于各种分布式系统。 Pulsar: Pulsar 是下一代云原生分布式消息流平台,最初由 Yahoo 开发。它结合了消息传递、存储和轻量级函数式计算,具有强大的可扩展性和低延迟。Pulsar 支持多租户、持久化存储和跨区域数据复制。 ActiveMQ: ActiveMQ 是一个较老的消息队列,目前已被认为过时,不推荐使用和学习。
13、为什么最后选择了 RocketMQ?
RocketMQ 经过阿里巴巴双十一等高并发场景的考验,拥有非常高的性能和吞吐量,可以处理海量的消息。 RocketMQ 提供了顺序消息、定时消息、事务消息等丰富的特性,可以满足各种业务场景的需求。 RocketMQ 使用 Java 开发,对于 Java 开发者来说更加友好,易于集成和使用。并且,对于国内的用户来说,RocketMQ 的生态还是非常不错的。 RocketMQ 提供了丰富的消息可靠性机制,例如消息发送重试、消息消费重试等。
编译型语言: https://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E8%AA%9E%E8%A8%80
[2]编译器: https://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E5%99%A8
[3]解释型语言: https://zh.wikipedia.org/wiki/%E7%9B%B4%E8%AD%AF%E8%AA%9E%E8%A8%80
[4]解释器: https://zh.wikipedia.org/wiki/直譯器
[5]即时编译: https://zh.wikipedia.org/wiki/即時編譯
[6]字节码: https://zh.wikipedia.org/wiki/字节码
[7]Java: https://zh.wikipedia.org/wiki/Java
[8]LLVM: https://zh.wikipedia.org/wiki/LLVM
[9]基本功 | Java 即时编译器原理解析及实践: https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html
[10]新一代垃圾回收器 ZGC 的探索与实践 - 美团技术团队: https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html
[11]Redis-exporter: https://github.com/oliver006/redis_exporter
[12]redis-stat: https://github.com/junegunn/redis-stat
📌Java 后端技术面试准备强烈推荐《Java 面试指北》 和 JavaGuide ,400 多人参与维护完善,质量非常高。另外,目前的面试趋势是场景题变多,可以参考《后端面试高频系统设计&场景题》(20+高频系统设计&场景面试题)进行准备!
⭐面经详解合集:Java后端面经详解
专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入我的知识星球 ,和 3w+球友一起准备面试!