Java基础

Java基础

1. 实例方法和静态方法有什么不一样? 实例方法属于对象,必须通过对象实例调用,可以访问实例变量和静态变量,在方法调用时隐式传入 this 引用。静态方法属于类本身,通过类名直接调用(也可通过实例但不推荐),只能访问静态变量和静态方法,没有 this 引用。 核心区别: | 维度 | 实例方法 | 静态方法 | | — | — | — | | 归属 | 实例对象 | 类 | | 调用方式 | obj.method() | ClassName.method() | | this/super | 可用 | 不可用 | | 访问实例变量 | 可以 | 不可以 | | 访问静态变量 | 可以 | 可以 | | 多态/重写 | 支持 | 不支持(隐藏) | | 内存 | 方法区一份,通过对象调用 | 方法区一份,类加载时绑定 | 设计原则:如果方法不依赖于对象状态(不需要访问实例变量),应声明为静态方法,如工具类方法 Math.abs()Collections.sort()。 — ## 2. Java中的异常有哪几类?分别怎么使用? Java 异常体系根类是 Throwable,分为两大分支: Error:JVM 级别严重错误,程序无法处理。如 OutOfMemoryErrorStackOverflowError。不应 try-catch。 Exception:程序可处理的异常,分两类: + 受检异常(Checked Exception):编译期强制处理,必须 try-catch 或 throws 声明。如 IOExceptionSQLException。用于可预见的恢复场景(文件不存在、网络中断)。 + 非受检异常(Unchecked Exception)RuntimeException 及其子类,编译期不强制。如 NullPointerExceptionIllegalArgumentException。通常是编程错误。 java // 受检异常处理 try { FileInputStream fis = new FileInputStream("file.txt"); } catch (IOException e) { // 恢复或提示 } finally { // 释放资源 } // 非受检异常:预防为主 if (str != null) { str.length(); // 而不是直接调用导致 NPE } 最佳实践:不要捕获后不处理(空 catch),不要用异常控制业务流程,finally 中释放资源(Java 7+ 可用 try-with-resources)。 — ## 3. 常用的集合类有哪些?比如List如何排序? Collection 接口: + List:有序可重复。ArrayList(数组,随机访问快)、LinkedList(双向链表,插入删除快)、Vector(线程安全,已过时)。 + Set:不重复。HashSet(HashMap 实现,O(1))、LinkedHashSet(保持插入顺序)、TreeSet(红黑树,有序)。 + Queue:队列。LinkedList(双端队列)、PriorityQueue(堆实现)、ArrayDeque(循环数组双端队列)。 Map 接口HashMap(数组+链表/红黑树)、LinkedHashMap(保持顺序)、TreeMap(红黑树有序)、ConcurrentHashMap(线程安全)、Hashtable(遗留类)。 List 排序java // 方式一:Collections.sort + Comparable List<String> list = Arrays.asList("c", "a", "b"); Collections.sort(list); // String 已实现 Comparable // 方式二:Comparator 自定义排序 Collections.sort(list, (a, b) -> b.compareTo(a)); // 降序 // 方式三:List.sort(Java 8+) list.sort(Comparator.naturalOrder()); list.sort(Comparator.comparing(User::getAge).reversed()); Collections.sort() 底层使用归并排序(TimSort),稳定排序,O(n log n),对于部分有序数据有优化。 — ## 4. ArrayList和LinkedList内部的实现大致是怎样的?区别和适应场景? ArrayList:底层是 Object[] 数组。默认初始容量 10,每次扩容 1.5 倍(newCapacity = oldCapacity + (oldCapacity >> 1)),通过 Arrays.copyOf 复制。随机访问 O(1),尾部插入均摊 O(1),中间插入/删除 O(n)(需搬移元素)。内存连续,对 CPU 缓存友好。 LinkedList:底层是双向链表。每个节点存储数据、前驱指针、后继指针。随机访问 O(n),头尾操作 O(1),中间插入删除 O(1)(已知位置的前提下,但需要先遍历定位)。内存非连续,额外存储指针开销。 | 场景 | 推荐 | | — | — | | 频繁随机访问(get/set) | ArrayList | | 频繁在尾部追加 | ArrayList | | 频繁头部/中间插入删除 | LinkedList | | 遍历 | ArrayList(缓存友好) | | 作为栈/队列 | LinkedList 或 ArrayDeque | 实际项目中 ArrayList 的使用频率远高于 LinkedList,因为大多数场景是随机访问 + 尾部追加。 — ## 5. 内存溢出是怎么回事? 内存溢出(OutOfMemoryError)指 JVM 堆内存不足,无法为新对象分配空间,并且 GC 之后仍无法腾出足够内存。 常见原因: + 堆内存设置过小-Xmx 不足,新对象不断创建超出上限。 + 内存泄漏:无用对象仍被引用无法回收(如静态集合持有对象引用)。 + 大对象分配:一次性读取大文件到内存。 + 永久代/元空间满(Java 7 之前 PermGen,Java 8+ Metaspace):动态生成大量类(如 CGLIB 代理)。 + 直接内存溢出:NIO 的 DirectByteBuffer 超过 -XX:MaxDirectMemorySize。 排查:jmap -dump:format=b,file=heap.hprof <pid> 导出堆,用 MAT/JProfiler 分析大对象和 GC Roots 引用链。 — ## 6. ClassLoader有什么用? ClassLoader(类加载器)负责将 .class 字节码加载到 JVM 内存中,生成 Class 对象。 三大作用: 1. 加载:按全限定名找到类的字节码并加载。 2. 命名空间隔离:不同加载器加载的类相互隔离(即使全限定名相同)。 3. 安全:双亲委派模型防止核心类被篡改。 双亲委派模型plain Bootstrap ClassLoader(加载 rt.jar) ↕ Extension ClassLoader(加载 jre/lib/ext) ↕ Application ClassLoader(加载 classpath) ↕ 自定义 ClassLoader 加载时先向上委托,父加载器能加载则子加载器不再加载。破坏者:Tomcat 各 WebApp 使用独立 ClassLoader 实现隔离;OSGi 网状委派;热部署框架(如 JRebel)。 — ## 7. ==equals 的区别? ==:比较。基本类型比较数值,引用类型比较内存地址(两个引用是否指向同一对象)。 equalsObject 类方法,默认实现也是 ==(比较地址)。需要内容比较的类必须重写 equals(如 StringInteger、自定义类)。 java String a = new String("hello"); String b = new String("hello"); a == b; // false,不同对象 a.equals(b); // true,内容相同 String c = "hello"; String d = "hello"; c == d; // true,字符串常量池 重写 equals 的规范(来自 Object 文档):自反性、对称性、传递性、一致性、与 null 比较返回 false。必须同时重写 hashCode,否则 HashMap/HashSet 会出问题。 — ## 8. hashCode 方法的作用? hashCode() 返回对象的哈希码(int),用于哈希表结构的定位(HashMap、HashSet、Hashtable)。 核心约定(Java 规范): + 同一对象多次调用必须返回相同值(前提是不修改 equals 比较用到的字段)。 + a.equals(b) == truea.hashCode() == b.hashCode()。 + a.equals(b) == false 不要求 hashCode 不同,但不同可提升哈希表性能。 工作流程:HashMap.put(key, value) → 计算 key.hashCode() → 定位桶 → 若冲突,再通过 equals 判断是否同一 key。 若不重写 hashCode:两个 equals 相等的对象放入 HashMap,却落到不同桶,导致 get 返回 null。 — ## 9. Object类中有哪些方法?列举3个以上。 java.lang.Object 是所有类的根类,主要方法: | 方法 | 说明 | | — | — | | getClass() | 返回运行时 Class 对象(反射入口) | | hashCode() | 返回哈希码,用于 HashMap 等 | | equals(Object obj) | 判断对象是否”相等”,默认比较地址 | | toString() | 返回字符串表示,默认 类名@十六进制hashCode | | clone() | 创建并返回副本(需实现 Cloneable,浅拷贝) | | finalize() | GC 回收前调用(已废弃,Java 9+ 标记 deprecated) | | notify() / notifyAll() / wait() | 线程间通信方法(必须在 synchronized 块内调用) | — ## 10. NIO是什么?适用于何种场景? NIO(New/Non-blocking I/O,java.nio)是 Java 1.4 引入的高性能 I/O API。 核心组件: + Channel:双向通道,代替 Stream(FileChannel、SocketChannel、ServerSocketChannel)。 + Buffer:数据容器,所有 I/O 通过 Buffer 进行(ByteBuffer、CharBuffer 等),position/limit/capacity 三元组管理。 + Selector:多路复用器,单线程管理多个 Channel,select() 阻塞直到有事件就绪。 与传统 IO 对比:传统 IO 一个连接一个线程,阻塞读写;NIO 一个线程管理成千上万连接,只有就绪的连接才处理。 典型场景: + 高并发网络服务(聊天服务器、推送服务) + 网关/代理服务 + 大文件零拷贝传输:FileChannel.transferTo() 直接内核态传输,不经过用户态。 Netty 框架封装了 NIO 复杂性,是事实上的网络编程标准。 — ## 11. HashMap数据结构、扩展策略,Hash冲突攻击如何防范,如何实现线程安全的HashMap? 数据结构(JDK 8+):数组 + 链表 + 红黑树。数组每个元素是一个桶,链表长度 ≥ 8 且数组长度 ≥ 64 时链表转为红黑树,退化为 ≤ 6 时转回链表。平衡了查找效率(O(log n) vs O(n))和内存开销。 扩展策略:默认容量 16,负载因子 0.75。当 size > capacity * loadFactor 时,扩容为 2 倍,重新计算每个元素的桶位置(原位置或原位置+旧容量)。 Hash 冲突攻击:攻击者构造大量 hash 相同的 key,使所有元素落入同一桶,退化为链表 O(n),CPU 被耗尽。防范: + JDK 8 引入红黑树,最坏 O(log n)。 + 扰动函数 (h = key.hashCode()) ^ (h >>> 16) 让高位参与运算,降低冲突概率。 线程安全的 HashMap: + Collections.synchronizedMap(new HashMap<>()):每个方法加 synchronized,性能差。 + Hashtable:遗留类,全表锁。 + ConcurrentHashMap:JDK 7 分段锁(Segment),JDK 8 CAS + synchronized 锁单个桶,并发度高。 — ## 12. JVM内存结构,GC算法,CMS、G1的原理。 (详细展开见 JVM 章节) JVM 内存结构:程序计数器、虚拟机栈(线程私有)、本地方法栈、堆(线程共享)、方法区(元空间)。 GC 算法: + 标记-清除:标记存活 → 清除未标记。产生碎片。 + 标记-整理(压缩):标记后移动存活对象到一端,消除碎片。 + 复制算法:两块内存,一块满了将存活复制到另一块,清空第一块。适合新生代(对象死亡率高)。 + 分代收集:新生代用复制,老年代用标记-整理/清除。 CMS:老年代并发收集器。初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清除。低停顿但产生碎片、对 CPU 敏感。 G1:面向局部收集的收集器。Region 划分堆,优先回收垃圾最多的 Region(Garbage First)。初始标记 → 并发标记 → 最终标记 → 筛选回收。可预测停顿时间。 — ## 13. NIO模型,select/epoll的区别,多路复用的原理。 I/O 模型演进: + 阻塞 I/O:线程在 read/write 时阻塞,一个线程一个连接。 + 非阻塞 I/O:轮询检查是否就绪,CPU 空转。 + I/O 多路复用:一个线程通过 select/poll/epoll 监控多个 fd,就绪才处理。 + 异步 I/O:内核完成 I/O 后回调通知,Java 7 NIO.2 的 AsynchronousChannel。 select vs poll vs epoll(Linux): | | select | poll | epoll | | — | — | — | — | | 数据结构 | 固定位图 | 链表 | 红黑树 + 就绪链表 | | fd 限制 | 默认 1024 | 无 | 无 | | 扫描方式 | O(n) 遍历所有 | O(n) 遍历所有 | O(1) 回调 | | fd 复制 | 每次调用复制 | 每次调用复制 | 只注册一次 | | 适用 | fd 少 | fd 少 | 海量连接 | 多路复用原理:Selector 向内核注册感兴趣的 Channel 和事件(OP_READ/OP_WRITE),select() 阻塞等待,内核检测到事件就将就绪的 Channel 返回,应用层遍历处理。 — ## 14. Java中一个字符占多少个字节?扩展再问int、long、double占多少字节。 | 类型 | 字节数 | 位数 | 范围 | 备注 | | — | — | — | — | — | | byte | 1 | 8 | -128~127 | | | short | 2 | 16 | -32768~32767 | | | char | 2 | 16 | 0~65535 | UTF-16,单个 BMP 字符 | | int | 4 | 32 | -231~231-1 | | | long | 8 | 64 | -263~263-1 | | | float | 4 | 32 | ±3.4E38 | IEEE 754 | | double | 8 | 64 | ±1.8E308 | IEEE 754 | | boolean | JVM 依赖 | | | 数组中 1 字节,单独 4 字节 | char 深入:Java 内部使用 UTF-16 编码。BMP 字符(基本多文种平面)占 2 字节(1 个 char),增补字符(如 emoji)占 4 字节(2 个 char,即代理对)。String.length() 返回 char 数量,不一定等于字符数,应用 codePointCount()。 — ## 15. 创建一个类的实例都有哪些办法? 1. new 关键字new User() 2. 反射Class.forName("User").newInstance()constructor.newInstance() 3. clone():实现 Cloneable,调用 super.clone()(浅拷贝,不调构造器) 4. 反序列化ObjectInputStream.readObject()(不调构造器) 5. UnsafeUnsafe.allocateInstance(Class)(不调构造器,底层内存分配) 6. 方法句柄(Java 7+):MethodHandles.lookup().findConstructor() 7. Lambda/方法引用Supplier<User> s = User::new; s.get(); 8. 动态字节码:CGLIB 生成子类,Javassist 等 — ## 16. final/finally/finalize的区别? + final:修饰类(不可继承)、方法(不可重写)、变量(基本类型值不可变、引用类型引用不可变但对象内容可变)。 + finally:异常处理的一部分,无论是否异常都执行(除非 System.exit(0) 或 JVM 崩溃)。用于释放资源。 + finalize()Object 方法,GC 回收前调用一次。已废弃(Java 9 @Deprecated),因为执行时机不确定、不保证执行、性能差。替代方案:try-with-resourcesCleaner(Java 9+)。 — ## 17. LinkedBlockingQueue与ArrayBlockingQueue的区别,适用场景? | | ArrayBlockingQueue | LinkedBlockingQueue | | — | — | — | | 底层 | 数组(定长) | 单向链表(可指定容量,默认 Integer.MAX_VALUE) | | 锁 | 单锁(put/take 共用) | 双锁(putLock + takeLock),吞吐更高 | | 内存 | 预分配连续内存 | 动态分配节点,额外对象开销 | | 公平性 | 支持公平锁(构造参数) | 不支持 | 场景:固定大小场景用 ArrayBlockingQueue(简单、预分配);高吞吐场景用 LinkedBlockingQueue(双锁分离入队和出队操作);注意 LinkedBlockingQueue 默认几乎无界,可能 OOM。 — ## 18. Session/Cookie的区别? | | Cookie | Session | | — | — | — | | 存储位置 | 浏览器客户端 | 服务器端 | | 安全性 | 低(可被篡改、窃取) | 高(仅传 sessionId) | | 容量 | 4KB | 无限制(受服务器内存影响) | | 跨域名 | 不可跨域 | 不可直接跨服务器(需共享存储如 Redis) | | 生命周期 | 可设置过期时间 | 默认会话结束失效 | | 网络开销 | 每次请求附带 | 仅传 sessionId | 典型流程:服务器创建 Session,生成 sessionId 通过 Set-Cookie 返回浏览器,后续请求浏览器自动携带 Cookie 中的 sessionId,服务器根据 sessionId 找到对应 Session。 — ## 19. String/StringBuffer/StringBuilder的区别?扩展再问它们的实现? | | String | StringBuffer | StringBuilder | | — | — | — | — | | 可变性 | 不可变(final 类) | 可变 | 可变 | | 线程安全 | 安全(不可变) | 安全(synchronized) | 不安全 | | 性能 | 拼接产生新对象 | 中等 | 最快 | String 实现(JDK 9+):byte[] value + coder 标识(Latin1 或 UTF-16),不再使用 char[]。常量池优化:字面量在字符串常量池,intern() 可手动入池。 StringBuffer/StringBuilder 实现:继承 AbstractStringBuilder,内部是 byte[] value,默认容量 16(无参)或 str.length() + 16(带参)。append()count + len > value.length 则扩容:newCapacity = (oldCapacity << 1) + 2。最终通过 Arrays.copyOf() 复制。toString() 返回 new String(不共享数组)。 — ## 20. Servlet的生命周期? 1. 加载与实例化:容器加载 Servlet 类并调用无参构造器创建实例(一次)。 2. 初始化:调用 init(ServletConfig config),只执行一次。可重写做资源初始化。 3. 服务:每次请求调用 service(ServletRequest req, ServletResponse res),根据 HTTP 方法分发到 doGet()/doPost() 等。 4. 销毁:容器关闭或移除时调用 destroy(),只执行一次。释放资源。 线程模型:Servlet 单实例多线程(默认),多请求共享同一实例,因此 Servlet 中不要使用实例变量保存请求相关状态(线程不安全)。 — ## 21. 如何用Java分配一段连续的1G的内存空间?需要注意些什么? java // 方式一:直接分配大数组 byte[] array = new byte[1024 * 1024 * 1024]; // 1G // 方式二:使用 DirectByteBuffer(堆外内存) ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024); // 方式三:Unsafe(不安全,不推荐) Unsafe unsafe = ...; long addr = unsafe.allocateMemory(1024 * 1024 * 1024L); 注意事项: + JVM 堆内存需 >= 1G,设置 -Xmx2g 以上(考虑对象头和碎片)。 + 堆外内存需设 -XX:MaxDirectMemorySize。 + 32 位 JVM 有 4G 进程地址空间限制,基本无法分配 1G 连续堆;必须用 64 位。 + GC 压力:大对象直接进入老年代(超过 -XX:PretenureSizeThreshold),可能触发 Full GC。考虑 G1 或 ZGC。 + 碎片问题:堆内存在碎片时可能无法分配 1G 连续空间,即使总空闲 > 1G。 — ## 22. Java有自己的内存回收机制,但为什么还存在内存泄露的问题呢? GC 回收的是不可达对象。内存泄漏指对象已经无用但仍可达,GC 无法回收。 常见泄漏场景: + 静态集合static List<Object> list = new ArrayList<>(),不断 add 但不 remove。 + 未关闭资源:IO 流、Connection 未 close,底层缓冲区占用内存。 + 内部类持有外部引用:非静态内部类隐式持有外部类引用,若内部类生命周期长于外部类。 + ThreadLocal 未 remove:线程池中线程复用,ThreadLocal 值一直存在。 + 监听器未取消注册:Observer 模式中未 detach。 + 字符串常量池(JDK 7 之前 PermGen):大量 intern() 导致 PermGen OOM。 排查:jmap dump 后用 MAT 分析,查看 GC Roots 最短路径,找出不该存活的对象为何可达。 — ## 23. Java里面用对象作为Key需要注意些什么?如何实现hashCode? 注意点: 1. 不可变性:Key 对象在放入 Map 后如果修改了 equals/hashCode 相关字段,会导致无法找到原始条目。最佳实践是使用不可变对象作为 Key。 2. 必须同时重写 equals 和 hashCode。 3. hashCode 分布均匀:避免大量冲突,否则退化为链表/红黑树,影响性能。 实现 hashCodejava public class Person { private String name; private int age; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Person)) return false; Person p = (Person) o; return age == p.age && Objects.equals(name, p.name); } @Override public int hashCode() { return Objects.hash(name, age); // JDK 7+ } } JDK 7+ 推荐 Objects.hash()。质数选择 31(31 * h + val):31 是奇质数,可用移位优化 31 * i == (i << 5) - i。结合所有关键字段,避免依赖不参与 equals 的字段。 — # JVM ## 1. JVM堆的基本结构是怎样的? 堆是 JVM 管理的最大一块内存,几乎所有对象都在堆上分配。 分代结构plain ┌─────────────────────────────────┐ │ 堆 (Heap) │ │ ┌──────────────┬─────────────┐ │ │ │ 新生代 │ 老年代 │ │ │ │ ┌───┬───┬───┐│ (Old Gen) │ │ │ │ │Eden│S0 │S1 ││ │ │ │ │ │ │ │ ││ │ │ │ │ └───┴───┴───┘│ │ │ │ └──────────────┴─────────────┘ │ └─────────────────────────────────┘ + 新生代(Young Generation):Eden(8/10)+ Survivor0(1/10)+ Survivor1(1/10),默认比例 -XX:SurvivorRatio=8。新对象在 Eden 分配,Minor GC 后存活对象复制到 Survivor,经历多次(默认 15 次)GC 仍存活晋升到老年代。 + 老年代(Old Generation):存放长期存活对象和大对象。发生 Major/Full GC 时回收。 + 字符串常量池(JDK 7+ 移到堆中)、静态变量也存储在堆中。 — ## 2. JVM的垃圾回收算法有哪几种?CMS收集算法的流程是什么? GC 算法: + 标记-清除(Mark-Sweep):标记存活 → 清除未标记。简单但产生碎片。 + 标记-整理(Mark-Compact):标记存活 → 移动到一端 → 清理边界外。消除碎片,但移动对象耗时。 + 复制(Copying):两块区域,存活对象从一块复制到另一块。适合新生代(少量存活),无碎片但浪费空间。 + 分代收集(Generational Collection):新生代复制,老年代标记-整理/清除。 CMS 流程(老年代并发低停顿收集器): 1. 初始标记(Initial Mark,STW):标记 GC Roots 直接可达对象,停顿短。 2. 并发标记(Concurrent Mark):与用户线程并发,遍历对象图。 3. 并发预清理(Concurrent Preclean):处理并发标记阶段变化的引用。 4. 重新标记(Remark,STW):修正并发期间的标记变化(Card Table)。停顿比初始标记稍长。 5. 并发清除(Concurrent Sweep):并发清除垃圾。 6. 并发重置(Concurrent Reset):重置内部数据结构。 缺点:CPU 敏感、浮动垃圾、产生碎片(需 -XX:+UseCMSCompactAtFullCollection)。 — ## 3. JVM有哪些常用的启动参数可以调整? | 参数 | 作用 | | — | — | | -Xms | 初始堆大小 | | -Xmx | 最大堆大小 | | -Xmn | 新生代大小 | | -Xss | 线程栈大小(默认 1M) | | -XX:NewRatio | 老年代/新生代比例(默认 2) | | -XX:SurvivorRatio | Eden/Survivor 比例(默认 8) | | -XX:MetaspaceSize | 元空间初始大小 | | -XX:MaxMetaspaceSize | 元空间最大大小 | | -XX:+UseSerialGC | 串行 GC | | -XX:+UseParallelGC | 并行(吞吐量)GC | | -XX:+UseConcMarkSweepGC | CMS GC | | -XX:+UseG1GC | G1 GC | | -XX:MaxGCPauseMillis | G1 目标停顿时间 | | -XX:+PrintGCDetails | 打印 GC 详情 | | -XX:+PrintGCDateStamps | GC 时间戳 | | -Xloggc | GC 日志输出路径 | | -XX:+HeapDumpOnOutOfMemoryError | OOM 时 dump 堆 | | -XX:HeapDumpPath | dump 文件路径 | — ## 4. 如何查看JVM的内存使用情况? 命令行工具bash jstat -gc <pid> 1000 # 每秒输出 GC 统计 jstat -gcutil <pid> 1000 # 百分比形式 jmap -heap <pid> # 堆配置与使用概况 jmap -histo:live <pid> # 存活对象统计(触发 Full GC) jcmd <pid> GC.heap_info # 堆信息 图形化:jvisualvm、JConsole、Arthas(dashboardmemory 命令)、MAT(离线分析 dump)、JProfiler。 程序内java Runtime runtime = Runtime.getRuntime(); long maxMemory = runtime.maxMemory(); // -Xmx long totalMemory = runtime.totalMemory(); // 已分配 long freeMemory = runtime.freeMemory(); // 已分配中空闲 long used = totalMemory - freeMemory; — ## 5. Java程序是否会内存溢出? 会。常见 OOM 类型: + java.lang.OutOfMemoryError: Java heap space:堆内存不足。 + java.lang.OutOfMemoryError: Metaspace(JDK 8+,之前是 PermGen space):加载类过多。 + java.lang.OutOfMemoryError: unable to create new native thread:线程数超过 OS 限制。 + java.lang.OutOfMemoryError: Direct buffer memory:堆外内存不足。 根本原因:要么内存配置过小,要么存在内存泄漏,要么创建过多对象/线程。 — ## 6. 你常用的JVM配置和调优参数都有哪些?分别有什么作用? 典型线上配置: plain -server -Xms4g -Xmx4g -Xmn2g -Xss256k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/app/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app/ -XX:+DisableExplicitGC -XX:ParallelGCThreads=4 -XX:ConcGCThreads=2 + -Xms = -Xmx:避免堆动态扩缩的开销。 + -Xss256k:线程栈大小,线程多时调小节省内存。 + -XX:+DisableExplicitGC:禁止 System.gc()。 + -XX:MaxGCPauseMillis=200:G1 调优核心参数,平衡吞吐与延迟。 + GC 日志:用于事后分析,配合 GCeasy/gceasy.io 等在线分析工具。 — ## 7. Java内存分代模型、GC算法,以及JVM常见的启动参数有哪些? 内存分代模型: 新生代(Eden + S0 + S1)→ Minor GC,对象年龄达到阈值(默认 15)进入老年代 → Major/Full GC。 对象晋升老年代的条件: 1. 年龄超过 -XX:MaxTenuringThreshold。 2. 动态年龄判断:Survivor 中同龄对象总和 > Survivor 空间一半,该年龄及以上直接晋升。 3. 大对象直接进入老年代(超过 -XX:PretenureSizeThreshold)。 4. 分配担保失败:Minor GC 后 Survivor 放不下直接进老年代。 GC 算法:复制(新生代)、标记-整理/清除(老年代)、G1(Region 局部收集)、ZGC/Shenandoah(低延迟)。 常见参数:同第 3/6 题。补充:-XX:+UseAdaptiveSizePolicy(动态调整新生代比例)、-XX:GCTimeRatio(吞吐量目标)。 — ## 8. CMS算法的过程是怎样的?CMS回收过程中JVM是否需要暂停? CMS(Concurrent Mark Sweep)以最短回收停顿时间为目标,流程: | 阶段 | 是否 STW | 说明 | | — | — | — | | 初始标记 | (短) | 标记 GC Roots 直达对象 | | 并发标记 | 否 | 遍历对象图 | | 并发预清理 | 否 | 处理并发标记期间引用变化 | | 可中断预清理 | 否 | 等待 Minor GC,减少重新标记负担 | | 重新标记 | (较长) | 修正并发标记结果(处理新增引用) | | 并发清除 | 否 | 回收垃圾 | | 并发重置 | 否 | 重置 CMS 数据结构 | CMS 大部分时间与用户线程并发,但仍有两个 STW 阶段。CMS 不整理碎片,老年代碎片严重时退化为 Serial Old(单线程,长 STW)。可用 -XX:CMSInitiatingOccupancyFraction 设置触发阈值(默认 92% JDK 8)。 jstat -gcutil 观察:S0/S1/E/O/P(或 M)各代使用率、YGC/FGC 次数和时间。 — ## 9. 什么情况下会出现OOM(堆内存、永久代、方法区、虚拟机栈)? + 堆内存 OOM:对象过多或内存泄漏。-Xmx 过小或内存泄漏(静态集合、未关闭连接)。错误信息:Java heap space。 + 元空间 OOM(JDK 8+,原永久代):动态生成大量类(CGLIB 代理、JSP、反射生成)。错误信息:Metaspace。解决:-XX:MaxMetaspaceSize 调大。 + 直接内存 OOM:NIO 的 DirectByteBuffer 或 Unsafe 分配堆外内存超限。错误信息:Direct buffer memory。 + 线程创建 OOM:线程数超过 OS 限制(/proc/sys/kernel/threads-max)。错误信息:unable to create new native thread。 + 虚拟机栈:线程请求栈深度超限抛 StackOverflowError(一般是递归过深);若栈可动态扩展但内存不足抛 OOM。 — ## 10. Java内存结构(堆结构:新生代S0/S1/Eden,老年代,持久代) 详见第 1 题。补充各分区说明: + Eden:绝大多数新对象分配区。Minor GC 时存活对象复制到 Survivor。 + S0/S1(From/To):两个 Survivor 交替使用,始终保持一个为空(To 区,Minor GC 后空闲)。减少碎片,方便下次复制。 + 老年代:存活时间长的对象。GC 频率低但回收耗时长。 + 元空间(JDK 8+):替代永久代,使用本地内存(不再受 -XX:MaxPermSize 限制),存放类元信息、方法信息、常量池(JDK 7+ 字符串常量池已移到堆)。 默认新生代:老年代 = 1:2,Eden:S0:S1 = 8:1:1。 — ## 11. 常用的GC策略有哪些?什么时候会触发YGC,什么时候触发FGC? GC 策略:Serial、Parallel Scavenge/Parallel Old(吞吐量优先)、CMS(低延迟)、G1(平衡)、ZGC/Shenandoah(超低延迟,JDK 11/15+)。 YGC(Minor GC)触发时机:Eden 区满时。频繁发生,停顿短。 FGC(Full GC)触发时机: + 老年代空间不足(分配大对象或晋升对象时)。 + 元空间不足。 + System.gc() 显式调用(若未 disabled)。 + CMS 并发失败(Concurrent Mode Failure)退化为 Serial Old。 + Minor GC 分配担保失败(老年代剩余空间 < 新生代所有对象之和)。 FGC 停顿长,尽量避免。优化方向:减少对象晋升(调整新生代大小/晋升阈值)、避免大对象分配。 — # 数据结构与算法基础 ## 1. 说一下几种常见的排序算法和分别的时间复杂度。 | 排序算法 | 平均时间 | 最好时间 | 最坏时间 | 空间 | 稳定性 | | — | — | — | — | — | — | | 冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 | | 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 | | 插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 | | 希尔排序 | O(n log n) | O(n) | O(n²) | O(1) | 不稳定 | | 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 | | 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 不稳定 | | 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 | | 计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 | | 桶排序 | O(n+k) | O(n) | O(n²) | O(n+k) | 稳定 | | 基数排序 | O(nk) | O(nk) | O(nk) | O(n+k) | 稳定 | 快速排序核心:分治,选 pivot 分区,递归。Java Arrays.sort() 对基本类型用双轴快排(Dual-Pivot QuickSort),对象用 TimSort(归并+插入,稳定)。 — ## 2. 什么是跳表(SkipList)? 跳表是一种多层有序链表,通过空间换时间实现高效查找,平均 O(log n)。 结构:底层是完整有序链表,上层是索引层(跳跃式前进),每层元素按概率(通常 p=0.5)随机提升到上层。 plain Level 2: 1 ───────────→ 7 Level 1: 1 ──→ 3 ──→ 5 ──→ 7 ──→ 9 Level 0: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 查找 6:L2 从 1→7(超了)→ 回退到 L1→5→7(超了)→ 回退到 L0→6。 应用:Redis 有序集合(ZSet)、ConcurrentSkipListMap/Set(并发跳表,无需全局锁)、LevelDB Memtable。 与红黑树对比:实现简单(无旋转)、范围查询友好(找到起点顺链表遍历)、并发友好(局部修改影响小)。 — ## 3. 如何确认一个链表有环?进一步,如何确认环的入口位置? 判环:快慢指针(Floyd 判圈算法)。slow 每次走 1 步,fast 每次走 2 步,有环则必相遇。 java public boolean hasCycle(ListNode head) { if (head == null) return false; ListNode slow = head, fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) return true; } return false; } 找环入口:相遇后,一个指针回到 head,两指针每次各走 1 步,再次相遇即为入口。 java public ListNode detectCycle(ListNode head) { if (head == null) return null; ListNode slow = head, fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { ListNode ptr = head; while (ptr != slow) { ptr = ptr.next; slow = slow.next; } return ptr; } } return null; } 原理:设 head 到入口距离 a,环长 b。相遇时 slow 走了 a + x,fast 走了 a + x + nb = 2(a + x),得 a = (n-1)b + (b-x)。head 指针走 a 步到入口,slow 同时走 a 步也到入口。 — ## 4. 如何遍历一棵二叉树? java // 前序遍历:根 → 左 → 右 void preOrder(TreeNode root) { if (root == null) return; System.out.print(root.val); preOrder(root.left); preOrder(root.right); } // 中序遍历:左 → 根 → 右(BST 得有序序列) void inOrder(TreeNode root) { if (root == null) return; inOrder(root.left); System.out.print(root.val); inOrder(root.right); } // 后序遍历:左 → 右 → 根(用于删除、计算子树) void postOrder(TreeNode root) { if (root == null) return; postOrder(root.left); postOrder(root.right); System.out.print(root.val); } // 层序遍历(BFS) void levelOrder(TreeNode root) { if (root == null) return; Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { TreeNode node = queue.poll(); System.out.print(node.val); if (node.left != null) queue.offer(node.left); if (node.right != null) queue.offer(node.right); } } 非递归实现使用显式栈(前中后序)或队列(层序)。 — ## 5. 如何反转一个LinkedList(链表)? java // 迭代法(O(n) 时间,O(1) 空间) public ListNode reverseList(ListNode head) { ListNode prev = null, curr = head; while (curr != null) { ListNode next = curr.next; // 保存后继 curr.next = prev; // 反转指针 prev = curr; // prev 前进 curr = next; // curr 前进 } return prev; } // 递归法 public ListNode reverseListRecursive(ListNode head) { if (head == null || head.next == null) return head; ListNode newHead = reverseListRecursive(head.next); head.next.next = head; // 后继的 next 指向自己 head.next = null; // 断开原链接 return newHead; } — ## 6. HashSet的底层实现方式是什么? HashSet 底层完全委托给 HashMap。元素作为 HashMap 的 key,value 统一使用一个共享的 PRESENT 伪值(new Object())。 java // HashSet 源码(简化) public class HashSet<E> { private transient HashMap<E, Object> map; private static final Object PRESENT = new Object(); public HashSet() { map = new HashMap<>(); } public boolean add(E e) { return map.put(e, PRESENT) == null; } public boolean remove(Object o) { return map.remove(o) == PRESENT; } public boolean contains(Object o) { return map.containsKey(o); } } 特点:无序(HashMap 无序)、O(1) 增删查、元素唯一(HashMap key 唯一)、允许 null(一个)、非线程安全。 LinkedHashSet 基于 LinkedHashMap 维护插入顺序;TreeSet 基于 TreeMap(红黑树)实现元素排序。 # 多线程/并发 ## 1. Java中常见的锁有哪些? | 锁类型 | 接口/类 | 特点 | | — | — | — | | 内置锁 | synchronized | JVM 层面,自动加解锁,非公平,可重入 | | 互斥锁 | ReentrantLock | API 层面,可公平/非公平,可中断,可超时,tryLock() | | 读写锁 | ReentrantReadWriteLock | 读共享、写独占,适合读多写少。写锁可降级为读锁 | | 信号量 | Semaphore | 控制同时访问资源的线程数(许可证) | | 倒计数锁 | CountDownLatch | 等待计数归零,一次性 | | 循环栅栏 | CyclicBarrier | 所有线程到达屏障后一起继续,可重复 | | 条件锁 | Condition | Lock.newCondition(),精确唤醒(await/signal) | | 自旋锁 | CAS + 循环 | 不阻塞线程,循环重试,适合临界区短的场景 | | 偏向锁/轻量级锁 | JVM 内置优化 | synchronized 的优化,逐步升级 | — ## 2. 原子类(Atomic类)是如何保证原子性的? 原子类(AtomicIntegerAtomicLongAtomicReference 等)基于 CAS(Compare And Swap):一条 CPU 原子指令(cmpxchg + lock 前缀),比较内存值与预期值,相同则替换。 java // AtomicInteger.incrementAndGet() 原理 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } // Unsafe public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); // 读最新值 } while (!compareAndSwapInt(o, offset, v, v + delta)); // CAS return v; } Unsafe 提供 native 方法直接操作内存。CAS 三大问题: 1. ABA 问题:值被改为 B 再改回 A,CAS 无法察觉 → 用 AtomicStampedReference/AtomicMarkableReference。 2. 自旋开销:高竞争下 CAS 频繁失败,CPU 空转。 3. 只能保证一个共享变量原子性AtomicReference 包装多个变量。 — ## 3. volatile 关键字的作用是什么?可见性问题的原因是什么? volatile 两大作用: 1. 保证可见性:写 volatile 变量时 JVM 发送 Lock 前缀指令,将缓存行写回主内存,并使其他 CPU 缓存行失效(MESI 协议),后续读必须从主内存重新加载。 2. 禁止指令重排序:内存屏障(Memory Barrier)阻止屏障前后的指令重排序。 可见性问题原因:现代 CPU 多级缓存(L1/L2/L3),各核心有自己的缓存,线程 A 修改的变量可能仅在 L1 Cache,线程 B 读到的是旧值。 java // 经典 DCL 单例 public class Singleton { private volatile static Singleton instance; // volatile 防止指令重排序 public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // 三步:分配内存、初始化、赋值引用 } } } return instance; } } volatile 不保证原子性count++(读→改→写)仍需要 synchronized 或 AtomicInteger。 — ## 4. 如何实现一个线程安全的数据结构? 三种途径: 1. 内置锁包装:每个方法加 synchronized,如 VectorHashtable。简单但并发度低。 2. 装饰器Collections.synchronizedList(new ArrayList<>()),返回包装类,每个方法加锁。注意迭代时需要手动同步。 3. 并发数据结构(J.U.C):使用精细锁或无锁算法。如 ConcurrentHashMap(JDK 8 CAS + synchronized 锁单个桶)、CopyOnWriteArrayList(写时复制、读无锁)、ConcurrentLinkedQueue(CAS 无界队列)。 设计原则:最小化锁粒度、读写分离(COW)、CAS 替代锁、不可变对象天然安全。 — ## 5. 如何避免死锁? 死锁四必要条件(缺一不可):互斥、持有并等待、不可抢占、循环等待。破坏任一即可。 避免方法: 1. 锁顺序一致:所有线程按相同顺序获取锁。 java // bad: 线程 A lock(a) → lock(b), 线程 B lock(b) → lock(a) // good: 统一 lock(a) → lock(b) 2. 锁超时ReentrantLock.tryLock(timeout, unit),超时释放已获锁。 3. 减少锁持有时间:缩小同步块,不在锁内做 IO/网络调用。 4. 使用无锁结构:CAS 原子类,ConcurrentHashMap。 5. 死锁检测jstack <pid> 查看线程 BLOCKED 状态和锁等待链;JConsole 死锁检测。 — ## 6. 如何解决ABA问题? ABA 问题:线程 1 读 A,线程 2 将 A→B→A,线程 1 的 CAS 仍成功但中间状态已被改变。 解决方案: + 版本号/时间戳AtomicStampedReference<V>,每次更新 stamp+1。 java AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0); int stamp = ref.getStamp(); ref.compareAndSet(100, 200, stamp, stamp + 1); + 标记位AtomicMarkableReference<V>,布尔标记(简化版)。 Java 中 AtomicIntegerAtomicLong 无 ABA 防护,需版本号时用 AtomicStampedReference。 — ## 7. synchronized 关键字的作用是什么? synchronized 是 Java 内置的互斥同步机制,保证同一时刻只有一个线程执行同步块。 三种用法java // 1. 实例方法锁(锁 this 对象) public synchronized void method() {} // 2. 静态方法锁(锁 Class 对象) public static synchronized void staticMethod() {} // 3. 同步代码块(锁指定对象) synchronized (obj) { /* 临界区 */ } 实现原理:编译后在同步块前后插入 monitorentermonitorexit 指令,每个对象关联一个 Monitor(监视器锁)。计数器记录重入次数,退出时计数器-1。 锁升级过程(JDK 6+优化):无锁 → 偏向锁 → 轻量级锁(CAS 自旋) → 重量级锁(OS 互斥量)。逐步升级不可降级,基于竞争度自适应。 — ## 8. volatile 关键字的作用是什么? 同第 3 题。补充核心点: 内存语义: + volatile 写:JMM 将该线程本地内存中的共享变量值刷新到主内存。 + volatile 读:JMM 将该线程本地内存置为无效,从主内存读取。 Happens-Before:volatile 写 happens-before 后续对该变量的读。结合传递性可实现简单的线程间通信。 java // 标志位模式 volatile boolean flag = false; // Thread A: flag = true; // 写 // Thread B: if (flag) {} // 读,可见 — ## 9. Java内存模型(JMM)是怎样的? JMM(Java Memory Model)定义多线程间共享变量的访问规则,规范 happens-before 关系。 核心抽象:每个线程有本地内存(缓存抽象),保存共享变量副本。线程间通过主内存通信。 Happens-Before 规则(8 条): 1. 程序次序:同一线程内,代码在前先于代码在后。 2. 锁规则:解锁 happens-before 后续加锁。 3. volatile 规则:volatile 写 happens-before 后续 volatile 读。 4. 传递性:A hb B, B hb C → A hb C。 5. 线程启动Thread.start() hb 线程内所有操作。 6. 线程终止:线程所有操作 hb Thread.join() 返回。 7. 中断规则interrupt() hb 被中断线程检测到中断。 8. 终结器规则:对象构造完成 hb finalize() 开始。 JMM 通过禁止特定重排序和强制主内存同步来保证可见性、有序性。 — ## 10. HashMap在多线程环境下使用需要注意什么?为什么? 严禁在多线程环境直接使用 HashMap,可能导致: 1. 死循环(JDK 7):扩容时 transfer() 使用头插法,并发 resize 可能形成环形链表,get() 陷入死循环,CPU 100%。 2. 数据丢失:put 操作并发冲突时覆盖。 3. size 不准确size++ 非原子操作。 解决:使用 ConcurrentHashMap。JDK 7 分段锁(Segment),JDK 8 CAS + synchronized 锁单个桶,并发度高。 — ## 11. Java程序中启动一个线程是用run()还是start()必须用 **start()run() 只是普通方法调用,在当前线程执行,不会创建新线程。start() 通知 JVM 创建新线程,该线程执行 run()java Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName())); t.run(); // 输出 "main"(当前线程) t.start(); // 输出 "Thread-0"(新线程) start() 只能调用一次,重复调用抛 IllegalThreadStateException。 — ## 12. 什么是守护线程?有什么用? 守护线程(Daemon Thread)是服务线程,当所有非守护线程结束时 JVM 自动退出,即使守护线程还在运行。thread.setDaemon(true) 设置,需在 start() 前调用。 **用途:GC 线程、后台定时任务、日志刷新、连接池心跳检测等。 注意:守护线程中 finally 块不保证执行(JVM 退出时直接终止),不要做重要资源释放。 — ## 13. 什么是死锁?如何避免? 详见第 5 题。补充示例: java // 死锁示例 Object lockA = new Object(), lockB = new Object(); new Thread(() -> { synchronized (lockA) { Thread.sleep(100); synchronized (lockB) { } // 等待 lockB } }).start(); new Thread(() -> { synchronized (lockB) { Thread.sleep(100); synchronized (lockA) { } // 等待 lockA } }).start(); — ## 14. 线程和进程的差别是什么? | | 进程 | 线程 | | — | — | — | | 定义 | 程序的一次执行,资源分配基本单位 | CPU 调度基本单位 | | 资源 | 独立地址空间、文件描述符 | 共享进程地址空间、文件 | | 通信 | IPC(管道、消息队列、共享内存、Socket) | 共享内存直接通信 | | 切换开销 | 大(切换页表、刷新 TLB) | 小(仅切换寄存器和栈) | | 独立性 | 一个进程崩溃不影响其他 | 一个线程崩溃可能导致整个进程崩溃 | | 创建开销 | 大 | 小 | Java 线程与 OS 线程 1:1 映射(主流 JVM 实现)。协程是用户态线程,M:N 映射,如 Kotlin 协程、Project Loom 的 Virtual Thread。 — ## 15. Java里面的ThreadLocal是怎样实现的? 每个 Thread 内部持有 ThreadLocalMap(自定义哈希表,Entry 使用弱引用引用 ThreadLocal key)。 java // ThreadLocal.set() public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // 获取当前线程的 Map if (map != null) map.set(this, value); // key = this(ThreadLocal对象), value = 值 else createMap(t, value); } 内存泄漏问题:Entry 的 key(ThreadLocal)是弱引用,GC 后可被回收,但 value 仍是强引用。若线程不结束(线程池),value 无法回收 → 内存泄漏。解决:使用后必须调用 remove()。 ThreadLocalMap 用线性探测法解决冲突(不是链表)。 — ## 16. ConcurrentHashMap的实现原理是什么? JDK 7:分段锁(Segment extends ReentrantLock),默认 16 段。每段内是 HashEntry 数组 + 链表。Segment 之间并发,段内互斥。 JDK 8(大改): + 数据结构:Node 数组 + 链表/红黑树,与 HashMap 一致。 + 并发控制:CAS + synchronized。 - put:若桶为空,CAS 尝试设置 Node;若桶非空,对该桶首个节点加 synchronized。 - 扩容:多线程协助数据迁移(transfer),通过 sizeCtl 协调。 + 不支持 null key/value(与 HashMap 不同)。 + 计算 size 不直接加锁,通过 CounterCell 数组分片计数 + CAS,避免竞争。 — ## 17. sleepwait的区别是什么? | | sleep | wait | | — | — | — | | 归属 | Thread 静态方法 | Object 实例方法 | | 锁 | 不释放锁 | 释放锁 | | 使用位置 | 任意地方 | 必须在 synchronized 块内 | | 唤醒 | 时间到自动唤醒 | notify()/notifyAll() 或时间到 | | 中断 | 抛 InterruptedException | 抛 InterruptedException | java synchronized (lock) { lock.wait(); // 释放 lock,线程进入 WAITING } Thread.sleep(1000); // 持有锁时 sleep 不会释放锁 — ## 18. notifynotifyAll的区别是什么? + notify():随机唤醒一个在 wait set 中等待的线程。 + notifyAll():唤醒所有等待线程。 推荐使用 **notifyAll(),除非确定只有一个等待线程或所有等待线程等价(否则 notify() 可能唤醒错误的线程,导致预期被唤醒的线程一直等待)。 必须在 synchronized 块内调用,否则抛 IllegalMonitorStateException。 — ## 19. volatile 关键字的作用是什么? (同第 3、8 题,此处为重复题目,答案一致) — ## 20. ThreadLocal 的作用与实现原理是什么? **作用:为每个线程提供独立的变量副本,实现线程隔离,避免参数传递。典型场景:数据库连接(Spring 事务管理)、Session 管理、日期格式化(SimpleDateFormat 非线程安全)。 实现原理同第 15 题。每个 Thread 内部持有 ThreadLocalMap,以 ThreadLocal 对象为 key 存储线程专属值。 — ## 21. 两个线程如何串行执行? java // 方式一:join() Thread t1 = new Thread(() -> { /* 任务1 */ }); Thread t2 = new Thread(() -> { /* 任务2 */ }); t1.start(); t1.join(); // 等待 t1 结束 t2.start(); t2.join(); // 方式二:单线程线程池 ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(task1); executor.submit(task2); executor.shutdown(); // 方式三:CountDownLatch CountDownLatch latch = new CountDownLatch(1); new Thread(() -> { task1(); latch.countDown(); }).start(); new Thread(() -> { latch.await(); task2(); }).start(); // 方式四:CompletableFuture CompletableFuture.runAsync(task1).thenRun(task2); — ## 22. 上下文切换是什么含义? CPU 从一个线程切换到另一个线程,需要保存当前线程状态(寄存器、程序计数器、栈指针)并恢复目标线程状态。频繁切换导致 CPU 大量时间用于切换而非执行任务。 开销来源:保存/恢复寄存器、TLB 刷新、Cache 污染、内核态切换。 减少上下文切换:无锁并发、CAS 算法、协程、合理线程池大小(CPU 核数 * 2CPU 核数 + 1)、避免不必要的线程创建。 — ## 23. 可以在运行时强制杀掉(kill)一个线程吗? 不可以。 Java 的 Thread.stop() 已被废弃(@Deprecated),因为它会立即释放所有锁,导致被保护的对象处于不一致状态(如写了一半的银行转账),且不保证 finally 块执行。 替代方案: + 协作式中断:用 interrupt() + 检查 isInterrupted()InterruptedExceptionjava while (!Thread.currentThread().isInterrupted()) { /* 工作 */ } + 标志位volatile boolean running = true;,外部设 false。 + Future.cancel(true):通过中断实现。 — ## 24. 什么是条件锁、读写锁、自旋锁、可重入锁? + 条件锁(Condition)Lock.newCondition(),支持多个等待队列,精确唤醒。如生产者-消费者场景用两个 Condition 分别管理 notFullnotEmpty。 + 读写锁(ReadWriteLock):读-读共享不互斥,读-写互斥,写-写互斥。适合读多写少。ReentrantReadWriteLock。 + 自旋锁:线程不挂起,循环尝试获取锁。适合临界区极小场景。JVM 中轻量级锁即自旋获取,默认自旋 10 次(可设)。 + 可重入锁:同一线程可重复获取已持有的锁。synchronizedReentrantLock 都支持。实现原理:记录持有线程 + 计数。 — ## 25. 什么是协程? 协程(Coroutine)是用户态线程,由程序自身调度而非 OS 内核调度。特点: + 轻量:创建开销极小(KB 级 vs MB 级线程栈)。 + 非抢占式调度:主动让出(yield),无上下文切换开销。 + 适合 IO 密集:高并发时避免线程阻塞和大量线程切换。 Java 的 Project Loom(JDK 21 正式发布 Virtual Thread):Thread.ofVirtual().start(task),阻塞 IO 时自动挂起协程释放 OS 线程,IO 完成后恢复。调度器使用 ForkJoinPool。 Kotlin 协程、Go goroutine、Python asyncio 都是协程实现。 — ## 26. ThreadPoolExecutor线程池的实现原理是什么? 核心参数:corePoolSize(核心线程)、maximumPoolSize(最大线程)、keepAliveTime(空闲存活时间)、workQueue(阻塞队列)、RejectedExecutionHandler(拒绝策略)。 执行流程: 1. execute(task) → 当前线程数 < corePoolSize:新建核心线程。 2. 核心线程满 → 任务入 workQueue。 3. 队列满 → 线程数 < maximumPoolSize:新建非核心线程。 4. 达到 maximumPoolSize → 执行拒绝策略(AbortPolicy 抛异常、CallerRunsPolicy 调用者执行、DiscardPolicy 丢弃、DiscardOldestPolicy 丢弃最旧)。 Worker 类实现 AQS,线程反复从队列取任务执行(getTask())。非核心线程空闲超 keepAliveTime 后销毁。 — ## 27. J.U.C包下常见类的使用。 Lock vs synchronized:Lock 支持公平锁、可中断获取、超时获取、多条件等待。synchronized 无需手动释放、JVM 优化好(锁升级)。优先 synchronized,需要高级特性时用 Lock。 BlockingQueue: + take() vs poll()take() 阻塞直到有元素;poll() 立即返回 null 或超时返回 null。 + put() vs offer()put() 阻塞直到有空间;offer() 立即返回 false 或超时。 原子类AtomicInteger/AtomicLong/AtomicBoolean(基本类型)、AtomicReference(引用)、AtomicStampedReference(版本号)、LongAdder(高并发统计,分片减少 CAS 竞争)。 — ## 28. False Sharing(伪共享)、Cache Line、可见性与原子性 + Cache Line(缓存行):CPU 缓存的最小单位(通常 64 字节)。CPU 按缓存行而不是单个变量读写内存。 + False Sharing(伪共享):两个不相关的变量落在同一缓存行,线程 A 修改变量 X 使整个缓存行失效,线程 B 访问变量 Y 时需重新加载,尽管 B 不关心 X。严重降低并发性能。 + 解决伪共享填充(Padding),在变量前后填充无用字段使其独占缓存行。Java 8 @Contended 注解(需 JVM 参数 -XX:-RestrictContended)。 java // 手动填充解决伪共享 public class PaddedCounter { // 前 56 字节填充 long p1, p2, p3, p4, p5, p6, p7; volatile long value = 0; // 后 56 字节填充 long p8, p9, p10, p11, p12, p13, p14; } 可见性:volatile、synchronized、Lock 保证。原子性:synchronized、Lock、CAS 原子类保证。 # Linux使用问题分析排查 ## 1. 硬链接和软链接的区别是什么? | | 硬链接 | 软链接(符号链接) | | — | — | — | | 本质 | 同一 inode 的多个文件名 | 独立文件,内容指向目标路径 | | 跨文件系统 | 不支持 | 支持 | | 跨目录 | 不能链接目录(防环) | 可以 | | 原文件删除 | 仍可访问(引用计数-1) | 失效(悬空链接) | | 命令 | ln source link | ln -s source link | 硬链接共享 inode,删除原文件只是引用计数减 1,数据不丢失。软链接类似 Windows 快捷方式。 — ## 2. inode是什么? inode(索引节点)是 Unix/Linux 文件系统存储文件元数据的数据结构,每个文件/目录对应一个 inode。 inode 包含:文件类型、权限、所有者、大小、时间戳、数据块指针(指向实际数据位置)。不包含文件名(文件名在目录项中)。 关键特性: + 文件系统 inode 数量固定(格式化时确定),inode 耗尽即使磁盘有空间也无法创建文件。 + 硬链接共享同一 inode。 + ls -i 查看 inode 号,df -i 查看 inode 使用率。 — ## 3. Linux常用命令有哪些? | 类别 | 命令 | | — | — | | 文件 | ls, cp, mv, rm, find, touch, cat, tail, head, less | | 文本 | grep, awk, sed, wc, sort, uniq, cut | | 权限 | chmod, chown, chgrp | | 进程 | ps, top, htop, kill, pkill, lsof | | 网络 | netstat, ss, ping, telnet, curl, wget, tcpdump, iftop | | 磁盘 | df, du, fdisk, mount, iostat | | 系统 | free, vmstat, uptime, uname, dmesg | | 压缩 | tar, gzip, zip, unzip | — ## 4. 怎么看一个Java线程的资源耗用? bash top -Hp <pid> # 查看该进程下所有线程的 CPU/内存占用 # 找到高 CPU 线程的 tid(十进制),转为十六进制 printf "%x\n" <tid> # 得到 nid(十六进制) jstack <pid> | grep -A 20 <nid> # 在 thread dump 中定位该线程的栈 也可使用 pidstat -t -p <pid> 查看线程级统计。 — ## 5. Load过高的可能性有哪些? Load Average 表示运行队列中进程数 + 不可中断睡眠进程数(等待 IO)的平均数。 Load 高原因: + CPU 密集型:计算任务多,CPU 利用率高。 + IO 密集型:大量磁盘/网络 IO,进程处于 D 状态(不可中断睡眠),Load 高但 CPU 可能不高。 + 线程数过多:上下文切换频繁,sys CPU 高。 + 锁竞争:大量线程 BLOCKED 等待锁。 + 内存不足:频繁 swap,IO 飙升。 排查:top(看 us/sy/wa)、vmstat(r 列运行队列、b 列阻塞)、iostat -x(磁盘 util)、pidstat。 — ## 6. /etc/hosts 文件有什么作用? 本地静态主机名到 IP 的映射表,DNS 解析前优先查询。格式:IP 主机名 别名plain 127.0.0.1 localhost 192.168.1.10 db-server 用途:本地开发绑定域名、屏蔽广告(指向 127.0.0.1)、集群节点互信配置。优先级由 /etc/nsswitch.confhosts: files dns 决定(files 在前则先查 hosts)。 — ## 7. /etc/resolv.conf 文件有什么作用? DNS 客户端配置文件,指定域名解析服务器。 plain nameserver 8.8.8.8 nameserver 114.114.114.114 search example.com options timeout:2 attempts:3 nameserver 指定 DNS 服务器(最多 3 个),search 指定域名搜索后缀,options 设置超时和重试。注意:NetworkManager 可能动态覆盖此文件。 — ## 8. 如何快速地将一个文本中所有的”abc”替换为”Xyz”? bash sed -i 's/abc/Xyz/g' filename.txt # -i 原地修改,s 替换,g 全局(一行内所有匹配) 若含特殊字符需转义:sed -i 's/\/path\/a/\/path\/b/g'。也可用 perl -i -pe 's/abc/Xyz/g' 支持正则。 — ## 9. 你常用的Linux下用来进行网络和磁盘I/O分析的工具有哪些? + 网络tcpdump(抓包)、wireshark(图形化抓包)、ss/netstat(连接状态)、iftop(实时流量)、iperf(带宽测试)、curl/ab/wrk(压测)。 + 磁盘 I/Oiostat -x(磁盘利用率、await)、iotop(进程级 IO)、dstat(综合)、blktrace(块层追踪)、fio(IO 性能基准测试)。 — ## 10. 你常用的Linux下用来进行内存和CPU分析的工具有哪些? + CPUtop/htop(实时)、mpstat(多核)、pidstat(进程/线程)、perf(性能剖析)、vmstat(系统整体)。 + 内存free -h(概览)、top(进程级)、vmstat(swap)、pmap(进程内存映射)、slabtop(内核 slab 缓存)。 — ## 11. 发现磁盘空间不够,如何快速找出占用空间最大的文件? bash df -h # 查看各挂载点使用率 du -sh /* 2>/dev/null # 根目录各子目录大小 du -h /path | sort -rh | head -20 # 找出最大的 20 个目录 find / -type f -size +100M -exec ls -lh {} \; # 大于 100M 的文件 ncdu /path # 交互式磁盘分析工具 — ## 12. Java服务端问题排查(OOM、CPU高、Load高、类冲突等) OOM-XX:+HeapDumpOnOutOfMemoryError 自动 dump,MAT 分析大对象、GC Roots 引用链。 CPU 高top -Hp <pid> 找高 CPU 线程 → 转十六进制 → jstack 定位代码栈。 Load 高vmstat 看 r/b 列,iostat -x 看磁盘,pidstat 看上下文切换(cswch/s、nvcswch/s)。 类冲突jstack/jmap -histo 看加载的类,-verbose:class 打印类加载,arthassc/classloader 命令。 工具链:Arthas(在线诊断神器)、jstack、jmap、jstat、jcmd、MAT、VisualVM。 — ## 13. Java常用问题排查工具及用法 | 工具 | 用途 | | — | — | | top | 实时进程/线程 CPU、内存 | | iostat | 磁盘 IO 利用率、await | | vmstat | 进程、内存、swap、IO、CPU 整体 | | sar | 历史系统统计(CPU/内存/IO/网络) | | tcpdump | 网络抓包分析 | | jvisualvm | 图形化监控、线程、堆、采样 | | jmap | 堆 dump、对象统计、堆配置 | | jconsole | JMX 图形化监控 | bash jmap -dump:format=b,file=heap.hprof <pid> # 导出堆 jmap -histo:live <pid> | head -20 # Top 20 对象 jstat -gcutil <pid> 1000 # GC 统计 — ## 14. Thread dump文件如何分析? jstack <pid> > thread.dump 生成线程快照。 关键字段plain "http-nio-8080-exec-1" #15 daemon prio=5 os_prio=0 tid=0x... nid=0x4a2b runnable java.lang.Thread.State: RUNNABLE at com.example.Service.process(Service.java:42) at ... 分析要点: + Runnable:运行中或就绪。若大量 Runnable 且 CPU 高,定位热点代码。 + BLOCKED:等待锁。看 waiting to lock <0x...>,结合持有锁的线程定位死锁。 + WAITING/TIMED_WAITING:等待条件(wait/join/park)。 + nid:操作系统线程 ID(十六进制),与 top -Hp 对应。 + 死锁检测jstack 末尾会打印 Found one Java-level deadlock。 — ## 15. grepawksed 的使用,是否自己写过Shell脚本? grep:文本搜索。grep -r "error" /var/log/grep -v "DEBUG" filegrep -E "a|b"(扩展正则)。 awk:文本处理语言,按列处理。awk -F',' '{sum+=$3} END {print sum}' data.csvawk '$1 > 100 {print $0}'sed:流编辑器。sed 's/old/new/g'(替换)、sed -n '10,20p'(打印行)、sed '/pattern/d'(删除行)。 Shell 脚本示例(批量重启服务): bash #!/bin/bash for svc in nginx mysql redis; do if ! systemctl is-active --quiet $svc; then echo "$svc is down, restarting..." systemctl restart $svc fi done — ## 16. 常见的CPU Load过高、us(用户态CPU)过高,一般是什么问题? us 高(用户态 CPU):应用代码消耗 CPU,通常是: + 死循环 / 无限递归 + 频繁 GC(对象创建过多) + 复杂计算(加密、压缩、正则回溯) + 大量序列化/反序列化 排查:top -Hp <pid> 找线程 → jstack 定位 → jstat -gcutil 看 GC 频率。 sy 高(内核态 CPU):系统调用频繁,如大量上下文切换、网络 IO、锁竞争。 wa 高(IO 等待):磁盘瓶颈,进程等待 IO 完成。 — ## 17. 常见的内存问题一般有哪些? + 堆内存溢出:对象过多/泄漏,-Xmx 不足。 + 内存泄漏:静态集合、ThreadLocal 未 remove、未关闭资源。 + 频繁 Full GC:老年代增长快、元空间不足、大对象分配。 + swap 使用:物理内存不足,频繁换页,性能骤降。 + 堆外内存泄漏:DirectByteBuffer、Netty 堆外内存、JNI 分配。 排查:free -h(物理内存)、top(RES 列)、jmap -histo(对象分布)、jstat -gc(GC 统计)、pmap(进程内存映射)。 # 框架使用 ## 1. Spring中Bean的生命周期是怎样的? 1. 实例化:反射调用构造器创建 Bean 实例(未填充属性)。 2. 属性填充(Populate):依赖注入(@Autowired、setter、构造器)。 3. Aware 接口回调BeanNameAwareBeanFactoryAwareApplicationContextAware 等。 4. BeanPostProcessor 前置处理postProcessBeforeInitialization。 5. 初始化@PostConstructInitializingBean.afterPropertiesSet() → 自定义 init-method。 6. BeanPostProcessor 后置处理postProcessAfterInitialization(AOP 代理在此生成)。 7. 就绪:Bean 可使用。 8. 销毁:容器关闭时 @PreDestroyDisposableBean.destroy() → 自定义 destroy-method。 — ## 2. SpringMVC或Struts处理请求的流程是怎样的? SpringMVC 流程: 1. 请求到达 DispatcherServlet(前端控制器)。 2. HandlerMapping 查找对应 Handler(Controller 方法)。 3. HandlerAdapter 执行 Handler(参数绑定、数据校验)。 4. 执行返回 ModelAndView。 5. ViewResolver 解析视图。 6. 渲染视图返回响应。 Struts2 流程:请求 → StrutsPrepareAndExecuteFilterActionMappingActionProxyAction 执行 → Result 渲染。基于 Filter + 值栈(OGNL),每个请求创建 Action 实例(非单例)。 — ## 3. Spring AOP解决了什么问题?是怎么实现的?AOP与CGLIB、ASM的关系是什么? 解决的问题:横切关注点(日志、事务、安全、监控)与业务逻辑解耦,避免代码重复。 实现原理:动态代理。 + JDK 动态代理:目标类实现接口时,基于 InvocationHandler 生成接口代理。 + CGLIB:目标类无接口时,继承目标类生成子类,重写方法(不能代理 final 类/方法)。 与 ASM 关系:CGLIB 底层使用 ASM(字节码操作框架)在运行时生成子类字节码。ASM 是更底层的字节码操作库,Spring、CGLIB、Lombok 都依赖它。 织入时机:Spring AOP 是运行时织入(代理);AspectJ 支持编译时/类加载时织入(更强大,无代理开销)。 — ## 4. Spring事务的传播属性是怎么回事?它会影响什么? 传播行为定义方法间事务如何传播(已存在事务时如何处理)。 | 传播行为 | 行为 | | — | — | | REQUIRED(默认) | 有则加入,无则新建 | | REQUIRES_NEW | 挂起当前事务,新建独立事务 | | SUPPORTS | 有则加入,无则以非事务执行 | | NOT_SUPPORTED | 挂起当前事务,非事务执行 | | MANDATORY | 必须存在事务,否则抛异常 | | NEVER | 必须无事务,否则抛异常 | | NESTED | 嵌套事务(保存点,可部分回滚) | 影响:事务边界、隔离级别、回滚范围。如 REQUIRES_NEW 内层事务独立提交,外层回滚不影响内层;NESTED 内层回滚到保存点,外层回滚内层也回滚。 — ## 5. Spring中BeanFactory和FactoryBean有什么区别? + BeanFactory:Spring 容器根接口,负责 Bean 的创建、配置、管理。ApplicationContext 是其子接口(增强:国际化、事件、AOP 等)。 + FactoryBean:一种特殊的 Bean,本身是工厂,用于创建复杂对象(如 MyBatis 的 SqlSessionFactoryBean、整合第三方框架)。getObject() 返回实际对象,getObjectType() 返回类型,isSingleton() 是否单例。 java public class MyFactoryBean implements FactoryBean<MyService> { public MyService getObject() { return new MyService(); } public Class<?> getObjectType() { return MyService.class; } public boolean isSingleton() { return true; } } // 容器中 getBean("myFactoryBean") 得到 MyService 实例 // getBean("&myFactoryBean") 得到 FactoryBean 本身 — ## 6. Spring框架中IOC的原理是什么? IOC(Inversion of Control,控制反转):对象创建和依赖管理交给容器,而非硬编码在类中。 原理: 1. 配置读取:XML / 注解 / Java Config 定义 Bean 定义(BeanDefinition)。 2. BeanFactory 启动:扫描、解析配置,注册 BeanDefinition 到容器。 3. 实例化getBean() 时反射创建实例(懒加载或预实例化)。 4. 依赖注入:通过构造器、setter、字段反射注入依赖(三级缓存解决循环依赖)。 5. 生命周期管理:Aware 回调、初始化、销毁。 DI(依赖注入)是 IOC 的具体实现方式。 — ## 7. Spring的依赖注入有哪几种方式? 1. 构造器注入(推荐):不可变对象、必填依赖、便于测试。 java public UserService(UserRepository repo) { this.repo = repo; } 2. Setter 注入:可选依赖、可重新配置。 java @Autowired public void setRepo(UserRepository repo) { this.repo = repo; } 3. 字段注入@Autowired 直接标注字段。简洁但不利于测试(无法 final、反射注入)。 4. 接口注入(已淘汰):实现特定接口由容器回调注入。 Spring 4.3+ 单构造器可省略 @Autowired。 — ## 8. Struts的工作流程是怎样的? 1. 客户端请求 → StrutsPrepareAndExecuteFilter(核心过滤器)。 2. ActionMapping 根据 URL 匹配 Action 配置。 3. ActionProxy 创建,通过 ConfigurationManager 读取配置。 4. 经过拦截器栈(参数封装、校验、国际化等)。 5. 调用 Actionexecute() 方法。 6. 根据返回值(SUCCESS/INPUT 等)由 Result 渲染视图(JSP/JSON)。 7. 响应返回客户端。 Struts2 基于 XWork 命令模式,每个请求创建新 Action 实例(非单例),通过 OGNL 表达式访问值栈。 — ## 9. 用Spring如何实现一个切面? java @Aspect @Component public class LogAspect { // 切点:com.example.service 包下所有方法 @Pointcut("execution(* com.example.service..*(..))") public void serviceLayer() {} // 前置通知 @Before("serviceLayer()") public void before(JoinPoint jp) { System.out.println("调用: " + jp.getSignature()); } // 环绕通知 @Around("serviceLayer()") public Object around(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); System.out.println("耗时: " + (System.currentTimeMillis() - start)); return result; } // 异常通知 @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex") public void afterThrowing(Exception ex) { System.err.println("异常: " + ex.getMessage()); } } 启用:@EnableAspectJAutoProxy(Java Config)或 <aop:aspectj-autoproxy/>(XML)。 — ## 10. Spring如何实现数据库事务? 基于 AOP + 事务管理器(PlatformTransactionManager)java @Service public class UserService { @Transactional( propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, timeout = 30 ) public void transfer(Long from, Long to, BigDecimal amount) { accountMapper.decrease(from, amount); accountMapper.increase(to, amount); } } 原理@Transactional 生成代理,方法执行前开启事务(TransactionInterceptor),成功提交,异常回滚。底层通过 DataSourceTransactionManager 绑定 Connection 到 ThreadLocal,保证同一事务使用同一连接。 — ## 11. Hibernate和MyBatis这类ORM框架的区别是什么?什么是ORM,解决的痛点是什么? ORM(Object-Relational Mapping):将关系型数据库表映射为面向对象模型,解决对象-关系阻抗失配(对象有继承/关联,表是扁平结构)。 Hibernate(全自动 ORM): + 全映射,HQL 面向对象查询,自动生成 SQL。 + 一级/二级缓存,脏检查自动更新。 + 学习曲线陡,复杂 SQL 难优化,N+1 问题。 MyBatis(半自动 SQL 映射): + 手写 SQL(XML/注解),灵活可控,易优化。 + 结果集自动映射为对象。 + 无二级缓存(需集成),无 HQL。 选择:复杂查询/高性能用 MyBatis;快速 CRUD/领域模型复杂用 Hibernate/JPA。 — ## 12. Spring IOC的生命周期是怎样的? 详见第 1 题。补充初始化顺序: @PostConstructInitializingBean.afterPropertiesSet()init-method 三者都执行,顺序如上。@PreDestroyDisposableBean.destroy()destroy-method 为销毁顺序。 — ## 13. Hibernate对一二级缓存的使用,以及对Lazy-Load的理解? 一级缓存(Session 级):默认开启,同一 Session 内重复查询同一对象只查一次。生命周期绑定 Session。 二级缓存(SessionFactory 级):跨 Session 共享,需显式配置(Ehcache、Redis)。适合读多写少、非实时数据。 查询缓存:缓存查询结果集(需二级缓存配合)。 Lazy-Load(延迟加载):关联对象默认延迟加载(如 @OneToMany(fetch = LAZY)),访问时才发 SQL。解决 N+1 问题,但Session 关闭后访问延迟属性抛 **LazyInitializationException。解决:Open Session in View 模式、DTO 投影、fetch join 预加载。 — ## 14. 不依赖Spring,自己用代码如何实现IOC和AOP? **IOC 实现java public class SimpleContainer { private Map<String, Object> beans = new HashMap<>(); public void register(String name, Object bean) { beans.put(name, bean); } public Object getBean(String name) { return beans.get(name); } // 依赖注入:扫描 @Autowired 字段,反射赋值 public void inject() throws Exception { for (Object bean : beans.values()) { for (Field f : bean.getClass().getDeclaredFields()) { if (f.isAnnotationPresent(Autowired.class)) { f.setAccessible(true); f.set(bean, beans.get(f.getType().getSimpleName())); } } } } } AOP 实现:JDK 动态代理 + InvocationHandler,在 invoke() 中织入前置/后置逻辑。 — ## 15. RPC的负载均衡、服务发现是怎么做的? 服务发现: + 注册中心(ZooKeeper/etcd/Consul/Nacos/Eureka)保存服务实例列表。 + 服务启动时注册,下线时注销(心跳保活)。 + 消费者订阅变更,本地缓存实例列表。 负载均衡: + 客户端负载均衡(Ribbon/Dubbo):消费者从本地列表选节点。算法:随机、轮询、加权轮询、一致性 Hash、最小活跃数。 + 服务端负载均衡(Nginx/LVS):服务端统一入口分发。 健康检查:心跳、TCP 探活、失败剔除。 — ## 16. 几种推送模型的区别是什么? | 模型 | 原理 | 实时性 | 缺点 | | — | — | — | — | | 轮询(Polling) | 客户端定时请求 | 差(取决于间隔) | 浪费带宽、延迟高 | | 长轮询(Long Polling) | 请求挂起直到有数据/超时 | 较好 | 连接占用、需重连 | | SSE(Server-Sent Events) | 服务端单向推送(HTTP) | 好 | 单向(仅服务端→客户端) | | WebSocket | 全双工长连接 | 最好 | 实现复杂、需心跳保活 | | HTTP/2 Server Push | 服务端主动推资源 | 好 | 仅推送关联资源 | 选型:实时聊天/游戏用 WebSocket;通知/行情用 SSE 或长轮询;普通数据用轮询。 # 数据库相关 ## 1. MySQL InnoDB的特点? + 事务支持:ACID,行级锁,MVCC(多版本并发控制),支持 READ COMMITTED 和 REPEATABLE READ(默认)。 + 行级锁:锁粒度小、并发高。共享锁(S)、排他锁(X)、间隙锁(Gap Lock)、临键锁(Next-Key Lock),解决幻读。 + MVCC:通过 undo log 实现,每个事务看到快照版本,读不阻塞写、写不阻塞读。 + B+树聚簇索引:主键索引叶子存储完整行数据,二级索引叶子存储主键值(回表)。 + 崩溃恢复:Redo Log(物理日志,崩溃恢复)+ Undo Log(逻辑日志,回滚 + MVCC)+ Double Write Buffer(防止页断裂)。 + 自适应哈希索引:热点数据页自动在 Buffer Pool 中建立哈希索引加速。 — ## 2. 乐观锁和悲观锁的区别是什么? | | 悲观锁 | 乐观锁 | | — | — | — | | 假设 | 冲突频繁,每次操作都加锁 | 冲突少,操作时不加锁,提交时检查 | | 实现 | SELECT ... FOR UPDATE,行锁 | 版本号/CAS:UPDATE SET x=x+1, version=version+1 WHERE version=? | | 适用 | 写冲突多、持锁时间长 | 读多写少、冲突少 | | 性能 | 锁等待,吞吐低 | 无锁等待,失败重试成本低 | | 死锁 | 可能 | 不可能 | 示例sql -- 悲观锁:事务 A 锁住行,事务 B 等待 SELECT stock FROM product WHERE id=1 FOR UPDATE; UPDATE product SET stock=stock-1 WHERE id=1; -- 乐观锁: UPDATE product SET stock=stock-1, version=version+1 WHERE id=1 AND version=@currentVersion; -- 受影响行数为 0 说明冲突,需重试 — ## 3. 数据库隔离级别是什么?有什么作用? 事务隔离级别用于解决并发事务带来的问题: | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 | | — | — | — | — | — | | READ UNCOMMITTED | 是 | 是 | 是 | 不加锁 | | READ COMMITTED | 否 | 是 | 是 | MVCC 快照读,每次语句读最新 | | REPEATABLE READ(MySQL默认) | 否 | 否 | 部分(InnoDB 临键锁解决) | MVCC + Next-Key Lock | | SERIALIZABLE | 否 | 否 | 否 | 共享锁,事务串行 | 脏读:读到未提交事务的数据。不可重复读:同一事务两次读取结果不同(被其他事务 update)。幻读:同一事务两次读取结果集行数不同(被其他事务 insert)。 — ## 4. MySQL主备同步的基本原理是什么? 基于 Binlog 的主从复制: 1. Master:事务提交时,将变更记入 Binlog。 2. Slave:I/O 线程连接 Master,拉取 Binlog 写入 Relay Log(中继日志)。 3. Slave:SQL 线程重放 Relay Log 中的事件。 Binlog 格式: + STATEMENT:记录 SQL 语句(非确定性函数如 NOW() 不安全)。 + ROW:记录每行变更(精确但日志大)。 + MIXED:混合模式(默认 Statement,必要时 Row)。 半同步复制:Master 等待至少一个 Slave 确认收到 Binlog 后返回客户端,减少数据丢失。GTID:全局事务 ID,简化主从切换。 — ## 5. 如何从一张表中查出name字段包含”XYZ”的所有行? sql SELECT * FROM table_name WHERE name LIKE '%XYZ%'; 注意 LIKE '%...%' 无法使用普通索引(前缀模糊),大数据量性能差。优化:Elasticsearch 全文索引、MySQL 5.7+ InnoDB FULLTEXT 索引配合 MATCH ... AGAINST。 — ## 6. 索引的数据结构是什么? MySQL InnoDB 使用 B+树B+树特性: + 所有数据在叶子节点,非叶子仅作索引(减少层高)。 + 叶子节点形成有序双向链表,支持范围查询。 + 单个节点大小 = 一页(16KB),高扇出,3-4 层可存储千万级数据。 与 B 树区别:B 树非叶子也存数据,范围查询需中序遍历;B+树范围查询沿叶子链表顺序扫描,磁盘 IO 更少。 字典树(Trie):字符串索引场景(前缀匹配、搜索建议),非 MySQL 默认,可在 Elasticsearch 中通过前缀查询实现。 — ## 7. 如何优化数据库性能? 多维优化: + SQL 优化:加索引(explain 看执行计划)、避免 SELECT 、分页优化(延迟关联)、批量操作(foreach → INSERT INTO ... VALUES (...), (...))、避免在索引列用函数。 + 索引:覆盖索引避免回表、联合索引最左前缀、避免无效索引。 + 架构:分库分表(ShardingSphere)、主从读写分离、缓存(Redis)。 + 硬件:SSD、加大内存(Buffer Pool 越大越好)。 + 连接池:HikariCP(高性能)、合理设置连接数。 + 业务优化:减少不必要的查询、异步处理、限流降级。 慢 SQL 排查slow_query_logEXPLAIN(type=ALL 全表扫描需关注、key_len、rows) → OPTIMIZER TRACE 查看优化器决策。 — ## 8. SQL在什么情况下不会使用索引? + 索引列参与运算:WHERE age + 1 = 20 + 索引列使用函数:WHERE DATE(create_time) = '2024-01-01' + 模糊查询左匹配WHERE name LIKE '%abc' + 隐式类型转换:WHERE phone = 13800138000(phone 是 varchar,MySQL 会将字符串转数字导致索引失效) + !=<>NOT INNOT EXISTS:负向查询通常不会走索引 + OR 条件中其中一个条件无索引:全表扫描 + 联合索引不满足最左前缀 + 优化器判断全表扫描更快(数据量少、回表代价大) — ## 9. 一般在什么字段上建立索引? + WHERE 条件高频字段:过滤频繁的字段。 + ORDER BY / GROUP BY 字段。 + JOIN 关联字段。 + 高选择性字段(区分度高):如身份证号(unique率高),而非性别(区分度低不合适作为独立索引)。 + 覆盖索引:查询列全在索引中,避免回表。 原则:选择性 = COUNT(DISTINCT col) / COUNT(*) 越大越好。避免在小表、频繁更新表建过多索引。 — ## 10. 如何从一张表中查出name字段不包含”XYZ”的所有行? sql SELECT * FROM table_name WHERE name NOT LIKE '%XYZ%'; 同样 NOT LIKE 无法使用索引。对 NULL 值需额外注意:NOT LIKE '%XYZ%' 不匹配 NULL,如需包含:WHERE name NOT LIKE '%XYZ%' OR name IS NULL。 — ## 11. MySQL中B+树索引的实现机制、行锁的实现机制,以及SQL优化策略。 B+树索引:同一页内记录按主键排序,页间通过指针连接。主键索引聚簇存储(叶子存整行),二级索引叶子存主键值(需回表,覆盖索引除外)。页分裂时需移动数据,建议主键自增整数减少分裂。 行锁实现:锁加在索引记录上(不是行数据上)。若查询不走索引,退化为表锁。行锁类型:Record Lock(索引记录锁)、Gap Lock(间隙锁,防幻读)、Next-Key Lock(前两者组合,范围锁)。 SQL 优化策略: + 使用 EXPLAIN 分析,关注 type(range/ref 好,ALL 差)、rows(扫描行数)、Extra(Using filesort/Using temporary 需优化)。 + 大分页 LIMIT 100000, 20 → 改为延迟关联:SELECT * FROM t JOIN (SELECT id FROM t LIMIT 100000,20) tmp ON t.id=tmp.id。 + Count 优化:COUNT(*) 走辅助索引,MyISAM 直接取统计值,InnoDB 需扫描。 — ## 12. Redis的RDB和AOF持久化机制是什么?如何实现高可用和集群? RDB(快照):定期把内存数据 dump 到磁盘(fork 子进程写 .rdb 文件)。恢复快、文件紧凑,但可能丢失最近数据。命令:SAVE(阻塞)/ BGSAVE(后台)。 AOF(追加日志):每次写操作追加日志(appendonly.aof),重写压缩。三种策略:always(每条 fsync,最安全)、everysec(每秒,平衡)、no(OS 决定)。数据安全但文件大、恢复慢。 混合持久化(Redis 4.0+):AOF 重写时,将 RDB 格式写入 AOF 文件头部 + 后续增量 AOF。兼具两者优点。 高可用: + 主从复制:读写分离,异步复制。 + 哨兵(Sentinel):监控、自动故障转移、通知。 + 集群(Cluster):16384 槽位分布式,数据分片,自动故障转移,无中心节点。 — ## 13. 如何解决高并发场景下的减库存问题? 核心问题:超卖。 方案: 1. 数据库乐观锁UPDATE product SET stock=stock-1 WHERE id=? AND stock >= 1,利用行锁保护。简单但数据库压力大。 2. Redis 原子操作DECR key(原子),或 Lua 脚本原子性检查+扣减。性能高但需处理 Redis 与 DB 数据一致。 3. Redis + 消息队列异步落库:先 Redis 扣减,MQ 异步同步到 DB。最终一致性。 4. 分段库存:将 1000 库存分为 10 段,每段 100,减少锁竞争。 防止超卖关键:检查扣减必须是原子操作,不能先查再扣(非原子)。 — ## 14. MySQL存储引擎中索引的实现机制是什么? (同第 6、11 题)补充: MyISAM:非聚簇索引,数据文件和索引文件分离。索引叶子存储数据行地址InnoDB:聚簇索引(主键索引),叶子存整行数据。建议使用递增主键(避免频繁页分裂)。二级索引叶子存主键值,需回表。 — ## 15. 数据库事务的隔离级别有哪些? 同第 3 题。补充 InnoDB 各隔离级别默认加锁行为: + READ COMMITTED:仅 Record Lock,无 Gap Lock(可能幻读)。 + REPEATABLE READ:Next-Key Lock(Record + Gap),解决幻读(InnoDB 默认)。 — ## 16. 行锁与表锁的区别?乐观锁与悲观锁的区别? 行锁 vs 表锁: + 行锁:InnoDB 支持,锁特定行,并发高但开销大。需命中索引,否则退化为表锁。 + 表锁:MyISAM 默认,LOCK TABLES 或 InnoDB 索引失效时。并发低但开销小。 乐观锁/悲观锁见第 2 题。 # 网络协议与网络编程 ## 1. TCP建立连接的过程是怎样的?(三次握手) plain Client Server | SYN, seq=x → | 1. 客户端请求建立连接 | ← SYN, ACK, seq=y, ack=x+1 | 2. 服务端确认并请求建立连接 | ACK, seq=x+1, ack=y+1 → | 3. 客户端确认 为什么是三次不是两次:防止旧的重复连接请求导致服务端创建无用连接。若两次握手,SYN 延迟到达服务端时,服务端直接 ESTABLISHED,而客户端已经放弃,造成资源浪费。 为什么不是四次:当没有数据要发送时,服务端可将 SYN + ACK 合并为一次传输。 — ## 2. TCP断开连接的过程是怎样的?(四次挥手) plain Client Server | FIN, seq=x → | 1. 客户端:我没有数据要发了 | ← ACK, ack=x+1 | 2. 服务端:收到(可能还有数据在处理) | ← FIN, seq=y | 3. 服务端:我也没有数据要发了 | ACK, seq=x+1, ack=y+1 → | 4. 客户端:确认收到 TIME_WAIT(2MSL):主动关闭方在发送最后一次 ACK 后等待 2MSL。目的:①确保最后的 ACK 被对方收到;②让网络上延迟的旧 TCP 段过期消失。 — ## 3. 浏览器发生302跳转背后的逻辑是什么? 302 是 HTTP 临时重定向状态码(307 严格意义不改变方法)。 流程: 1. 浏览器请求 http://example.com/old。 2. 服务器返回 302 Found + Location: http://example.com/new。 3. 浏览器自动向新 URL 发起 GET 请求。 302 vs 301:301 永久重定向(浏览器缓存,SEO 权重转移),302 临时重定向(浏览器不缓存,下次仍请求原 URL)。 影响:302 可能导致 POST 变为 GET(浏览器实现差异),307 严格保持原方法。 — ## 4-6. (重复题目,同1-3) — ## 7. HTTP协议的交互流程是怎样的?HTTP和HTTPS的差异是什么?SSL的交互流程是怎样的? HTTP 交互:客户端发送 Request(Method + URL + Headers + Body),服务端返回 Response(Status Code + Headers + Body)。无状态协议。 HTTPS = HTTP + TLS/SSL: + 数据加密传输,防窃听。 + 身份认证(CA 证书),防伪装。 + 数据完整性校验,防篡改。 TLS 握手流程(简化): 1. ClientHello:支持的加密套件 + 随机数。 2. ServerHello:选择加密套件 + 随机数 + 证书。 3. 客户端验证证书,生成 Pre-Master Secret,用服务器公钥加密发送。 4. 双方根据三个随机数生成会话密钥(对称加密密钥,如 AES)。 5. 后续通信使用对称加密(效率高)。 — ## 8. REST和HTTP是什么关系?大家都说REST很轻量,你对REST风格如何理解? REST(Representational State Transfer)是一种架构风格,HTTP 是实现 REST 的传输协议(不是唯一实现,但最常用)。 REST 六大原则: 1. Client-Server:关注点分离。 2. 无状态:每个请求包含所有信息,服务器不保存客户端状态。 3. 可缓存:响应标记可缓存/不可缓存。 4. 统一接口:URL 表示资源,HTTP 动词表示操作。 5. 分层系统:可加代理/网关/负载均衡。 6. 按需代码(可选):传输可执行代码。 RESTful 设计GET /users/123(查)、POST /users(增)、PUT /users/123(改)、DELETE /users/123(删)。资源用名词复数,而非动词。 — ## 9. TCP的滑动窗口协议有什么用?讲讲原理。 滑动窗口实现流量控制,防止发送方发送过快导致接收方缓冲区溢出。 原理: + 接收方在 ACK 中带上窗口大小(rwnd),告知发送方可发送的数据量。 + 发送方维护发送窗口,只能发送窗口内的数据。 + 窗口根据 ACK 向右滑动(已确认的数据移出,新数据进入窗口)。 plain 发送窗口 = [已发送未确认 | 可发送未发送 | 不可发送] ← 窗口大小 → 与拥塞控制的区别:滑动窗口是端到端流量控制,拥塞控制(慢启动、拥塞避免)是网络层面的控制。 — ## 10. HTTP协议都有哪些方法? | 方法 | 作用 | 幂等 | 安全 | | — | — | — | — | | GET | 获取资源 | 是 | 是 | | POST | 创建资源 | 否 | 否 | | PUT | 整体替换 | 是 | 否 | | PATCH | 部分修改 | 否 | 否 | | DELETE | 删除 | 是 | 否 | | HEAD | 获取响应头(无 Body) | 是 | 是 | | OPTIONS | 查询支持的方法(CORS 预检) | 是 | 是 | | TRACE | 回显请求(调试) | 是 | 是 | | CONNECT | 建立隧道(代理) | 否 | 否 | — ## 11. 交换机和路由器的区别是什么? | | 交换机 | 路由器 | | — | — | — | | 工作层 | 数据链路层(L2) | 网络层(L3) | | 寻址方式 | MAC 地址 | IP 地址 | | 转发依据 | MAC 地址表 | 路由表 | | 广播域 | 隔离冲突域,不隔离广播域 | 隔离广播域 | | 典型用途 | 局域网内设备互联 | 连接不同网络/广域网 | 三层交换机具备部分路由功能(基于 IP 转发)。 — ## 12. 什么是VLAN?有什么作用? VLAN(Virtual LAN,虚拟局域网):将物理局域网逻辑分割为多个广播域。同一 VLAN 内设备可以通信,不同 VLAN 间通信需三层设备(路由器/三层交换机)。 作用: + 隔离广播域,减少广播风暴。 + 提高安全性,不同部门的流量隔离。 + 灵活网络管理,设备物理移动无需改配置。 + 802.1Q 标准在以太帧头插入 VLAN Tag(12 位 VLAN ID,0-4095)。 — ## 13. 什么是VXLAN?有什么作用? VXLAN(Virtual eXtensible LAN):Overlay 网络技术,用 UDP 封装 L2 帧在 L3 网络上传输。VNI(24 位,支持 16M 个虚拟网络)远超 VLAN 的 12 位(4096)。 作用: + 突破 VLAN 数量限制(云计算多租户)。 + 跨数据中心二层互通(大二层网络)。 + VM 迁移时保持 IP/MAC 不变。 + 典型实现:Flannel、Calico(容器网络)。 — ## 14. HTTP协议详解:报文结构、断点续传、多线程下载、什么是长连接? 报文结构plain Request: GET /index.html HTTP/1.1 Host: www.example.com Accept: text/html ← 空行 [Body] Response: HTTP/1.1 200 OK Content-Type: text/html Content-Length: 1234 ← 空行 <html>...</html> 断点续传Range: bytes=500-999(请求指定范围),服务端返回 206 Partial Content + Content-Range: bytes 500-999/2000多线程下载:每个线程请求不同 Range(如线程 1: 0-499, 线程 2: 500-999),并发下载后合并。 长连接(Keep-Alive):HTTP/1.1 默认,一个 TCP 连接发送多个 HTTP 请求,减少握手开销。Connection: keep-alive。 — ## 15. TCP协议详解:建连过程、慢启动、滑动窗口、七层模型。 建连:三次握手(见第 1 题)。 慢启动:拥塞控制算法。初始 cwnd=1,每收到 ACK 指数增长(cwnd *= 2),直到达到 ssthresh 或丢包,进入拥塞避免阶段(线性增长)。 滑动窗口:见第 9 题。 OSI 七层:物理层→数据链路层→网络层→传输层→会话层→表示层→应用层。TCP/IP 四层:网络接口层→网际层→传输层→应用层。 — ## 16. WebService协议(WSDL/SOAP格式),与REST协议的区别是什么? | | SOAP WebService | REST | | — | — | — | | 协议 | 基于 XML 的 SOAP 协议 | 基于 HTTP 的资源 | | 描述 | WSDL(Web Service Description Language) | 无标准描述(OpenAPI/Swagger 可选) | | 传输 | HTTP/SMTP/TCP 等 | 仅 HTTP(S) | | 格式 | 仅 XML | JSON/XML/任何格式 | | 复杂性 | 重(SOAP 信封、WSDL 定义) | 轻(简单 HTTP 请求) | | 安全性 | WS-Security 内置 | 依赖 HTTPS + OAuth 等 | 现代 API 首选 REST(JSON),遗留系统/企业集成中仍有 SOAP。 — ## 17. 是否了解SPDY/HTTP2.0协议? HTTP/2(基于 Google SPDY): + 二进制分帧:取代文本协议,解析更高效。 + 多路复用:一个连接同时发送多个请求/响应(Stream 并发),无队头阻塞。 + 头部压缩:HPACK 算法,减少重复头部带宽消耗。 + 服务端推送:Server Push,预推资源。 + 流优先级:可指定请求优先级。 HTTP/3 基于 QUIC(UDP),进一步解决 TCP 队头阻塞,0-RTT 建连。 — ## 18. NIO的好处是什么?Netty线程模型是怎样的?什么是零拷贝? NIO 好处:非阻塞 + 多路复用,单线程管理海量连接,避免操作系统线程创建/切换开销。 Netty 线程模型: + BossGroup(通常 1 线程):接受连接,注册到 WorkerGroup。 + WorkerGroup(默认 CPU 核数2):处理 IO 读写,通过 Pipeline(责任链模式)处理编解码、业务逻辑。 + 每个 Channel 绑定一个 EventLoop 线程,避免线程安全问题。 零拷贝: + 内存映射MappedByteBuffer,文件直接映射到内存,无需用户态拷贝。 + sendfileFileChannel.transferTo(),数据从磁盘直接发送到网卡(内核态传输,减少 2 次上下文切换 + 2 次拷贝)。 + Netty 的 CompositeByteBuf:逻辑组合多个 ByteBuf,无需物理拷贝。 # Redis等缓存系统/中间件/NoSQL/一致性Hash等 ## 1 & 4. 列举一个常用的Redis客户端的并发模型。 Jedis:同步阻塞,每个连接一个物理 Socket。与线程池配合(JedisPool),连接非线程安全。简单但并发有限,适合低并发场景。 Lettuce(Spring Boot 2.0+ 默认):基于 Netty,异步非阻塞。单个连接可被多个线程共享(线程安全),支持同步/异步/响应式(Reactive)API。高并发场景更优。 Redisson:基于 Netty,提供分布式对象(分布式锁、队列、Map 等),偏重于分布式服务层抽象。 — ## 2 & 5. HBase如何实现模糊查询? HBase 是列式存储数据库,基于 RowKey 有序存储,不支持原生 SQL 模糊查询实现方式: + RowKey 前缀扫描Scan + PrefixFilter(RowKey 前缀匹配),利用字典序排序,效率高。 + SingleColumnValueFilter + 正则:对列值做正则匹配,但需要全表扫描该列,性能差。 + 二级索引 + Phoenix/Solr:Phoenix 提供 SQL 层和索引,Cloudera Search 集成 Solr 做全文索引。 + Elasticsearch 同步:HBase 数据通过协处理器(Coprocessor)同步到 ES,由 ES 负责模糊搜索。 — ## 3 & 6. 列举一个常用的消息中间件,如果消息要保序如何实现? Kafka:分布式发布-订阅消息系统。 保序实现: + Kafka 分区内有序(partition 内保证写入顺序 = 读取顺序)。 + 确保相同 key 的消息路由到同一分区。如订单消息 key=orderId。 + 单线程消费每个分区。 + 全局有序:只用一个 partition,牺牲并发能力。 + RocketMQ 支持全局顺序消息(OrderlyMessage)和分区顺序消息。 — ## 7. 如何实现一个Hashtable?你的设计如何考虑Hash冲突?如何优化? java public class SimpleHashTable<K, V> { private Node<K, V>[] table; private int size; private static final float LOAD_FACTOR = 0.75f; static class Node<K, V> { K key; V value; Node<K, V> next; } public void put(K key, V value) { int idx = (key.hashCode() & 0x7fffffff) % table.length; // 链表头插/尾插 for (Node<K,V> e = table[idx]; e != null; e = e.next) { if (e.key.equals(key)) { e.value = value; return; } } table[idx] = new Node<>(key, value, table[idx]); size++; if (size > table.length * LOAD_FACTOR) resize(); } } 冲突解决: + 链表法(分离链接)→ 长链表转红黑树(JDK 8 HashMap)。 + 开放地址法(线性探测/平方探测),ThreadLocal 使用。 + 再哈希法。 优化: + 扰动函数减少冲突:h ^ (h >>> 16)。 + 容量为 2 的幂,(n-1) & hash 替代取模。 + 动态扩容 + 渐进式 rehash(Redis)。 — ## 8. 分布式缓存中,一致性Hash的原理是什么? 问题:普通 hash(key) % N 在节点增减时,大部分缓存失效(大量请求穿透到 DB)。 一致性 Hash: + 将哈希空间组织为环形(0~2^32-1)。 + 计算节点 hash 分布在环上。 + key 的 hash 沿环顺时针查找第一个节点。 + 节点增减只影响相邻节点,减少缓存失效。 虚拟节点:每物理节点对应多个虚拟节点(如 150 个),均匀分布在环上,避免数据倾斜。 应用:Redis Cluster 的 16384 槽位(按槽映射而非直接 Hash 环,但思想类似)。Dubbo 一致性 Hash 负载均衡。 — ## 9. LRU算法、Slab分配机制是什么?如何减少内存碎片? LRU(Least Recently Used):最近最少使用淘汰策略。实现:HashMap + 双向链表,O(1) 访问/淘汰。Redis 使用近似 LRU(采样淘汰)。 LFU(Least Frequently Used):最少使用频次淘汰,Redis 4.0 引入。 Slab 分配器:Memcached 使用。预先分配不同大小的内存块(slab class),对象按大小存入对应 class,减少外部碎片。但会产生内部碎片(分配块大于实际对象)。 减少内存碎片jemalloc(Redis 默认内存分配器)、tcmalloc(Google 支持,多线程高效)。使用对象池复用对象(Go sync.Pool)。 — ## 10. 如何解决缓存单机热点问题? 热点 key:某些 key 被高并发频繁访问,单机成为瓶颈。 解决: + 多副本:热点 key 复制多份到不同节点,key 加上随机后缀(hotkey_0, hotkey_1…),客户端随机读取。 + 本地缓存:热点数据缓存在 JVM 内存(Caffeine/Guava Cache),设置短过期时间。结合 MQ 广播失效。 + 读写分离:热点数据从 Slave 读。 + 限流降级:热点 key 保护性限流,返回默认值。 — ## 11. 什么是布隆过滤器(Bloom Filter)?其实现原理是什么? 布隆过滤器是概率型数据结构,高效判断元素是否在集合中。 原理: + 长度为 m 的位数组 + k 个哈希函数。 + 添加:对元素计算 k 个哈希值,将对应位置 1。 + 查询:k 个位全为 1 → 可能存在(可能误判);任一位为 0 → 一定不存在。 特性: + 空间效率极高(1 亿数据约 120MB)。 + 有误判(False Positive):说不存在一定不存在,说存在可能不存在。 + 不可删除(Counting Bloom Filter 支持)。 应用:Redis 缓存穿透防护(不存在的数据不去查 DB)、Google Bigtable、爬虫 URL 去重。 — ## 12. Memcached与Redis的区别是什么? | | Memcached | Redis | | — | — | — | | 数据类型 | 纯 K-V(String) | String/List/Set/ZSet/Hash/Stream 等 | | 持久化 | 不支持 | RDB + AOF | | 集群 | 客户端一致性 Hash | Redis Cluster(服务端) | | 线程模型 | 多线程 | 单线程(6.0+ IO 多线程) | | 内存管理 | Slab | jemalloc | | 事务/发布订阅 | 不支持 | 支持 | | 性能 | 略高(纯 K-V) | 场景更丰富 | | 过期策略 | 惰性删除 | 惰性 + 定期删除 | 选择:简单 K-V 缓存用 Memcached;需要丰富数据结构、持久化、高可用用 Redis。 — ## 13. ZooKeeper有什么功能?其选举算法是如何进行的? ZK 功能: + 分布式协调:分布式锁、配置中心、服务发现、选主。 + 数据模型:节点树(ZNode),支持 Watcher 事件监听。 + 一致性:ZAB(原子广播)协议,保证顺序一致性。 Leader 选举(Fast Leader Election): 1. 每节点投票给自己(myid, zxid)。 2. 各节点广播投票,收到比自身更大的(zxid 优先,再比较 myid),改投。 3. 反复直到过半节点投给同一节点 → 当选 Leader。 — ## 14. MapReduce的过程是怎样的?如何用MapReduce实现两个数据源的联合统计? 过程: 1. Input Split:切分输入文件。 2. Map:并行处理每个 split,输出 (key, value) 对。 3. Shuffle:按 key 分区排序,发送到对应 Reducer。 4. Reduce:聚合同一 key 的所有 value。 两表 Join(如订单表 + 用户表统计): + Map 端 Join:小表广播到所有 Mapper 内存(Distributed Cache)。 + Reduce 端 Join:Map 输出 key=joinKey,value 标记来源(tag)。Reducer 中合并。 # 设计模式与重构 ## 1. 你在设计一个工厂类的包时会遵循哪些原则? + 单一职责:一个工厂负责一类产品创建。 + 开闭原则:新增产品类型不修改已有代码。 + 依赖倒置:高层不依赖低层,都依赖抽象。 + 接口隔离:工厂接口精简,不强迫客户端依赖不需要的方法。 + 命名规范XxxFactory,方法 create()/getInstance()。 — ## 2. 你能列举一个使用了Visitor或Decorator模式的开源项目/库吗? + Visitor:ASM 字节码框架的 ClassVisitorMethodVisitor。遍历字节码结构,不同访问者做不同操作(生成代理、插入埋点)。 + Decorator:Java IO:BufferedReader(Reader)InputStreamReader(InputStream) 层层装饰。Servlet Filter 链(FilterChain)。 — ## 3. 你在编码时最常用的设计模式有哪些?在什么场景下使用? + 单例:配置管理、连接池。 + 工厂方法/抽象工厂:创建复杂对象、解耦创建逻辑。 + 策略:替换算法(不同支付、折扣计算)。 + 模板方法:框架(Spring JdbcTemplate、AbstractApplicationContext)。 + 观察者:事件驱动(Spring Event、MQ 消费)。 + 代理:AOP、RPC 调用代理。 + 责任链:Filter 链、Servlet Filter、Netty Pipeline。 + 适配器:兼容旧接口。 — ## 4. 如何实现一个单例? 见第 6 题(详细代码在下方)。 — ## 5. 代理模式(动态代理)的原理是什么? JDK 动态代理Proxy.newProxyInstance() + InvocationHandler。运行时生成实现接口的代理类字节码($ProxyN),所有方法调用转发到 InvocationHandler.invoke()CGLIB 动态代理:生成目标类的子类,重写方法。MethodInterceptor.intercept() 拦截。Spring 中目标类无接口时使用。 — ## 6. 单例模式详解:懒汉模式、饿汉模式、并发初始化如何解决? java // 饿汉式:类加载时实例化,线程安全 public class EagerSingleton { private static final EagerSingleton INSTANCE = new EagerSingleton(); private EagerSingleton() {} public static EagerSingleton getInstance() { return INSTANCE; } } // 懒汉式 DCL(双重检查锁定) public class LazySingleton { private volatile static LazySingleton instance; // volatile 防重排序 private LazySingleton() {} public static LazySingleton getInstance() { if (instance == null) { // 第一次检查(无锁) synchronized (LazySingleton.class) { if (instance == null) { // 第二次检查(持锁) instance = new LazySingleton(); } } } return instance; } } // 静态内部类(推荐):懒加载 + 线程安全(JVM 保证) public class InnerClassSingleton { private InnerClassSingleton() {} private static class Holder { static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); } public static InnerClassSingleton getInstance() { return Holder.INSTANCE; } } // 枚举单例(最安全,反序列化/反射无法破坏) public enum EnumSingleton { INSTANCE; public void doSomething() {} } — ## 7. JDK源码里面有哪些让你印象深刻的设计模式使用? + 单例Runtime.getRuntime()System.console()。 + 工厂Calendar.getInstance()Executors.newFixedThreadPool()。 + 建造者StringBuilderStream.Builder。 + 装饰器:IO 流:BufferedInputStream(new FileInputStream())。 + 适配器Arrays.asList()(数组 → List)。 + 模板方法AbstractListInputStream.read()。 + 迭代器Iterator(所有 Collection)。 + 观察者java.util.ObservableEventListener。 + 代理java.lang.reflect.Proxy。 — ## 8. Reactor模式是什么? Reactor 模式是事件驱动的 IO 多路复用模型。 角色: + Reactor:注册/分发事件(Selector)。 + Handler:处理具体事件(Accept、Read、Write)。 模式: + 单 Reactor 单线程:Redis 网络模型。 + 单 Reactor 多线程:Handler 中 IO 处理 + 业务线程池。 + 主从 Reactor 多线程:Netty 的 Boss + Worker EventLoopGroup。MainReactor 负责 accept,SubReactor 负责 IO 读写。 Java NIO 的 Selector 就是 Reactor 模式的实现。 # 学习与进取心 & 抗压能力 & 稳定性考察 (此部分为主观问答,无标准答案。以下提供参考思路。) Q: 平时关注哪些技术? 关注云原生(K8s/Docker)、微服务(Spring Cloud)、高并发架构、中间件源码阅读等。 Q: 看哪些技术博客? InfoQ、美团技术博客、阿里技术、ByteByteGo、Stack Overflow、GitHub Trending。 Q: 看过哪些技术书籍? 《深入理解Java虚拟机》(周志明)、《Java并发编程实战》、《高性能MySQL》、《设计数据密集型应用》(DDIA)、《Effective Java》、《领域驱动设计》。 Q: 最近研究什么新技术? Project Loom 虚拟线程、GraalVM 原生编译、Rust 在基础设施领域的应用、Quarkus 框架。 Q: 如何管理项目/团队? 敏捷 Scrum、任务拆解 WBS、定期回顾、技术分享、知识文档沉淀。 Q: 如何看待加班? 支持必要时的冲刺加班(上线、故障恢复),但反对常态化加班,应通过技术优化提效。 Q: 接手历史模块? 先熟悉业务+架构文档,读测试用例理解行为,逐步重构(先加测试→小重构→验证),不要一次性大改。 # 开放性问题 ## 1. 大文件4G,内存256M,如何排序? 外部排序(External Sort): 1. 分块:将 4G 文件分成 N 个小块,每块能放入内存(如 200M/块)。 2. 块内排序:每块读入内存,使用快速排序/归并排序后写回磁盘。 3. 多路归并:从每块读一小部分到内存(输入缓冲区),建最小堆,每次输出最小值到最终文件。 若数据量大、IO 压力大,可结合置换-选择排序生成更长初始归并段。也可使用 sort 命令(GNU sort 内部实现了外部排序)。 — ## 2. 硬盘坏了会发生什么?程序如何处理? 场景:应用写日志/数据到坏盘 → IO 异常/超时 → 该节点不可用。 处理: + 高可用设计:多副本(RAFTr)、多节点集群,单点故障不中断服务。 + 健康检查:定期检测磁盘状态(SMART)。 + 异常处理:捕获 IOException,记录告警,自动切换备机。 + 数据持久化:关键数据写日志(Raft Log、Kafka),不依赖本地持久化。 + 冗余:数据库主从、Redis 哨兵、多数据中心。 — ## 3. 实现一个消息队列系统。 核心模块: + Broker:接收、存储、分发消息。文件顺序写 + 内存映射(如 Kafka partition segment)。 + Producer:附带主题和 key,负载均衡到分区。 + Consumer:拉模式(长轮询)或推模式。偏移量管理(内存或外部存储)。 + 存储:CommitLog(顺序追加)+ ConsumeQueue(索引),定期清理过期日志。 + 高可用:主从复制、ISR(Kafka)、Raft(RocketMQ DLedger)。 简化实现:LinkedBlockingQueue 做内存队列 + 文件追加做持久化。 — ## 4. 如何设计一个高可用的架构? + 无状态服务:水平扩展,前置负载均衡(Nginx/LVS)。会话外置(Redis)。 + 数据层高可用:主从+自动故障转移。读写分离。 + 多机房/多活:异地多活(数据同步冲突处理)。 + 限流降级熔断:Sentinel/Hystrix。 + 监控告警:Prometheus + Grafana + ELK。 + 发布:灰度发布、蓝绿部署、回滚能力。 — ## 5. 多次Hash解决URL重复访问问题。 布隆过滤器(见 Redis 第 11 题)。多个哈希函数降低误判率。 — ## 6. 全局唯一ID问题。 方案: + UUID:简单但有重复概率,128 位,非递增(影响数据库索引)。 + Snowflake(雪花算法):64 位 = 时间戳(41 位)+ 机器 ID(10 位)+ 序列号(12 位)。单调递增、高性能。 + 数据库自增auto_increment,单点瓶颈。 + Redis INCR:原子递增。 + 美团 Leaf百度 UidGenerator:分布式 ID 生成器。 — ## 7. 秒杀如何设计? + 前端:静态化 CDN,按钮防重(置灰),验证码。 + 网关层:限流(令牌桶)。 + 服务层:Redis 预减库存(原子 Lua 脚本),MQ 削峰异步下单。 + 数据库:乐观锁扣库存 UPDATE ... WHERE stock >= 1。 + 隔离:秒杀独立部署,不冲击主业务。 + 数据一致性:Redis 扣减成功才入 MQ,MQ 消费失败重试+补偿。 — ## 8. 如何进行性能优化? + 测量先行:压测(JMeter/wrk),找瓶颈,不可凭感觉优化。 + 代码:算法优化、减少锁竞争、批量操作、缓存热点数据。 + 数据库:索引优化、SQL 优化、读写分离、缓存。 + 网络:CDN、长连接复用、压缩(Gzip/Brotli)。 + JVM:GC 调优、堆内存合理分配。 + 架构:异步化、MQ 削峰、无状态水平扩展。 — ## 9. 发现CPU 100%,如何排查? 1. top 找到高 CPU 的 Java 进程 PID。 2. top -Hp <pid> 找高 CPU 的线程 ID(tid)。 3. printf "%x\n" <tid> 转十六进制。 4. jstack <pid> | grep -A 30 <nid> 定位线程栈。 5. 分析:死循环、频繁 GC、正则回溯、无限递归、HashMap 并发死循环等。 — ## 10. 实现一个分布式打点系统。 + SDK:异步批量上报(本地缓存 + 定时/满量 flush)。 + 网关:接收请求,负载均衡。 + 消息队列:Kafka 缓冲削峰。 + 实时计算:Flink/Storm 实时聚合(PV/UV/分维度统计)。 + 存储:时序数据库(InfluxDB/Prometheus)+ OLAP(ClickHouse)。 + 展示:Grafana 仪表盘。 — ## 11. taobao.com和tmall.com的互相登录问题。 单点登录(SSO): + CAS 协议:统一认证中心,获取 Ticket,跳转验证。 + OAuth 2.0 / OpenID Connect:类似微信扫码登录。 + JWT:共享密钥,各子域名验证 Token 签名。 + Cookie + 父域共享Set-Cookie: .taobao.com 同域下共享(tmall.com 和 taobao.com 是不同域,不适用)。 + 实际方案:存储在 *.taobao.com 下统一认证,taobao.com 和 tmall.com 通过跳转统一登录页实现。 — ## 12. 如何快速对一个2亿数据的List进行排序? 若数据在内存中,多线程排序: java // ForkJoinPool 并行排序 int[] array = new int[200_000_000]; Arrays.parallelSort(array); 若超越内存:外部排序(归并排序,见第 1 题)。分批排序 + 多路归并。 # 笔试题 ## 1. Java部分 ### 1.1 equals==的区别? 详见 Java基础第 7 题。 ### 1.2 写出一个程序,接受一个字符串,反转后输出。 java public class StringReverse { public static void main(String[] args) { Scanner sc = new Scanner(System.in); String input = sc.nextLine(); // 方式一:StringBuilder String reversed = new StringBuilder(input).reverse().toString(); System.out.println(reversed); // 方式二:手动反转 char[] chars = input.toCharArray(); int i = 0, j = chars.length - 1; while (i < j) { char tmp = chars[i]; chars[i] = chars[j]; chars[j] = tmp; i++; j--; } System.out.println(new String(chars)); } } ### 1.3 常见HTTP状态码和Content-Type 常见状态码: + 2xx 成功:200 OK、201 Created、204 No Content + 3xx 重定向:301 永久、302 临时、304 Not Modified(缓存) + 4xx 客户端错误:400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、405 Method Not Allowed + 5xx 服务端错误:500 Internal Server Error、502 Bad Gateway、503 Service Unavailable、504 Gateway Timeout 常见 Content-Type: + application/json:JSON 格式,键值对用双引号 + application/x-www-form-urlencoded:表单格式,key=value&key2=value2,特殊字符 URL 编码 + multipart/form-data:文件上传,boundary 分隔 + text/html:HTML 文档 + application/octet-stream:二进制流下载 application/json vs application/x-www-form-urlencoded:JSON 支持嵌套结构,JSON 用 POST Body 传输;Form 编码仅扁平键值对,可放在 URL Query String 或 POST Body。 ### 1.4 Java中的Math.round(-1.5)等于多少? -1Math.round() 规则:正数四舍五入,负数向 0 方向舍入(Math.floor(x + 0.5))。 + Math.round(1.5) = 2 + Math.round(-1.5) = -1 + Math.round(-1.6) = -2 ### 1.5 CAS自旋锁的优缺点是什么? 优点: + 不需要线程挂起/唤醒(避免内核态切换)。 + 适合锁持有时间极短的场景(< 1µs)。 + 无死锁问题。 缺点: + CPU 空转浪费(高竞争下更严重)。 + 不适合锁持有时间长的场景。 + ABA 问题。 + 仅保证单个变量原子性。 + 非公平(可能线程饥饿)。 ## 2. SQL部分 表结构: + Student(stuId, stuName, stuAge, stuSex) + Course(courseId, courseName, teacherId) + Scores(stuId, courseId, score) + Teacher(teacherId, teacherName) ### 2.1 查询姓”李”的老师的个数。 sql SELECT COUNT(*) AS count FROM Teacher WHERE teacherName LIKE '李%'; ### 2.2 查询平均成绩大于60分的同学的学号和平均成绩。 sql SELECT stuId, AVG(score) AS avg_score FROM Scores GROUP BY stuId HAVING AVG(score) > 60; 说明WHERE 过滤原始行,HAVING 过滤聚合结果。此处必须用 HAVING 而非 WHERE,因为需要对聚合后的 AVG(score) 做条件判断。 ## 3. Linux部分 ### 3.1 如何找到 /tmp 目录及子目录下是否包含一个 error.log 文件? bash find /tmp -name "error.log" -type f -type f 仅查找普通文件(排除目录),-type d 查找目录。 ### 3.2 如何实现Linux服务器之间的文件传输? bash # SCP(SSH 协议,安全) scp file.txt user@host:/path/ scp -r dir/ user@host:/path/ # 递归传输目录 # RSYNC(增量同步,支持断点续传,常用于备份) rsync -avz file.txt user@host:/path/ rsync -avz --progress /src/ user@host:/dest/ # SFTP(交互式) sftp user@host > put file.txt > get remote_file.txt # 使用 nc (netcat) 快速传输 # 接收端: nc -l 1234 > file.txt # 发送端: nc host 1234 < file.txt ## 4. 业务部分 ### 4.1 有了解或者做过XXX相关的系统吗? (需根据实际经验回答) — ## 5. 方向部分 ### 5.1 你觉得自己在项目中遇到的挑战有哪些?是如何克服的? (需根据实际经验回答。示例:高并发瓶颈 → 引入缓存+异步+分库分表;代码质量 → 推行 Code Review + 单测覆盖 + SonarQube。) ### 5.2 你觉得自己的抗压能力怎么样? (需根据实际情况回答)