线性探测与链地址法的剖析

为什么我们需要两种冲突解决策略? 哈希表的核心矛盾在于:哈希函数将无限的定义域映射到有限的地址空间。根据鸽巢原理(Pigeonhole Principle),冲突是必然的。于是,计算机科学家们在"时间换空间“和”空间换时间“的永恒博弈中,诞生了两大流派: 开放定址法(Open Addressing):所有元素都存储在数组本身,冲突时寻找其他空位——原地消化。 链地址法(Separate Chaining):数组存储指针,冲突的元素在数组外形成链表——外部延展。 这两者看似只是实现细节的差异,实则反映了对内存模型、缓存行为、并发控制、最坏情况保证等底层哲学的不同理解。 一、线性探测(Linear Probing):极简主义的得与失 1.1 算法精要 线性探测的规则极其简单: 插入: h(k), h(k)+1, h(k)+2, ... (mod m) 查找: 同插入序列,直到找到或遇到空位 删除: 标记为"墓碑"(DELETED),不能物理清空 关键洞察:线性探测的查找路径是确定性的,这意味着它拥有完美的CPU缓存预取特性(Prefetching),在现代CPU架构下,连续内存访问的速度比指针跳转快一个数量级。 1.2 主聚集(Primary Clustering)的数学本质 这是线性探测最致命的缺陷,也是理解其性能天花板的钥匙。 假设负载因子为 α(α = n/m),当插入一个新元素时,它在某个位置发生冲突的概率不是简单的 α,而是该位置所在的连续占用块的长度占比。这个正反馈机制导致: 一旦形成长度为 L 的连续占用块,新元素落入该块的概率为 L/m 落入后,块长度变为 L+1,进一步增加未来冲突概率 最终导致占用块像雪崩一样快速增长 数学结论(Knuth 的经典分析): 成功查找的平均探测次数 ≈ 0.5 * (1 + 1/(1-α)) 插入的平均探测次数 ≈ 0.5 * (1 + 1/(1-α)^2) 当 α = 0.9 时,插入需要平均约 50.5 次探测,性能崩溃。这也是为什么线性探测要求负载因子严格控制在 0.7 以下。 1.3 删除与墓碑的隐藏代价 线性探测的删除必须使用墓碑,这带来三重悲剧: 空间污染:墓碑占用的位置无法被新数据直接覆盖(需要判断是否允许覆盖,复杂化逻辑) 查询退化:大量的墓碑会导致查找遍历过长的已删除序列 需要周期性重建(Rehash):当墓碑比例超过阈值(如 30%),需要重新插入所有存活元素 // 墓碑导致的查找效率下降示意 [ A ] [ 墓碑 ] [ 墓碑 ] [ 墓碑 ] [ B ] [ 空 ] // 查找 B:需要跳过 3 个墓碑,即使它们是空的,线性探测依然要检查 1.4 线性探测的"真香"场景 尽管有上述缺陷,线性探测在工业界依然被广泛使用,原因在于: ...

2026-07-11 05:00:18 PM · 2 分钟

Docker Healthcheck 万金油

模板一:网络服务万金油(只要有网络端口,通杀) 如果容器是 Web 服务(如 Nginx、Tomcat、Node.js)、数据库(如 Redis、MySQL)或者各种中间件,只要它暴露了 TCP 端口,且容器内带有最基础的 sh,这就是最无敌的万金油配置。 它利用了 Linux 内核原生 </dev/tcp 漏洞,完全不依赖 curl 或 wget 。 healthcheck: # 语法解释:尝试与容器本地的 8080 端口建立 TCP 连接,失败则退出码为 1 test: ["CMD-SHELL", "sh -c '</dev/tcp/127.0.0.1/8080' || exit 1"] interval: 10s # 10秒探一次,既保证敏锐度,又不会给系统带来TCP连接压力 timeout: 3s # 握手超过3秒没反应,说明网络栈或主线程已经卡死 retries: 3 # 连续失败3次(大约30秒内)正式宣判死刑 start_period: 30s # 30秒新手保护期,给服务腾出启动和绑定端口的时间 注:使用时只需要把 8080 改成你容器内部的实际端口(如 Redis 改成 6379,Nacos 改成 8848)即可。 模板二:Java/Spring Boot 专属万金油(解决高 CPU 与 Full GC 假死) 对于 Java 应用,有时候端口虽然勉强能连上,但实际上 JVM 内部因为内存溢出(OOM)或者恐怖的 CPU 占用,已经完全无法处理业务了。 这时候用 Java 自带的诊断工具 jcmd 去拍 JVM 的脑袋,是最精准的万金油手段(前提是使用 JDK 镜像,非精简的 JRE)。 ...

