社招面经深度解析:Java 核心与系统底层
1. 说出你记得的排序算法
核心回答:
常见的排序算法可以分为两大类:
- 比较类排序:冒泡排序、快速排序、插入排序、选择排序、归并排序、堆排序、希尔排序。
- 非比较类排序:计数排序、桶排序、基数排序。
加分项(面试官想听的):
不要只报菜名,要按时间复杂度和稳定性来分类记忆:
- O(n²):冒泡、插入、选择(适合小规模数据)。
- O(n log n):快排、归并、堆排(大规模数据首选)。
- O(n):计数、桶、基数(特定场景,如整数范围已知)。
- 稳定性:归并、插入、冒泡是稳定的;快排、堆排、选择是不稳定的。
2. 快排、冒泡实现思路
冒泡排序 (Bubble Sort)
- 思路:像水底的气泡一样,每一轮遍历数组,比较相邻的两个元素,如果顺序错误就交换。每一轮结束后,最大的元素会“浮”到数组末尾。
- 代码逻辑:双重循环,外层控制轮数,内层进行两两比较交换。
- 特点:最简单,但效率低。优化点:如果某一轮没有发生任何交换,说明已经有序,可以提前退出。
快速排序 (Quick Sort)
- 思路:分治法(Divide and Conquer)。
- 选基准 (Pivot):从数组中挑出一个元素作为基准。
- 分区 (Partition):重新排序数列,所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准后面。
- 递归:递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
- 关键点:
Partition函数的实现是核心。 - 优化:随机选择 Pivot 可以避免在最坏情况(已排序数组)下退化为 O(n²)。
3. TCP 为什么要四次挥手?
参考视频
https://www.bilibili.com/video/BV1MUi5BCEjz/?spm_id_from=333.337.search-card.all.click&vd_source=cd2f9979518516c6f9eeff53f8d9f13c


