每日问答java基础篇

📅 2026/8/11 7:40:03
每日问答java基础篇
每日问答第一天 java基础篇1.JVM 运行时数据区哪些是线程私有哪些线程共享程序计数器会不会 OOM线程私有程序计数器、虚拟机栈、本地方法栈线程共享堆、元空间 (Metaspace替代永久代)程序计数器唯一一个没有 OOM 的区域记录当前线程执行字节码行号。2.讲下 G1 GC 核心设计Region 是什么Remembered Set 作用Mixed GC 什么时候触发RegionG1 不再固定新生代老年代整块划分把整个堆切分成很多大小相等的 Region每个 Region 可以充当 Eden、Survivor、Old。可以灵活分配不用连续大内存。Remembered Set(RSet)解决跨 Region 引用扫描问题。老年代对象引用新生代对象如果没有 RSetYGC 时要扫描全部老年代开销巨大。RSet 记录哪些老 Region 里的对象引用了本 Region 的对象YGC 只扫描 RSet 记录的这部分不用遍历整个老堆。Mixed GCG1 的核心不是 FullGC。当堆占用达到-XX:InitiatingHeapOccupancyPercent默认 45%开启并发标记标记完成后执行 Mixed GC。Mixed GC同时回收部分 Eden Survivor 若干收益高的 Old Region优先回收垃圾多的老年代 Region实现可控停顿。区分YoungGC只回收 EdenSurvivorMixedGCY 部分 OldFullGC单线程 STWG1 尽量避免Mixed 搞不定才会走到 FullGC3.说一下 G1 中并发标记流程有哪几个阶段哪个阶段会 STW核心定位面向大堆内存目标是可控的 GC 停顿用户设置最大停顿时间比如 200ms不追求完全消除 STW尽量把 STW 压到阈值以内。核心设计思想把整个 Java 堆切分成大量大小相同的Region默认 1M32M。Region 角色动态可变Eden、Survivor、Old、Humongous存大对象大于 Region 一半就是大对象。不再严格固定新生代、老年代整块边界灵活回收。RSet(Remembered Set)解决跨 Region 引用YGC 不用扫描整个老年代。优先回收垃圾最多的 Region回收收益最大化。G1 有两类 GCYoung GC、Mixed GC尽量避免 Full GC。G1 完整并发标记 回收 5 大阶段初始标记Initial Mark【STW】只标记 GC Roots 直接可达的对象停顿很短依附在 Young GC 之后执行。并发标记Concurrent Mark【不 STW业务线程同时跑】从 GC Roots 遍历整个堆标记存活对象并发过程中业务还在产生新引用会产生漏标问题靠 SATB 快照算法解决。重新标记Remark【STW】修正并发标记期间因为用户线程运行产生的引用变化补全存活对象标记。SATB 的快照在这里处理。清理Cleanup【部分 STW】统计每个 Region 的垃圾占比筛选出回收收益高的 Old Region不真正回收内存只做统计排序。触发条件堆占用达到InitiatingHeapOccupancyPercent默认 45%就启动上面整套并发标记流程。Mixed GC真正回收【STW】并发标记跑完之后执行 Mixed GC。回收全部 Eden 全部 Survivor 一批收益高的 Old Region。会参考你设置的最大停顿时间控制本次选多少个 Old Region保证 STW 不超预期。反复多轮 Mixed GC逐步清理老年代不是一次性全部回收。补充几个高频坑点Humongous 大对象直接分配在 Humongous Region不会进老年代大对象不会经过 Survivor容易直接占满堆引发 OOM。Remembered Set (RSet)每个 Region 维护 RSet记录别的 Region 对自己的引用。YGC 的时候只扫描 RSet不用扫描全部老年代解决跨代引用开销。Full GCMixed GC 跟不上对象产生速度没有足够空闲 RegionG1 就会退化为单线程 STW Full GC停顿时间会暴涨线上要尽量避免。参数-XX:MaxGCPauseMillis200只是目标期望值不是保证值堆太大、对象太多依然会超时。口语简短版G1 把堆切分成很多 Region角色动态变化依靠 RSet 处理跨 Region 引用。当堆占用达到 45% 触发并发标记分为初始标记 (STW)、并发标记、重新标记 (STW)、清理。标记完成后执行 MixedGC一次性回收 Eden、Survivor 加上部分垃圾多的老年代 Region依据设置的最大停顿时间控制回收数量实现可控 STW。如果回收速度跟不上对象分配会退化为 FullGC线上要避免4.线上看到频繁 FullGC你排查思路是什么会看哪些日志、用什么工具可能哪些原因核心逻辑先看 GC 日志定位现象 → 工具抓现场 → 归类根因 → 对应解决方案第一步先看 GC 日志拿到现象开启 GC 日志参数-XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/xxx/gc.log看日志重点信息FullGC 发生频率、每次 STW 耗时堆各代Eden、Survivor、Old、Metaspace 内存变化每次 FullGC 之后老年代内存是不是降不下去降不下去 内存泄漏还是一回收完很快又被打满内存分配太快第二步工具抓取现场jstat -gc pid 1000实时看 GC 统计观察老年代上涨速度jmap -dump:formatb,filexxx.hprof pid发生 FullGC 时 dump 堆快照尽量业务低峰或者触发 OOM 自动 dump不要在高峰期 dump会 STW 卡死服务分析 dump 文件MAT 工具看大对象、对象引用链找泄漏点Arthasheapdumpjvm命令快速查看运行时堆情况第三步高频根因分类面试必须说出来内存泄漏对象逻辑不再使用但还有引用无法释放FullGC 后老年代内存下降很少很快再次占满。常见集合没清理、ThreadLocal 不 remove、静态集合持有大量对象、第三方连接不关闭。老年代内存分配过快短时间大量大对象直接进入老年代 / Humongous 大对象过多Mixed GC 回收速度赶不上对象创建速度G1 降级 FullGC。元空间 Metaspace 溢出动态类大量生成Groovy、CGLIB 动态代理元空间持续涨触发 FullGC。显式调用 System.gc ()代码、框架主动触发强制 FullGC日志会带System.gc()标记。G1 参数配置不合理InitiatingHeapOccupancyPercent设置过高并发标记启动太晚堆太小MaxGCPauseMillis设置过小Mixed GC 回收的 Old Region 太少老年代堆积越来越多。第四步对应处理泄漏MAT 找引用链修复代码释放对象ThreadLocal 用完 remove大对象业务优化避免短期大批量大对象调整大对象逻辑Metaspace调大元空间上限排查动态类生成逻辑System.gc加参数-XX:DisableExplicitGC屏蔽G1 参数调整 IHOP合理设置堆大小不要把停顿时间设得过于苛刻✨口述精简版首先查看 GC 日志确认 FullGC 频率、回收后堆内存变化。使用 jstat 观察内存实时走势必要时 dump 堆快照用 MAT 分析。常见原因内存泄漏、大量大对象快速占满老年代、元空间溢出、代码主动调用 System.gcG1 参数不合理。如果回收后内存降不下去基本就是内存泄漏顺着引用链定位无用对象如果分配太快就要优化业务对象产生逻辑调整 JVM 参数。双亲委派是什么有什么好处什么时候要破坏1、是什么类加载器是用来把.class字节码加载进 JVM。JDK 内置三层类加载器层级关系启动类加载器 (Bootstrap ClassLoader)C 实现加载 jre/lib 下面核心 rt.jar 等基础类Object、String 这些。扩展类加载器 (Extension ClassLoader)Java 实现加载 jre/lib/ext 扩展 jar 包。应用程序类加载器 (AppClassLoader系统类加载器)加载我们项目 classpath 下自己写的代码、第三方 jar。双亲委派规则加载一个类的时候先向上委托父加载器尝试加载父加载器加载不了自己才去加载。不是继承关系是组合委派每个加载器持有父加载器引用不是继承。加载流程AppClassLoader 收到加载请求不自己加载交给父扩展类加载器。扩展类加载器收到请求继续向上委托给启动类加载器。启动类加载器看自己路径下找不到这个类返回。回到扩展类加载器自己路径找不到返回。回到 AppClassLoader才在 classpath 去加载我们的类。2、双亲委派两大好处面试必说安全防止核心类被篡改比如我们自己写一个java.lang.String类。按照委派会优先交给启动类加载器加载 JDK 原生 String不会加载你自己写的恶意版本避免破坏核心类库。类全局单一保证类唯一性同一个类全限定名 类加载器作为唯一标识。委派保证核心类只被启动类加载器加载一次不会重复加载多份。3、什么时候要破坏双亲委派破坏 不向上委托当前加载器直接自己去加载类。典型场景SPI 机制JDBC 驱动Driver 接口是 rt.jar由启动类加载器加载但是各个数据库厂商驱动 jar 是我们项目的在 classpath。启动类加载器看不到 classpath 下驱动。解决方案线程上下文类加载器拿到 AppClassLoader直接加载驱动类打破向上委派。热部署、热加载比如 OSGi 框架模块热更新。不同模块需要独立类加载器同一个类名要加载多份需要打破双亲委派。自定义插件化插件 jar 动态加载隔离插件类每个插件独立类加载器。面试口述精简版答题直接输出双亲委派是类加载的约定类加载收到加载请求优先委托父加载器加载父加载器无法加载自己才加载。加载器分为启动、扩展、应用类加载器是组合而非继承关系。好处第一保护 JDK 核心类不被篡改保证安全第二保证类的唯一性避免重复加载。需要破坏场景JDBC SPI热部署 OSGi插件化比如 JDBC接口由启动类加载器加载驱动实现在应用路径启动加载器看不到使用上下文类加载器直接加载实现类打破委派。高频坑点双亲委派不是语法强制是ClassLoader.loadClass()代码逻辑实现可以重写破坏。Bootstrap 启动类加载器没有 Java 对象是 C 写的所以扩展类加载器的父是 nul最后大致说下G1 下对象完整生命周期串联Java 启动→对象分配→晋升→GC→STW→参数配置前提使用G1 垃圾收集器JDK8G1 不再把堆固定切割成整块 Eden/Survivor/Old而是Region 颗粒。总堆大小由-Xms(初始堆)-Xmx(最大堆) 控制整个堆被切分成大小相等 Region单个 Region 大小 JVM 自动算1M~32M。一、Java 启动堆内存怎么划分JVM 启动操作系统分配堆内存切成 N 个 Region。Region 类型动态可变不是写死Eden Region新对象优先分配在这里多个 Region 组成 Eden 区没有固定连续空间Survivor Region(S0/S1)存活新生代对象放这里两组S0、S1Old Region老年代对象Humongous Region大对象对象大小 1/2 Region直接放这里不属于 Eden不经过 Survivor⚠️重点对比 Parallel GC/CMSCMS 是固定比例Eden:Survivor8:1:1G1 没有固定比例Eden、Survivor 的 Region 数量动态变化JVM 根据停顿目标自动调整。G1 怎么修改各区域大小G1不能直接手动设置 Eden 大小没有-XX:NewRatio这种硬比例强制控制可以设但不推荐。总堆-Xms4g -Xmx4g固定堆 4G生产建议 XmsXmx 避免运行时扩容耗性能Region 大小-XX:G1HeapRegionSize16M只能是 1/2/4/8/16/32M2 的幂控制新生代整体占比-XX:G1NewSizePercent5堆最小 5% 作为新生代-XX:G1MaxNewSizePercent60堆最大 60% 作为新生代EdenSurvivor 总和不能超过堆的 60%JVM 在这个区间动态增减 Eden 的 Region 数量。❌G1 不要用 - Xmn 强制固定新生代大小会破坏 G1 自适应停顿控制。二、对象分配对象出生放在哪里普通小对象优先分配到Eden Region大对象大于 1/2 Region直接分配 Humongous Region跳过 Eden、跳过 Survivor直接算作老年代逻辑。TLAB线程本地分配缓冲。每个线程优先在自己 TLAB 分配对象减少锁竞争TLAB 满了再去 Eden 公共区分配。三、对象什么时候离开 Eden进入 Survivor、Old触发YGCEden 占满Eden 全部用完触发 YGC【STW】标记 Eden 存活对象复制到 SurvivorS0Eden 全部清空。Survivor 里面对象每经历一次 YGC对象年龄 age1。对象晋升到老年代 Old 的 4 个条件满足任意一个就晋升年龄达到阈值 MaxTenuringThreshold默认 15多次 YGC 还活着age 涨到 15晋升 Old Region。Survivor 空间不够YGC 之后存活对象太多Survivor 放不下直接把部分对象拷贝到老年代。非常常见对象大于大对象阈值→ Humongous直接老年代。动态年龄判断重点Survivor 中相同年龄所有对象总大小 Survivor 一半等于或大于该年龄的全部对象直接晋升老年代不用等到 15。这个动态规则很多人漏是线上很多对象提前进老年代的元凶。Survivor 机制S0、S1永远一个空。每次 YGC 把存活复制到空的那块上一块全部清空。四、GC 触发时机 STW 梳理G11YGCYoung GC✅触发Eden 的 Region 全部耗尽分配新对象没有 Eden 空间✅做什么回收全部 Eden回收 Survivor存活对象复制到另一块 Survivor满足条件晋升 Old。✅全程 STW。✅不会清理老年代不会启动并发标记。2MixedGC 完整链路重点串联随着运行对象不断晋升 Old老年代 Region 越来越多。老年代占用达到 IHOP 阈值-XX:InitiatingHeapOccupancyPercent45默认 开启【并发标记周期】这只是后台启动标记不立刻 GC并发标记周期 4 步初始标记依附 YGC 之后执行STW标记 GC Roots 直接对象并发标记业务线程继续跑GC 线程遍历堆标记存活不 STWSATB 快照解决并发漏标重新标记 RemarkSTW修正并发期间引用变化Cleanup 清理阶段部分 STW统计各个 Old Region 垃圾占比排序不回收内存✅并发标记全部跑完之后等待下一次 YGC 到来。此时这次 YGC 不再是普通 YGC升级为Mixed GC【STW】MixedGC 行为回收全部 Eden Survivor同时挑选垃圾最多的一批 Old Region回收参考-XX:MaxGCPauseMillis200目标停顿时间控制本次回收多少 Old Region。MixedGC 不会一次性回收全部老年代会连续多轮 MixedGC 逐步清理老年代。3Full GCG1 尽量避免单线程 STW停顿巨大触发条件任意一条需要分配对象堆找不到空闲 RegionMixedGC 回收速度赶不上对象产生速度。Humongous 大对象找不到连续的 Region。元空间 Metaspace 耗尽。代码调用System.gc()没有配置-XX:DisableExplicitGC。并发标记还没跑完堆直接被打满直接降级 FullGC。FullGC单线程 STW压缩整理整个堆性能很差线上告警指标。五、STW 汇总哪些阶段会停顿YGC全程 STWMixedGC全程 STW并发标记周期初始标记STW很短挂靠 YGC并发标记无 STW业务正常跑Remark 重新标记STWCleanup部分 STWFullGC全程长时间 STW注意并发标记 ≠ GC 回收并发标记只是标记哪些对象存活真正回收工作发生 YGC/MixedGC回收阶段才 STW。六、完整故事线从头到尾串一遍Java 进程启动设置 Xms/Xmx 确定堆总大小堆被切分成大量等大 Region分为 Eden、Survivor、Old、Humongous。新对象优先分配 Eden大对象直接进入 Humongous。Eden 占满触发 YGCSTW存活对象复制到 Survivor对象 age 增加。对象满足晋升条件age 到阈值、Survivor 放不下、动态年龄规则进入 Old 老年代。老年代占用堆达到 45%启动后台并发标记周期包含初始标记 (STW)、并发标记、重新标记 (STW)、Cleanup 统计。标记完成后下一次 YGC 升级 MixedGCSTW回收新生代 部分垃圾收益高的老年代 Region多轮逐步清理老年代。如果 MixedGC 回收跟不上内存分配没有空闲 Region、大对象分配失败、元空间溢出或者手动 System.gc触发 FullGC长时间 STW。G1 不支持硬编码 Eden 大小通过 G1MaxNewSizePercent 控制新生代最大占堆比例Region 大小可以手动指定。易错坑总结❌45% 触发 MixedGC → ✅45% 只是启动并发标记标记结束后等下一次 YGC 才 MixedGC❌G1 可以设置 - Xmn 固定新生代 → ✅不推荐会破坏 G1 自适应停顿❌对象 age 必须 15 才晋升老年代 → ✅有动态年龄判断可能很早晋升❌RSet 在并发标记阶段维护 → ✅RSet 靠写屏障业务线程写引用的时候实时维护不是 GC 阶段才生成。