2026-07-11 04:30:54 PM · 1 分钟

JAVA程序员防翻车指南

一、基础类型与字面量(1-10) int 最大值 2,147,483,647(2³¹-1),最小值 -2,147,483,648(-2³¹) long 最大值 9,223,372,036,854,775,807(2⁶³-1),字面量加 L float 字面量必须加 f,否则默认 double short 范围 -32,768 ~ 32,767,byte 范围 -128 ~ 127 字面量下划线:1_000_000 等于 1000000,仅 Java 7+ 0x 开头是十六进制,0b 开头是二进制,0 开头是八进制(容易误判) char 占 2 字节,Character.SIZE = 16,但 Unicode 扩展字符需 2 个 char(surrogate pair) boolean 只有 true/false,不能和 0/1 互转(和 C 不同) Integer.valueOf(127) == Integer.valueOf(127) 为 true,128 为 false(缓存池 -128~127) Integer.parseInt("") 抛 NumberFormatException,Integer.parseInt(null) 也抛 二、运算符与表达式(11-18) & 和 && 区别:& 两边都执行(位运算或逻辑与),&& 短路 | 和 || 同理,|| 短路 ^ 按位异或,~ 按位取反(包含符号位) >> 算术右移(补符号位),>>> 无符号右移(补 0) i++ 和 ++i 的字节码区别:前者先加载后自增,后者先自增后加载 += 隐式类型转换:short s = 1; s += 1; 合法,s = s + 1; 编译错误 三元运算符 ? : 必须返回相同类型,否则自动类型提升 字符串拼接用 + 在循环内会生成大量 StringBuilder,循环外编译器自动优化 三、String 相关(19-28) String 不可变,底层 char[] 被 final 修饰且不暴露修改方法 字符串常量池在堆中(Java 8+),intern() 手动入池 new String("abc") 创建 2 个对象(常量池 + 堆),"abc" 只创建 1 个(若池中没有) String.equals() 比较内容,== 比较引用地址 String.compareTo() 按字典序比较,返回差值 String.format() 使用 %s %d %f 占位符 substring() 在 Java 7 前共享 char[],Java 7+ 创建新数组(解决内存泄漏) split() 参数是正则,. 需转义为 \\. replace() 替换所有字符,replaceAll() 支持正则 StringBuilder 线程不安全,StringBuffer 线程安全(方法加 synchronized),但慢 四、equals 与 hashCode(29-34) 重写 equals 必须重写 hashCode,否则 HashMap/HashSet 失效 equals 必须满足:自反、对称、传递、一致、非空(x.equals(null) 返回 false) hashCode 相等的两个对象 equals 不一定相等(哈希冲突) Objects.equals(a, b) 自动判空,避免 null.equals() Arrays.deepEquals() 比较多维数组,Arrays.equals() 只比较一维 List.equals() 要求顺序相同,Set.equals() 仅要求元素相同(顺序无关) 五、异常处理(35-44) try-catch-finally 中 finally 在 return 之前执行,但返回值已在 finally 前计算好 finally 中有 return 会覆盖 try/catch 的返回值 finally 中修改返回的引用对象内容会生效(如 List.add()) 不要在 finally 中写 return,也不要在其中抛出异常(会掩盖原始异常) try-with-resources 要求资源实现 AutoCloseable,关闭顺序:先声明的后关闭 受检异常(Exception 子类)必须 try-catch 或 throws,RuntimeException 非受检 异常链:catch (SQLException e) { throw new RuntimeException(e); } 保留堆栈 printStackTrace() 不应用在生产日志中,用 log.error("msg", e) Throwable 包括 Error 和 Exception,Error 不应该捕获(如 OutOfMemoryError) finally 中关闭资源时可能抛异常,要用 try-catch 包裹或使用 try-with-resources 六、集合框架(45-62) ArrayList 初始容量 10,扩容 1.5 倍(oldCapacity + (oldCapacity >> 1)) ArrayList 随机访问 O(1),中间插入删除 O(n),LinkedList 相反 LinkedList 实现了 Deque,可作为队列/栈使用 HashMap 容量为 2 的幂,默认 16,负载因子 0.75,扩容 2 倍 HashMap 索引计算:(n - 1) & hash,hash = key.hashCode() ^ (h >>> 16) Java 8+ HashMap 链表长度 > 8 且容量 >= 64 时转红黑树,< 6 时退化为链表 HashMap 的 key 为 null 时,hash 值为 0,存在 table[0] ConcurrentHashMap 分段锁(Java 7)→ synchronized + CAS(Java 8+) Hashtable 线程安全但全表锁,已淘汰 TreeMap 基于红黑树,有序(按 key 自然序或 Comparator) LinkedHashMap 按插入顺序或访问顺序(accessOrder=true 可实现 LRU) HashSet 底层是 HashMap,TreeSet 底层是 TreeMap PriorityQueue 小顶堆,Comparator.reverseOrder() 可转大顶堆 ArrayDeque 优于 Stack(Stack 已废弃,ArrayDeque 线程不安全但更快) Collections.synchronizedList() 包装为线程安全,但迭代时仍需手动同步 CopyOnWriteArrayList 读多写少场景,写时复制整个数组 List.of() / Set.of()(Java 9+)返回不可变集合,不能增删改 ArrayList.subList() 返回视图,对子列表操作会影响原列表,反之亦然 七、并发与线程(63-80) 创建线程的三种方式:Thread、Runnable、Callable(有返回值) start() 启动线程,run() 只是普通方法调用 synchronized 可锁实例方法、静态方法(锁 Class)、代码块 wait() 释放锁,sleep() 不释放锁,yield() 让出 CPU 但不释放锁 wait()/notify() 必须在 synchronized 块中调用,否则抛 IllegalMonitorStateException notify() 随机唤醒一个,notifyAll() 唤醒全部 死锁四条件:互斥、请求与保持、不可抢占、循环等待 破坏死锁:按固定顺序加锁、tryLock() 超时、使用 ReentrantLock volatile 保证可见性和有序性(禁止指令重排),不保证原子性 AtomicInteger 用 CAS 实现原子操作,incrementAndGet() 底层是 Unsafe.compareAndSwapInt ThreadLocal 用完后必须 remove(),否则线程池复用导致内存泄漏 ThreadLocal 底层是 ThreadLocalMap,key 是弱引用,value 是强引用 ExecutorService 用 shutdown() 优雅关闭,shutdownNow() 立即关闭 线程池核心参数:核心线程数、最大线程数、存活时间、阻塞队列、拒绝策略 四种拒绝策略:AbortPolicy(抛异常)、CallerRunsPolicy(调用者执行)、DiscardPolicy(丢弃)、DiscardOldestPolicy(丢弃最旧) CachedThreadPool 最大线程数 Integer.MAX_VALUE,容易 OOM,慎用 FixedThreadPool 使用无界队列,任务积压可能导致 OOM Future.get() 阻塞,CompletableFuture 异步回调更灵活 八、I/O 与 NIO(81-88) File 只是路径抽象,不表示真实文件,exists() 判断是否存在 FileInputStream 读取字节,FileReader 读取字符(默认编码可能乱码) BufferedReader 用 readLine() 按行读取,Files.readAllLines() 更简洁 InputStreamReader 可指定字符集:new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8) try-with-resources 自动关闭多个资源,用分号分隔 Path/Paths/Files(Java 7+)替代 File,Files.walk() 遍历目录树 NIO 核心:Channel + Buffer + Selector(多路复用) ByteBuffer 读写切换需调用 flip(),clear() 清空,compact() 压缩 九、JVM 与内存(89-95) JVM 内存区域:堆、栈、方法区(元空间)、程序计数器、本地方法栈 堆分代:年轻代(Eden + S0 + S1)→ 老年代,默认比例 8:1:1 OutOfMemoryError 常见原因:堆 OOM、元空间 OOM、直接内存 OOM、栈溢出 栈溢出 StackOverflowError,递归过深或无终止条件 System.gc() 仅建议 GC,不保证执行 finalize() 已废弃(Java 9+),对象自救不靠谱 类加载器:Bootstrap(rt.jar)→ Extension → Application,双亲委派模型 十、日期与时间(96-100) java.util.Date 可变、线程不安全,已废弃,用 java.time.*(Java 8+) LocalDate / LocalTime / LocalDateTime 不含时区,ZonedDateTime 含时区 Instant 时间戳(秒/纳秒),Duration 计算时间差,Period 计算日期差 格式化用 DateTimeFormatter 线程安全,替代 SimpleDateFormat(线程不安全) LocalDateTime.now() 获取当前时间,parse("2026-07-09") 按 ISO 标准解析 十一、语法糖与常见坑(101-110) 泛型编译期擦除,运行时无泛型信息(List<String> 和 List<Integer> 在运行时相同) 可变参数本质是数组,public void method(String... args) 可传数组或逗号分隔 枚举 enum 默认继承 Enum,不能被继承,构造器私有 匿名内部类持有外部类引用(this$0),可能导致内存泄漏 接口默认方法 default,静态方法 static,接口中变量默认 public static final 抽象类可有构造器,接口无构造器 switch 支持 byte/short/char/int/String/enum,不支持 long/float/double break 跳出循环,continue 跳过本次,return 结束方法 标签 label: 可跳出外层循环,但不推荐使用 数组协变:String[] 是 Object[] 的子类,但集合泛型不变 十二、网络与反射(111-118) URL 和 URI 区别:URI 是标识符,URL 是定位符(包含协议) HttpURLConnection 原生 HTTP 客户端,但 Apache HttpClient / OkHttp 更常用 Socket 阻塞 I/O,ServerSocket 监听端口,accept() 阻塞等待连接 反射 Class.forName() 初始化类,ClassLoader.loadClass() 不初始化 Method.invoke() 性能差,但反射能绕过私有访问(setAccessible(true)) Proxy.newProxyInstance() 动态代理只能代理接口,CGLIB 可代理类 注解保留策略:SOURCE(源码)、CLASS(字节码)、RUNTIME(运行时) 序列化实现 Serializable 接口,serialVersionUID 用于版本兼容,不声明则自动生成 十三、设计原则与模式(119-123) 单例双重检查需 volatile,防止指令重排导致半初始化对象被读取 工厂模式解耦,策略模式消除 if-else,观察者模式事件驱动 依赖倒置:依赖抽象而非具体实现 开闭原则:对扩展开放,对修改封闭 里氏替换:子类可以替换父类出现的地方 十四、Linux 与常用命令(124-128) nohup java -jar app.jar & 后台运行,输出到 nohup.out jps 查看 Java 进程,jstack 打印线程堆栈,jmap 查看堆内存,jstat 监控 GC kill -15 优雅关闭(SIGTERM),kill -9 强制杀死(SIGKILL) tail -f app.log 实时查看日志,grep -C 10 "error" app.log 显示上下文 netstat -tlnp 查看端口占用,lsof -i:8080 查看指定端口 十五、数据库与 JDBC(129-133) PreparedStatement 防 SQL 注入,占位符 ?,编译一次执行多次 Statement 拼接字符串有注入风险,不用 ResultSet 游标从 1 开始,next() 移动指针,getString(1) 按索引取 事务隔离级别:读未提交、读已提交、可重复读、串行化 连接池用 HikariCP(默认)、Druid,不要手动创建连接 十六、Maven / Gradle(134-137) Maven 生命周期:clean → compile → test → package → install → deploy provided 作用域表示运行时由容器提供(如 servlet-api) 依赖传递:compile 传递,test 不传递,provided 不传递 版本冲突用 dependencyManagement 锁定版本,或用 exclude 排除 十七、工具与调试(138-142) System.out.println() 性能差,生产用日志框架(SLF4J + Logback) 日志级别:ERROR > WARN > INFO > DEBUG > TRACE assert 默认禁用,需 -ea 开启,生产慎用 Objects.requireNonNull() 判空抛 NullPointerException,比手动 if 简洁 Optional 用来表示可能为 null 的返回值,不要用作字段或参数 十八、编码规范与常识(143-150) 包名全小写,类名大驼峰,方法/变量小驼峰,常量全大写 + 下划线 方法名用动词(getUser),变量名用名词(userName) 布尔变量用 is / has / can 开头(isDeleted) 一个 .java 文件只能有一个 public 类,文件名必须和 public 类名一致 构造器不能 static,不能被 final 修饰 static 块在类加载时执行一次,{} 实例块在构造器前执行 匿名对象(new User())用完即抛,适合单次使用 永远不要在线上环境用 System.out 打印大对象,更不要用 e.printStackTrace()

