资讯动态

Java性能分析实战:VisualVM与JProfiler协同调优指南

发布时间:2026/10/16 19:00:05 来源:尧图企业网站定制
1. 项目概述为什么 Java 应用的“慢”不能靠猜而要靠看你有没有遇到过这样的场景线上服务响应突然变慢接口平均耗时从 200ms 涨到 2s监控图表上 CPU 使用率忽高忽低GC 频次翻倍但日志里找不到 ERROR堆栈里没报 OOM重启能缓解几小时接着又复发我带过的三个团队里有两位后端负责人第一反应是“加机器”——结果扩容后负载反而更不均衡还有一位直接让开发改了二十多处stream().parallel()上线后线程池满告警暴增。问题没解决成本倒先涨了三成。这背后不是代码写得差而是缺乏对 JVM 运行时状态的“可视化触觉”。VisualVM 和 JProfiler 不是两套炫酷的图形界面工具它们是 Java 应用的“听诊器内窥镜心电图仪”三位一体诊断系统VisualVM 是开源、轻量、开箱即用的“全科医生”能快速定位 CPU 热点、内存泄漏苗头、线程死锁迹象JProfiler 则是专业级“影像科主任”支持方法级调用链采样、对象生命周期追踪、数据库 SQL 耗时归因、甚至 JVM 内部 GC 线程调度细节。二者不是替代关系而是“初筛→精查→验证”的标准工作流。关键词VisualVM、JProfiler、Java 性能分析、JVM 调优、内存泄漏定位、CPU 热点识别这些词在真实生产环境中从来不是理论概念而是凌晨三点告警群里的具体截图、GC 日志里的时间戳、线程 dump 中的 WAITING 状态堆栈。这篇文章面向两类人一类是刚接手老系统的 junior 工程师面对千行 Spring Boot 启动日志和一堆Async注解不知从哪下手另一类是已有调优经验但长期依赖jstatjmap手动计算的 senior 开发者想把经验沉淀为可复现、可分享、可传承的标准化动作。全文不讲 JVM 内存模型八股文不列 GC 算法对比表只聚焦一件事当你面对一个“变慢了”的 Java 进程如何用 VisualVM 快速锁定嫌疑范围再用 JProfiler 深挖根因最后给出可落地的代码/配置/架构级优化方案。所有操作步骤均基于 JDK 8u292 与 JDK 17LTS双环境实测覆盖 Spring Boot 2.7.x 和 3.2.x 主流版本每一步命令、每一个勾选项、每一处阈值设置都附带“为什么这么选”的现场推理——比如为什么采样间隔设为 5ms 而非 1ms为什么堆内存快照要排除java.lang.String实例为什么线程分析必须开启“Record individual call stacks”。这不是工具说明书而是一份从故障现场带回的实战笔记。2. 工具选型逻辑与部署策略为什么不用 Arthas为什么不用 YourKit2.1 VisualVM 与 JProfiler 的能力边界不是“强弱”而是“视角”很多开发者一上来就问“哪个更好”这个问题本身就有陷阱。我曾在一个电商大促前夜用 VisualVM 发现某支付回调接口的ConcurrentHashMap.get()调用占比高达 68%但进一步下钻发现热点不在业务代码而在 Spring Security 的SecurityContextPersistenceFilter对ThreadLocal的反复初始化——这是典型的框架层资源争用VisualVM 的线程视图CPU 采样已足够定位。而当需要确认是否因new BigDecimal(123.45)导致短生命周期对象暴增拖慢 Young GC 时就必须切到 JProfiler 的“Allocation Hot Spots”视图按对象分配位置Source Code Location分组才能看到具体哪一行代码在每秒创建 12 万个BigDecimal实例。提示VisualVM 的核心价值在于“零侵入快速诊断”。它通过 JVM TI 接口 attach 到运行中进程无需重启、无需修改启动参数、无需添加 agent。而 JProfiler 必须以 Java Agent 方式启动或动态 attach这意味着它能看到更底层的 JVM 事件如对象晋升到 Old 区的精确时刻但也带来额外开销——实测显示在高吞吐场景下JProfiler 默认配置会使应用吞吐量下降 8%~12%。所以我的工作流永远是VisualVM 先跑 3 分钟确认问题类型CPU/内存/线程→ 若需深挖对象生命周期或 GC 细节再用 JProfiler 复现并采集。2.2 为什么不用 Arthas——场景错配的典型教训Arthas 确实强大watch、trace、jad命令堪称线上调试神器。但去年某金融客户的真实案例让我彻底放弃在性能分析中主推 Arthas他们用trace -n 100 com.xxx.service.PaymentService.process追踪支付流程结果发现 trace 结果里 92% 的耗时集中在org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept()而实际业务逻辑只占 3%。这是因为 Arthas 的字节码增强机制在代理链过长时会产生严重采样失真——它把 AOP 框架的拦截开销算进了业务方法。VisualVM 的 CPU 采样基于操作系统级 timer interruptJProfiler 使用 JVM TI 的CompiledMethodLoad事件二者都不依赖字节码重写数据保真度更高。Arthas 更适合“某个请求卡住了我要看它现在执行到哪一行”而 VisualVM/JProfiler 解决的是“为什么整体变慢了”。2.3 生产环境部署的三条铁律绝不允许在核心交易链路节点直接安装 GUI 客户端曾有团队把 JProfiler Desktop 直接装在订单服务所在的物理机上结果 GUI 渲染占用 1.2G 内存触发容器 OOMKilled。正确做法JProfiler Server无界面服务端部署在目标机器Desktop 客户端通过 SSH 端口转发连接ssh -L 8888:localhost:8888 usertarget所有数据传输走本地回环避免网络抖动影响采样精度。VisualVM 的插件必须精简到仅保留必要项默认安装的 “VisualGC”、“MBeans”、“VisualVM-MBean” 插件会持续轮询 JVM MBean对高并发服务造成额外压力。实测显示禁用除 “VisualGC”用于实时 GC 监控外的所有插件后VisualVM 自身 CPU 占用从 12% 降至 0.3%。操作路径Tools → Plugins → 取消勾选非必要插件 → 重启。JProfiler 的启动参数必须显式指定采样粒度直接使用-agentpath:/path/to/jprofiler/bin/linux-x64/libjprofilerti.soport8849启动JProfiler 会启用全量 profiling包括所有方法调用导致应用延迟飙升。必须添加samplingon,filteroff,heapoff参数关闭堆分析仅开启 CPU 采样“-agentpath:/path/to/jprofiler/bin/linux-x64/libjprofilerti.soport8849,samplingon,filteroff,heapoff”。这是生产环境保命参数务必写进启动脚本模板。3. VisualVM 实战四步法从“哪里慢”到“为什么慢”的精准定位3.1 第一步建立基线 —— 不做对比的监控都是耍流氓很多人一打开 VisualVM 就直奔“Sampler”标签页猛点“CPU”按钮这是最大误区。真正的分析始于“基线”。我在某物流系统调优中先用 VisualVM 连接一台未受流量冲击的预发节点持续采集 5 分钟记录三项核心指标CPU Usage稳定在 35%±3%Heap MemoryEden 区每 8 秒 GC 一次Old 区 42 分钟 GC 一次Threads活跃线程数 187±5其中http-nio-8080-exec-*线程池固定 200 个这组数据成为后续所有判断的标尺。当线上告警触发后我立刻连接故障节点发现 Eden 区 GC 频次变为每 1.2 秒一次Old 区 3 分钟就 Full GC —— 此时无需深入代码已能断定存在严重内存泄漏或对象创建风暴。基线不是可选项而是分析的起点。没有基线你看到的每个数字都是孤岛。3.2 第二步CPU 热点扫描 —— 用“火焰图思维”读取采样结果点击 “Sampler” → “CPU” → “Start CPU Sampling”等待 60 秒后停止。关键不是看顶部那个“最耗时方法”而是要理解 VisualVM 的采样逻辑它每 20ms 中断一次 JVM 执行记录当前线程栈顶方法。因此一个被频繁调用且执行时间长的方法如String.substring()会高频出现在采样结果中而一个执行时间极短但被调用百万次的方法如ArrayList.get()可能因采样间隔错过。我的实操技巧是在 “Call Tree” 视图中右键点击任意方法 → “Filter Out Selected Method”排除java.lang.Thread.run()、org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun()等框架/容器层方法聚焦业务代码切换到 “Hot Spots” 标签页按 “Self Time (ms)” 排序重点关注 “Self Time” 占比 15% 且 “Invocations” 1000 的方法对于 Spring Boot 应用特别关注org.springframework.web.servlet.DispatcherServlet.doDispatch()下游的HandlerMethod.invoke()调用这里暴露的是 Controller 层真实耗时。去年处理一个报表导出慢的问题VisualVM 显示com.xxx.report.ExportService.generateExcel()的 Self Time 占比 41%但 Invocations 仅 3 次。继续下钻发现其内部调用的org.apache.poi.xssf.streaming.SXSSFSheet.createRow()占比 38%这才是真正的瓶颈——POI 的流式写入在大数据量时会频繁触发磁盘 I/O。这个结论直接导向了架构优化改用 CSV 流式生成 前端 Excel 转换导出时间从 47 秒降至 1.8 秒。3.3 第三步内存泄漏初筛 —— 用“对象增长速率”代替“堆大小”点击 “Monitor” 标签页观察 “Heap” 曲线。很多人只看峰值这是错误的。真正的泄漏信号是“增长速率”。我设置了一个简单规则如果连续 3 次 Full GC 后Old 区内存使用量仍比前一次高 5% 以上即标记为高风险。例如某风控服务Full GC 后 Old 区占用从 1.2G → 1.26G → 1.33G虽未达 2G 堆上限但增速异常。此时切换到 “Sampler” → “Memory” → “Start Memory Sampling”重点看 “Classes” 标签页。不要被byte[]、char[]这类基础类型吓到要找“业务相关类”的实例数变化。我通常排序 “Instances” 列筛选出实例数 1000 且 “Size (B)” 100KB 的类。曾在一个消息消费服务中发现com.xxx.dto.MessagePayload实例数达 2.7 万而正常应 200。进一步用 “Objects” 标签页查看其引用链发现被org.springframework.kafka.listener.KafkaMessageListenerContainer的pendingRecordsMap 持有——根本原因是 Kafka 消费者配置了max.poll.records500但业务处理逻辑中存在同步 HTTP 调用导致单条消息处理超时pendingRecords积压无法释放。解决方案不是调大堆内存而是将 HTTP 调用改为异步 降级MessagePayload实例数回归正常。3.4 第四步线程死锁捕获 —— 三秒定位阻塞根源点击 “Threads” 标签页点 “Thread Dump” 按钮。VisualVM 会生成标准 jstack 输出但它的价值在于可视化呈现。重点关注 “Deadlock Detection” 区域如果显示 “No deadlocks found”别急着关掉——很多“伪死锁”表现为线程长时间 WAITING 或 TIMED_WAITING。我的检查清单查找状态为WAITING (on object monitor)且 “Stack Trace” 中包含Object.wait()的线程检查其等待的对象如java.lang.Object123abc被哪个线程持有查找状态为BLOCKED (on object monitor)的线程看其 “Waiting to lock” 的对象地址再搜索哪个线程正持有该锁特别注意java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await()这常是ReentrantLock.lockInterruptibly()的表现需结合业务逻辑判断是否合理。某支付网关曾出现大量线程卡在com.xxx.pay.GatewayService.invokeThirdParty()的synchronized块内。VisualVM 线程视图清晰显示23 个线程 BLOCKED 在java.lang.Object456def而线程http-nio-8080-exec-17正持有该锁其堆栈显示正在执行HttpClient.execute()等待第三方响应。根因是未设置 HTTP 超时第三方服务不可用导致锁被长期占用。解决方案是用try-with-resources包裹HttpClient并设置socketTimeout3000同时将同步调用改为CompletableFuture.supplyAsync()异步化。4. JProfiler 深度剖析从“现象”到“根因”的证据链构建4.1 方法调用链分析 —— 看清每一毫秒花在哪JProfiler 的 “Call Tree” 视图比 VisualVM 精细十倍。它不仅能显示方法自身耗时Self Time还能显示子方法总耗时Total Time和调用次数Invocations。关键是要理解三者关系Total Time Self Time Sum(Child Methods Total Time)。一个方法 Total Time 高但 Self Time 低说明它只是个“中转站”真正耗时在下游反之Self Time 高则说明该方法内部有密集计算或 I/O。我的实操步骤在 JProfiler Desktop 连接目标 JVM 后点击 “CPU Views” → “Call Tree”右键顶部节点 → “Filter” → 输入业务包名如com.xxx.service隐藏框架代码按 “Self Time (ms)” 排序找到 Top 3 方法对每个方法右键 → “Show Back Traces”查看哪些上游调用触发了它。某电商搜索服务曾出现com.xxx.search.SearchService.query()Self Time 仅 8ms但 Total Time 高达 1200ms。Back Traces 显示92% 的调用来自org.springframework.web.servlet.DispatcherServlet.doDispatch()而其子方法中com.xxx.search.es.ElasticsearchClient.search()占比 98%。继续下钻ElasticsearchClient.search()发现其RestHighLevelClient.search()调用中org.apache.http.impl.nio.client.InternalHttpAsyncClient.execute()的 Self Time 达 1150ms —— 这明确指向 HTTP 客户端配置问题。检查后发现setMaxConnPerRoute(1)导致连接复用率极低改为setMaxConnPerRoute(20)后搜索耗时从 1.2s 降至 180ms。4.2 对象分配追踪 —— 找出“短命对象制造机”内存泄漏不等于“对象不释放”更多时候是“对象创建太快”。JProfiler 的 “Allocation Hot Spots” 是唯一能准确定位对象诞生地的工具。它按“源代码位置”Source Code Location统计对象分配而非类名。操作要点启动 profiling 前确保勾选 “Record allocation stack traces”在 “Live Memory” → “Allocation Hot Spots” 视图中按 “Objects” 列排序重点关注 “Objects” 10000 且 “Object Size (B)” 100 的类点击类名右侧 “Allocation Stack Trace” 会显示具体哪一行代码在创建它。某金融风控系统中java.math.BigDecimal实例数每秒新增 8 万。Allocation Stack Trace 指向com.xxx.risk.RiskEngine.calculateScore()中的new BigDecimal(String.valueOf(score))。问题在于score 是 double 类型String.valueOf(double)会生成含多位小数的字符串如92.34500000000001导致 BigDecimal 构造时解析开销巨大。改为BigDecimal.valueOf(score)内部使用 long 构造后BigDecimal 创建量降至每秒 200 个Young GC 频次下降 70%。4.3 GC 行为深度解读 —— 看懂 CMS 与 G1 的“语言”JProfiler 的 “VM Telemetry” → “Garbage Collection” 视图是理解 GC 行为的终极入口。它不仅显示 GC 时间还展示每次 GC 的详细参数回收区域Young/Old、回收前/后内存、晋升对象大小、GC 原因Allocation Failure / Metadata GC Threshold / G1 Humongous Allocation。我的分析框架Young GC 频繁检查 Eden 区大小是否过小或是否存在对象创建风暴见 4.2Old GC 频繁但回收量小典型内存泄漏Old 区对象无法被回收G1 Humongous Allocation 触发 Full GC说明有超大对象 Region Size/2分配需检查byte[]、char[]创建逻辑Metadata GC Threshold 触发表示 Metaspace 不足需调大-XX:MaxMetaspaceSize。某 SaaS 平台升级 JDK 17 后G1 GC 频次暴增。JProfiler 显示大量 GC 原因为 “G1 Humongous Allocation”。排查发现新版本 Spring Boot 3.0 默认启用spring.mvc.throw-exception-if-no-handler-foundtrue导致 404 请求被包装为ResponseStatusException其堆栈信息包含完整 URL 和查询参数生成超大String对象。解决方案配置server.error.include-messageneverGC 频次回归正常。4.4 数据库 SQL 耗时归因 —— 打破“慢 SQL”黑盒JProfiler 的 “Database” 视图能自动拦截 JDBC 调用无需修改代码。它按 SQL 文本分组显示执行次数、总耗时、平均耗时、最大耗时并关联到调用它的 Java 方法。关键技巧在 “Database” → “SQL Statements” 中按 “Total Time (ms)” 排序点击慢 SQL右侧 “Call Stacks” 显示哪些 Java 方法执行了它右键 SQL → “Show Execution Plan”直接查看数据库执行计划需数据库驱动支持。某 CRM 系统报表页面加载慢JProfiler 显示一条SELECT * FROM customer WHERE status ? AND create_time ?耗时 8.2s。Call Stacks 指向com.xxx.crm.report.CustomerReportService.generateReport()。执行计划显示未使用status字段索引。但更深层问题是该 SQL 在循环中被调用 127 次每次create_time条件不同。根因是业务逻辑设计缺陷——应改为单次查询 内存过滤。优化后页面加载从 12.7s 降至 420ms。5. 调优方案落地与效果验证从“改了”到“稳了”的闭环5.1 JVM 参数调优不是玄学而是基于数据的工程决策所有 JVM 参数调整必须回答三个问题针对什么问题如 Young GC 频繁 → 调大-Xmn调整依据是什么如 VisualVM 显示 Eden 区每 2 秒 GC 一次当前-Xmn512m按 8 秒基线推算需-Xmn2g如何验证效果调整后用相同压测脚本对比 GC 频次与 P99 延迟我坚持的黄金参数组合JDK 8/11/17 通用-Xms4g -Xmx4g避免堆动态伸缩带来的 GC 波动-XX:UseG1GCG1 在大堆场景下更可控-XX:MaxGCPauseMillis200G1 的目标停顿时间非绝对保证-XX:G1HeapRegionSize2M避免 Humongous 对象-XX:PrintGCDetails -Xloggc:/path/to/gc.log必须开启 GC 日志这是调优的原始凭证。某内容平台将-Xmn从 1g 调至 2.5g 后Young GC 频次从 15 次/分钟降至 4 次/分钟但 P99 延迟反而上升 12%。JProfiler 发现G1 Evacuation Pause时间增加原因是 Region 太大导致复制开销上升。最终方案是保持-Xmn2.5g但将-XX:G1HeapRegionSize从默认 1M 改为 512K平衡了 GC 频次与停顿时间。5.2 代码级优化的“三不原则”不改框架源码曾有团队为优化 Spring 的BeanFactory初始化重写了DefaultListableBeanFactory结果升级 Spring Boot 版本后崩溃。正确做法是利用Primary、ConditionalOnMissingBean等扩展点不引入新中间件为解决缓存穿透有团队引入布隆过滤器却因序列化问题导致 CPU 占用飙升。优先用Cache.putIfAbsent(key, value)Optional.empty()缓存空值不牺牲可维护性用Unsafe替换ArrayList提升性能除非你负责维护 JDK。用Arrays.sort()替代手写快排现代 JVM 的 intrinsic 优化已让它足够快。某社交 App 的消息列表页原逻辑是for (Message m : messages) { userMap.put(m.getUserId(), userService.findById(m.getUserId())); }。JProfiler 显示userService.findById()占比 63%。优化不是加 Redis 缓存而是批量查询userService.findByIds(messages.stream().map(Message::getUserId).collect(Collectors.toList()))数据库查询从 N 次降至 1 次接口 P95 从 1.8s 降至 320ms。5.3 效果验证的“双盲测试法”调优后必须做两件事回滚验证将 JVM 参数或代码改回原状确认问题重现灰度发布在 5% 流量节点部署新版本用 Prometheus Grafana 监控jvm_gc_collection_seconds_count、http_server_requests_seconds_sum等指标对比基线偏差 5% 即告警。某支付系统上线新 GC 参数后灰度节点 P99 延迟下降 22%但 Full GC 次数增加 3%。深入分析发现新参数使 Young GC 更激进更多对象提前晋升到 Old 区虽降低 Young GC 延迟却增加了 Old GC 压力。最终采用折中方案-XX:G1NewSizePercent30 -XX:G1MaxNewSizePercent60在两者间取得平衡。6. 常见问题与独家避坑指南那些文档里不会写的真相6.1 VisualVM 连不上远程 JVM先检查这五件事检查项正确做法错误做法为什么重要JVM 启动参数添加-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port9999 -Dcom.sun.management.jmxremote.authenticatefalse -Dcom.sun.management.jmxremote.sslfalse只加-Dcom.sun.management.jmxremote缺少端口和认证参数JVM 不会启动 JMX RMI 服务防火墙iptables -I INPUT -p tcp --dport 9999 -j ACCEPT认为云服务器安全组已开放就忽略本地防火墙Linux 默认 iptables 会拦截所有外部连接RMI 注册表绑定-Djava.rmi.server.hostname服务器公网IP不设置或设为localhostRMI 服务端返回localhost:9999客户端无法连接VisualVM 插件安装 “VisualVM-MBeans” 插件依赖默认插件无此插件VisualVM 无法识别远程 JMX 连接JDK 版本兼容性VisualVM 2.1.7 支持 JDK 17用 JDK 8 的 VisualVM 连接 JDK 17低版本 VisualVM 无法解析高版本 JVM 的 MBean 结构我踩过的最深的坑某阿里云 ECS 实例VisualVM 显示连接成功但 “Monitor” 标签页为空。排查 3 小时才发现该实例启用了 “安全加固” 功能自动关闭了1099端口RMI 注册表默认端口而 JMX 配置中未显式指定com.sun.management.jmxremote.rmi.port。解决方案添加-Dcom.sun.management.jmxremote.rmi.port9999并在安全组开放 9999 端口。6.2 JProfiler 采样数据“失真”可能是这些隐形开关在作怪“Instrumentation” vs “Sampling” 模式混淆Instrumentation 模式会修改字节码注入计时逻辑精度高但开销大Sampling 模式基于 OS timer开销小但可能漏采。生产环境必须用 Sampling且采样间隔 ≥5ms默认 20ms 即可“Filter” 配置不当JProfiler 默认过滤java.*、javax.*包这会导致看不到sun.nio.ch等底层 I/O 调用。若分析网络延迟需在 Filter 中取消勾选sun.*“Heap walking” 开启此功能会遍历整个堆内存查找对象引用链对 4G 堆内存服务单次操作可能耗时 2 分钟并导致应用暂停。生产环境 profiling 时务必关闭“Record only allocations in selected packages”勾选此项后JProfiler 只记录指定包下的对象分配但若业务代码调用org.apache.commons.lang3.StringUtils而该包未被选中则StringUtils创建的String不会计入。应关闭此选项用 “Allocation Hot Spots” 视图后过滤。某视频平台用 JProfiler 分析直播推流延迟发现org.bytedeco.javacv.FFmpegFrameGrabber.grabFrame()耗时异常。开启 “Instrumentation” 后数据飙升实测发现是 FFmpeg 的 native 调用被过度采样。切换回 Sampling 模式并将采样间隔设为 10ms数据回归真实。6.3 “调优后更慢了”检查你的压测方法论90% 的“越调越慢”源于压测方式错误用 JMeter 单线程跑 100 次请求这测的是串行处理能力而非并发性能压测期间开着 VisualVM 实时采样采样本身消耗 CPU污染测试结果未预热 JVMJIT 编译器需 1 万次调用才将热点方法编译为机器码压测前必须用 5 分钟预热流量。我的标准压测流程启动应用用curl -X GET http://localhost:8080/actuator/health确认就绪执行 5 分钟预热ab -n 10000 -c 100 http://localhost:8080/api/health关闭所有监控工具用wrk -t12 -c400 -d30s http://localhost:8080/api/data进行正式压测压测结束后立即用 VisualVM 采集 2 分钟运行时数据作为调优依据。某教育平台压测 APIP99 延迟从 800ms 升至 1.2s。检查发现压测脚本中Content-Type设为application/json;charsetUTF-8而 Spring Boot 默认 JSON 解析器 Jackson 对带 charset 的请求头处理更慢。改为application/json后延迟回归正常。6.4 那些年我们信以为真的“最佳实践”“堆内存越大越好”错。堆过大导致 Full GC 停顿时间指数级增长。实测显示32G 堆的 G1 Full GC 平均耗时 4.2s而 8G 堆仅为 0.8s。建议单实例堆内存 ≤8G“G1 GC 一定比 CMS 好”错。CMS 在 JDK 8 下对低延迟场景更优G1 在 JDK 11 才成熟。某实时风控系统从 CMS 切 G1 后P99 延迟波动从 ±15ms 扩大到 ±80ms“加 -XX:UseStringDeduplication 就能省内存”错。该参数仅对 G1 有效且需配合-XX:UnlockExperimentalVMOptions对 ZGC 无效。更重要的是它只 dedupString的char[]对StringBuilder无效。我最后分享一个血泪教训某团队为提升性能给所有服务加上-XX:TieredStopAtLevel1禁用 C2 编译器认为解释执行更稳定。结果上线后 CPU 占用从 40% 暴涨至 95%。JProfiler 显示Interpreter方法调用占比 89%。恢复默认TieredStopAtLevel0后C2 编译的热点方法使 CPU 占用降至 32%。性能优化的第一课永远是相信 JVM 工程师的默认选择。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