百度一面,侃了三个半小时

文摘   2024-08-07 10:28   陕西  

看到有小伙伴说百度一面,约的周日下午,面了三个半小时,然后挂了。。。

题目还有些难度。

一起来看看题目吧,百度的题目还是有两把刷子。

一 讲一讲 G1 到 FullGC 的退化过程


在 Java 中,GC 是自动管理内存的关键部分。

不同的垃圾回收器采用不同的策略来处理对象的生命周期和内存分配,以达到最佳的性能与资源使用。

G1(Garbage-First)垃圾回收器和 FullGC 都是 Java 运行时环境中的重要组成部分。

G1 是 Java 7 Update4 及之后版本中引入的一种垃圾回收器。G1 将堆空间划分为多个小的区域(Region),每个区域可以充当 Eden、Survivor 或 Old 区。G1 通过预测暂停时间目标来决定哪些区域需要被回收。

那么什么是 G1 的退化?

当 G1 无法满足应用程序的暂停时间目标时,它可能会退化到类似 Serial Collector 的行为,此时它会停止整个应用程序进行垃圾回收,这就是所谓的“混合 GC”了,英文是 Mixed GC。

Mixed GC 会收集老年代的一部分以及年轻代的所有区域,这样可以减少暂停时间。如果 Mixed GC 仍然无法满足暂停时间目标,那么可能需要执行一次 FullGC,这个时候就可以理解为 G1 退化成 FullGC 了。

FullGC 则是一种全面的垃圾回收操作,发生 FullGC 会清理整个堆内存,包括年轻代、老年代以及永久代/元数据区。FullGC 通常比其他类型的 GC 更耗时,因为它需要处理更多的内存区域。FullGC 发生的场景一般来说有如下几种:

  • 堆内存不足,无法分配新的对象。
  • 老年代空间不足。
  • 永久代/元数据区满。
  • 系统显式调用 System.gc() 或 Runtime.runFinalization()。

在使用 G1 的情况下,FullGC 通常作为最后的保留手段,当 G1 无法通过正常的 Mixed GC 来满足内存需求时就会发生 FullGC,那么一般来说下面这些原因可能导致 G1 变为 FullGC:

  • 应用程序的内存消耗模式发生了变化,导致 G1 的预测模型失效。
  • 堆中的对象存活率异常高,导致 G1 无法有效回收内存。
  • 系统调用了 System.gc(),强制执行 FullGC。

为了防止 G1 退化到 FullGC,可以通过调整 JVM 参数来优化 G1 的行为,比如增加堆大小、调整最大暂停时间目标、或增加 G1 的并行线程数等等。

可以根据实际情况调整 JVM 参数,这样能让 G1 更有效地管理内存,减少 FullGC 的发生频率。

二 Java 一个进程运行的比较卡顿了,线上表现就是响应耗时长,如何定位它卡顿的原因?



这是个常规题目。

一般来说排查步骤可能是这样:

  1. 查看系统监控

首先检查是否有可用的监控工具,比如 Prometheus、Grafana、Zabbix 等,利用这些工具去查看 CPU、内存、磁盘 I/O、网络 I/O 等等资源的使用情况。

  1. JVM 监控

检查 JVM 的健康状况,使用 JMX(Java Management Extensions)或 VisualVM 或者其他工具,观察 CPU 使用率、堆内存使用情况、非堆内存使用、线程状态等。

  1. 日志分析

检查应用程序的日志文件,寻找错误信息或警告,了解是否有明显的异常抛出或长时间运行的方法。

  1. GC 日志分析

分析 GC 日志,确定是否因为频繁的垃圾回收导致性能下降,特别关注 FullGC 的频率和持续时间。当然,第 2步 或者 5 五步也可以通过工具分析 GC 日志。

  1. 性能分析

使用像 JProfiler、YourKit 或者是 VisualVM 这样的性能分析工具进行 CPU Profiling 和 Memory Profiling,找出热点方法和内存泄漏点。

  1. Heap Dump 和 Thread Dump

生成 Heap Dump 文件和 Thread Dump 文件,分析内存中对象的分布和线程的状态,检查是否有死锁或线程阻塞。

  1. 其他检查

a. 检查数据库和外部服务。b. 压测 c. 代码评审。d. 操作系统检查。e. 硬件检查。

三 多个 client 访问一个 loadBaLance,在存在多个 service 的情况下,客户端这边有一个连接池,连接池里边是长连接的方式,这种长连接有什么缺点




长连接可以减少 TCP 三握四挥的开销,提高网络通信效率,但是存在诸多问题:

  1. 资源占用

长连接会占用更多的服务器资源,比如文件描述符和内存。每个连接都需要一定的资源来维护,包括缓冲区、连接状态等。在高并发场景下,过多的长连接可能导致资源耗尽。

  1. 连接老化