2026-07-09 10:15:46 PM · 4 分钟

漫谈AI上下文长度

flowchart TD A[2018–2021512–2K Token金鱼记忆时代] -->|只能处理短文、短句、单轮问答| B[2022–20234K–128K Token中上下文普及时代] B -->|可处理论文、合同、小说、中型代码库RAG 成为企业标准方案| C[2024–2025200K–1M Token百万上下文实验期] C -->|纸面支持 1M,但遗忘、成本、速度问题明显仅限量 Beta| D[2026–20271M–2M Token1M 量产普惠时代] D -->|1M 成为旗舰标配可处理整卷宗、整代码库、全年业务日志| E[2028–20292M–10M Token超百万窗口分化时代] E -->|2M 以上面向行业定制长视频、大型代码库、多年日志联合分析| F[2030+10M+ 弹性记忆原生长记忆智能体时代] 一、大模型上下文完整进化时间线(2018–2026,分4个时代) 1. 初代Transformer时代:512–2K(2018–2021,金鱼记忆) 2018 GPT-1 / BERT:512 token,仅短文、短句分类,多轮对话必丢前文 2019 GPT-2:1024 token,能写短篇故事,连续对话3–5轮就遗忘历史 2020 GPT-3:2048 token(2K),行业通用标准,仅支持短提示词、少量示例,长文档必须分段/摘要处理 2021 早期国产(文心一言初代、GLM-1):统一2K上限,无长文本能力 时代特征:没有原生长文本能力,所有长内容必须靠人工拆分、外部摘要,RAG雏形出现。 2. 中上下文普及时代:4K–128K(2022–2023,工业化可用) 2022 ChatGPT(GPT-3.5):4K;年末升级16K,日常聊天够用,长文档仍吃力 2023.3 GPT-4 首发:8K,专业文档、代码单文件处理门槛打开 2023.7 Claude 2:100K,首个量产十万级窗口,法律、长篇论文场景爆发 2023.11 GPT-4 Turbo:128K,全球主流商用标配门槛,单本小说完整载入 2023年底 国产跟进:通义、GLM、混元开放32K/64K;开源Llama 2、Qwen 7B固定4K–32K 2023.12 Gemini 1.0 Ultra 纸面宣称1M,但仅实验室封闭测试,无法商用落地 时代里程碑:128K成为专业AI标配;RAG成为行业标准方案,弥补窗口不足。 3. 百万上下文实验期:200K–2M纸面(2024–2025,纸面强、实际弱) 2024.2 Gemini 1.5 Pro:原生2M token,全球首个公开百万级模型,但存在严重「中间遗忘Lost in the Middle」,后半段文档召回暴跌,仅适合摘要,不适合精细推理,仅限开发者限量Beta 2024 Claude 3 Opus:200K,稳定可靠,法律行业主力,无严重遗忘问题 2025 上半年:各大厂商放出1M Beta(Claude Sonnet 4、Gemini 2.0),长上下文计费溢价极高、响应慢、显存开销巨大,企业少量试用,普通用户无法接触 2025 国产开源:Qwen、DeepSeek推出64K–128K底座,少量实验版支持256K 时代特征:1M只是技术噱头,标称≠有效;硬件成本、遗忘问题、价格三重门槛,无法大规模落地。 ...

