面试模块:42 道题,接住下一问
record 和普通类、Lombok 生成的类有什么本质区别?
record 是语言层面的透明数据载体:编译后是 final class,隐式继承 java.lang.Record,组件被编译为 private final 字段和同名访问器,equals/hashCode/toString 由编译器按全部组件生成。
它不能声明额外的实例字段,也不能继承其他类;可以实现接口。构造器分 canonical(全参)和 compact(省略参数列表、只写校验逻辑)两种。
和 Lombok 的区别在于:record 是语言规范的一部分,语义固定、可被模式匹配解构;Lombok 是编译期代码生成,本质仍是普通类,字段可变性、继承和生成策略都可以自定义。
sealed 接口的 permits 有什么约束?它和枚举、final 的关系是什么?
sealed 限制谁可以实现/继承:直接子类型必须在 permits 中列出(同一源文件内可省略),且每个子类型必须声明为 final、sealed 或 non-sealed。
sealed 类型和子类型通常必须在同一模块;未命名模块中则必须在同一包。它描述的是封闭的继承体系,不是访问控制。
枚举天然是封闭的(常量集合固定),可以看作 sealed 思想在值层面的体现;final 禁止继承,sealed 则允许受控的继承。
permits 不是「允许谁能访问」,而是「允许谁继承」。反射可通过 isSealed() 和 getPermittedSubclasses() 读取这些信息。if (o instanceof String s && s.length() > 0) 中 s 为什么能用?换成 || 为什么不行?
模式变量采用流作用域(flow scoping):编译器分析控制流,只在「确定匹配成功」的路径上让变量可见。&& 右侧只在左侧为 true 时求值,因此 s 一定已绑定。
|| 右侧在左侧失败时也会求值,此时 s 未绑定,所以不可见。同理,!(o instanceof String s) 之后直接使用 s 也不合法,除非通过提前 return 等方式让后续路径必然匹配。
这不是简单的块作用域,而是结合布尔短路、break/continue/throw 的控制流分析。
模式 switch 的穷尽性和分支顺序是怎么判定的?
编译器从上到下匹配,第一个命中的分支获胜。因此更具体的模式必须放在更宽泛的模式之前,否则后面的分支不可达,编译器报 dominance 错误。
穷尽性只对封闭类型有意义:sealed 接口要求覆盖所有 permitted 子类型,enum 要求覆盖所有常量(或提供 default)。Object 等开放类型必须写 default。
守卫(when)不计入穷尽性:case Circle(double r) when r > 0 不能证明覆盖了 Circle,仍需一个无守卫的 Circle 分支或 default。
MatchException,而不是静默走默认分支。record pattern 如何解构嵌套结构?匹配失败时会发生什么?
record pattern 按组件顺序递归匹配:case Line(Point(int x1, int y1), Point(int x2, int y2)) 先确认外层是 Line,再对两个组件分别匹配 Point 模式。
任一组件匹配失败,整个模式就失败,控制流转到下一个 case;已经绑定的变量不会泄漏。组件若是 null,嵌套类型模式无法匹配 null,整个模式失败。
组件通过访问器读取,引用类型绑定的是引用而非副本;record pattern 只能用于 record,普通类仍需类型模式加访问器。
var 只是让编译器推断组件类型。文本块(text block)的缩进和转义规则是什么?
文本块用 """ 包围,开分隔符后必须换行。编译器会去掉所有行共有的附带空白(incidental whitespace),保留相对缩进;结束分隔符的位置会影响基准缩进。
常见转义:\ 放在行尾表示不换行,\s 表示一个空格(防止行尾空格被去掉),""" 需要转义。文本块仍然是 String,可用 formatted 做占位替换。
与普通字符串相比,文本块适合 SQL、JSON、HTML 等多行内容,减少转义和字符串拼接。
\n 和真实换行有区别吗?""" 放在行首会改变结果。var 是动态类型吗?哪些地方不能用?
var 是局部变量类型推断,类型在编译期确定并写入字节码,不是动态类型,也不影响运行时性能。
可用于局部变量、for 循环变量、try-with-resources 变量;不能用于字段、方法参数、返回类型,也不能声明为 null 后再赋值。lambda 参数自 Java 11 起可用 var,但必须所有参数统一写法。
它减少的是类型噪音,不是类型安全。可读性优先:类型从右侧一目了然时用 var,否则写显式类型。
var 能声明匿名类实例吗?var x = null; 无法推断类型,编译失败;var 也不能用于 lambda 参数(除非配合注解/混合参数的特殊场景)。ArrayList 和 LinkedList 怎么选?为什么很少用 LinkedList 做队列?
ArrayList 基于动态数组,随机访问 O(1),尾部追加均摊 O(1),中间插入/删除 O(n),内存紧凑、缓存友好。LinkedList 基于双向链表,头尾操作 O(1),按索引访问 O(n),每个节点额外两个引用,缓存局部性差。
LinkedList 虽然实现了 Deque,但作为队列时每次操作都要分配节点,且迭代访问跳来跳去;ArrayDeque 用循环数组实现双端队列,通常更快、更省内存。
选择原则:绝大多数列表场景用 ArrayList;需要双端队列用 ArrayDeque;只有频繁在已知节点位置插入/删除且持有节点引用时,LinkedList 才可能有优势。
LinkedList.get(i) 是 O(n),在 for 循环里按索引遍历会退化成 O(n²)。HashMap 的底层结构、哈希扰动和树化条件是什么?
JDK 8+ 的 HashMap 是「数组 + 链表 + 红黑树」。键的 hashCode 先经过扰动 (h = key.hashCode()) ^ (h >>> 16),让高位参与索引计算;下标为 (n - 1) & hash,因此容量必须是 2 的幂。
默认负载因子 0.75,元素数超过阈值就扩容为两倍。单个桶的链表长度达到 8 且容量至少 64 时树化为红黑树;树节点减少到 6 时退回链表。容量不足 64 时优先扩容而不是树化。
树化阈值 8 基于泊松分布:哈希均匀时链表达到 8 的概率极低,树化是防御极端哈希碰撞的兜底。
HashMap 为什么线程不安全?ConcurrentHashMap 怎么解决?
HashMap 的 put、resize 等操作没有同步。并发扩容时多个线程可能同时改写 table、丢失 entry,JDK 7 的头插法还可能形成环链;JDK 8 改善了扩容算法,但仍然不是线程安全的。
ConcurrentHashMap 在 JDK 8+ 用「CAS + synchronized 锁桶头」:空桶用 CAS 插入,非空桶只锁当前桶,写冲突范围小;扩容支持多线程协助迁移。size 通过 CounterCell 分散计数。
它的迭代器是弱一致的:不抛 ConcurrentModificationException,但不保证看到某个时刻的完整快照;也不允许 null 键和 null 值。
Collections.synchronizedMap 和 ConcurrentHashMap 有什么区别?ConcurrentHashMap 的复合操作仍要用 computeIfAbsent、merge 等原子方法;先 get 再 put 依然有竞态。ConcurrentHashMap 的 computeIfAbsent 是原子的吗?有什么坑?
是原子的:映射函数在对应桶的锁内执行,多个线程同时计算同一个 key 时只有一个会真正执行并写入。
坑在于映射函数里不要再对同一个 map 做修改,否则可能死锁或抛 IllegalStateException;映射函数也不应做耗时操作,因为它持有桶锁,会阻塞同桶的其他写入。
如果映射函数返回 null,不会建立映射;需要计数或合并时优先用 merge 或 compute。
JDK 21 的 SequencedCollection 解决了什么问题?reversed 是视图吗?
它把「有序集合的两端」统一成一套 API:addFirst/addLast、getFirst/getLast、removeFirst/removeLast、reversed。List、Deque、LinkedHashSet、SortedSet、LinkedHashMap、SortedMap 都实现了对应接口。
reversed() 返回视图,不是副本;读写都落到原集合,只是方向相反。视图上的 addFirst 等于原集合的 addLast,反之亦然。
并非所有实现都支持改序:TreeSet/TreeMap 的顺序由比较器决定,addFirst/putFirst 会抛 UnsupportedOperationException;LinkedHashSet.addFirst 对已存在元素是「移动到最前」。
Collections.reverse 和 reversed() 怎么选?reversed() 是零拷贝视图,原集合改变后视图立即反映;需要独立快照要显式复制。如何用 LinkedHashMap 实现一个简单的 LRU 缓存?
构造时把 accessOrder 设为 true,每次 get/put 会把访问到的条目移到链表尾部;重写 removeEldestEntry,当 size 超过容量时返回 true,LinkedHashMap 会删除最久未使用的头节点。
示例:new LinkedHashMap<>(capacity, 0.75f, true) { protected boolean removeEldestEntry(Map.Entry e) { return size() > capacity; } }。
它不是线程安全的,也没有过期时间;生产级缓存通常用 Caffeine。JDK 21 的 SequencedMap 还提供了 putFirst/pollLastEntry 等端点操作。
removeEldestEntry,不是 remove;返回 true 只表示允许删除最老条目,实际删除由父类完成。平台线程和虚拟线程的本质区别是什么?
平台线程与操作系统内核线程 1:1 绑定,创建成本高、栈空间预留大,数量受 OS 限制;阻塞时会占住内核线程。
虚拟线程由 JVM 调度到少量载体线程(carrier)上,阻塞时把栈卸载到堆上的 Continuation,让出载体线程。它可以创建几十万个,适合 I/O 密集、大量短任务。
虚拟线程提升的是阻塞场景的吞吐上限,不是单任务速度;CPU 密集任务仍应使用固定大小的平台线程池。
java.lang.Thread,支持 ThreadLocal、InterruptedException 等线程语义。JDK 21 中哪些操作会导致虚拟线程 pinning?如何排查?
在 synchronized 块/方法中阻塞、执行 native 方法或 Foreign Function、类初始化过程中阻塞、Object.wait(),都会让虚拟线程无法卸载,从而钉住载体线程。
排查:启动参数 -Djdk.tracePinnedThreads=full 打印钉住栈;JFR 打开 jdk.VirtualThreadPinned 事件,观察频率和持续时间。
规避:把同步块中的阻塞调用改为 ReentrantLock + Condition,缩短 native 调用,避免在 static 初始化中做 I/O。JDK 24 的 JEP 491 已移除 synchronized 的 pinning 限制。
为什么不应该池化虚拟线程?并发上限怎么控制?
虚拟线程创建成本极低,用完即弃;池化会引入复用、排队和状态残留,还会掩盖真正的稀缺资源。正确做法是每个任务新建虚拟线程,用 Executors.newVirtualThreadPerTaskExecutor()。
并发上限应由下游资源决定:数据库连接用连接池或 Semaphore,HTTP 调用用客户端连接池,文件句柄用信号量。虚拟线程负责等待,信号量负责稀缺资源的并发许可。
不要用固定大小的 ThreadPoolExecutor 包虚拟线程工厂,那等于给取号单设置固定窗口,既失去弹性又容易误解并发模型。
线程池的核心参数怎么设置?为什么推荐有界队列?
核心参数:corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler。任务先填核心线程,再进队列,队列满才扩到最大线程,仍满则触发拒绝策略。
CPU 密集任务线程数约等于核数(或核数+1);I/O 密集任务可按「核数 × (1 + 等待时间/计算时间)」估算,但更可靠的方法是用 Little's Law 结合压测:并发 = 吞吐 × 平均延迟。
推荐有界队列 + 明确拒绝策略(如 CallerRunsPolicy 或自定义降级),避免无界队列导致内存膨胀和延迟失控;线程命名、异常处理和监控同样重要。
Executors.newFixedThreadPool 使用无界队列,maximumPoolSize 实际不起作用;生产环境应显式构造 ThreadPoolExecutor。thenApply 和 thenCompose 有什么区别?CompletableFuture 的异常怎么处理?
thenApply 做同步转换:T -> U,结果包装成 CompletableFuture<U>;thenCompose 做扁平化:T -> CompletableFuture<U>,避免嵌套的 CompletableFuture<CompletableFuture<U>>。
异常处理:exceptionally 提供降级值,handle 同时处理结果和异常,whenComplete 观察但不改变结果。allOf 等待全部完成,anyOf 取第一个成功。
默认使用 ForkJoinPool.commonPool;阻塞型任务应传入自定义 Executor,避免占满公共池。Java 9+ 可用 orTimeout/completeOnTimeout 设置超时。
thenApply 里返回另一个 CompletableFuture 会得到嵌套结构;需要扁平化时用 thenCompose。synchronized 和 ReentrantLock 怎么选?AQS 是什么?
synchronized 是 JVM 内置锁,语法简单、自动释放,并经过锁消除、锁粗化等优化;ReentrantLock 是 JDK 实现的显式锁,支持可中断、超时、公平锁、多个 Condition,需要 finally 解锁。
AQS(AbstractQueuedSynchronizer)用 volatile int state 表示同步状态,加一个 CLH 变体的等待队列;ReentrantLock、Semaphore、CountDownLatch 都基于它实现。
虚拟线程场景下,synchronized 中的阻塞会 pin 载体线程(JDK 21),因此长阻塞临界区优先用 ReentrantLock;普通短临界区 synchronized 足够。
ThreadLocal 为什么会内存泄漏?虚拟线程下要注意什么?
ThreadLocalMap 的 key 是 WeakReference<ThreadLocal>,value 是强引用。ThreadLocal 对象被回收后 key 变 null,但 value 仍被 Entry 强引用;线程长期存活时,这些 value 无法回收。
正确做法是用完调用 remove(),尤其是线程池场景;不要依赖 GC 清理 stale entry。
虚拟线程数量巨大,每个线程一份 ThreadLocal 副本会放大内存开销;跨任务传递优先用显式参数或 ScopedValue(JDK 21 预览),并避免依赖 InheritableThreadLocal 的隐式继承。
结构化并发解决了 invokeAll 的什么问题?
invokeAll 把任务提交后,任务与提交者的生命周期就脱钩了:提交者可能提前返回,任务仍在跑;某个任务失败时,其他任务不会自动取消;异常分散在 Future 里容易漏检。
结构化并发用作用域组织任务树:fork 创建子任务,join 等待,作用域关闭时所有子任务必须终止。ShutdownOnFailure 任一失败即取消其余,ShutdownOnSuccess 任一成功即取消其余。
它让取消和错误沿任务树传播,避免孤儿任务;JDK 21 中仍是预览 API,需要 --enable-preview。
G1 和 ZGC 怎么选?
G1 是 JDK 9+ 默认 GC,按 region 管理堆,追求可预测的停顿(默认目标 200ms),适合大多数服务;ZGC 用着色指针和读屏障做并发标记/转移,停顿通常小于 1ms,适合大堆、低延迟场景。
ZGC 的代价是读屏障开销、更高的内存占用和不支持压缩 oops;小堆、吞吐优先的批处理通常 G1 或 Parallel 更划算。
选型要看延迟目标、堆大小、分配率和吞吐要求,用真实流量压测 GC 日志,而不是只看「谁停顿最低」。
-XX:+ZGenerational 才启用分代模式。G1 的 region、Humongous 对象和 SATB 分别是什么?
G1 把堆划分为大小相等的 region(通常 1–32MB),每个 region 动态扮演 Eden、Survivor 或 Old;Young GC 回收年轻代 region,Mixed GC 同时回收部分老年代 region。
Humongous 对象是超过 region 一半大小的对象,会占用连续多个 region,分配和回收成本更高;应尽量避免超大数组/字符串。
SATB(Snapshot-At-The-Beginning)是 G1 的并发标记算法:在标记开始时给对象图拍快照,通过写前屏障记录引用变化,保证并发标记不漏对象。
-Xmn、NewRatio 等固定年轻代比例的参数;它需要动态调整 young region 数量。分代 ZGC 相比非分代 ZGC 改进了什么?
非分代 ZGC 每次回收整个堆,堆越大,并发标记/转移的总工作量越大,吞吐和分配停滞越明显。分代 ZGC 把对象分为年轻代和老年代,年轻代频繁、廉价地回收,老年代按需回收。
它需要 remembered set 记录老年代到年轻代的跨代引用,避免年轻代回收时扫描整个老年代;着色指针和读屏障仍是并发转移的核心。
JDK 21 通过 -XX:+UseZGC -XX:+ZGenerational 开启;JDK 23 起分代成为 ZGC 默认。
JDK 21 编译的 class 文件版本号是多少?如何排查 UnsupportedClassVersionError?
class 文件 major version = JDK 版本 + 44,JDK 21 是 65(JDK 8 是 52,11 是 55,17 是 61)。
javap -v X.class | grep major 可查看版本;UnsupportedClassVersionError 表示运行该 class 的 JVM 版本低于编译目标。
解决:用目标 JVM 支持的 --release 重新编译,或升级运行环境;不要只改 -source/-target,它们不会限制对新 API 的使用。
常见 OOM 有哪些类型?分别怎么排查?
Java heap space:堆内存不足,检查内存泄漏、增大 -Xmx、分析 heap dump(-XX:+HeapDumpOnOutOfMemoryError)。
Metaspace:类元数据耗尽,常见于动态生成类/类加载器泄漏;检查 -XX:MaxMetaspaceSize 和类加载器引用。
Direct buffer memory:堆外直接内存不足,检查 Netty/NIO 的 ByteBuffer 释放;Unable to create native thread:线程数或系统资源耗尽。
GC overhead limit exceeded:GC 花费大量时间却回收很少,通常是堆太小或对象存活率过高。
从 JDK 8 到 21,默认 GC 和 GC 家族有哪些变化?
JDK 8 默认 Parallel GC;JDK 9 起 G1 成为默认。CMS 在 JDK 9 弃用、JDK 14 移除;ZGC 在 JDK 15 正式,JDK 21 增加分代模式。
Serial、Parallel 仍保留,分别适合小堆/客户端和吞吐优先的批处理;Epsilon(无操作 GC)用于性能测试。
升级时不要沿用旧 GC 参数:JDK 8 的 CMS/ParNew 组合在新版本可能直接启动失败,应按新默认值重新评估。
String 为什么设计成不可变?字符串常量池和 intern 是什么关系?
不可变带来线程安全、可缓存 hashCode、可安全共享,也避免作为 HashMap key 时被修改;同时支撑字符串常量池的安全复用。
常量池保存字符串字面量和 intern 后的引用;"a" + "b" 在编译期折叠为 "ab" 并进入常量池,而运行时拼接产生新对象。intern() 把字符串放入池并返回池中引用。
不可变不等于常量池自动生效:new String("a") 仍会在堆上创建对象,只有字面量和显式 intern 才复用池中实例。
String.intern() 在大量动态字符串场景可能造成常量池膨胀和性能问题,慎用。Java 中 + 拼接字符串在底层是怎么实现的?
编译期常量拼接会被折叠;包含变量的 + 在 JDK 9+ 编译为 invokedynamic 调用 StringConcatFactory,由运行时选择拼接策略,不再固定生成 StringBuilder。
循环中每次 + 仍可能创建新 String 和中间对象,应该用 StringBuilder(单线程)或 StringBuffer(多线程)。
JDK 15+ 的多行文本用文本块,String.formatted 可读性更好;模板字符串在 JDK 21 仍是预览。
+ 优化成 StringBuilder;跨迭代的拼接无法自动复用。Optional 的 orElse 和 orElseGet 有什么区别?Optional 应该怎么用?
orElse 无论 Optional 是否有值都会先计算默认值;orElseGet 只在空时才调用 Supplier。默认值构造代价高时用 orElseGet。
map 做可能为空的转换,flatMap 用于返回 Optional 的转换,避免嵌套;ifPresentOrElse 同时处理有值/空值。
Optional 主要用作方法返回值表达「可能没有结果」,不建议作为字段、方法参数或集合元素;它不能替代 null 检查的全部场景。
Optional.get() 在空时抛 NoSuchElementException;优先用 orElseThrow 明确异常语义。Stream 的惰性求值体现在哪里?中间操作和终端操作如何区分?
中间操作(map/filter/sorted/limit)返回新的 Stream,只构建流水线,不执行;终端操作(collect/forEach/reduce/count)才触发遍历。
短路操作(findFirst/anyMatch/limit)可以在满足条件时提前结束;没有终端操作,中间操作什么都不会发生。
Stream 只能消费一次,重复使用会抛 IllegalStateException;有状态操作(sorted/distinct)可能需要缓冲全部元素,影响内存和并行性能。
Collectors.toMap 遇到重复 key 会怎样?怎么处理 null 值?
默认的 toMap(keyMapper, valueMapper) 在重复 key 时抛 IllegalStateException;应提供 merge 函数,例如 (a, b) -> b 保留后值。
toMap 内部使用 Map.merge,value 为 null 时会抛 NullPointerException;需要允许 null 值时,用 HashMap::new 作为 supplier 并手动处理,或先过滤。
需要保持顺序时指定 LinkedHashMap::new;需要分组时用 groupingBy 而不是先 toMap 再合并。
java.time 如何处理夏令时?yyyy 和 uuuu 有什么区别?
ZonedDateTime 携带时区规则。春季跳过的本地时间不存在,atZone 会自动前移到有效时间;秋季重叠的本地时间出现两次,默认选较早偏移,可用 withLaterOffsetAtOverlap() 选较晚偏移。
yyyy 是 era-based 年份(公元前/公元),uuuu 是 proleptic year;现代日期格式化应使用 uuuu,避免公元前/公元边界问题。
Instant 是时间线上的点,LocalDateTime 没有时区,不能直接表示某个瞬间;跨时区计算应保留 ZoneId 或 Offset。
LocalDateTime 做跨时区调度会忽略 DST;应使用 ZonedDateTime 或 Instant 加时区规则。JDK 11 的 HttpClient 怎么用?和 HttpURLConnection 比有什么优势?
HttpClient.newBuilder().connectTimeout(...).build() 创建客户端;HttpRequest.newBuilder(URI).GET()/POST(BodyPublishers.ofString(...)) 构建请求;send 同步、sendAsync 异步。
它支持 HTTP/2、WebSocket、异步 CompletableFuture、连接池和拦截器式配置,API 比 HttpURLConnection 更现代;默认使用 HTTP/2(服务端支持时)。
虚拟线程场景下,同步 send 可以搭配虚拟线程获得高并发;注意设置连接超时、请求超时和响应体处理,避免连接泄漏。
preview、incubator、experimental 三种状态有什么区别?
Preview:已进入 JDK 核心的语言/API 草案,用 --enable-preview 开启,编译和运行都要加;可能在后续版本修改或移除。
Incubator:独立模块(如 jdk.incubator.vector),用 --add-modules 使用,API 可能大改。
Experimental:需要 -XX:+UnlockExperimentalVMOptions 的 VM 选项,属于运行时实验特性;三者都不是正式兼容承诺。
为什么预览特性编译和运行都要加 --enable-preview?class 文件有什么标记?
编译期需要允许解析预览语法和 API;运行期 JVM 需要允许加载预览 class。缺一边都会失败。
预览 class 的 minor version 是 65535(0xFFFF),major version 仍是目标 JDK 版本;JVM 看到 65535 就要求 --enable-preview。
javap -v 可查看 minor version;预览 class 不能拿到其他 JDK 版本运行,必须用对应版本重新编译。
--release 21 与 --enable-preview 要同时配置;只加其中一个会得到难以理解的编译/运行错误。JDK 21 中哪些重要特性是正式的,哪些还是预览?
正式:虚拟线程(444)、SequencedCollection(431)、Record Patterns(440)、Pattern Matching for switch(441)、分代 ZGC(439,产品特性)。
预览:String Templates(430)、Unnamed Patterns and Variables(443)、Unnamed Classes and Instance Main Methods(445)、Scoped Values(446)、Structured Concurrency(453)、Foreign Function & Memory API(442,第三轮预览)。
孵化:Vector API(448,第六轮)。升级时要逐项查 JEP 状态,不能把预览当正式。
从 JDK 8 升级到 21,主要风险点有哪些?
先走 LTS 路径(8 → 11 → 17 → 21),每步解决编译、依赖和运行时问题。重点检查:移除的 Java EE/CORBA 模块(11)、Nashorn(15)、CMS(14)、RMI Activation(17)。
强封装使 sun.* 和 JDK 内部 API 的反射访问默认失败,需要 --add-opens 临时放行;动态加载 Agent 在 21 会警告(JEP 451)。
工具链上使用 --release 21 而不是 -source/-target,用 jdeps 分析依赖,升级所有字节码增强/Agent/序列化框架,并准备回滚方案。
什么是强封装?--add-opens 应该怎么用?
强封装指 JDK 内部包(如 sun.misc、jdk.internal.*)默认不向外部开放深度反射;JDK 16 起默认拒绝非法访问,17 起 --illegal-access 不再能整体放宽。
--add-opens java.base/java.lang=ALL-UNNAMED 可以临时开放某个包的深度反射,--add-exports 开放编译期访问;它们是迁移过渡手段,不是长期方案。
正确做法是替换内部 API(如用 VarHandle 替代 Unsafe、用标准 API 替代 sun.*),并推动依赖库升级。
--add-opens 和 --add-exports 的区别?JDK 21 动态加载 Java Agent 的警告是什么意思?
JEP 451 让 JVM 在运行期动态加载 Agent 时打印警告,为未来默认禁止做准备;通过 -javaagent 在启动时加载不受影响。
常见来源:APM、Mock 框架、字节码增强工具在运行时 attach。可以临时用 -XX:+EnableDynamicAgentLoading 显式允许。
长期方案是升级工具,改为启动时通过 -javaagent 加载,或使用工具提供的受支持接入方式。
JDK 8 之后有哪些常用的东西被移除或弃用?
移除:Java EE 与 CORBA 模块(11)、Nashorn JavaScript 引擎(15)、CMS GC(14)、RMI Activation(17);Security Manager 在 17 弃用,并在后续版本逐步限制启用。
弃用:finalize()(18 弃用)、Thread.stop/suspend/resume(长期弃用)、Runtime.getRuntime().exec 的部分不安全用法应改用 ProcessBuilder。
还有默认行为变化:G1 成为默认 GC(9)、强封装(16/17)、动态 Agent 警告(21)。迁移时要查对应版本的 release notes。
--release 和 -source/-target 有什么区别?跨版本编译要注意什么?
--release N 同时设定字节码版本和可用的 JDK API 签名(通过 ct.sym),能防止误用高于 N 的 API;-source/-target 只影响语法和字节码版本,仍可能链接到当前 JDK 的新 API。
跨版本编译应使用目标 JDK 的 --release,并固定构建 JDK 版本;多版本 jar 可用 Multi-Release 清单为不同 JDK 提供实现。
运行低版本 JVM 时,高版本 class 会抛 UnsupportedClassVersionError;预览 class 还要求同版本 JVM 和 --enable-preview。
-source 8?-source/-target 不能阻止你调用新 API,编译可能通过但运行期 NoSuchMethodError。怎么刷这套题HOWTO
- 先按主题筛:一次只攻一个主题,避免题目切换导致理解断裂。
- 先自答再展开:用 30 秒说出结论 + 一条原因 + 一个例子。
- 重点看追问:追问模拟面试官的第二层问题,能接住才算真会。
- 最后看版本陷阱:同一 API 在不同 JDK 的状态差异,是最容易答错的点。
- 标记掌握:不确定的不要标记;隔天再复习一遍。
难度说明LEVELS
- 高频:面试中出现频率高,或属于 JDK 21 的核心卖点。
- 进阶:需要理解机制、版本边界或实现取舍,答好能拉开差距。