# 腾讯天美后台开发一面面经
# java编译和运行流程是什么?
- Java 源文件先经过
javac编译成class字节码文件,这一步会经历词法分析、语法分析、语义分析和字节码生成。 - 程序运行时,JVM 会通过类加载机制把
class文件加载进内存,经历加载、验证、准备、解析、初始化几个阶段,生成运行时需要的类元信息。真正执行时,JVM 的执行引擎会先通过解释器逐条解释字节码,对于热点代码再交给 JIT 编译成本地机器码执行,因此Java 既有跨平台能力,也能兼顾运行效率。
# JIT了解吗?
- JIT 就是即时编译器,会把运行过程中频繁执行的热点字节码编译成机器码,避免同一段代码反复解释执行。
- 它的好处是程序运行一段时间后性能会明显提升,尤其是热点循环、频繁调用的方法,JIT 还会做内联、逃逸分析、标量替换、锁消除这类优化。但是在启动初期仍然要解释执行,而且编译本身也有开销,所以 Java 程序通常有一个“预热”过程。
# 常见的GC算法有哪些?
常见 GC 算法主要有标记-清除、复制、标记-整理。
标记-清除算法会先标记存活对象,再回收未标记对象,优点是实现简单,缺点是容易产生内存碎片。
复制算法把内存分成两块,每次只用一块,回收时把存活对象复制到另一块,优点是效率高,缺点是空间利用率低,比较适合新生代。
标记-整理会在标记存活对象后,把存活对象向一端移动,再清理边界外的内存,优点是没有碎片,适合老年代。
分代收集是让新生代、老年代根据各自特点采用不同算法。

# 介绍一下G1垃圾收集器?
- G1 是面向大堆内存和低停顿场景设计的垃圾收集器,它把整个堆拆成很多大小相等的 Region,不再要求新生代和老年代必须是物理上连续的一整块。它维护了一个优先级列表,优先回收“垃圾最多、回收价值最高”的 Region,目标是尽量在用户设定的停顿时间内完成回收。
- G1 的核心过程一般包括初始标记、并发标记、最终标记、筛选回收,其中很多阶段可以和用户线程并发执行。回收时会做对象复制和整理,所以相比 CMS 更不容易产生内存碎片,线上更容易控制 Full GC 风险。

