JVM垃圾回收器:CMS与G1详解
面试中常被问及JVM的垃圾回收器,其中CMS(Concurrent Mark-Sweep) 和 G1(Garbage-First) 是两个经典且重要的收集器。
CMS的设计目标是获取最短的垃圾收集停顿时间,它主要分为四个阶段:初始标记、并发标记、重新标记、并发清除。其“并发”特性体现在大部分工作线程可以与垃圾收集线程同时运行,但这也带来了缺点,例如对CPU资源敏感、无法处理“浮动垃圾”,以及收集结束后会产生内存碎片。
G1收集器则面向服务端应用,旨在替代CMS。它将堆内存划分为多个大小相等的独立区域(Region),并优先回收价值最高(即垃圾最多)的区域,这也是其名称的由来。G1的运作过程同样复杂,包括初始标记、并发标记、最终标记、筛选回收等阶段。与CMS相比,G1能提供更可预测的停顿时间模型,并最终进行压缩整理,有效避免了内存碎片问题。
Java并发工具包(JUC)深度探讨
除了最常被问到的 ConcurrentHashMap,Java并发工具包(JUC)内容非常丰富。它主要包含以下几大部分:
- 锁机制:如
ReentrantLock、StampedLock、ReadWriteLock 等,提供了比synchronized关键字更灵活、功能更强大的锁控制。
- 并发集合:除了
ConcurrentHashMap,还有 CopyOnWriteArrayList、ConcurrentLinkedQueue、BlockingQueue 的各种实现(如 ArrayBlockingQueue、LinkedBlockingQueue)等,它们是线程安全的高性能集合。
- 原子类:
AtomicInteger、AtomicReference 等,通过CAS(Compare-And-Swap)操作实现无锁的线程安全编程。
- 并发工具类:最核心的当属 线程池(
ThreadPoolExecutor),此外还有用于控制并发线程数量的 Semaphore、用于线程间同步的 CountDownLatch 和 CyclicBarrier 等。
线程池的核心使用与实践
在实际项目中,创建线程池主要有两种方式:
- 通过
Executors 工厂类创建(如 newFixedThreadPool, newCachedThreadPool)。
- 直接通过
ThreadPoolExecutor 的构造函数手动创建。
阿里巴巴Java开发手册明确不推荐使用 Executors 来创建线程池,主要是因为其预设的几种方式存在潜在风险。例如,newFixedThreadPool 和 newSingleThreadExecutor 使用的等待队列是无界的 LinkedBlockingQueue,可能导致任务堆积耗尽内存;newCachedThreadPool 和 newScheduledThreadPool 允许创建的线程数量为 Integer.MAX_VALUE,可能创建大量线程耗尽CPU和内存资源。因此,手动配置 ThreadPoolExecutor 的七个核心参数是更佳实践:
corePoolSize:核心线程数
maximumPoolSize:最大线程数
keepAliveTime:空闲线程存活时间
unit:时间单位
workQueue:任务队列
threadFactory:线程工厂
handler:拒绝策略
网络I/O模型:多路复用技术
I/O多路复用是一种高效的网络I/O模型,它允许单个线程监听多个文件描述符(如Socket)的读写事件。常见的实现有 select、poll 和 epoll。
- select/poll:本质上都是轮询机制,需要遍历所有被监控的文件描述符来检查事件是否就绪。当连接数很大时,性能线性下降。
select 有文件描述符数量限制(通常1024),poll 使用链表结构则没有此限制。
- epoll:是Linux下高性能的多路复用实现。它采用了事件通知机制,通过
epoll_ctl 注册文件描述符和关心的事件,当事件就绪时,内核会通过 epoll_wait 直接返回就绪的事件列表,避免了无效的遍历,效率不随连接数增加而显著下降。
Redis数据结构与应用
Redis不仅支持五种基础数据结构,还提供了一些特殊类型,使其能应对更复杂的场景。
五大基础数据结构:
- String(字符串):最简单的键值对,可用于缓存、计数器等。
- Hash(哈希):适合存储对象,如用户信息。
- List(列表):双向链表,可实现消息队列、最新列表等。
- Set(集合):无序、元素唯一的集合,适用于共同关注、抽奖等。
- Zset(有序集合):在Set基础上为每个元素关联一个分数(score),可用于排行榜、带权重的队列。
三种特殊数据结构:
- Bitmaps(位图):通过位操作实现二值状态统计,如用户签到。
- HyperLogLog:用于基数统计(估算不重复元素数量),占用内存极小。
- Geospatial(地理位置):存储地理位置信息,并进行距离计算、范围查找等。
缓存一致性解决方案
保证缓存(如Redis)与数据库(如MySQL)的数据一致性是一个经典问题。没有银弹,通常需要在一致性强度、性能和复杂度之间权衡。常见的策略有:
- Cache Aside Pattern(旁路缓存模式):最常用的模式。读时先读缓存,缓存没有则读库并回写缓存;更新时先更新数据库,再删除缓存(而非更新)。此模式可能产生短时间的数据不一致,但实现简单。
- Read/Write Through(读写穿透):应用将缓存作为主要数据源,缓存层负责自己与数据库的同步。对应用透明,但实现复杂。
- Write Behind/Back Caching(异步写回):更新时只更新缓存,缓存异步批量写回数据库。性能最好,但存在数据丢失风险。
在实际的数据库与中间件应用中,通常会结合消息队列或订阅数据库Binlog等方式,来异步刷新或删除缓存,以最终达成一致性。
消息队列Kafka的核心机制
如何保证消息消费顺序?
Kafka保证的是分区(Partition)内的消息有序,而非整个主题(Topic)有序。因此,要保证某一类消息的顺序消费,需要将这类消息都发送到同一个分区。这通常通过为消息指定相同的Key来实现,因为Kafka默认的分区器会根据Key的哈希值将消息分配到特定分区。
Kafka高性能的奥秘
Kafka的极高性能得益于其多方面的精妙设计,存储层设计是核心之一:
- 顺序读写:Kafka将消息持久化到磁盘,但采用了追加写入(Append-only) 的方式,充分利用了磁盘顺序读写速度远快于随机读写的特性。
- 零拷贝(Zero-Copy):在消费者读取数据时,Kafka利用操作系统的
sendfile 系统调用,将数据直接从磁盘文件(PageCache)发送到网络Socket,避免了内核缓冲区与用户缓冲区之间的多次拷贝,极大减少了CPU开销和上下文切换。
- 页缓存(PageCache):Kafka重度依赖操作系统的页缓存来存储数据,而不是在JVM堆内维护缓存。这减少了GC压力,并且读操作可以直接命中页缓存,速度极快。
- 分区与并行:Topic被分为多个分区,散布在多个Broker上,生产与消费都可以并行处理,水平扩展能力极强。
- 批量处理:生产者支持批量发送消息,消费者也支持一次拉取一批消息,有效减少了网络I/O次数。
这些设计,尤其是在大数据流处理场景下,共同造就了Kafka的高吞吐量。
RPC基础概念
RPC(Remote Procedure Call,远程过程调用) 是一种计算机通信协议,允许程序调用位于另一个地址空间(通常是网络上的另一台机器)的子程序或服务,而无需显式编码远程调用的细节。调用者感知不到调用的方法是本地的还是远程的。其简单原理通常包括:客户端存根(Stub)序列化参数并发送请求,服务端存根接收请求、反序列化参数、调用本地方法,再将结果序列化返回给客户端。常见的RPC框架有gRPC、Dubbo、Thrift等。
|