2026-07-09 08:58:34 PM · 2 分钟

一个程序员应该知道的各种锁

1. 悲观锁 vs 乐观锁(心态问题) 悲观锁:觉得肯定有人抢厕所,进去就反锁门(加锁),用完再开。 → Java里的 synchronized 和 ReentrantLock 都是这种。安全,但慢。 乐观锁:觉得没人抢,不锁门,但上厕所时盯着门把手(版本号/时间戳),如果发现被人动过(冲突),就重试。 → Java里的 CAS(比较并交换) 就是这种,比如 AtomicInteger。快,但冲突多时会反复重试。 2. 公平锁 vs 非公平锁(排队问题) 公平锁:先来后到,乖乖排队。 → new ReentrantLock(true)。公平,但效率低(大家都得排队)。 非公平锁:新来的可以插队,如果锁刚好释放,它就直接抢。 → synchronized 和默认的 ReentrantLock 都是非公平。效率高,但可能导致某些线程饿死(一直抢不到)。 3. 可重入锁(同一个人的多次进入) 你进了厕所A,发现里面还有个小隔间B,你能直接进B,不用再掏钥匙。 → synchronized 和 ReentrantLock 都支持。同一个线程可以多次获取同一把锁,防止自己把自己卡死。 4. 读写锁(分情况管理) 厕所分蹲位(写锁)和洗手池(读锁)。 多人同时洗手(读)没问题。 但有人蹲坑(写)时,别人既不能蹲也不能洗手(全阻塞)。 → ReentrantReadWriteLock。适合读多写少的场景。 5. 共享锁 vs 排他锁(权限级别) 排他锁(写锁):厕所门一锁,谁也别进。 共享锁(读锁):可以多人同时看同一份文件(但不能改)。 → 读写锁就是共享/排他的典型实现。 6. 偏向锁 → 轻量级锁 → 重量级锁(锁升级,JVM自动优化) 这是 synchronized 的底层升级过程(为了性能): 偏向锁:厕所只认你一个人,你每次来都不用掏钥匙(无竞争)。 轻量级锁:偶尔有别人来,你们用“自旋”方式(原地转圈等)抢,不挂起线程(省资源)。 重量级锁:竞争激烈,排队挂起(操作系统介入,慢)。 → 这是JVM自动做的,你不用管,但知道它存在即可。 7. 自旋锁(不睡觉,死等) 厕所被占,你不去排队睡觉,而是在门口原地转圈(循环检查),等它释放。 → 适合持有锁时间很短的情况,避免线程挂起/唤醒的开销。Java里的 CAS 就是自旋思想。 8. 分段锁(分块管理) 一个大厕所分成多个小隔间,锁只锁其中一间,不影响其他间。 → ConcurrentHashMap 早期就是用分段锁,提升并发度(现在改用CAS+细粒度锁了)。 一张图总结(按使用场景选): 场景 推荐锁 简单同步,代码少 synchronized 需要可中断、超时、公平等灵活功能 ReentrantLock 读多写少(如缓存) ReentrantReadWriteLock 计数器、自增等简单操作 AtomicXXX(乐观锁) 追求极致性能,竞争不激烈 偏向锁/轻量级锁(JVM自动)