# 新生代的结构组成了解吗?
- 新生代通常由 Eden 区 + 两个 Survivor 区组成,也就是From区和To区。
- 大多数新对象会优先分配到 Eden 区,当 Eden 满了会触发一次 Minor GC。Minor GC 后仍然存活的对象,会从 Eden 和 From 区复制到 To 区,下一次 GC 时 From 和 To 会互换角色。
- 这三个区域的默认常见比例是 8:1:1,也就是 Eden 比两个 Survivor 区大很多,因为大多数对象都bu的。
# eden区、survive区和老年代分别用的什么回收算法?
- Eden 和 Survivor 区主要使用复制算法,因为新生代对象存活率低,复制成本相对小,回收效率高。
- 老年代对象存活率更高,如果还用复制算法,额外预留的空间成本太大,所以通常采用标记-清除或者标记-整理。
# 介绍一下装箱和拆箱?
装箱就是把基本类型转换成对应的包装类型,比如
int -> Integer;拆箱就是把包装类型转换回基本类型,比如Integer -> int。Java的自动装箱和自动拆箱是将
int放进集合时会自动装箱,从Integer参与算术运算时会自动拆箱。使用时要注意包装类型可能为
null,自动拆箱时会触发 NPE;还有频繁装箱拆箱会带来额外对象创建和性能开销。另外像
Integer的==比较时,小范围整数会走缓存,导致比较结果为不相等。
# 哪些常见的类用到了适配器模式?
- 比如Java 中的 InputStreamReader 和 OutputStreamWriter,它们把字节流适配成字符流。
- Spring MVC 里的 HandlerAdapter 也是典型适配器模式,它把不同风格的 Controller 适配成
DispatcherServlet能统一调用的形式。 - Spring AOP 里也有适配器思想,不同通知类型最终会被适配成统一的拦截器调用链。
# 红黑树特点?
- 红黑树本质上是自平衡二叉搜索树,它不追求绝对平衡,但能保证树的高度始终维持在对数级别。
- 红黑树的特点是根节点是黑色、红色节点不能相邻、任意节点到叶子节点路径上的黑色节点数相同。因此红黑树的最长路径不会超过最短路径的 2 倍,所以查找、插入、删除复杂度都能稳定在 O(logN) 。
- 相比 AVL 树,红黑树旋转和调整次数更少,所以更适合插入删除频繁的工程场景,像
TreeMap、TreeSet、HashMap树化桶都会用到它。
# B树和B+树的区别是什么?
B树在叶子节点和非叶子节点都存储数据,每个节点存储的索引值少,树的深度大,且叶子节点之间没有关联,进行范围查询时需要进行回溯,效率比较低。
B+树只在叶子节点存储数据,在非叶子节点存储索引,且叶子节点之间使用双向链表连接,支持范围查询,并且左右子树高度差不超过1,能够保证查询效率稳定。
所以MySQL索引的核心数据结构是B+树,是适配磁盘存储的多路平衡查找树。
# hashmap底层结构?
- JDK1.8 里的 HashMap 底层是数组 + 链表 / 红黑树。
put元素时,先根据 key 的hashCode()做扰动运算,再和数组长度取模定位桶位;如果桶为空就直接插入,如果冲突就挂到链表或者红黑树里。- 如果链表长度达到树化阈值 8,并且数组容量达到最小树化容量 64时,链表会转换成红黑树,把最坏时间复杂度从 O(N) 优化到 O(logN) 。
- 当元素数量超过扩容阈值时,HashMap 会进行2 倍扩容,重新分配元素位置。
# tcp三次握手、四次挥手过程是什么?
三次握手的过程是:客户端发送
SYN,客户端进入SYN_SENT状态;服务端回复SYN + ACK后服务器进入SYN_RCVD状态,客户端再回复ACK,随后客户端进入ESTABLISHED状态,服务器收到ACK后也进入ESTABLISHED状态,这样双方都确认了收发能力正常,连接建立。
四次挥手的过程是:主动关闭方发送
FIN,被动关闭方回复ACK;等被动关闭方数据发送完,再发送自己的FIN,最后主动关闭方回复ACK,连接关闭。挥手需要四次,本质上是因为 TCP 是全双工协议,双方关闭发送通道通常要分开确认。
主动关闭方最后会进入 TIME_WAIT,主要是为了保证最后一个 ACK 能被对方收到,同时避免旧连接报文影响新连接。

# 发送方的滑动窗口机制了解吗?
- 滑动窗口本质上是 TCP 做流量控制和提升传输效率的一种机制,发送方不需要每发一个字节就等一次确认,而是可以在窗口范围内连续发送多个数据段。
- 发送方会维护一个“已发送但未确认”的窗口,收到接收方 ACK 后,窗口向前滑动,继续发送后续数据。
- 接收方还会通过通告窗口大小告诉发送方自己当前还能接收多少数据,避免发送过快把对方缓冲区打满。
- 配合超时重传、快速重传等机制,TCP 可以在保证可靠性的同时提高链路利用率。
# redis持久化机制是什么?
- Redis 主要有两种持久化方式:RDB 和 AOF,现在也支持混合持久化。实际生产中常见做法是 RDB 和 AOF 配合使用,兼顾恢复速度和数据安全。
- RDB是把某个时刻的内存快照持久化到磁盘,优点是文件紧凑、恢复快,缺点是两次快照之间如果 Redis 宕机,可能会丢一段时间的数据。
- AOF是把写命令按追加日志的方式记录下来,恢复时重放命令,优点是数据更完整,缺点是文件更大、恢复速度通常比 RDB 慢。
- Redis 还可以通过
appendfsync配置刷盘策略,在性能和数据安全之间做平衡;AOF 文件过大时还会进行重写。

