三轮技术练习:JVM 分代与 GC、点击穿透、半同步与 chunked

三轮技术练习:JVM 分代与 GC、点击穿透、半同步与 chunked

今天三轮分别是 JVM、前端、数据库和网络。第一轮答得顺,后两轮各留了一个缺口,第二轮的点击穿透我把成因说反了,第三轮 GTID 那两个值和心跳都没答上。下面从这几个缺口展开。

JVM 内存区域:哪些线程私有,哪些共享

线程私有的是程序计数器、虚拟机栈、本地方法栈,共享的是堆和方法区。对象本身在堆里;方法里的局部变量在虚拟机栈的栈帧局部变量表里;static 成员和类元信息在方法区。

方法区在 JDK 8 之后由元空间实现,用的是本地内存,不像永久代那样挤在堆里。这一点答题时没提,核对文档时补上的。

分代假说里的弱分代假说说的是:绝大多数对象朝生夕灭,能熬过第一次 GC 的很少。新生代就是新对象先待的地方,GC 频繁;熬过好几轮还没被回收的对象挪进老年代。

Minor GC 只回收新生代,Full GC 扫的是整堆加方法区。我答题时说 Full GC 是“全杀掉”,这个说法不准确,它回收的同样是不可达对象,只是扫描范围最大、停顿最长。

对象进老年代有几种情况:年龄到阈值、大对象直接进、新生代放不下时走空间分配担保。还有一个我漏了,叫动态年龄判定,Survivor 里同龄对象的总大小超过 Survivor 一半,这批对象就提前晋升,不用等年龄够。

堆满和栈满

Java heap space 是堆不够,一般原因是对象创建太多、缓存没上限、集合只增不减。StackOverflowError 是栈满了,典型来源是递归没有出口。

GC 判活用可达性分析,从 GC Root 顺着引用链找,够不着的才回收。所以一个对象已经不再使用、但还被引用着,照样不会被回收,堆会一直涨。两个容易中的姿势:静态集合只增不删;线程池里线程复用,ThreadLocal 这类值被强引用,不 remove 就漏。

排查工具这题我答得比较全。jmap -histo:live <pid> 列出各类对象数量和占用,jmap -dump:format=b,file=heap.hprof <pid> 导出堆,再拿 MAT、VisualVM、JProfiler 看支配树和 GC Root 引用链。有个细节当时没提:-histo:live 会先触发一次 Full GC 再统计存活对象,线上执行前要掂量。

300ms 的来历

早年手机页面支持双击缩放,浏览器分不清一次点击是单击还是双击,就等 300ms 才派发 click。head 里写了 width=device-width, initial-scale=1,等于告诉浏览器视口宽度就是理想宽度、不需要缩放,这 300ms 就没了。touch-action: manipulation 是在元素上关掉双击缩放手势,效果类似。

事件顺序是 touchstart、touchend、mousedown、mouseup、click。移动端浏览器靠模拟鼠标事件派发 click,如果在 touchend 的回调里调 preventDefault(),后面的 mouse 事件和 click 都不来。

点击穿透,成因我说反了

我当时的说法是“中间一层被阻止了,手指抬起后浏览器把点击落到下层”。核对下来这个解释站不住。

实际链路是这样:touchend 之后浏览器还会补发那个合成 click,在没禁用双击缩放的页面上就是 300ms 后。如果 touchend 里把上层浮层隐藏或者移除了,等合成 click 真正派发时,命中测试落在了下层元素上,下层就被点中了。preventDefault 挡的是模拟鼠标事件,跟“浮层已经没了”是两条线,我把它们混成一条了。

修法三种:touchend 里 preventDefault() 拦掉模拟鼠标事件;关浮层时加一层约 300ms 的透明遮罩拖住点击;改用 Pointer Events,它不分触摸和鼠标两套事件。

自己写懒加载

老写法是在 scroll 回调里逐张图调 getBoundingClientRect() 跟视口比。问题有三个:滚动时回调触发得非常密;每读一次布局信息都可能逼浏览器把布局算完,频繁触发重排;防抖节流还得自己写。

IntersectionObserver 把“进没进可视范围”交给浏览器判断,状态变化时异步回调。三个配置里,root 指定哪个祖先当视口,null 就是浏览器视口;rootMargin 跟 CSS 的 margin 一样给视口范围扩一圈;threshold 指定交叉比例到多少触发回调。rootMargin: '200px' 常用是为了提前加载,用户还没滚到图就已经开始下。

