Java全核心-阿里大厂面试-Gemini版

张开发
2026/4/10 2:25:08 15 分钟阅读

分享文章

Java全核心-阿里大厂面试-Gemini版
完善更新中......一、Java 核心基础1、Java 四大引用与 ThreadLocal 深度拷问【核心连环炮】面试官说一下 Java 的四大引用及其实际业务场景面试官ThreadLocal 为什么要用弱引用不用行不行面试官既然用了弱引用为什么还会内存泄漏真实业务中怎么防范【P6级满分答法】“四大引用分别是强、软、弱、虚。 在我的实际开发中软引用SoftReference常用于内存敏感的本地缓存比如 MyBatis 的底层缓存机制或大图片缓存在发生 OOM 之前 JVM 会自动回收它们。弱引用WeakReference最经典的场景就是ThreadLocalMap的 Key只要发生 GC 就会被回收。虚引用PhantomReference业务代码极少直接用一般配合引用队列ReferenceQueue使用我研究过 NIO 的源码DirectByteBuffer分配堆外内存时就是用虚引用Cleaner 机制来监控对象被回收的时刻从而调用Unsafe.freeMemory()释放操作系统物理内存。关于ThreadLocal 它底层维护了一个ThreadLocalMapKey 是 ThreadLocal 实例弱引用Value 是具体的业务对象强引用。为什么用弱引用如果 Key 是强引用那么即使业务代码里把ThreadLocal实例置为null只要线程存活比如在线程池中这个 Entry 就会一直存在导致 Key 和 Value 都无法回收。用了弱引用后GC 时 Key 会被自动回收此时 Key 变成null。内存泄漏的根本原因虽然 Key 被回收了但 Value 依然被强引用着Thread - ThreadLocalMap - Entry - Value。如果线程池复用核心线程这个nullKey 对应的 Value 就会永远驻留内存。我的落地规范在拦截器Interceptor或 AOP 的afterCompletion/finally代码块中绝对强制调用remove()方法。”【致命追问及解答】追问 1父子线程怎么传递 ThreadLocal如果是线程池里的异步任务呢解答父子线程可以用InheritableThreadLocal。但在线程池场景下InheritableThreadLocal会失效因为线程是复用的不会触发初始化。阿里开源了TransmittableThreadLocal(TTL)它通过修饰Runnable/Callable在任务提交时抓取当前线程的上下文在任务执行前重放执行后恢复完美解决了全链路追踪TraceId在异步线程池中的传递问题。2、线程底层与锁机制 volatile / synchronized / Lock 【核心连环炮】面试官volatile 和 synchronized 的底层区别是什么面试官详细讲讲 synchronized 的锁升级过程会降级吗面试官ReentrantLock 里的 AQS 原理是什么公平锁和非公平锁怎么实现的【P6级满分答法】“volatile是轻量级同步机制保证了可见性和有序性但不保证原子性。在底层它是通过插入内存屏障Memory Barrier来禁止 CPU 指令重排并通过缓存一致性协议如 MESI强制将修改刷新到主存并让其他线程的工作内存缓存行失效。synchronized是 JVM 层面的重量级锁保证了三大特性。底层是基于对象头Mark Word和 Monitor 管程实现的monitorenter和monitorexit指令。锁升级过程JDK 1.6 优化无锁 / 偏向锁一开始认为只有一个线程执行在对象头里记录 Thread ID。下次该线程来直接放行几乎零开销。轻量级锁自旋锁当有第二个线程来竞争时偏向锁撤销升级为轻量级锁。线程会在自己的栈帧里建立 Lock Record用 CAS 操作尝试将对象头替换为指向自己栈帧的指针。如果没拿到会原地自旋CAS 轮询不阻塞挂起因为线程上下文切换代价太大。重量级锁如果自旋超过一定次数或者自适应自旋失败或者第三个线程加入了竞争锁就会膨胀为重量级锁。此时底层调用操作系统的 Mutex Lock未拿到锁的线程会被放进阻塞队列挂起不消耗 CPU。”【致命追问及解答】追问 1刚刚提到 volatile 的内存屏障具体是怎么插的解答在写操作前插入 StoreStore写操作后插入 StoreLoad读操作后插入 LoadLoad 和 LoadStore。最耗时的是 StoreLoad 屏障。追问 2锁能降级吗解答HotSpot JVM 实际上是支持锁降级的但它只发生在STWStop The World的 GC 阶段VM Thread 会把空闲的重量级锁降级所以对应用层开发来说一般默认“锁只能升级不能降级”。追问 3ReentrantLock 非公平锁的效率为什么比公平锁高解答公平锁每次必须到 AQS 队列尾部排队而非公平锁在调用lock()时会直接先无脑执行一次 CAS 尝试抢锁如果此时锁刚好释放它就能直接插队拿到锁。这减少了线程被挂起和唤醒的上下文切换开销吞吐量大幅提升代价是可能产生线程饥饿。3、线程池与并发架构【核心连环炮】面试官线程池的七大参数是什么工作流程是怎样的面试官你们线上业务的线程池是怎么配的面试官如果线上流量突增队列满了拒绝策略选哪个【P6级满分答法】“七大参数包括核心线程数(corePoolSize)、最大线程数(maxPoolSize)、存活时间(keepAliveTime)、时间单位、阻塞队列(workQueue)、线程工厂(threadFactory)、拒绝策略(handler)。工作流任务进来先看核心线程满没满没满就创建满了就塞进队列队列也满了就创建非核心线程直到达到最大线程数最后连最大线程数都满了就执行拒绝策略。线上真实业务落地如果是IO 密集型如调用外部 RPC、查数据库通常配置2N 1N为CPU核数或者按公式CPU核数 * (1 等待时间/计算时间)计算。 如果是CPU 密集型如复杂的内存数据聚合、排序通常配N 1。多出来的一个是为了防止缺页中断等异常导致的 CPU 空闲。拒绝策略默认是AbortPolicy抛异常。但是在我们的核心交易链路/打标服务中一般自定义拒绝策略。策略内部会将遭拒绝的任务信息记录到日志或者发到 MQ 中后台跑一个补偿定时任务去重试保证任务绝对不丢失。”【致命追问及解答】追问 1怎么在不重启服务的情况下动态调整线上线程池的参数解答利用 JDK ThreadPoolExecutor 预留的setCorePoolSize()和setMaximumPoolSize()方法。我们可以结合配置中心如 Nacos/Apollo监听配置的变更事件。一旦监听到修改直接获取 Spring 容器中的线程池 Bean调用set方法实时生效。追问 2Tomcat 的线程池工作流程和 JDK 原生的一样吗解答完全不一样JDK 原生是“先塞队列满了再开扩容线程”这适合 CPU 计算。但 Tomcat 面对的是 Web 请求它重写了TaskQueue的offer方法当核心线程满了它会先判断如果当前线程数小于最大线程数直接返回 false队列假装自己满了从而强制线程池先去创建最大线程来处理请求直到达到最大线程数了再真正塞入队列排队。这是一种“优先响应请求”的极致优化。4、CAS 与 Atomic 高级原理【核心连环炮】面试官讲讲 CAS 原理及 ABA 问题面试官高并发下 CAS 一直自旋失败导致 CPU 飙升怎么解决【P6级满分答法】“CASCompare And Swap包含三个关键值内存地址 V、旧的预期值 A、要修改的新值 B。只有当 V 的值等于 A 时才将 V 改为 B。它底层是依赖 CPU 的cmpxchg指令实现的原子操作。ABA 问题线程 1 读到值是 A此时线程 2 将 A 改成了 B又马上改回了 A。线程 1 执行 CAS 时发现值还是 A认为没被改过操作成功。虽然对简单数字没影响但在链表等复杂数据结构并发操作中会导致严重 Bug。解决方案使用 JDK 的AtomicStampedReference加一个版本号Stamp每次修改不仅比较值还比较版本号A1 - B2 - A3就能彻底解决。高并发下的 CPU 飙升问题如果几百个线程同时对一个 AtomicInteger 循环 CAS 失败会造成极大的 CPU 资源浪费。优化方案在 JDK 8 中如果是统计/计数场景我会直接用LongAdder替代AtomicLong。它的核心思想是“分段分离”类似 JDK7 的 ConcurrentHashMap。底层维护了一个Base值和一个Cell数组并发低时直接 CAS 更新 Base并发高时各个线程通过 Hash 路由到不同的 Cell 元素上独立进行 CAS 累加。最后sum()时把 Base 和所有 Cell 累加起来即可。极大地减少了热点冲突。”5、HashMap 底层深度解析【核心连环炮】面试官HashMap 在 JDK 7 和 JDK 8 的主要区别面试官为什么 JDK8 链表长度到 8 才会转红黑树到 6 又退化回链表面试官HashMap 线程不安全具体体现在哪里【P6级满分答法】“区别JDK7 是 数组单向链表采用头插法。JDK8 是 数组单向链表红黑树采用尾插法。JDK8 在扩容时做了优化不再需要重新计算 hash只需判断(hash oldCap) 0如果为真就留在原位为假就移动到原位置 oldCap。红黑树阈值为 8 的原因底层源码的注释明确说明这符合泊松分布Poisson distribution。在默认负载因子 0.75 下同一个桶里发生 hash 碰撞到达 8 个元素的概率只有千万分之六。也就是说转红黑树是一个兜底的防爆机制平时几乎不会触发。退化阈值设为 6 而不是 8是为了形成缓冲带避免 7 和 8 之间频繁的树化和解树化浪费 CPU。并发不安全的体现JDK 7 的死链问题并发扩容时头插法会导致链表节点顺序翻转如果两个线程时间差正好卡在节点转移的过程会形成环形链表下次get()时触发死循环导致 CPU 100%。JDK 8 的数据覆盖问题尾插法解决了死链但在put()阶段如果两个线程同时判断当前位置没有哈希冲突准备执行写入时发生时间片切换后面的线程会直接覆盖前一个线程写入的值。”【致命追问及解答】追问 1那如果要求线程安全JDK8 的 ConcurrentHashMap 是怎么设计的解答摒弃了 JDK7 的 Segment 分段锁太重采用了CAS synchronized的极致细粒度锁。只锁当前链表或红黑树的头节点。只要不发生 Hash 碰撞不同的头节点完全可以并发写入使用 CAS 写入头节点发生碰撞时再用synchronized锁住头节点往下遍历。追问 2ConcurrentHashMap 调用size()方法在高并发下怎么保证性能的解答思想和上面提到的LongAdder一模一样维护了一个baseCount和一个CounterCell[]数组。高并发写入时不强制去更新baseCount而是把元素增量 CAS 到自己的CounterCell上最后统计时求和即可。6、泛型、反射、注解实际业务落地【核心连环炮】面试官泛型的类型擦除是什么如何突破类型擦除获取真实泛型面试官反射性能差在哪里你在业务中用过反射和注解吗【P6级满分答法】“泛型擦除Java 的泛型是伪泛型编译阶段会将泛型擦除为 Object 或边界类extends。这意味着运行期的 ListString 和 ListInteger 其实是同一个类。突破擦除阿里实战重点在解析复杂 JSON如 Fastjson / Jackson时经常遇到无法反序列化泛型集合的问题。我们业务代码中普遍采用匿名内部类TypeReference的方式。因为泛型类虽然被擦除但如果作为父类被子类继承泛型信息会保存在子类的字节码常量池Signature 属性中。TypeReference 就是通过getClass().getGenericSuperclass()动态拿到了原始泛型信息。反射与注解的落地反射的性能损耗主要在方法区查找 Method 对象和参数装箱拆箱/安全校验。但 JDK 做了优化当反射调用某个方法超过 15 次InflationThresholdJVM 会动态生成字节码GeneratedMethodAccessor将反射调用转为直接调用性能大幅回升。真实业务落地我主导过一个接口防刷限流注解的开发。定义RateLimit(permits 10, time 1)注解。将注解打在 Controller 接口上。利用 Spring AOP 定义切面Pointcut拦截该注解。在Around环绕通知中通过反射获取方法签名上的注解信息限流阈值然后结合Redis Lua 脚本实现分布式的滑动窗口限流。如果超限直接抛出业务异常阻断流程。这让系统与底层的限流逻辑完全解耦。”建议在回答时遇到熟悉的地方一定要主动出击比如讲 CAS 时主动带出 LongAdder 的优化讲线程池时主动带出 Tomcat 的区别这在 P6 面试中叫作“展现技术深度与视野”。二、JVM 调优1、JVM 内存模型与对象分配的“千层套路”【核心连环炮】面试官说一下 JVM 内存模型对象一定分配在堆上吗面试官元空间Metaspace和永久代有什么区别为什么要换【P6级满分答法】“JVM 内存区域主要分为程序计数器、虚拟机栈、本地方法栈这三个是线程私有的、堆和元空间这两个是线程共享的。 但在实际底层实现中对象不一定全部分配在堆上。如果开启了逃逸分析-XX:DoEscapeAnalysis和标量替换JVM 在 JIT 编译期间发现一个对象的作用域没有逃出方法内部就会将其打散成基本类型直接在虚拟机栈上分配。随着方法出栈内存直接回收极大减轻了 GC 压力。此外为了解决多线程并发分配内存的锁竞争问题堆内存中还有一个TLAB本地线程分配缓冲每个线程在 Eden 区有一块私有的小空间对象优先在这里无锁分配。关于元空间与永久代 JDK 8 之前叫永久代用的是 JVM 的堆内存JDK 8 之后改为元空间直接使用本地物理内存Native Memory。 替换的核心原因有两点一是永久代大小很难预估太小容易报java.lang.OutOfMemoryError: PermGen space特别是在大量使用 CGLIB 动态生成代理类的 Spring 项目中二是字符串常量池在 JDK 7 时已经移到了堆中JDK 8 彻底移除永久代把类的元数据放到本地内存只要物理内存够就不容易 OOM。”【致命追问及解答】追问 1既然元空间用的是物理内存那还需要监控和调优吗如果不设上限会怎样解答必须设上限-XX:MaxMetaspaceSize。如果不设它默认可以无限使用物理内存。一旦代码中存在动态类加载的内存泄漏比如使用自定义 ClassLoader 但没有卸载或者 ThreadLocal 导致 ClassLoader 泄漏会把操作系统的物理内存吃干引发 OOM 甚至导致 Linux 的 OOM Killer 把整个 Java 进程强杀。2、垃圾回收器演进从 CMS 到 G1再到 ZGC 的降维打击【核心连环炮】面试官CMS 的并发三色标记是怎么回事会有什么漏洞面试官G1 是怎么解决 CMS 的碎片的它的停顿时间为什么可控面试官你了解 ZGC 吗它凭什么能做到亚毫秒级的停顿【P6级满分答法】“CMS是以获取最短回收停顿时间为目标的收集器。核心过程分为初始标记STW、并发标记、重新标记STW、并发清除。 在并发标记阶段采用的是三色标记法白未访问灰本身已访问但成员变量未访问完黑全部访问完。漏洞漏标问题当且仅当满足两个条件时会漏标1. 黑色对象增加了一条指向白色对象的引用2. 灰色对象删除了指向该白色对象的引用。导致本该存活的白色对象被当成垃圾回收这是极其致命的。CMS 的补救采用增量更新Incremental Update机制。只要黑色对象插入了对白色对象的引用就把这个黑色对象记录下来在‘重新标记’阶段把它变回灰色重新扫描一遍。缺点是重新扫描的代价较大。G1 的破局G1 放弃了传统的物理分代改为逻辑分代和物理分区Region。它将堆分成 2048 个大小相同的 Region。解决碎片整体看是标记-整理局部看是复制算法每次回收都会把存活对象复制到空闲 Region不会产生内存碎片。解决漏标采用SATB原始快照机制。在并发标记开始时保留一份对象图逻辑快照。如果灰色对象删除了指向白色对象的引用就把这个引用记录下来。在最终标记阶段只扫描这些被删除的引用把它们当作存活对象处理产生一点浮动垃圾但避免了全量重扫极大缩短了 STW。停顿可控G1 会在后台维护一个收益优先队列。根据用户设置的-XX:MaxGCPauseMillis优先回收那些垃圾最多、回收耗时最短的 Region。ZGC 的降维打击G1 虽然优秀但它的 Region 复制阶段转移阶段仍然需要 STW堆越大停顿越长。而ZGC 实现了并发转移。它通过染色指针Colored Pointers*在指针的高位记录对象状态配合*读屏障Load Barrier。即使对象在并发转移阶段被移动了物理地址业务线程访问该对象时读屏障也会通过染色指针的转发状态自动将指针修正到新地址。这使得 ZGC 的 STW 时间与堆大小完全无关目前 JDK 17 中已经能稳定在 1ms 以内。”3、线上 OOM 排查实战与 Arthas 炫技【核心连环炮】面试官线上机器突然报警 OOM你怎么快速定位是内存溢出还是内存泄漏面试官如果 Dump 文件有几十个 G本地电脑打不开怎么办面试官如果不用 Dump 文件你会用 Arthas 怎么排查【P6级满分答法】“收到报警第一时间先看日志里 OOM 的后缀 如果是Java heap space大概率是数据量激增或者内存泄漏。 如果是GC overhead limit exceeded说明系统 98% 的时间都在 GC但只回收了不到 2% 的内存这是典型的老年代内存泄漏前兆。 如果是Metaspace重点查动态类加载或反射。 如果是unable to create new native thread说明线程数超了操作系统的ulimit限制。标准化排查流程我们在生产环境会默认开启-XX:HeapDumpOnOutOfMemoryError。拿到 Dump 文件后使用 Eclipse MAT 进行分析。重点看Dominator Tree支配树和Histogram直方图直接就能找出占用 Retained Heap深堆最大的对象。然后右键Path to GC Roots排除虚引用和弱引用就能清晰看到这个大对象是被哪个业务代码里的静态集合或线程的局部变量强引用着导致无法回收。这就是区分溢出和泄漏的核心如果是溢出大对象一般是业务必需的如果是泄漏通常是某些本该清空的 HashMap/List 没有清空。几十个 G 的 Dump 文件处理绝不能拉到本地处理。我会直接在生产环境或一台高配的跳板机上使用jhat或者通过脚本执行 MAT 的无头模式Headless Mode./ParseHeapDump.sh dump.hprof让服务器自己解析生成 HTML 报告我只看最终的报告页面。使用 Arthas 实时排查如果 OOM 还没发生只是内存水位告警不能随便触发 HeapDump会导致长 STW。我会立刻挂载 Arthas输入dashboard实时观察堆内存各区域的水位和 GC 频率。输入thread -n 3查看当前最忙的三个线程看是不是 GC 线程在疯狂自旋或者业务线程在处理死循环。如果怀疑是某个静态缓存一直变大我会用ognl命令直接查看应用上下文中的某个 HashMap 的size()。使用profiler start采集 CPU 或内存的火焰图直观看出哪段代码在疯狂分配对象。”4、JVM 调优方法论与场景实战【核心连环炮】面试官遇到频繁的 Minor GCYoung GC怎么解决面试官什么是“过早晋升Premature Promotion”如何调优参数避免面试官如果没有性能瓶颈你会主动去调优 JVM 参数吗【P6级满分答法】“频繁的 Minor GC如果 Minor GC 极度频繁通常是因为新生代Eden 区给得太小或者短生存周期的对象创建速率Allocation Rate极高。我会先通过 GC 日志-Xloggc计算出每秒对象分配速率。如果机器内存允许最直接的办法是适当调大新生代的比例调整-XX:NewRatio或增大整体 Heap 大小。过早晋升核心痛点过早晋升是指本该在年轻代被回收的短命对象因为 Survivor 区空间不足或 Survivor 比例不合理触发了 JVM 的动态年龄判断机制Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半被提前送到了老年代。这会迅速填满老年代导致极具破坏性的 Full GC。调优动作观察 GC 日志看对象晋升的平均年龄。如果远小于默认的 15说明出现了过早晋升。增大 Survivor 区的大小调小-XX:SurvivorRatio比如从默认的 8 调到 6。如果是突发的大流量可以适当上调-XX:TargetSurvivorRatio默认 50%可以调到 70%-80%来提高 Survivor 区的利用率。如果是确定的长生命周期对象如本地缓存可以通过-XX:PretenureSizeThreshold让大对象直接进入老年代避免在 Survivor 区来回复制。调优的最高心法如果没有发生明显的性能瓶颈响应时间变慢、CPU 飙高、频繁 Full GC我绝对不会为了调优而调优。JVM 发展到 JDK 8 和之后的版本其自适应启发式算法Ergonomics已经非常聪明。人为乱调参数往往适得其反。调优一定是基于明确的数据支撑监控告警、压测数据、GC 日志和具体的业务痛点去进行的。”

更多文章