2026-07-09 11:15:34 AM · 1 分钟

Docker 常用配置

Kafka Kafdrop services: kafdrop: image: obsidiandynamics/kafdrop container_name: kafdrop ports: - "9000:9000" environment: KAFKA_BROKERCONNECT: "localhost:9092" SERVER_SERVLET_CONTEXTPATH: "/" restart: unless-stopped KRaft 模式 services: kafka: image: apache/kafka:3.9.0 container_name: kafka hostname: kafka ports: - "9092:9092" environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 # 注意:这里不要换行,不要逗号 KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 # 外部访问地址 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk volumes: - ./data:/var/lib/kafka/data restart: unless-stopped

2026-07-09 11:04:37 AM · 1 分钟

Java GC 是如何驱赶“老年群体”的

在 Java 的世界里,“分代年龄”(Age) 是专门为 JVM 垃圾回收(GC)设计的一个计数器。 简单来说,它记录了一个 Java 对象在垃圾回收中活过了多少轮。就像打游戏刷副本一样,对象每在垃圾回收的“清洗”中幸存下来一次,它的分代年龄就会 +1。 1. 为什么需要“分代年龄”? 这源于 Java 垃圾回收领域著名的 弱分代假说(Weak Generational Hypothesis): 绝大多数的 Java 对象都是“朝生夕死”的。 比如你在一个方法里 new 了一个局部变量,方法结束了,这个对象就没用了。但也有极少数对象(如 Spring 的 Bean、数据库连接池)会一直存活。 为了高效管理内存,JVM 把堆内存分成了两大区域: 新生代(Young Generation): 存放刚出生、寿命短的对象。 老年代(Old Generation): 存放寿命长、常驻内存的对象。 “分代年龄”就是对象从“新生代”晋升到“老年代”的资格证和进度条。 2. 它的工作流程是怎样的? 我们可以把新生代想象成一个“新手村”,老年代想象成“满级主城”: 出生(0岁): 绝大多数新对象在新生代的 Eden(伊甸园)区 出生。 第一次渡劫(1岁): 发生了一次 Minor GC(新生代垃圾回收)。如果这个对象没被回收,它会幸存下来,被移动到 Survivor(幸存者)区,同时它的分代年龄变成 1 岁。 继续熬资历(每活过一轮 +1岁): 以后每发生一次 Minor GC,只要它还在幸存者区折腾且没死掉,它的年龄就会加 1 岁。 晋升老年代: 当它的年龄达到一定阈值(默认是 15 岁,由 JVM 参数 -XX:MaxTenuringThreshold 决定)时,JVM 就会认为:“这小伙子挺能活,应该是个核心常驻对象。” 于是把它晋升(Promote)到老年代。 3. 它存在对象的什么地方? 正如我们在讨论“锁”时提到的,分代年龄存在于每个 Java 对象的 对象头(Object Header)的 Mark Word 里面。 ...