# 介绍一下mysql的索引?
- MySQL InnoDB 最常用的索引结构是B+ 树,主键索引是聚簇索引,叶子节点直接存整行数据;普通索引一般是二级索引,叶子节点存主键值。
- 通过二级索引查到主键后,再去聚簇索引取整行数据,这一步就叫回表;如果查询字段都在索引里,就可以走覆盖索引避免回表。
- 联合索引要注意最左前缀原则,查询条件要尽量从索引最左列开始匹配。
- 工程上建立索引不是越多越好,因为索引会占空间,也会增加插入、更新、删除时的维护成本。
# 乐观锁有哪些?
- 乐观锁的核心思想是先假设冲突少,更新时再校验数据有没有被别人改过。
- 最常见的实现是版本号机制,更新时带上旧版本号,比如
where id = ? and version = ?,更新成功后把版本号加一。 - 另一类常见实现是 CAS,也就是比较并交换,JUC 里的很多原子类底层就是这个思路。
- 乐观锁适合读多写少、冲突不高的场景;如果冲突很频繁,重试成本会比较高。
# 常用哪些linux指令?
- 日常排查我常用
ls、pwd、cd、cat、less、tail -f、head这些查看文件和日志的命令。 - 查进程和资源会用
ps、top、htop、free -h、df -h、du -sh、vmstat。 - 查文本会用
grep、awk、sed、find,定位端口和连接常用netstat或ss。 - 如果是 Java 服务线上排查,还会结合
jps、jstack、jmap、jstat这类命令一起用。
# es结构是什么?
- Elasticsearch 从逻辑上看,核心概念有 Cluster、Node、Index、Document;一个索引会被拆成多个 Shard,每个分片还可以有 Replica 做高可用。
- 从底层存储看,ES 建立的是倒排索引,会把“词项 -> 文档”的映射维护起来,所以特别适合全文检索。
- 一个分片底层又会包含多个 Segment,写入的数据先进入内存缓冲区,刷新后生成新 segment,查询时会同时查多个 segment,后续再通过 merge 合并。
# 缓存击穿、穿透、雪崩的区别

- 缓存穿透是查询一个本来就不存在的数据,请求既打不到缓存,也查不到数据库,结果每次都落到数据库。
- 缓存击穿是某个热点 key 失效瞬间,大量并发请求同时打到数据库,重点在“单个热点 key”。
- 缓存雪崩是大量 key 在同一时间失效,或者 Redis 整体故障,导致大量请求同时压向数据库,重点在“大面积失效”。
- 穿透常见解决方式是参数校验、缓存空值、布隆过滤器;击穿常见方案是互斥锁、热点数据不过期、后台异步刷新;雪崩则通常通过过期时间打散、集群高可用、多级缓存、限流熔断来兜底。
# docker用过吗?怎么查询每个docker部署的数量
- 用过,平时主要用 Docker 做服务打包、环境隔离和快速部署,像开发环境本地起 MySQL、Redis、Nginx 也会直接用 Docker。
- 查看当前运行中的容器可以用
docker ps,如果要看所有容器可以用docker ps -a。 - 如果想统计每个镜像部署了多少个容器,可以这样查:
docker ps --format "{{.Image}}" | sort | uniq -c,这样就能按镜像维度统计运行数量。 - 如果是想看某个服务的容器实例数,也可以结合
docker ps --filter "ancestor=镜像名"来筛选。
# JVM调参需要注意什么?
- JVM 调参不能脱离业务场景和监控数据,不能一上来就机械地调参数,通常要先看GC 日志、堆使用率、Full GC 次数、停顿时间、吞吐量、对象晋升情况。
- 常见基础参数有
-Xms、-Xmx、-Xmn、-XX:NewRatio、-XX:SurvivorRatio、-XX:MaxMetaspaceSize等,一般会把-Xms和-Xmx设成一样,减少运行时动态扩容带来的抖动。 - 新生代不能配得太小,否则 Minor GC 过于频繁;老年代不能配得太小,否则对象容易提前晋升导致 Full GC 压力变大。
- 收集器也要按目标选型,如果更关注停顿时间可以考虑 G1;如果更关注吞吐量,可以考虑 Parallel GC。
- 线上调参最好配合压测和灰度验证,不然很容易把一个局部优化变成新的稳定性问题。
← 26.3.7腾讯Java秋招 腾讯云面经 →
评论
验证登录状态...