核心原因:TCP 是全双工通信。
- 三次握手建立连接时,服务端在收到 SYN 后,可以将 ACK 和 SYN 合并发送,因为此时服务端没有数据要发给客户端。
- 四次挥手断开连接时:
- 客户端发送 FIN,表示“我发完了”。
- 服务端收到 FIN,回复 ACK。注意:此时服务端可能还有数据没发完,所以不能立刻发 FIN,只能先回 ACK。
- 等服务端数据发完后,服务端再发送 FIN,表示“我也发完了”。
- 客户端收到 FIN,回复 ACK,进入
TIME_WAIT状态。
为什么不能合并? 因为服务端接收到关闭请求时,应用层可能还有业务数据在处理,必须等数据处理完才能关闭发送通道。
4. 什么是进程?和线程的区别?
- 进程 (Process):是资源分配的最小单位。每个进程都有独立的内存空间(代码段、数据段、堆栈等)。进程之间互不干扰,安全性高。
- 线程 (Thread):是 CPU 调度的最小单位。线程共享进程的内存空间(堆、方法区),但拥有独立的栈空间。
区别对比表:
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 独立内存空间 | 共享进程资源 |
| 开销 | 创建/切换开销大 | 创建/切换开销小 |
| 通信 | 复杂(IPC) | 简单(共享变量) |
| 稳定性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
5. 并发进程会有什么问题?
- 竞态条件 (Race Condition):多个进程/线程同时访问共享资源,且执行顺序依赖时序,导致结果不可预测。
- 死锁 (Deadlock):两个或多个进程互相持有对方需要的资源,并等待对方释放,导致永久阻塞。
- 饥饿 (Starvation):某个进程长期得不到所需的资源,无法执行。
- 活锁 (Livelock):进程虽然没有阻塞,但一直在重复相同的操作,无法推进任务。
6. 进程的通信方式有哪些?
Linux 下常见的 IPC (Inter-Process Communication) 方式:
- 管道 (Pipe):半双工,数据只能单向流动。父子进程间常用。
- 命名管道 (FIFO):允许无亲缘关系的进程通信。
- 消息队列 (Message Queue):克服了管道只能承载无格式字节流的缺点,可以实现消息的随机查询。
- 共享内存 (Shared Memory):最快的 IPC 方式。映射一段能被其他进程访问的内存。但需要配合信号量解决同步问题。
- 信号量 (Semaphore):主要用于进程间的同步和互斥,不传输数据,只传输信号。
- 信号 (Signal):用于通知接收进程某个事件已经发生(如
kill -9)。 - 套接字 (Socket):可用于不同机器间的进程通信。
7. 什么是虚拟内存?
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
核心机制:通过 MMU (内存管理单元) 和页表,将虚拟地址映射到物理地址。
8. 除了内存扩展,虚拟内存其他的作用?
面试官问这个,是想考察你对操作系统设计哲学的理解。除了让程序能使用比物理内存更大的空间,虚拟内存还有以下关键作用:
- 内存保护与隔离:每个进程都有独立的虚拟地址空间,进程 A 无法直接访问进程 B 的内存,提高了系统的安全性和稳定性。
- 简化内存管理:程序员不需要关心物理内存的碎片化问题,只需要操作连续的虚拟地址。操作系统负责后台的页面置换。
- 按需加载 (Lazy Loading):程序启动时,不需要将整个可执行文件全部加载到内存,只需加载当前需要的部分,加快了启动速度。
- 写时复制 (Copy-on-Write):在
fork()创建子进程时,父子进程共享物理页面,只有当其中一方尝试写入时,才复制一份新的物理页面。这极大地提高了 fork 的效率。
9. Linux 常见的命令背一下
建议按功能分类记忆,不要死记硬背:
- 文件操作:
ls,cd,pwd,mkdir,rm,cp,mv,touch,chmod,chown - 文本查看/处理:
cat,less,head,tail,grep,awk,sed,wc - 进程管理:
ps,top,htop,kill,pkill,jobs,bg,fg - 网络相关:
netstat,ss,curl,wget,ping,telnet,ifconfig/ip addr - 系统状态:
df(磁盘),du(目录大小),free(内存),uptime(负载)
10. Linux 怎么实现批量替换文本?
最常用的是 sed 命令。
-
单个文件替换:
sed -i 's/old_string/new_string/g' filename.txt-i: 直接修改原文件。s: substitute (替换)。g: global (全局替换,否则只换每行第一个)。
-
批量替换目录下所有文件:
find . -name "*.java" -exec sed -i 's/old_string/new_string/g' {} \;或者结合
xargs:grep -rl 'old_string' . | xargs sed -i 's/old_string/new_string/g'
11. 现在有一个程序部署在 Linux 上运行很慢,说一下排查思路
这是一个经典的全链路排查问题,思路要由外向内,由系统到应用:
-
看系统整体负载:
- 使用
top或htop查看 CPU、内存使用率。 - 使用
vmstat查看是否有大量的上下文切换或 IO 等待。 - 使用
dmesg查看内核日志是否有 OOM 或硬件错误。
- 使用
-
定位具体进程/线程:
- 如果 CPU 高:
top -H -p <pid>查看哪个线程占用 CPU 高。 - 如果 IO 高:使用
iotop或iostat查看是哪个进程在疯狂读写磁盘。 - 如果网络慢:使用
sar -n DEV或iftop查看网络流量。
- 如果 CPU 高:
-
深入应用内部 (Java 为例):
- CPU 高:拿到高 CPU 线程 ID,转换为十六进制,使用
jstack <pid> | grep <hex_tid> -A 20查看堆栈,定位到具体代码行。 - 内存泄漏/GC 频繁:使用
jstat -gcutil <pid> 1000观察 GC 频率和耗时。使用jmapdump 堆内存分析。 - 死锁/等待:直接使用
jstack <pid>查看是否有DEADLOCK字样或大量BLOCKED线程。
- CPU 高:拿到高 CPU 线程 ID,转换为十六进制,使用
-
外部依赖:
- 检查数据库慢查询日志。
- 检查下游接口响应时间。
12. Linux 有什么检测死锁的工具?
-
Java 应用:
jstack <pid>:最直接,输出中会明确提示Found one Java-level deadlock。jcmd <pid> Thread.print:效果同 jstack。- VisualVM / JConsole:图形化工具,有专门的死锁检测按钮。
-
C/C++ 或其他系统级死锁:
gdb:附加到进程,查看线程堆栈。perf:生成火焰图,观察线程状态。strace:跟踪系统调用,看是否卡在futex或lock相关调用上。
13. 对 volatile 的理解
volatile 是 Java 提供的轻量级同步机制,它保证了两个特性:
- 可见性 (Visibility):当一个线程修改了 volatile 变量的值,新值会立即刷新到主内存,并使其他线程工作内存中的缓存失效,其他线程读取时必须从主内存重新加载。
- 禁止指令重排序 (Ordering):通过插入内存屏障 (Memory Barrier),防止编译器和处理器对指令进行重排序,保证有序性。
注意:volatile 不保证原子性。例如 i++ 操作,即使是 volatile,在多线程下也是不安全的。
14. 对 synchronized 的理解
synchronized 是 Java 关键字,基于 JVM 的 Monitor 对象实现,是一种悲观锁。
- 用法:修饰实例方法(锁当前实例)、静态方法(锁 Class 对象)、代码块(锁指定对象)。
- 底层原理:
- JDK 1.6 之前:重量级锁,直接依赖操作系统的 Mutex Lock,用户态和内核态切换开销大。
- JDK 1.6 之后:引入了锁升级机制,性能大幅提升。
- 偏向锁:只有一个线程访问时,标记线程 ID,避免 CAS。
- 轻量级锁:多个线程交替执行,使用 CAS 旋转锁,避免挂起。
- 重量级锁:竞争激烈时,膨胀为 OS 互斥锁,线程挂起。
— are 15. synchronized 在不同方法上的锁粒度?两个线程并发访问不同的方法锁会发生什么?
锁粒度取决于锁的对象是谁:
-
修饰实例方法:锁的是当前实例对象 (
this)。- 如果两个线程访问同一个对象的两个不同的 synchronized 方法,它们会互斥,因为锁的是同一个对象。
- 如果两个线程访问不同对象的 synchronized 方法,它们不会互斥。
-
修饰静态方法:锁的是类的 Class 对象。
- 无论创建多少个实例,所有线程访问该类的静态 synchronized 方法都会互斥。
-
修饰代码块
synchronized(obj):锁的是括号里的obj对象。
结论:两个线程并发访问同一个实例的不同 synchronized 方法,会发生竞争,串行执行。因为它们争夺的是同一把锁(实例锁)。
16. Java 是只有值传递吗?为什么?
是的,Java 只有值传递。
- 基本数据类型:传递的是值的副本。方法内修改不影响原变量。
- 引用数据类型:传递的是引用的副本(即对象在堆内存地址的拷贝)。
- 你可以在方法内通过这个引用副本修改对象内部的属性(因为指向同一个堆对象)。
- 但你不能在方法内改变原引用指向的对象(例如
obj = new Object()),因为这只是改变了副本的指向,原引用不变。
为什么有人觉得是引用传递? 因为他们看到了对象属性的修改。但本质上,传递进来的“地址”本身是按值拷贝的。
17. Hashmap 为什么使用红黑树?
在 JDK 1.8 中,HashMap 引入了红黑树优化。
- 背景:HashMap 底层是数组 + 链表。当 Hash 冲突严重时,链表会变得很长,查询效率从 O(1) 退化为 O(n)。
- 为什么选红黑树?
- 当链表长度超过阈值(默认 8)且数组长度超过 64 时,链表转为红黑树。
- 红黑树是一种自平衡二叉查找树,查询时间复杂度稳定在 O(log n)。
- 相比 AVL 树(严格平衡),红黑树的插入和删除旋转次数更少,综合性能更适合读多写少的场景。
- 为什么不一直用红黑树?
- 红黑树节点占用空间比链表节点大(需要存储父节点、颜色、左右子节点指针)。
- 在冲突较少时,链表的局部性更好,且 O(n) 在 n 很小时(<8)并不比 O(log n) 慢多少,反而更省内存。
总结:红黑树是为了防御极端 Hash 冲突情况,保证最坏情况下的查询性能,是一种空间换时间的策略。
希望这份解析能帮助你更好地准备面试!如果有具体的代码实现需求或更深层次的原理探讨,欢迎随时交流。

2783

被折叠的 条评论
为什么被折叠?