2026-07-09 10:19:20 AM · 1 分钟

为什么每个java对象都有锁的属性?这样不是浪费内存吗

1. 为什么当初要这样设计?(设计初衷) Java 诞生于 20 世纪 90 年代中期,当时多线程编程(并发)正在兴起。Java 的设计者高斯林(James Gosling)希望 Java 成为一门简单易用的多线程语言。 简化并发编程 如果对象没有内置锁,你每次想给一段代码加锁,都必须显式地创建一个 Lock 对象: // 如果没有内置锁,你得这么写: Lock myLock = new ReentrantLock(); void doSomething() { myLock.lock(); try { // 业务逻辑 } finally { myLock.unlock(); } } 而 Java 设计者引入了 synchronized 关键字,直接让对象充当锁,把复杂的同步简化成了: // 极简的同步写法 synchronized(this) { // 业务逻辑 } “万物皆对象,万物皆可为锁”的设计,让开发者不需要管理一堆乱七八糟的锁对象,只要拿到目标对象,就能直接进行同步控制。 2. 这样不浪费内存吗?JVM 是怎么“偷懒”的? 你担心的内存浪费,JVM 工程师早就想到了。他们绝对不会傻到给每个刚 new 出来的对象都分配一个沉重的操作系统级别的锁(Monitor)。 JVM 解决这个问题的核心思路是:按需分配,动态升级。 秘密武器:对象头(Object Header) 在 Java 中,每个对象的内存结构里都有一个叫 Mark Word(标记字) 的区域(在 64 位虚拟机上占 8 个字节 / 64 位)。 ...

