大厂Java面试实录:从Java SE到微服务,电商场景下的技术拷问与谢飞机翻车合集
大厂Java面试实录:从Java SE到微服务,电商场景下的技术拷问与谢飞机翻车合集
面试开场
北京,某互联网大厂二面会议室。面试官王工,十多年后端架构经验,面无表情,手里握着一份简历。对面坐着谢飞机,三年工作经验,简历上写着“精通Java、Spring Cloud、高并发、分布式”,实际水平约等于“会用IDE跑通Demo”。
王工扫了一眼简历,又看了眼谢飞机那自信满满的笑容,开口了。
“谢先生你好,我们开始吧。先聊聊基础。”
第一轮:核心语言、JVM与构建工具
王工:“你简历上写精通Java SE,那我问你,Java 8和Java 17分别有哪些重要特性?在实际项目中,你用过哪些?”
谢飞机(心中暗喜:这题我会):“Java 8有Lambda表达式、Stream流、Optional,还有新的日期时间API。Java 17……嗯,Java 17是LTS版本,好像有var关键字?不对,var是Java 10的。Java 17有密封类sealed class,还有文本块。我平时主要用Java 8,因为公司老项目多,Java 17只是看过博客。”
王工(微微点头):“基础还行。那你再说说JVM内存结构,以及常见的垃圾回收器?”
谢飞机(高兴过头,开始放飞):“JVM内存嘛,就是堆和栈。堆里面分新生代、老年代,还有一个叫‘永久代’吧?后来变成了元空间。栈就是存方法调用的局部变量。垃圾回收器我知道CMS、G1,还有最新那个ZGC?CMS会有碎片,G1是大区……呃,反正就是新生代用复制算法,老年代用标记清除或标记整理。有什么不对吗?”
王工(眉头皱了一下):“‘永久代’和‘元空间’的关系是?G1的Region是怎么划分的?”
谢飞机(开始冒汗):“这个……元空间是JDK8之后替代永久代的,存类的元信息,用的是本地内存。Region就是一堆格子,每个格子可以是Eden、Survivor、Old,然后G1会优先回收垃圾最多的Region。呃,细节我就不太清楚了。”
王工:“好,那说说你们项目的构建工具。Maven和Gradle的区别?Maven的依赖管理机制是什么?遇到过依赖冲突吗?”
谢飞机(松一口气,这题知道):“Maven用XML,Gradle用Groovy DSL,Gradle的构建速度快,支持增量编译。Maven的依赖管理是坐标,groupId、artifactId、version,然后从中央仓库下载。依赖冲突遇到过,就是多个依赖引入了同一个jar包的不同版本,Maven有‘就近原则’,然后可以用exclusion排除。不过有时候我直接暴力删掉冲突的jar包,反正能跑就行。”
王工:“嗯……能跑就行,这句话有点危险。不过Maven的依赖仲裁机制你确实知道一点。第一轮先到这。接下来我们模拟一个电商场景。”
第二轮:电商场景下的数据库、缓存与消息队列
王工:“假设我们要做一个电商平台,用户下单后,订单系统需要记录订单和商品信息。你会怎么设计订单表?关键索引怎么建?数据量大之后怎么处理?”
谢飞机(眼睛一亮):“订单表嘛,就是order表,有id、user_id、total_amount、status、create_time。订单明细表order_item,有id、order_id、sku_id、price、quantity。索引的话……给user_id建索引,因为要查用户的订单;给status建索引?好像也没啥用。数据量大就分库分表,比如用ShardingSphere,但我没用过,我们之前就是单表。”
王工:“那如果单表有5000万条数据,你怎么优化查询?”
谢飞机(挠头):“可以加缓存啊!用Redis存热点订单。或者就只查最近三个月的数据,把老数据归档到冷库。嗯,还可以用ES来做搜索……具体怎么搞我没做过。”
王工:“好。假设现在你有一个商品详情接口,QPS很高。你用Redis做缓存,那你怎么保证MySQL和Redis的数据一致性?”
谢飞机(自信满满):“这个我熟!我们之前是这么做的:先删缓存,再更新数据库。不对!应该是先更新数据库,再删缓存。还是先删缓存再更新数据库?嗯……我记得有个Cache Aside模式。先查缓存,没查到就读数据库,然后写回缓存。更新的时候……先更新数据库,再让缓存失效?不过这样可能有并发问题,就是更新过程中有请求读到旧数据。其实我们项目里就直接设置了Redis过期时间,比如10分钟,不一致就等过期呗。”
王工(表情复杂):“那如果缓存失效瞬间大量请求穿透到数据库,怎么办?”
谢飞机:“加锁!用分布式锁或者本地锁。或者用布隆过滤器,把有的数据先过滤一下。但是布隆过滤器有误判,不过没关系,反正是防止后面没有的请求打DB。”
王工(开始有了兴趣):“那你再说说,用户下单后,我们需要给其他系统发消息,比如通知物流、扣减库存。你如何保证消息不丢?”
谢飞机:“用Kafka!Kafka可以通过ack机制保证消息可靠,还有副本机制。我们当时就是生产者同步发送,然后消费者手动提交offset。但是……如果Kafka挂了怎么办?我就重启呗。消息重复消费就用幂等性,比如在数据库里加一个唯一键,或者用状态机判断。不过具体实现我没怎么写,是架构师搞的。”
王工(追问):“那如果订单超时未支付,你怎么做关单操作?”
谢飞机:“定时任务!每5分钟扫一次订单表,把超时的订单状态改成关闭。要是数据量太大,扫描太慢还可以分页扫。我们项目就这么干的。”
王工(皱眉):“如果订单量巨大,定时扫描能扛得住吗?有没有听过延迟消息、时间轮?”
谢飞机:“听过,RabbitMQ有死信队列,Kafka好像不支持延迟。RocketMQ有延迟消息。我们没用过,我们是直接扫表的。”
王工:“嗯,这个项目经验很有‘特色’。第二轮问题结束,我们进入第三轮。”
第三轮:微服务、安全、监控与CI/CD
王工:“你们系统用了Spring Cloud,你具体用过哪些组件?服务之间如何调用?”
谢飞机:“用过Eureka做注册中心,还有OpenFeign做远程调用,还有Spring Cloud Gateway?其实主要用Zuul。负载均衡用Ribbon。配置中心用Spring Cloud Config。不过现在不是都用Nacos和Sentinel吗?我就自己学习的时候看过一点。”
王工:“那你说说,如果某个服务响应特别慢,你怎么保证调用方不被拖垮?”
谢飞机(有点慌):“限流!降级!熔断!用Sentinel或者Hystrix。设置超时时间,如果调用失败就返回一个兜底的假数据。比如商品推荐服务挂了,就返回一个空的推荐列表。熔断就是如果失败率超过阈值,就断开所有请求,过一段时间再半开尝试。这个我懂,但具体配置……我一般直接配默认值。”
王工:“那如何保证服务的接口安全?比如一个下单接口,如何防止篡改和重放攻击?”
谢飞机:“用JWT!用户登录后返回一个token,然后前端每次请求带上,后端用拦截器校验token是否合法。JWT里有签名,防止篡改。重放攻击……嗯,可以用时间戳?但JWT默认没有这个机制。OAuth2是用来做授权的,但我不太清楚和JWT怎么配合。”
王工:“那密码你是怎么加密存储的?”
谢飞机:“用MD5。”
王工(眼睛盯着他):“MD5?加盐了吗?”
谢飞机:“加盐?哦对,加盐!就是随机字符串拼上去再MD5。不过好像现在推荐用BCrypt……对,Spring Security里有BCryptPasswordEncoder,但我没用过,我都是用MD5加盐。”
王工:“好。那你再说说,线上系统出现OOM,你怎么排查?”
谢飞机(紧张):“先看监控啊!用Prometheus和Grafana。如果有告警,就登录服务器,用jps看进程,然后jmap dump堆,再用MAT分析。不过……我一般直接重启,因为不会看MAT。”
王工:“那你对微服务的链路追踪了解吗?如果用户请求很慢,你怎么定位是哪个服务的问题?”
谢飞机:“用SkyWalking吧,或者Zipkin。它会生成一个TraceId贯穿所有服务,然后各个节点记录时间。但我看不太懂那些链路图,就知道哪个服务耗时最长,然后让它加索引或者加缓存。”
王工:“最后一个问题。你们从代码加到上线,自动化的流程是什么样的?怎么使用Docker和Kubernetes?”
谢飞机(仿佛抓住救命稻草):“哦,我们用GitLab CI!写.gitlab-ci.yml,里面定义构建和部署。先编译打包,然后构建Docker镜像,推到镜像仓库,再通过Kubernetes的YAML文件部署。但我Dockerfile只会写FROM openjdk:8,然后COPY jar包,CMD java -jar。Kubernetes的Pod、Service、Deployment我分得清,Deployment管理Pod,Service提供入口,但是我每次都是复制现成的YAML改一改,报错了就上网搜。”
王工(深呼一口气):“好,谢先生,你的回答让我对你的技术水平有了很全面的了解。我们这边大概已经清楚了。你先回去等通知吧。”
谢飞机:“等通知?是等offer吗?”
王工:“嗯……是等‘进一步的评估结果’。我们会在三个工作日内联系你。”
谢飞机走出会议室,心中暗喜:“今天的面试我基本全答上来了,应该稳了!” 王工在评分表写下一行字:“基础薄弱,概念混淆,生产经验严重不足,不通过。”
附录:面试问题深度解析(小白学习版)
下面针对面试官提出的每个问题,结合业务场景给出详细知识点,帮助你从“谢飞机”进化成“王工”。
第一轮:Java SE、JVM、构建工具
1. Java 8和Java 17的重要特性
- Java 8(2014年,革命性版本):
- Lambda表达式(
(x) -> x * 2)简化匿名内部类,配合函数式接口使用。 - Stream API:可以对集合进行声明式处理,如
list.stream().filter(x -> x > 0).map(x -> x * 2).collect(Collectors.toList()),支持并行流parallelStream。 - Optional类:避免空指针异常,用
Optional.ofNullable(...)包装可能为空的值。 - 新的日期时间API:
LocalDate、LocalTime、LocalDateTime、Duration、Period,线程安全且更直观。 - 接口默认方法和静态方法:用
default定义默认实现,不破坏实现类。 - ConcurrentHashMap从分段锁改为CAS + synchronized,性能提升。
- Lambda表达式(
- Java 17(2021年,LTS版本):
- 密封类
sealed:限制哪些类可以继承,如public sealed class Shape permits Circle, Square。 - 文本块
""" ... """:简化多行字符串,不需要写一堆\n。 - 增强的伪随机数生成器
RandomGenerator接口。 - 移除实验性的AOT/JIT编译器。
- 支持基于RISC-V的指令集架构。
- 密封类
- 实际项目中的使用:老项目多为Java 8,新项目可考虑17。Lambda和Stream能提高集合操作效率;日期API替代以前的
Date和Calendar。
2. JVM内存结构与垃圾回收器
- 运行时数据区:
- 堆(Heap):存放对象实例,是GC的主要区域。分为新生代(Eden、S0、S1)和老年代。对象先在Eden分配,Minor GC后存活对象进入S0/S1,经历过一定次数(默认15)后升入老年代。还有一些大对象直接进入老年代。
- 方法区(Method Area):存储类元信息、静态变量、常量。JDK 8后用元空间(Metaspace)代替永久代(PermGen),元空间使用本地内存,不受JVM堆大小限制,默认无上限,但可配置-XX:MaxMetaspaceSize。
- 虚拟机栈(VM Stack):每个线程私有的,存放栈帧,每个方法调用对应一个栈帧,内有局部变量表、操作数栈、动态链接、返回地址。栈深度溢出抛StackOverflowError。
- 本地方法栈:为native方法服务。
- 程序计数器:记录当前线程执行的字节码行号。
- 常见垃圾回收器:
- Serial GC:单线程,适合单核小堆。
- Parallel GC(JDK 8默认):多线程并行,注重吞吐量。
- CMS(Concurrent Mark Sweep):标记清除,并发收集,减少停顿,但会产生碎片,JDK 9后废弃,JDK 14移除。
- G1(JDK 9+默认):把堆分成大小相等的Region(默认2048个),每个Region可以是Eden、Survivor、Old、Humongous(大对象)。维护一个优先列表,跟踪回收价值最高的Region,兼顾停顿时间与吞吐量。可以通过
-XX:MaxGCPauseMillis设置暂停目标。 - ZGC(JDK 11引入,15转正):基于Region,使用染色指针和读屏障,停顿时间不超过10ms,适合超大堆(TB级)。
- 面试加分:能说出对象分配过程、GC Roots可达性分析、常见GC日志含义、如何调优(如调整新生代比例、晋升阈值)。
3. Maven与Gradle的区别、依赖管理机制
- 构建工具发展:Ant(纯脚本) -> Maven(约定优于配置) -> Gradle(又灵活又高效)。
- Maven:
- 使用
pom.xml,定义groupId、artifactId、version坐标。从中央仓库(或私服Nexus)下载依赖到本地仓库。 - 依赖仲裁规则:最短路径优先;声明顺序优先;提供dependencyManagement统一版本。
- 生命周期:clean、validate、compile、test、package、verify、install、deploy。
- 插件机制,如
maven-compiler-plugin指定Java版本。
- 使用
- Gradle:
- 使用Groovy或Kotlin DSL编写构建脚本,更灵活,支持增量构建、构建缓存、并行执行,速度通常比Maven快2-10倍。
- 依赖配置:
implementation(仅编译期依赖不传递)、api(传递)、compileOnly(不打包)、testImplementation等。 - 支持多项目构建,每个项目有
build.gradle。 - 常用命令:
./gradlew build、./gradlew test、./gradlew bootRun。
- 依赖冲突解决:优先使用Gradle版本冲突解决能力(选择最高版本);或用
resolutionStrategy强制指定;Maven中用<exclusions>排除,或使用dependencyManagement锁定版本。生产建议:用BOM(Bill of Materials)统一管理一组兼容版本,如Spring Boot BOM。
第二轮:电商场景下的数据库、缓存、消息队列
业务场景:用户下单 -> 写入订单数据 -> 扣减库存 -> 发消息给下游系统。关键难点是数据一致性、高并发、性能。
1. 订单表设计与索引
- 订单表(order)字段建议:
id(主键,可使用雪花ID,避免自增峰谷)order_no(业务订单号,唯一索引)user_id(用户ID,建立普通索引,用于查询用户订单)shop_id(店铺ID,多租户场景需要)total_amount(总金额,decimal,不要用float/double)status(订单状态:待支付、已支付、已发货、完成、关闭,可配合状态机)receiver_info(收件人信息,如果是ORM映射可将JSON用JSON格式字段)create_time、update_time(建立复合索引可按时间范围查询)version(乐观锁版本号,防止并发更新)
- 订单明细表(order_item):
id、order_id(外键逻辑关联,不建物理外键)、sku_id(商品规格ID)、product_name(冗余快照)、price(下单时价格)、quantity、promotion_discount。order_id必须建索引,或者使用联合分表键。
- 分库分表:
- 当单表数据量超过2000万,或容量超过100GB时,考虑分库分表。
- 分片策略:按
user_id(根据用户范围查询)、按order_no(全局唯一,分区均匀)或按时间(便于归档)。 - 常见组件:ShardingSphere(MySQL + Java,功能丰富)、MyCat(代理层)。
- 分库分表后带来的问题:全局主键(雪花算法)、跨库join(尽量通过冗余或聚合)、分布式事务(用Seata、TCC等)。
- 冷热分离:将已完成的订单归档到其他存储(如TiDB、MySQL历史库、ES),当前表只保留近几个月活跃数据。
2. Redis与数据库一致性
- Cache Aside(旁路缓存)模式:
- 读操作:先查Redis,没有则查DB,然后写回Redis,设置过期时间。
- 写操作:先更新DB,然后删除Redis中的缓存(注意:不是先删缓存再更新DB)。
- 为什么是删缓存而不是更新缓存?因为缓存更新成本高、可能被其他线程覆盖、热点数据可能不需要频繁更新。
- 一致性难点:
- 如果先更新DB再删缓存,可能在删缓存之前,旧缓存被其他请求读到?但概率非常低,因为删除操作很快。而先删缓存再更新DB,则另一个请求可能在DB更新前把旧数写回缓存,导致永久不一致。所以正确做法是“先更新DB,再删缓存”。
- 极端情况:删除缓存失败怎么办?用消息队列重试,或订阅数据库binlog(如Canal)异步删除。
- 缓存穿透:查询一个不存在的key,每次都会打到DB。解决方案:
- 布隆过滤器(Bloom Filter):将所有可能存在的数据hash到一个bitmap,查询时先判断key是否存在,不存在则直接返回。
- 缓存空值并设置短过期(如5分钟),防止恶意攻击。
- 接口参数校验,非法请求直接拦截。
- 缓存击穿:某个热点key失效瞬间大量请求同时访问DB。解决方案:
- 互斥锁(单机可用synchronized,分布式用Redisson的
tryLock),只有一个线程去查DB并回填缓存,其他线程等待后重试。 - 逻辑过期:不设置物理过期时间,而是存一个逻辑过期时间字段,异步刷新缓存。
- 互斥锁(单机可用synchronized,分布式用Redisson的
- 缓存雪崩:大量key同时失效导致DB压力过大。解决方案:
- 过期时间增加随机值(如基础时间 + 随机1-5分钟)。
- 部署Redis集群,主从+哨兵,防止单点故障。
- 多级缓存:本地Caffeine作为一级缓存,Redis作为二级缓存,减少各层压力。
3. Kafka消息可靠性
- 消息丢失场景与解决:
- 生产者发送失败:使用
producer.send(record, callback),开启acks=all,等待所有副本确认。设置retries重试。 - Broker端丢失:配置
replication.factor(副本数)大于1,min.insync.replicas(至少有多少副本同步成功才算写入成功)设为2,避免Leader宕机丢数据。 - 消费者端丢失:关闭自动提交
enable.auto.commit=false,业务处理成功后再手动提交offset。
- 生产者发送失败:使用
- 消息重复消费:消费者处理完业务后,在提交offset前宕机,重启后重复消费。解决方案是幂等性:
- 订单处理:在数据库表中增加唯一键(如
order_id + event_type),插入时用INSERT ... ON DUPLICATE KEY UPDATE或先select判断。 - 状态机:只允许合法状态流转,重复消息直接忽略。
- Redis分布式锁:处理前加锁,处理完释放。
- 订单处理:在数据库表中增加唯一键(如
- 消息积压:消费者消费能力不足导致消息堆积。解决方案:
- 增加消费者实例(消费者数小于分区数无意义,需要同时增加分区数)。
- 优化消费者逻辑,将耗时操作异步化。
- 在线扩容:临时创建新的topic,将积压消息转发到多个分区,多消费者处理。
4. 订单超时关单方案
- 定时任务扫描(简单但延迟高,DB压力大):
- 每秒或每5分钟扫描订单表中
create_time < now()-30min and status='待支付',批量变更状态。 - 适合小规模系统,但大规模下不推荐。
- 每秒或每5分钟扫描订单表中
- 延迟消息:
- RabbitMQ:使用死信队列(DLX),消息先发送到一条没有消费者的队列,设置过期时间(如30分钟),过期后转发到真正的消费队列,由消费者执行关单。
- RabbitMQ 3.9支持延迟消息插件
rabbitmq_delayed_message_exchange,直接设置x-delay。 - RocketMQ:原生支持定时消息(延迟级别,如1s、5s、10s、30s、1m等,最多延迟2h),发送时设置
setDelayTimeLevel,服务端到期投递给消费者。 - Kafka:没有延迟消息,但可以用时间轮(Netty HashedWheelTimer)或通过存储到KV并扫描。
- Redis过期监听:利用Redis的key过期事件(需开启notify-keyspace-events Ex),下单时设置一个
order:xxxkey,过期后通知回调触发关单。但Redis事件可能丢失,且过期后不一定立即触发,不适合强一致业务。 - 综合方案:对于中小项目,建议使用RocketMQ延迟消息或RabbitMQ插件,保证可靠性。大规模场景使用时间轮 + 数据库状态机补偿。
第三轮:微服务、安全、监控与CI/CD
1. Spring Cloud注册中心与服务调用
- 注册中心:Eureka(2.0停止维护)、Consul、Nacos(阿里开源,国内常用),负责服务注册与发现。
- 服务启动时向注册中心注册自己的IP+端口,定时发送心跳。
- 消费者从注册中心拉取服务列表,然后通过客户端负载均衡(Ribbon/Spring Cloud LoadBalancer)选择一个服务实例发起调用。
- 服务调用:OpenFeign(声明式HTTP客户端),定义接口加
@FeignClient("user-service"),然后像调用本地方法一样调用远程HTTP接口。Feign底层使用Ribbon做负载均衡,可以配置超时、重试、降级(fallback)。 - 网关:Spring Cloud Gateway(基于WebFlux,非阻塞)或Zuul(同步阻塞)。网关负责路由、鉴权、限流、日志等。
- 配置中心:Spring Cloud Config、Nacos Config、Apollo。配置动态刷新,不需要重启服务。
- 架构上要注意:微服务越多,链路越复杂,需要引入可观测性(日志、指标、链路追踪)。
2. 熔断、降级、限流
- 为什么需要:一个服务慢或失败会级联传播,比如A调用B,B调用C,C挂了导致B线程池占满,间接拖垮A。
- 熔断(Circuit Breaker):
- 类似保险丝,熔断器有三种状态:关闭(正常)、打开(直接失败)、半开(尝试放少量请求过去,成功率恢复则关闭)。
- 常用工具:Hystrix(已停止开发)、Resilience4j(轻量,支持Spring Boot 2)、Sentinel(阿里,功能强大)。
- 配置要点:失败率阈值(如50%)、滑动窗口大小、熔断超时时间、半开最大请求数。
- 降级(Fallback):
- 出口:当接口失败时,返回默认值或缓存数据。例如商品库存服务不可用,返回“库存充足”让用户下单继续,后续通过异步补偿。
- 需要注意降级策略要符合业务,不能误导用户。
- 限流(Rate Limiting):
- 控制QPS,防止突发流量打爆系统。常见算法:令牌桶(Guava RateLimiter、Sentinel)、漏桶、滑动窗口。
- 应用层面:如果QPS超过1000,就拒绝多余的请求,返回“系统繁忙,请稍后再试”。
- 配置层面:Nginx或网关层限流,如Spring Cloud Gateway的RequestRateLimiter过滤器。
3. 安全防护:JWT、OAuth2、密码加密
- JWT(JSON Web Token):
- 结构:Header.Payload.Signature。Payload中可承载用户id、角色、过期时间
exp、签发时间iat。 - 签名算法:HMAC(对称密钥)、RSA/ECDSA(非对称)。服务端验证签名确保token未被篡改。
- 无状态:服务端不保存session,方便水平扩展。
- 问题:token无法做到主动失效(如果被窃取则无法撤销)。改进:使用短有效期 + Redis黑名单,或使用Refresh Token。
- 结构:Header.Payload.Signature。Payload中可承载用户id、角色、过期时间
- OAuth2:
- 是一种授权框架,用于第三方应用访问用户资源。常见角色:资源所有者(用户)、客户端(第三方应用)、授权服务器、资源服务器。
- 授权模式:授权码模式(最完整,用于Web应用)、简化模式(SPA)、密码模式(信任客户端)、客户端凭证模式(服务间调用)。
- 与JWT的关系:OAuth2是规范,JWT是token格式。授权服务器签发的access_token可以是一个JWT。Spring Security OAuth2(新项目用Spring Authorization Server)实现。
- 密码加密存储:
- 绝对不能用MD5/SHA-1直接存储,因为彩虹表可以破解。加盐也不够,因为计算速度太快。
- 推荐BCrypt(
BCryptPasswordEncoder)或SCrypt、Argon2。BCrypt内置随机盐,密码串形如$2a$10$...,算法本身设计为慢hash,暴力破解成本高。 - 验证方式:用户输入密码,使用同一算法对密码进行hash并与存储值比对。
- 防重放攻击:
- 使用HTTPS/TLS确保传输安全。
- 幂等键:客户端发送请求时带一个唯一的requestId,服务端记录,再次收到相同requestId直接拒绝。
- 时间戳+nonce:请求头带时间戳(如±5分钟有效)和随机数,服务端缓存nonce,超过时间或重复的nonce拒绝。
- 其他安全:参数签名(防止篡改)、短信接口防刷(验证码、限制频率)、SQL注入防护(预编译)、XSS防护(HTML转义)。
4. OOM排查与监控
- 监控系统基础:
- Prometheus:时序数据库,拉取指标。Spring Boot集成Micrometer,暴露
/actuator/prometheus端点。 - Grafana:可视化仪表板,展示CPU、内存、GC、QPS等。
- 告警:Alertmanager 根据规则发Slack、邮件、钉钉。
- Prometheus:时序数据库,拉取指标。Spring Boot集成Micrometer,暴露
- OOM排查步骤:
- 确认OOM类型:
java.lang.OutOfMemoryError: Java heap space(堆溢出)、Metaspace(元空间溢出)、Unable to create new native thread(无法创建线程)。 - JVM启动时加上
-XX:+HeapDumpOnOutOfMemoryError,让JVM在OOM时自动生成heap dump文件(如java_pid1234.hprof)。 - 使用
jmap -heap <pid>查看堆使用情况,jstat -gcutil <pid> <interval>查看GC情况。 - 用MAT(Memory Analyzer Tool)或VisualVM分析hprof文件,找出内存泄漏对象。通常可以看到一个类持有了大量对象,比如往一个静态Map里放数据没有清理。
- 定位泄漏原因:如连接未关闭、Listener未移除、ThreadLocal使用不当、大集合缓存不设置过期。
- 线上紧急处理:先重启保命,再分析dump。
- 确认OOM类型:
5. 链路追踪
- 核心概念:
- TraceId:一次外部请求进入系统时生成一个全局唯一ID,贯穿所有调用链路。
- SpanId:记录一次内部调用,如一个HTTP调用、一次DB操作,有父子关系。
- 数据采集方式:在HTTP请求Header中传递
X-B3-TraceId、X-B3-SpanId等。
- 常用组件:
- Zipkin:Twitter开源的分布式追踪系统。
- Jaeger:CNCF开源,支持OpenTracing。
- SkyWalking:国产,基于字节码注入,应用无侵入,常用于Java/Kotlin等。
- 集成:Spring Cloud Sleuth(已废弃)或Micrometer Tracing,把TraceId和SpanId自动注入日志及Zipkin。调用链数据中包含每个服务的耗时,可以快速定位慢服务。
- 业务场景:用户反馈“下单很慢”,通过链路追踪看到“用户服务 800ms -> 库存服务 5s -> 支付服务 200ms”,进一步通过内存性能剖析找到库存服务慢的SQL。
6. CI/CD与容器化部署
- CI(持续集成):代码提交后自动编译、测试、静态检查。常见工具:Jenkins、GitLab CI、GitHub Actions。
- GitLab CI:在项目中写
.gitlab-ci.yml,定义stages(build, test, deploy),用runner执行。 - 例子:
stages: - build - deploy build: stage: build script: - mvn clean package artifacts: paths: [target/*.jar] deploy: stage: deploy script: - docker build -t myapp:$CI_COMMIT_SHA . - docker push myregistry/myapp:$CI_COMMIT_SHA - kubectl set image deployment/myapp myapp=myregistry/myapp:$CI_COMMIT_SHA - GitHub Actions:用
workflows/*.yml,类似。
- GitLab CI:在项目中写
- Docker:
- 一个镜像的Dockerfile示例:
FROM eclipse-temurin:17-jdk ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","/app.jar"] - 分层精简:把依赖层提前复制,利用缓存;多阶段构建(先编译再用精简JRE运行)。
- 注意事项:容器内不要用
apt-get装一堆东西,镜像要小;以非root用户运行降低安全风险。
- 一个镜像的Dockerfile示例:
- Kubernetes:
- 核心对象:
- Pod:最小运行单元,包含一个或多个容器。
- Deployment:管理Pod副本,滚动更新、回滚。
- Service:提供稳定访问入口,通过Label Selector关联Pod,ClusterIP在集群内访问,NodePort对外暴露。
- ConfigMap:配置,不在镜像中。
- 部署流程:
kubectl apply -f deployment.yaml-> 创建ReplicaSet -> 启动Pod -> Service转发。 - 常用命令:
kubectl get pods、kubectl logs pod-name、kubectl exec -it pod-name sh、kubectl describe pod。
- 核心对象:
- 完整DevOps流程:开发提交代码 -> CI触发构建、单元测试、扫描 -> 构建Docker镜像并推送 -> CD更新K8s Deployment -> 自动化集成测试 -> 监控告警。
写在最后
谢飞机的面试表现虽然搞笑,但反映了很多“前端搬砖型程序员”的通病:把“能运行”当成“会”,把“听说过”当成“精通”。真正的技术能力,是能在架构设计中做出合理决策、在故障排查中游刃有余、在业务场景中灵活应用。
如果你想应对大厂Java面试,建议:
- 深入理解JVM内存模型、GC调优、类加载机制。
- 掌握Spring Boot/Spring Cloud核心原理,如自动配置、启动过程、负载均衡算法。
- 熟悉Redis、Kafka等高并发中间件的使用场景以及可靠性痛点。
- 动手搭建一个完整的电商或社区微服务项目,记录遇到的实际问题。
- 阅读源码,关注开源社区,多看官方文档和权威博客。
记住:面试官问的不是“你有没有用过”,而是“你是否真的懂”。像谢飞机一样背概念,不如认真剖析一个案例。祝你面试顺利,拿到心仪的Offer!
