
“请说说你对String的理解。”面试官语气平淡却像一枚探针试图撬开你的知识体系。很多候选人心里一喜这题我背过。于是流利地吐出“不可变、final、常量池”几个关键词。但接着面试官追问一句“既然不可变为什么String str a b可以拼接”如果此刻你愣住前面所有背诵都成了笑话。基础题为何高频因为它们根本不是送分题而是筛选项——用来分辨你是“背过定义”还是“真正做过”。其实这个问题背后藏着对“不变性”immutability的深层理解。String的可变与不可变不是一道语法题而是一道权衡题。不可变带来了安全性参数传递不担心被修改、可缓存性哈希值可以安全地缓存和线程安全多线程读不需要同步。但代价是任何修改都会创建新对象循环拼接字符串会浪费大量内存。为此JVM在编译期做了若干优化但循环内的拼接依然逃不脱创建StringBuilder的宿命。面试官想听到的不是结论而是你对“牺牲了什么换来了什么”的思辨。String的“不可变”是一道哲学题如果你追问“为什么String类用final的byte[]数组JDK9存储而不是char[]”很多人会卡住。JDK8以前是char[]JDK9起改为byte[]并且新增coder字段区分Latin1和UTF-16编码。这看似是性能优化其实是JVM在内存占用与处理复杂度之间做的又一次权衡。面试中考到这种细节说明已经进入“版本演进”的深水区。String不可变还意味着它可以被安全地缓存和共享这就是字符串常量池存在的基石。但请你再想一步如果每次拼接都产生新对象那常量池岂不是垃圾满屋所以有了intern()主动入池有了G1垃圾回收的字符串去重。到这里你会发现一个“不可变”得先回答安全、混分配、常量池、垃圾回收、编码优化五层问题。而大多数候选人只停留在第一层。“不可变”是一粒种子能长出一棵名为“JVM与设计”的大树这才是面试官想要你展示的东西。equals与hashCode一对被拆散的冤家如果说String是Java小学题那equals和hashCode就是中学题。原因在于它们俩一旦没配合好HashMap和HashSet会死得很惨。我知道很多人背过“重写equals必须重写hashCode”但为什么因为HashMap在找key时先计算hashCode定位桶再用equals检查桶内元素。假设两个对象equals比较为true但hashCode不同它们就会被放在两个桶里。你往HashSet里添加了一个“等值”对象结果集合里出现了两个“重复”元素。更隐蔽的是你在HashMap中用“等值”对象去get返回的却是null——你的对象被彻底弄丢了。不遵守约定的hashCode就像一把永远打不开旧锁的新钥匙钥匙和锁明明配套却因为编号不同被拒之门外。为什么重写hashCode时推荐使用31因为它是一个奇素数可以降低碰撞又因为JVM底层可以用移位和减法运算即31 i (i 5) - i性能相当高。但更重要的是你要理解hashCode的“稳定性”约束在对象作为一个键被放进HashMap后绝不能让影响hashCode计算的字段发生改变。否则它就成了一个“游动的键”再也找不回来。很多人栽在这个坑里却从未想过这是基础题反复出现的原因——它考察的是你对“约定与契约”的敬畏。HashMap不只是数组加链表而是“对抗最坏情况”HashMap大概是Java面试中“最卷”的基础题。从数据结构到扩容从红黑树到并发问题随便一个点都能衍生出连环追问。第一层是结构数组链表当链表长度超过8且数组长度达到64时链表转红黑树。为什么选8因为泊松分布下链表元素个数达到8的概率已经低于千万分之一几乎不可能。换句话说红黑树不是常态而是为了应对“某个桶被恶意填充”的极端防线。面试官最喜欢问“为什么树化阈值是8”如果你能答出“要基于泊松分布解释”瞬间就拉开差距。再看扩容为什么初始容量是16负载因子是0.75因为2的幂方便位运算取模0.75是空间和时间的折中。JDK1.7采用头插法扩容时存在逆序和并发死循环问题JDK1.8改为尾插法长度大于8才树化。这些改动背后是让HashMap在多线程并发下不至于直接挂掉但请注意它依旧不是线程安全的。并发下的HashMap不是你帮我拦一下而是一言不合就死循环给你看。volatile你“看见”的只是冰山一角volatile是并发领域最言简意赅的关键字保证可见性禁止指令重排序但不保证原子性。很多人背出来很容易可当面试官问“它和synchronized有区别”时很多人的回答只能到“一个锁一个不锁”为止。你需要理解JMM每个线程都有自己的工作内存定期与主内存同步。如果没有volatile一个线程修改了变量另一个线程可能很久看不见。但volatile做了更多事——它在读写时插入内存屏障禁止编译器或CPU对相关指令重排。这解释了单例模式中的双重检查锁为什么必须用volatile修饰instance。因为看起来是“先赋值再返回”但CPU可能重排序为“先返回再赋值”另一个线程就会拿到一个未初始化完成的对象。这是一个经典的可见性有序性问题。更残酷的是volatile对i无能为力因为i是“读-写-改”三步volatile只约束了中间的内存阻隔并未把三步锁成原子。volatile不是锁它只是让你看见撕裂现场但无法阻止撕裂。synchronized从“重量级”到“轻量级”的进化史提起synchronized很多人想到的就是“锁”。但如果你知道它从JDK1.6开始经历了锁升级的过程——无锁、偏向锁、轻量级锁、重量级锁——就能明白JVM为了“高效并发”有多努力。刚创建对象时没有竞争只标记偏向锁一旦第二个线程尝试竞争偏向锁撤销升级为轻量级锁CAS自旋若自旋失败或竞争激烈才会膨大为重量级锁等待内核的信号量。这是一种“乐观到悲观”的演进默认世界是美好的出问题才走重流程。锁升级是JVM对并发压力的“量子态”不观测时一切从简一旦坍缩便成为重量级。但有趣的是JDK15起偏向锁被废弃。原因很简单维护偏向锁带来的性能收益已经跟不上现代应用场景的复杂性。这说明JVM本身也在不断“复盘”自己的设计。基础题之所以值得反复琢磨是因为连底层实现都在迭代而你如果只记住一个静态结论很快就会被淘汰。ThreadLocal弱引用是避风港还是暗礁ThreadLocal常用来保存线程私有数据但它的实现机制很“拧巴”ThreadLocalMap的Entry继承WeakReferenceThreadLocal?key是弱引用而value是强引用。为什么key要用弱引用如果key是强引用那么当外部ThreadLocal对象被置为null时ThreadLocalMap里还强引用着它导致它永远无法被GC回收用弱引用key会在下一轮GC被回收。但矛盾来了key被回收后value成了“无主”对象却依然被Entry强引用着于是出现了“脏条目”内存泄漏。ThreadLocal的弱引用设计看似灵巧实则开启了另一场与内存泄漏的持久战。解决之道只有一个用完必调remove()。特别是在线程池场景中线程被复用如果不清理上次任务存进去的变量会被下一次任务读到造成严重的业务数据串线。很多生产事故就是“一个没清理的ThreadLocal”引发的。所以面试官考你ThreadLocal往往接着问“你在项目中怎么用有没有遇到过OOM”其实就是想听你如何避雷。基础题之所以不能被忽视是因为每一个坑都有人用线上故障帮你试过了。类加载与双亲委派打破国境线才能做框架双亲委派模型的逻辑是一个类加载器收到加载请求先让父加载器尝试父加载器加载不了子加载器才自己上。这样做有两大好处防止核心API被篡改避免同一个类被加载两次。但很多人不知道“双亲委派”不是继承关系而是组合委托。更关键的是它并不是不可违反的“铁律”。JDBC就是一个典型JDBC驱动管理者在启动时需要加载第三方厂商驱动但jdbc.Driver类在rt.jar中启动类加载器已经加载了而各厂商的驱动实现却在classpath里启动类加载器根本看不到。于是JVM引入了线程上下文类加载器让启动类加载器“跨过边境”去加载实现类。Tomcat更是“叛逆”每个WebApp有独立的类加载器优先加载自己应用下的类为的是让不同应用能用不同版本的库。双亲委派的“国境线”上框架们开拓了一个个“经济特区”。这类基础题的意义在于它教会你避免僵化思维。任何规则都是在特定背景下诞生的当背景变化规则也必须被打破。面试官想听你承认双亲委派的局限性而不是把它奉为教条。JVM内存区域记住“户口本”不是目的拼出“水流动线”才是最后一道高频基础题是JVM运行时数据区。虚拟机栈、本地方法栈、程序计数器是线程私有的堆、方法区元空间是线程共享的。栈里装栈帧栈帧装局部变量表、操作数栈、动态链接、返回地址。这些背得滚瓜烂熟并不难难的是搞清楚运行时的“水流动线”new出的对象优先分配在Eden区经过Minor GC进入Survivor年龄够大再到Old区。对象也不一定都在堆上——通过逃逸分析如果对象不会被外部引用JIT会将其栈上分配甚至标量替换掉。如果你只把内存区域当成七张表格来背那你一定没有经历过大对象频繁GC时的头皮发麻。OOM是另一个经典追问。堆溢出是对象太多栈溢出是递归无底洞方法区溢出是动态生成太多类。排查OOM需要日志、MAT、jstat等工具但这已经超出“基础”的范畴。而面试官之所以频繁问内存区域是用它来检测你对“对象的一生”是否有整体感。了解一块地区是什么功能远不如知道对象在这块区域中如何诞生、移动和死亡来得重要。面试结束你走出大楼那道关于String的问题还在耳边回响。你突然意识到所有高频基础题都像一把钥匙打开的不是某个“标准答案”而是一扇扇通往更深世界的大门。基础题的最大价值在于它们每一次出现都能让你用新的经验重新审视旧的知识。当你开始琢磨“为什么可变性如此重要”“为什么哈希约束不能违背”“为什么锁要不断进化”你已经不是在备考而是在构建自己的计算机世界观。这才是“反复琢磨”的全部意义。