2026-07-09 10:11:43 AM · 1 分钟

H mod C = H & (C - 1) ?

在 Java 后端开发面试或源码阅读中,HashMap 永远是避不开的核心。很多人能熟练地背出它的扩容阈值、红黑树化条件,但当深入到源码底层,看到诸如 e.hash & (newCap - 1) 和 e.hash & oldCap 这样的位运算时,往往会陷入沉思。 一、 起源:为什么数组长度必须是 2 的次幂? 在散列表中,为了让数据均匀分布,最直观的想法是对哈希值进行取模(求余数): $$\text{索引位置} = \text{hash} \pmod{\text{Capacity}}$$然而,在 CPU 的底层底层执行中,除法和取模(%)是非常昂贵的算术操作(可能需要几十个时钟周期)。为了追求极致的性能,底层的数论定理为我们提供了一个完美的替代方案: 数学定理:当容量 $C$ 是 $2$ 的次幂($2^n$)时,对于任意整数 $H$,满足: $$H \pmod C \equiv H \ \& \ (C - 1)$$ 位运算(&)在 CPU 中只需要 1 个时钟周期!为了享受到这个性能红利,HashMap 在源码中将容量死死限制为 2 的次幂: // 默认初始容量 16 (2的4次方) static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 裁剪器的魔术:hash & (newCap - 1) 以容量 16 为例,16 - 1 = 15(二进制:0000 1111)。任何 Hash 值与 15 进行 & 运算,高位都会被无情“抹零”,只有最后 4 位被保留下来: ...

2026-07-08 05:38:40 PM · 2 分钟

限流降级研究笔记 更新中 

主流算法 固定窗口计数器 每秒一个计数器,+1超过阈值就拒绝 缺点 0.51s 和 1.51s 之间产生2倍流量 滑动窗口 把时间切成更细的小格,滑动统计 缺点 解决临界问题,但是实现复杂 漏桶 请求进桶,匀速流出,桶满则弃 缺点流出速度恒定,扛不了正常的突发 令牌桶 匀速往桶里放令牌,来请求拿令牌,没令牌被拒绝 缺点允许一定突发

2026-07-07 04:05:10 PM · 1 分钟