Java实习模拟面试实录:字节跳动后端二面全复盘(核心聚焦Java基础+并发+Spring+MyBatis+JVM)
关键词:字节跳动面试、Java基础、线程池、ThreadLocal、MyBatis、Spring Bean、JVM垃圾回收、实习面试复盘
引言
今天参加了字节跳动后端开发实习生岗位的第二轮技术面试,整体时长约40分钟。面试官提前进入会议,态度非常友好,题目以扎实的Java基础 + 框架原理 + 简单系统设计为主,难度适中但覆盖全面。令人惊喜的是,1小时后HR就通知进入三面!
本文将通过**“面试官提问 + 候选人回答”** 的对话形式,完整还原这场面试,并结合专业知识进行解析,帮助大家掌握字节这类大厂对Java实习生的核心考察点。
一、自我介绍与竞赛经历
面试官提问:
请做一个简短的自我介绍。
候选人回答:
您好!我是XX大学计算机专业的大三学生,主攻Java后端方向。在校期间系统学习了数据结构、操作系统、计算机网络和数据库等基础课程,GPA 3.7/4.0。
曾获蓝桥杯Java组省一等奖,并参与过两个Spring Boot项目开发,包括一个校园二手交易平台和一个简易任务调度系统。目前正在深入学习JVM、并发编程和分布式系统,希望能加入字节这样技术驱动的团队实习成长。
面试官追问:
蓝桥杯省一是用Java写的吗?
候选人回答:
是的,全程使用Java 语言完成所有算法题。比赛中主要用到了ArrayList、HashMap、优先队列(PriorityQueue)以及递归回溯等技巧。比如有一道图论题,我用邻接表+DFS实现,另一道动态规划题用了滚动数组优化空间。
二、Java核心基础:字符串、重载重写、接口演进
面试官提问:
说说
String、StringBuffer和StringBuilder的区别?
候选人回答:
这三者都是用于处理字符串的类,但有本质区别:
String:不可变(immutable),每次拼接都会创建新对象,适合少量字符串操作;StringBuffer:可变、线程安全,内部方法加了synchronized,适合多线程环境下的频繁拼接;StringBuilder:可变、非线程安全,性能比StringBuffer高,单线程推荐使用。
实际开发中,我们通常用
StringBuilder构建日志或SQL语句,避免String的内存浪费。
面试官提问:
方法重载(Overload)和重写(Override)有什么区别?
候选人回答:
这是两个完全不同的概念:
- 重载(Overload):发生在同一个类中,方法名相同但参数列表不同(类型、个数、顺序),返回值和访问修饰符可以不同。属于编译时多态。
- 重写(Override):发生在父子类之间,子类重新定义父类的非私有方法,要求方法名、参数列表、返回类型完全一致(协变返回除外),访问权限不能更严格。属于运行时多态。
比如
println(int)和println(String)是重载;Object.toString()被User.toString()重写就是 Override。
面试官追问:
哪些方法不能被重写?
候选人回答:
以下三类方法不能被重写:
private方法:子类不可见;final方法:明确禁止重写;static方法:属于类而非实例,不存在多态,子类只能“隐藏”它,不算重写。
另外,构造方法也不能被重写,因为它不属于普通方法。
三、函数式编程与接口演进(JDK8+)
面试官提问:
什么是函数式接口?常见的有哪些?
候选人回答:
函数式接口(Functional Interface)是指只包含一个抽象方法的接口,可以用@FunctionalInterface注解标记(非必须,但建议)。它是Java支持Lambda表达式的基础。
常见内置函数式接口包括:
Function<T, R>:接受一个参数,返回一个结果(如map操作);Consumer<T>:接受一个参数,无返回(如forEach);Supplier<T>:无参,返回一个结果(如工厂方法);Predicate<T>:接受一个参数,返回 boolean(如过滤条件)。
例如:
list.stream().filter(x -> x > 0).map(x -> x * 2)中就用到了Predicate和Function。
面试官追问:
JDK8之后,接口的定义发生了哪些变化?
候选人回答:
JDK8 对接口做了两大重要扩展:
- 允许定义默认方法(
default方法):提供默认实现,子类可选择是否重写,解决接口升级的兼容性问题; - 允许定义静态方法(
static方法):直接通过接口名调用,常用于工具方法。
JDK9 还增加了私有方法(
private),用于 default 方法之间的代码复用。
四、并发编程:异步、线程池、ThreadLocal
面试官提问:
Java中实现异步线程有哪些方式?
候选人回答:
主要有以下几种:
- 继承
Thread类(不推荐,Java单继承限制); - 实现
Runnable接口(常用); - 实现
Callable<V>+FutureTask:可获取返回值和异常; - 使用线程池(
ExecutorService):生产环境首选; - CompletableFuture:JDK8引入,支持链式异步编程;
- Spring 的
@Async:基于代理的异步方法。
我们项目中主要用
ThreadPoolExecutor自定义线程池 +CompletableFuture处理组合任务。
面试官深入追问:
在生产环境中,如何让主线程和子线程共享同一个请求日志ID,实现日志串联?怎么通信?
候选人回答:
这是一个典型的上下文传递问题。我们使用ThreadLocal来实现。
具体做法:
- 在请求入口(如 Filter 或 Interceptor)生成唯一
traceId; - 存入
ThreadLocal<String>; - 在异步任务提交前,手动将
traceId从主线程传递给子线程(因为ThreadLocal不跨线程); - 子线程在执行前 set 到自己的
ThreadLocal中; - 日志框架(如 Logback)通过 MDC(Mapped Diagnostic Context)自动输出
traceId。
注意:如果用线程池,必须使用
InheritableThreadLocal或阿里开源的 TransmittableThreadLocal(TTL)才能自动传递上下文。
面试官继续问:
ThreadLocal的实现原理是什么?
候选人回答:ThreadLocal并不是把变量存在自己里面,而是每个线程内部持有一个ThreadLocalMap,这个 Map 的 key 是ThreadLocal实例,value 是你要存储的值。
- 调用
set(value)时,实际上是Thread.currentThread().threadLocals.set(this, value); get()同理,从当前线程的 map 中取;- 内存泄漏风险:如果
ThreadLocal没有 remove,而线程长期存活(如线程池),会导致 value 无法回收。所以务必在 finally 块中调用remove()。
五、动态代理与MyBatis核心
面试官提问:
说说 Java 动态代理?应用场景?
候选人回答:
Java 动态代理分为两类:
- JDK 动态代理:基于接口,通过
Proxy.newProxyInstance()生成代理类,核心是InvocationHandler; - CGLIB 代理:基于继承,通过字节码生成子类,可代理无接口的类。
应用场景:
- Spring AOP(默认JDK代理,无接口时用CGLIB);
- MyBatis Mapper 接口的实现;
- RPC 框架中的 stub 生成;
- 事务控制、日志埋点等横切逻辑。
例如,MyBatis 的
UserMapper.selectById()实际是由 JDK 动态代理生成的实现类调用SqlSession执行 SQL。
面试官问:
MyBatis 中
#{}和${}有什么区别?
候选人回答:
这是 MyBatis 最经典的考点:
#{}:预编译占位符,会转为?,由 JDBCPreparedStatement设置值,防止 SQL 注入,推荐使用;${}:字符串替换,直接拼接到 SQL 中,有注入风险,仅用于动态表名、列名等无法预编译的场景。
例如:
SELECT * FROM user WHERE id = #{id}→ 安全;SELECT * FROM ${tableName}→ 危险,需严格校验tableName白名单。
面试官追问:
@Param注解的作用是什么?
候选人回答:
当 Mapper 方法有多个参数时,MyBatis 无法自动映射参数名(Java 编译后泛型擦除),需要用@Param("name")显式指定。
Userselect(@Param("id")Longid,@Param("status")Stringstatus);XML 中就可以用#{id}和#{status}。
若只有一个参数,可省略;如果是对象或 Map,也不需要。
面试官再问:
MyBatis 如何实现分表查询?
候选人回答:
分表通常有两种思路:
- 应用层路由:根据业务字段(如用户ID)计算表名,在 Service 层动态传入表名,XML 中用
${tableName}(注意安全校验); - ShardingSphere 等中间件:透明化分库分表,MyBatis 无需感知。
我们课程项目用第一种:
user_0,user_1, …, 根据userId % 4选择表,Mapper 方法加@Param("table"),XML 中SELECT * FROM ${table} WHERE id = #{id}。
六、Spring 核心:Bean、注解、生命周期
面试官提问:
Spring 中常用注解有哪些?
候选人回答:
按功能分类:
- 组件注册:
@Component,@Service,@Repository,@Controller - 依赖注入:
@Autowired,@Resource,@Qualifier - 配置类:
@Configuration,@Bean - Web:
@RestController,@RequestMapping,@PathVariable - AOP:
@Aspect,@Around - 生命周期:
@PostConstruct,@PreDestroy
面试官追问:
@Autowired可以用在哪些地方?
候选人回答:@Autowired可用于:
- 字段(Field):最常见,但破坏封装;
- Setter 方法:符合 JavaBean 规范;
- 构造函数:推荐方式,保证不可变性和依赖完整性;
- 任意方法(不常见)。
Spring 4.3+ 后,单构造函数可省略
@Autowired。
面试官问:
Spring 注册 Bean 的方式有哪些?
候选人回答:
主要有四种:
- 注解方式:
@Component及其衍生注解 +@ComponentScan; - Java Config:
@Configuration+@Bean方法; - XML 配置:
<bean class="..."/>(已少用); - Import / FactoryBean / Registrar:高级扩展方式。
面试官深入问:
如果想在 Bean 属性注入完成后执行一段逻辑,怎么做?
候选人回答:
有三种主流方式:
@PostConstruct:标注在方法上,Bean 初始化后自动调用(JSR-250标准);- 实现
InitializingBean接口:重写afterPropertiesSet(); ApplicationRunner/CommandLineRunner:整个 Spring Boot 应用启动完成后执行,适合全局初始化。
例如:缓存预热、连接池初始化,我会用
@PostConstruct。
七、JVM 垃圾回收:算法与回收器
面试官提问:
垃圾回收算法有哪些?
候选人回答:
主流算法:
- 标记-清除(Mark-Sweep):先标记存活对象,再清除死亡对象,会产生内存碎片;
- 复制(Copying):将内存分为两块,存活对象复制到另一块,无碎片但浪费空间,用于新生代;
- 标记-整理(Mark-Compact):标记后将存活对象向一端移动,无碎片,用于老年代;
- 分代收集(Generational):结合上述,按对象年龄分区处理。
面试官问:
主流的垃圾回收器有哪些?
候选人回答:
- Serial / Serial Old:单线程,适合客户端;
- ParNew:Serial 的多线程版,配合 CMS;
- Parallel Scavenge / Parallel Old:吞吐量优先,适合后台计算;
- CMS(Concurrent Mark Sweep):低停顿,但已废弃;
- G1:JDK9+ 默认,兼顾吞吐与停顿;
- ZGC / Shenandoah:超低停顿(<10ms),JDK11+。
面试官追问:
CMS 的原理是什么?
候选人回答:
CMS(Concurrent Mark Sweep)目标是最小化停顿时间,适用于 Web 应用。它分为四个阶段:
- 初始标记(STW):标记 GC Roots 直接关联对象,快;
- 并发标记:遍历整个对象图,与用户线程并发;
- 重新标记(STW):修正并发标记期间变动的对象;
- 并发清除:清除死亡对象,与用户线程并发。
缺点:CPU 敏感、浮动垃圾、内存碎片。JDK14 已移除。
八、算法 & 软性问题
面试官提问:
(现场 coding)实现一个二叉树的层序遍历。
候选人回答:
(快速写出 BFS + Queue 实现,略)
面试官问:
期望 base 哪里?能实习多久?
候选人回答:
base 希望在北京或上海,实习时间可保证6个月以上,每周5天,学校已协调好课程安排。
候选人反问环节:
Q1:请问后续还有几轮面试?主要考察什么?
面试官答:还有一轮交叉面(三面),侧重系统设计和编码深度。
Q2:团队主要做什么业务?
面试官答:我们负责字节内部的基础设施平台,比如任务调度、配置中心、可观测性系统,技术栈以 Java + Go 为主,大量使用 K8s 和自研中间件。
结语
字节二面给我最大的感受是:基础要牢,原理要清,表达要简。虽然题目不难,但每个问题都可能被连环追问,考验知识体系的完整性。
特别提醒:不要死记硬背,要用“工程师的语言”解释技术。比如讲 ThreadLocal 时,不仅要说出“每个线程有自己的副本”,还要点出“内存泄漏风险”和“线程池下的传递问题”。
希望这篇复盘能助你拿下心仪 offer!下一站,三面!
欢迎关注我的 CSDN 主页,持续更新大厂面试真题解析、Java 深度系列、实习避坑指南!
本文为模拟面试复盘,内容基于公开技术知识整理,仅供参考学习。