图加载完之后调 unobserve 取消观察,不然每次交叉状态变化还在白跑回调。加载前用固定宽高占位或者骨架屏把位置占住,排版才不跳。

生成器:一次只算一步

调 function* 声明的函数不会执行函数体,只返回一个生成器对象,每调一次 next() 才往下跑一步,返回 { value, done }。它身上有 Symbol.iterator,可以直接 for...of。yield 对应的是手写迭代器里 next() 返回对象的 value 那部分。

“永远往下产出自然数”的生成器不会撑爆内存,因为惰性求值,每次只算一个值就停住,调用方不 next 它就不动。yield* 是把另一个可迭代对象委托给生成器,逐个产出它的值。async/await 普及之前那些“生成器加自动跑 next”的异步库,靠的就是生成器能暂停、能从原地恢复。

窗口函数在执行链的哪一步

SQL 的执行顺序是 FROM、WHERE、GROUP BY、HAVING、SELECT(窗口函数在这层算)、ORDER BY、LIMIT。WHERE 执行的时候 rn 还不存在,HAVING 也在 SELECT 之前,同样看不到 SELECT 里定义的窗口别名,所以取每组最新一条只能套一层子查询。

SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at) 天然就是累计到当前这一笔,因为不写 ROWS BETWEEN 时的默认帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。想算含当前这笔往前 3 笔的滚动平均,写 ROWS BETWEEN 3 PRECEDING AND CURRENT ROW。

ROWS 按物理行数算,往前 3 行就是 3 行。RANGE 按排序值算,同一秒两笔 created_at 相同的记录互为“同组”,RANGE ... CURRENT ROW 会把同值的行全算进来。默认帧是 RANGE,所以同一秒多笔时累计值一次跳好几笔。

从库到底追上没有

Seconds_Behind_Master 不能当准话,三个场景它都会骗人:SQL 线程空闲时是 0;大事务刚拉过来还没开始重放,它也是 0;事务重放到一半,这个值也不动。判断标准是 I/O 线程的位点和 SQL 线程的执行位点是否相等。

这处我漏了具体看哪两个值。传统复制看 Read_Master_Log_Pos(I/O 线程收到哪)和 Exec_Master_Log_Pos(SQL 线程执行到哪);开了 GTID 看 Retrieved_Gtid_Set 和 Executed_Gtid_Set 是否一致。

半同步在保证什么

半同步解决的是主库提交成功、从库还没收到日志,主库就宕机导致数据丢失。它要求主库提交事务时至少一个从库把 binlog 收下并 ACK,才向客户端返回成功。等不到 ACK 会超时退化成异步复制,超时参数是 rpl_semi_sync_master_timeout,默认 10 秒。换句话说,它保的是大多数时候至少一个从库有这份日志,不保证绝对强一致。

两个等待点的差别在 ACK 的时机。AFTER_SYNC 是主库把 binlog 写完刷盘后、存储引擎提交之前等 ACK,ACK 到了才提交引擎、才回客户端,主库崩溃时从库手里有日志。AFTER_COMMIT 是先提交引擎再等 ACK,等待期间主库已经提交,这时崩溃就存在丢数据的窗口。

版本上有个容易踩的点:MySQL 8.0.26 起插件和变量名把 master、slave 换成了 source、replica,参数变成 rpl_semi_sync_source_timeout 这类,行为没变,旧名字只在装旧插件时才认。

长度未知的响应怎么发

边查边吐、总长未知时,头部写 Transfer-Encoding: chunked,每个分块前面带十六进制长度,收尾是 0 长度块加空行。连接还能接着复用,因为 chunked 是自定界消息,0 长度块明确告诉对端 body 结束了,不需要 Content-Length。

Connection: Upgrade 配 Upgrade: websocket 是请求把当前 HTTP 连接升级成 WebSocket,服务端同意回 101 Switching Protocols,之后这条 TCP 连接上跑的是 WebSocket 帧。WebSocket 全双工,两边都能主动推;SSE 是服务端单向推,基于普通 HTTP,浏览器自带重连;长轮询还是请求响应,只是把等待放在服务端。

心跳和 TCP keepalive

这处我直接漏了。光靠 TCP keepalive 不够,它的默认探测间隔是 Linux 的 tcp_keepalive_time,两小时起步;中间的 NAT、防火墙、负载均衡常常有几十秒的空闲超时,长时间没数据的连接会被它们单方面掐掉,对端还以为连着。应用层要自己发心跳,WebSocket 有 ping/pong 控制帧,业务层常见是 30 秒一个包,既保活也顺带探活。

← 返回文章列表