如果客户端和服务端之间的连接长时间没有活动,网络设备(像路由器、防火墙)或中间件可能因超时而关闭连接。这就导致看似正常的连接实际上已经断开,造成数据传输失败。

  1. 状态一致性

当使用长连接时,如果后端服务发生重启、升级或故障转移,客户端可能需要重新建立连接,否则可能尝试与已失效的服务实例通信,导致请求失败。

  1. 负载均衡复杂性

如果想要确保来自同一个客户端的请求在经过负载均衡器时始终被发到相同的后端实例上,以前可能 IP_HASH 就行,现在就需要更复杂的策略。

  1. 故障恢复

客户端需要有机制来检测长连接的故障并重新建立连接,很明显这会导致客户端变复杂。

  1. 资源泄露

如果没有适当的管理,长连接可能会导致资源泄露。例如,如果客户端忘记关闭不再使用的连接,这些连接会持续占用资源,直到达到连接池的最大限制。

  1. 安全性

长连接可能增加安全风险,因为它们提供了更长的时间窗口给攻击者来探测或利用潜在的安全漏洞。

四 如何写一段 Java 代码,稳定的触发两次 YoungGC,然后触发一次 FullGC,然后又是两次 YoungGC



这个问题有些难度,一开始想用 Java 的 强软弱虚 引用来实现,但是题目要求稳定触发。

大家知道 Java 的 GC 是 JVM 根据各种现场情况去决定执行时机的,想通过代码稳定触发并不易。

我们平时常见的 System.gc() 和 System.runFinalization() 方法并不保证会立即触发 GC,它们只是建议 JVM 进行 GC。

后来想了一个思路:

首先在项目启动的时候,配置 JVM 参数,手动设置 Xmx、Xms 以及 Xmn 参数,也就是运行时内存和新生代大小。

接下来向集合中添加大对象,去触发 YoungGC。

添加大对象的形式类似下面这样:

public class Demo01 {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        List caches = new ArrayList();
        caches.add(new byte[3 * _1MB]);
    }
}

这样就可以根据 Xmn 的大小以及添加到集合中的大对象,大致上算出触发 YoungGC 以及 FullGC 的时机,这个我自己测试了下似乎可以,在项目启动的时候添加 JVM 参数,让把 GC 日志打印出来,去观察 GC 日志,我感觉似乎没问题。

上面代码并非完整案例,只是给大家一个参考的思路。

虽然我自己测试成功的,但是触发 GC 的因素实在太多了,而且有很多是程序员无法控制的变量,例如存在内存碎片,那么就可能发生在新生代内存够用的情况下,发生 GC。

因此这道题我持保留意见,也欢迎大家给出经过代码验证的不同意见。

五 多核环境下,如何合理设置 BIO/NIO 的工作线程数






  1. BIO(Blocking I/O)

在 BIO 模式下,每个线程通常负责处理一个连接,因为读取和写入操作是阻塞的。因此,线程数应该足够低,以避免过多的上下文切换和 CPU 核心争用,但也要足够高,这样才能确保所有核心得到充分利用。

核心数与线程数的比例:常规策略是将线程数设置为 CPU 核心数的 1.5 到 2 倍。这是因为除了网络 I/O 之外,线程还需要处理一些计算密集型任务,而额外的线程可以补偿某些线程在等待 I/O 时的空闲时间。

监控和调整:在实际部署中,应该监控系统的 CPU 使用率、线程上下文切换次数以及平均线程等待时间。如果 CPU 使用率低于 90%,而线程等待时间较长,则可以适当增加线程数;反之,如果 CPU 使用率接近 100%,并且线程切换频繁,可能需要减少线程数。

  1. NIO(Non-blocking I/O)

在 NIO 模式下,一个线程可以同时处理多个连接,因为读取和写入操作是非阻塞的。这使得 NIO 非常适合高并发场景,但同时也需要更精细的线程配置。

选择合适的事件处理模型:NIO 通常使用事件驱动的模型,如 Reactor 或 Proactor 模型。在这些模型中,一个或几个线程负责监听事件,而其他线程则处理事件。这通常意味着只需要少量的线程来处理大量的连接。

线程数:对于 NIO,线程数通常设置为 CPU 核心数加上一小部分额外的线程,例如核心数 +1 或核心数 +2。这确保了每个核心都有一个线程专门用于处理计算任务,而额外的线程可以处理 I/O 操作。

考虑任务类型:如果应用程序除了 I/O 操作外还涉及大量计算,可能需要增加线程数以平衡计算和 I/O 负载。相反,如果大部分操作都是 I/O 密集型的,较少的线程就足够了。

总结下就是根据应用程序的具体需求和运行环境去设置,然后通过监控系统性能指标再去调整。


好啦,就是这五道题。小伙伴们感觉难度怎么样?除了第四题有点挑战,其他也算是常规题目了吧。


松哥聊编程
程序员小白学习修炼之道,小白程序员需要学习的内容都在这里了。
 最新文章