Java 25 的全新 CPU 时间分析器

历经三年多开发,并于去年启动集中攻关,我的 CPU 时间分析器最终随 OpenJDK 25 正式登陆 Java 平台。这是款实验性的新型分析器/方法采样器,能帮助您发现代码中的性能问题,相较现有采样器具有显著优势。本周及下周的博客文章将详细探讨此主题。本周我将阐述为何需要新分析器及其提供的信息;下周则将深入解析超越JEP文档的技术细节。本文将大量引用JEP 509的原文,在此感谢Ron Pressler——该文档本身就如同一篇精心撰写的博客文章。

图0:Java 25 的全新 CPU 时间分析器

在展示细节前,先聚焦当前JFR默认方法分析器的运作机制:

现有JFR分析策略

正如我先前博客文章驯服偏倚:JFR中基于安全点的无偏倚栈遍历所述,JDK 25的默认方法分析器也进行了变更。但其剖析策略仍保持不变。

在每个间隔(例如10或20毫秒)内,从线程列表中随机抽取五个Java线程和一个本机Java线程进行采样。该线程列表采用线性遍历方式,且跳过不符合请求状态的线程( 参考)。

元素周期表

问题所在?

该策略存在缺陷,正如今年FOSDEM上Jaroslav Bachorik与我的演讲所述 :

激进的子采样机制意味着实际采样间隔取决于核心数量和系统并行度。假设我们有一台可并行运行32个线程的大型机器。此时JFR最多采样19%的线程,将10毫秒的采样率延长至53毫秒。这是墙钟采样的固有特性,因为采样器会考虑系统中的所有线程。该数量可能无限大,因此子采样不可或缺。

然而该采样策略并非真正的墙钟采样,因为它优先处理Java线程。假设存在10个本机线程和5个Java线程的场景,采样器将始终选取所有Java线程,而仅选取一个本机线程。这种机制可能造成混淆,导致用户得出错误结论。

即使我们忽略这个问题,将当前策略称为“执行时间”,它也未必适合对所有应用程序进行性能分析。引用我的JEP(感谢Ron Pressler撰写了最终版本的大部分文本):

执行时间未必反映CPU时间。例如排序数组的方法,其全部时间都耗费在CPU上。其执行时间等同于消耗的CPU周期数。反之,从网络套接字读取数据的方法可能大部分时间都在空闲等待数据包传输。其消耗的时间中,仅有极小部分用于CPU操作。基于执行时间的剖析无法区分这两种情况。

即使是大量进行I/O操作的程序也可能受限于CPU。计算密集型方法相较于程序的I/O操作可能消耗较少执行时间,因此对延迟影响甚微——但它可能消耗程序大部分CPU周期,从而影响吞吐量。识别并优化此类方法可降低CPU消耗并提升程序吞吐量——但为此我们需要分析CPU时间而非执行时间。

JEP 509: JFR CPU时间分析(实验性)

执行时间示例

例如,考虑一个名为HttpRequests的程序,它包含两个线程,每个线程执行HTTP请求。其中一个线程运行tenFastRequests方法,该方法向响应时间为10毫秒的HTTP端点顺序发送十次请求;另一个线程运行oneSlowRequest方法,该方法向响应时间为100毫秒的端点发送单次请求。两个方法的平均延迟应大致相同,因此执行它们的总时间也应相近。

我们可通过以下方式记录执行时间分析事件流:

$ java -XX:StartFlightRecording=filename=profile.jfr,settings=profile.jfc HttpRequests client

JEP 509: JFR CPU-TIME PROFILING (EXPERIMENTAL)

相关程序可在GitHub获取。请注意需同时运行服务器实例,通过以下命令启动:

java HttpRequests server

JFR会以固定时间间隔将ExecutionSample事件记录至profile.jfr文件。每个事件捕获运行Java代码线程的堆栈跟踪,从而记录该线程当前执行的所有方法。(文件profile.jfc是JDK内置的JFR配置文件,用于配置运行时剖析所需的JFR事件。)

我们可通过JDK内置的jfr工具,从记录的事件流生成文本分析报告:

$ jfr view native-methods profile.jfr

                      Waiting or Executing Native Methods

Method                                                          Samples Percent
--------------------------------------------------------------- ------- -------
sun.nio.ch.SocketDispatcher.read0(FileDescriptor, long, int)        102  98.08%
...

这清楚地表明程序大部分时间都耗费在等待套接字I/O上。

我们可通过JDK Mission Control工具 (JMC)生成图形化性能剖析图,即火焰图

图1:Java 25 的全新 CPU 时间分析器

如图所示,oneSlowRequesttenFastRequests方法的执行时间相近,符合预期。

然而我们同样预期tenFastRequests的CPU消耗会高于oneSlowRequest,因为十轮请求创建与响应处理所需的CPU周期远多于单轮操作。若这些方法在多线程环境下并发运行,程序可能陷入CPU瓶颈,但执行时间剖析仍会显示大部分时间耗费在等待套接字I/O上。若能分析CPU时间,便可发现优化tenFastRequests而非oneSlowRequest能提升程序吞吐量。

JEP 509: JFR CPU时间分析(实验性功能)

此外,我们指出JEP中一个细微但关键的问题:对失败采样的处理。采样失败可能由多种原因导致,无论是被采样线程状态异常、栈遍历因信息缺失失败,还是其他诸多因素。然而默认的JFR采样器会忽略这些采样(其比例可能高达总采样的三分之一)。这使得解读“执行时间”剖析结果变得更加困难。

CPU时间剖析

如上文视频所示,每隔n毫秒CPU时间对所有线程进行采样可改善现状。此时每个线程的采样数量与其实际CPU消耗时间直接相关,无需进行子采样——因为硬件线程数量决定了可采样线程的上限。

Linux内核自2.6.12版本起通过定时器机制实现了精确测量CPU周期消耗的能力——该机制以固定CPU时间间隔而非固定实际时间间隔触发信号。Linux平台上多数分析工具均采用此机制生成CPU时间剖析图。

部分流行的第三方Java工具(如async-profiler)利用Linux的CPU计时器生成Java程序的CPU时间剖析。但此类工具需通过不受支持的内部接口与Java运行时交互,这种操作本质上存在安全隐患,可能导致进程崩溃。

我们应增强JFR功能,使其能利用Linux内核的CPU计时器安全地生成Java程序的CPU时间剖析。这将帮助众多在Linux上部署Java应用的开发者提升应用程序的运行效率。
JEP 509:JFR CPU时间剖析(实验性)

请注意,我并非反对使用async-profiler。它是一款功能强大的工具,已被广泛采用。但由于未嵌入JDK,其使用体验存在固有局限。尤其在安全点的新栈遍历机制下(详见消除偏倚:基于安全点的无偏栈遍历),使得当前JFR采样器更安全可靠。遗憾的是,该机制无法用于外部分析器——尽管我曾构思过相关API(详见驯服偏见:基于安全点的无偏栈遍历,但该项目已遗憾终止。

让我们继续之前的示例。

FR将利用Linux的CPU计时器机制,以固定CPU时间间隔采样每个运行Java代码的线程栈。每次采样结果都将记录在新型事件jdk.CPUTimeSample中。该事件默认处于禁用状态。

该事件类似于现有的用于执行时间采样的jdk.ExecutionSample事件。启用CPU时间事件不会以任何方式影响执行时间事件,因此两者可同时收集。
我们可以在启动时开始的记录中通过以下方式启用新事件:

$ java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true,filename=profile.jfr ...

借助新的CPU时间采样器,火焰图清晰显示应用程序几乎所有CPU周期都消耗在tenFastRequests中:

图2:Java 25 的全新 CPU 时间分析器

可通过以下方式获取热点 CPU 方法的文本化剖析结果(即那些在自身方法体内消耗大量周期而非调用其他方法的方法):

$ jfr view cpu-time-hot-methods profile.jfr

但在此特定示例中,该输出不如火焰图实用。

JEP 509: JFR CPU时间分析(实验性)

值得注意的是,CPU时间分析器还会报告采样失败和遗漏情况,但相关细节将在后文详述。

新分析器的缺陷

既然我已指出当前JFR方法采样器的所有问题,那么我也应该说明自身存在的问题。

最关键的问题在于平台支持,或者更准确地说,是缺乏支持:新分析器目前仅支持Linux系统。虽然这对于生产环境的分析可能不成问题——毕竟多数系统本就运行在Linux上——但在开发者机器上进行分析时却成了障碍。绝大多数开发工作都在Windows和Mac OS机器上进行。因此,无法使用与生产环境相同的分析器会严重影响开发效率。不过其他分析器也存在同样的问题。例如Async-profiler仅支持Mac OS的墙钟时间分析,完全不支持Windows。JetBrains的闭源版本或许能在Windows上实现CPU时间分析(详见GitHub问题)。不过由于我没有Windows设备,且网上找不到具体信息,无法确认此事。

另一个问题是,该分析器几乎是在最后一刻才加入的——例如Nicolai Parlog拍摄他的Java 25更新视频之后才加入。

图3:Java 25 的全新 CPU 时间分析器

 

[figcaption]
其视频帖下的BlueSky讨论
[/figcaption]

为何纳入JDK 25?

多数用户仅使用并接触JDK的LTS版本,因此我们希望将该功能纳入LTS版JDK 25供用户尝试。引用Markus Grönlund的说明:

我批准此PR基于以下理由:

  1. 我们已达到“足够好”的状态——我不再看到任何无法通过后续错误修复解决的根本性设计问题。
  2. 正如许多人指出的,本次PR仍包含诸多模糊点,主要涉及内存模型和线程交互——所有这些内容都应在集成后予以明确说明并严格规范。
  3. 该功能整体处于实验阶段,默认处于关闭状态。
  4. 今日是JDK 25截止前的倒数第二天。为使该功能有机会纳入JDK25,现阶段必须获得批准。

衷心感谢Johannes及所有参与者为准备此功能付出的辛勤努力。

谨致谢忱
Markus

PR评论

未解决问题

因此请谨慎使用该分析器。已知问题均不会破坏JVM运行。但当前合并后的分析器存在三个重要后续问题:

我已着手处理最后一个问题,并将尽快研究另外两个问题。请您亲自测试分析器并报告发现的所有问题。

新增的 CPUTimeSample 事件

旧版分析器包含两个事件 jdk.ExecutionSamplejdk.NativeMethodSample,新版为简化设计仅保留单一事件,因其不再区分本机线程与 Java 线程。如前所述,该事件命名为 jdk.CPUTimeSample

该事件包含五个不同字段:

  • stackTrace(可为空):记录的堆栈跟踪
  • eventThread:被采样的线程
  • failed(布尔值):采样器是否未能遍历堆栈跟踪?若为真则表示stackTrace为空
  • samplingPeriod:实际采样周期,由信号处理程序直接计算。下周将详细说明。
  • biased(布尔值):该采样是否为安全点偏置采样(即栈跟踪关联安全点帧而非采样请求生成时的实际帧,详见《驯服偏置:基于安全点的无偏栈遍历》)

您也可在JFR事件收集页面找到该事件。

在内部,分析器使用有限队列,这些队列可能发生溢出,导致事件丢失。丢失事件的数量会定期以jdk.CPUTimeSampleLoss事件的形式记录。该事件包含两个字段:

  • lostSamples:自上次jdk.CPUTimeSampleLoss事件以来丢失的采样数量
  • eventThread:丢失采样数据的线程

这两个事件能清晰呈现程序执行情况,包括相对精确的CPU时间消耗视图。

CPU时间分析器的配置

当前采样器的两个事件输出由period属性控制,用户可通过该属性配置采样间隔。当前CPU时间分析器存在的问题是:根据硬件线程数量可能产生过多事件。因此通过throttle设置控制jdk.CPUTimeSample事件。该设置可配置为采样间隔或事件输出上限值。

当直接设置间隔值(如default.jfc中的“10ms”)时,系统将每10毫秒对每个线程进行CPU时间采样。这最多会产生每秒100×硬件线程数个事件。在10个硬件线程的机器上,当所有线程均受CPU限制时,每秒最多产生1000个事件;若为128个硬件线程的机器,则每秒最多产生12800个事件。

另一方面,将throttle设置为类似“500/s”的速率(如profile.jfc所示),可将每秒事件数限制在固定速率。其实现原理是根据硬件线程数量选择合适的采样间隔。对于“500/s”速率和十个硬件线程的机器,采样间隔应为20毫秒。在128硬件线程的机器上,该值为0.256毫秒。

需要说明的是,修复CPU时间分析器中的间隔重计算问题与分析过程中硬件线程数量变化时的重计算机制相关。

新增JFR视图

除新增的两个事件外,您还可通过jfr view VIEW_NAME profile.jfr命令使用以下两个新视图:

cpu-time-hot-methods显示执行次数最多的25个方法列表。这些方法在栈顶出现频率最高(运行带1毫秒限流的示例时):

                       Java Methods that Execute the Most from CPU Time Sampler (Experimental)

Method                                                                                                Samples Percent
----------------------------------------------------------------------------------------------------- ------- -------
jdk.jfr.internal.JVM.emitEvent(long, long, long)                                                           35  72.92%
jdk.jfr.internal.event.EventWriter.putStringValue(String)                                                   1   2.08%
jdk.internal.loader.NativeLibraries.load(NativeLibraries$NativeLibraryImpl, String, boolean, boolean)       1   2.08%
jdk.internal.logger.LazyLoggers$LazyLoggerAccessor.platform()                                               1   2.08%
jdk.internal.jimage.ImageStringsReader.unmaskedHashCode(String, int)                                        1   2.08%
sun.net.www.ParseUtil.quote(String, long, long)                                                             1   2.08%
java.net.HttpURLConnection.getResponseCode()                                                                1   2.08%
java.io.BufferedInputStream.read(byte[], int, int)                                                          1   2.08%
java.util.HashMap.hash(Object)                                                                              1   2.08%
sun.nio.ch.NioSocketImpl$1.read(byte[], int, int)                                                           1   2.08%
java.util.Properties.load0(Properties$LineReader)                                                           1   2.08%
java.lang.StringLatin1.regionMatchesCI(byte[], int, byte[], int, int)                                       1   2.08%
java.util.stream.AbstractPipeline.exactOutputSizeIfKnown(Spliterator)                                       1   2.08%
sun.nio.fs.UnixChannelFactory$Flags.toFlags(Set)                                                            1   2.08%

第二个视图cpu-time-statistics提供以下统计数据:成功采样数、失败采样数、偏置采样数、总采样数及丢失采样数:

CPU Time Sample Statistics
--------------------------
Successful Samples: 48
Failed Samples: 0
Biased Samples: 0
Total Samples: 48
Lost Samples: 14

所有丢失采样均由被采样的Java线程执行虚拟机内部代码所致。此视图在验证性能分析是否完整时尤为有用。

结论

在JDK 25中实现这个新分析器确实费了番功夫,但我认为值得。OpenJDK现已内置CPU时间分析器,可记录遗漏采样。该实现基于JFR的新协作采样方法——该方法同样在几天前才进入JDK 25。CPU时间分析具有诸多优势,尤其当您需要定位实际浪费CPU资源的代码时。

本文是关于新分析器的两篇系列文章中的第一篇。下周将深入解析分析器的具体实现细节。

本文是我在SAP公司SapMachine团队的工作成果,致力于让分析技术更易于大众使用。

附注:我已向多个会议提交了题为《从构想到JEP:OpenJDK开发者优化性能分析的历程》的演讲稿,摘要如下:您是否好奇JFR等性能分析工具在OpenJDK中的运作原理及其优化路径?本次演讲将带您见证我历时三年的OpenJDK性能分析优化之旅——尤其聚焦方法采样技术:从最初构想与现有方案的缺陷,到多次草稿实现与JEP版本迭代,全程记录挫折与结识的伙伴。这是一段充满血汗与C++的奋斗史。
遗憾的是,该提案尚未获得录用。

本文文字及图片出自 Java 25’s new CPU-Time Profiler

共有 122 条评论

  1. 过去6-8年间,Java开发者社区始终是创新与酷炫功能的源泉,令人叹为观止!

    • 同时感谢甲骨文对这门语言的精心维护。

      • 我本不想赞同这种观点,但不得不赞同。

        事实是,尽管甲骨文堪称科技界的祸害,但Java在其管理下却蓬勃发展。这很奇怪,因为我认识的人中没人会为Java付钱给他们。我真心好奇这些公司究竟是谁,他们的动机又是什么!

        • 说真的。当年Sun被收购时,我还暗忖:“至少他们会毁掉Java吧……”

          结果呢?Java在甲骨文的庇护下反而蓬勃发展。

          • Java承受的怨言远超其应得。“有些语言人人抱怨,有些语言无人问津。”

            大部分怨气源于90年代末至2000年代初泛滥的“企业级设计模式”垃圾,而非语言本身。编写简洁明了、复杂度适中且性能优异的Java代码完全可行。

            从积极方面看,在我使用过的所有语言中,Java在代码长期可维护性方面绝对名列前茅。正因如此,它在拥有长期运行的关键业务代码库的大型企业中被广泛采用。被称为“1990/2000年代的COBOL”绝非贬义,作为编程语言它在各方面都远胜COBOL。Java绝非糟糕的编程语言,而COBOL则会让你痛恨编程生涯。

            除非通过JNI突破JVM边界,否则它也是安全的语言。若不计脚本语言,它是首个实现大规模部署的安全语言。当然安全并不意味着没有安全漏洞,只是不易出现内存错误等特定类型安全漏洞和稳定性问题。

            JVM实属卓越的工程杰作,在我看来它代表着计算领域被我们错失的全新方向。我们选择紧贴底层硬件,承受着安全、可移植性、代码复用等诸多痛点,却未曾投身于能消除各类兼容性、复用性与可移植性问题的受管执行环境。

            当前Java最大的缺陷在于JNI接口——与核心语言截然不同,它简直糟糕透顶。其次是JVM仍存在内存消耗过大的问题。虽然CPU性能表现优异,在特定负载下甚至可与C或Rust媲美,但它依然会大量占用内存资源。

            • 目前我认为针对Java最大的批评在于JNI
              那么你会很高兴得知它已被FFM取代:https://openjdk.org/jeps/454(并非适用于所有场景,但几乎涵盖绝大多数情况)。

              > 第二大诟病是JVM依然是个内存大户

              强烈建议观看今年国际内存管理研讨会(ISMM)关于此主题的主题演讲:https://www.youtube.com/watch?v=mLNFVNXbw7I

              简而言之(当然我在此过度简化了演讲内容):若每个CPU核心使用的内存低于1GB,则很可能以损害性能的方式在CPU和内存之间进行权衡——即 你正在浪费宝贵的资源(CPU)来节省无法有效利用的资源(内存)——因为机器能处理的工作量取决于这两种资源中_先耗尽_的那种,因此应按硬件提供的比例使用它们。引用计数回收器乃至手动内存管理(除非几乎完全使用内存池机制)都以牺牲CPU性能为代价优化内存占用。换言之,JVM正是利用更充裕的内存资源来节省成本更高的CPU资源。

            • > 我认为当前Java最大的缺陷在于JNI,它与核心语言截然不同,简直糟糕透顶。

              JNI的设计初衷只是“够用就好”,而它确实做到了。新的FFM API旨在替代JNI的大部分场景,但它追求的是“完美”。因此新API耗时多年开发,而JNI的开发却相当迅速。

              虽然更早推出FFM API固然理想,但JNR和JNA等替代方案早已存在多年。开发JNI替代方案本就不存在紧迫性。

            • 自“企业级”Java时代以来,该语言已发生巨大演变。许多繁琐的仪式被简化,它也不再死守“作为Smalltalk的编译型静态类型继承者”这一教条。

              • …但前辈们仍断言Java永远不会获得像getter/setter这类提升开发体验的改进?这简直像是他们的尊严受到了威胁。

                • 我不明白你的意思。Java记录类不需要getter/setter方法。如果你需要接口功能,可以使用Lombok。

                  • 记录类型是最新引入的特性。Lombok通过直接编写字节码来实现该功能。

                • 啥?Java向来都有getter和setter啊

                  • 说个客观事实就被点踩了?

                    • (我没给你点踩)
                      我在帖子中指的是Java拒绝采用VB、C#、JavaScript、Swift等语言的“属性”机制(即使用与字段相同的语法调用方法)。

            • 我讨厌Java,因为调试Java代码比调试汇编更痛苦

              • 作为IntelliJ用户,我实在困惑:Java调试功能究竟缺了什么?

                • 这类抱怨多来自没用过Java IDE的人,或者不知道JVM支持等待调试器启动执行(这对处理Spring之类的错误和模板代码很有帮助)。

                  话虽如此,Java里确实存在大量“企业级”垃圾代码,但那些开发者搞砸任何代码库都是必然的。

                • 这评论让我震惊。Java调试功能强大到我能在本地机器上调试远程服务器。

                  • 当本地环境完全无法复现问题时,我确实有过几次在生产系统上操作的经历。这套调试生态系统实在太棒了。

              • 你在Java领域有多少工作经验?这句话实在令我惊讶,因为Java拥有业内顶尖的调试和故障排除工具。

              • 这绝对是我在HN上见过最离谱的观点之一。简直荒谬至极。在我看来,Java堪称全球最易调试的编程语言。其调试工具成熟、稳定且功能强大。

              • 这观点实在离谱。Java的调试支持堪称业界顶尖。

              • 虽然不愿承认,但这恰恰说明你不是Java开发者。它调试起来非常简单。

              • 发现又一个依赖printf调试的家伙。

            • 根据我的经验,Java的问题在于缺乏标准化工具链。

              要构建一个普通的Java软件,你必须安装特定版本的JDK,下载特定的构建系统(Ant、Maven、Gradle、Bazel),祈祷一切首次就能顺利运行——若失败,就得调试那份最可能的XML规范文件,在千行错误输出中寻找无效依赖项…

              Java急需的是类似Python的uv调试工具。

              有评论提到调试Java本身就是噩梦,这让我想起调试过无数Spring Boot项目。堆栈跟踪里90%都是模板代码层,注解像玩布娃娃一样把我从代码库这头甩到那头…

              诚然,这本质上并非Java的问题,而是Spring的问题。然而Spring极可能出现在企业级项目中。

              • 我认为这纯粹是你的个人经历。难道每种语言不需要安装特定版本的编译器或解释器吗?尝试构建任何复杂项目难道不是第一次就成功?我在Go代码库中工作时,绝非简单执行“go build”就能搞定。实际流程是:尝试go build,哎呀需要用make/justfile,哎呀别忘了还得安装jq或其他随机工具。复杂项目本就复杂。

                • 完全不是吗?现代语言(Go、Rust、Zig)的构建系统与旧式混乱(C++、Java、Python)存在明显差异。你只需一个工具就能完成所有需求,从格式化、代码检查到包管理和构建。无需在Maven和Gradle之间抉择,也无需拼凑五个勉强兼容的第三方程序。

                  > 我参与过的Go代码库中,构建流程绝非简单执行“go build”。

                  这番话颇具讽刺意味,恰恰印证了你本意相反的观点。若说多数Go项目仅需`go build`即可构建,这本身就是极高的赞誉。

                  • 我预计大多数Java项目会通过mvn package或gradle build进行构建。但这并不意味着过程总是如此简单。简单项目可以轻松构建,而复杂项目绝非单一工具所能胜任。在Rust和Go领域,大量案例表明开发者仍需借助make或just等工具。

                    • 然而gradle build或mvn install不会自动选择正确的Java版本进行编译,甚至不会提示版本错误。Rust、Go乃至Scala的SBT都会提供此类提示。

                    • 通常只需定义源代码和编译目标,然后使用支持这些版本的JDK即可。对精确版本的依赖实属罕见。Gradle和Maven都不是独立的原生工具,它们都运行在相同的JVM上,因此甚至无法感知你的具体操作系统配置和可用JDK。但它们会明确告知你当前JDK是否支持你的源代码或编译目标。

                    • > 然而,gradle build或mvn install并不会自动选择合适的Java版本来构建代码。

                          build.gradle.kts
                          java {
                              toolchain {
                                   languageVersion = JavaLanguageVersion.of(17)
                                   //进阶:通过 vendor = JvmVendorSpec.<X> 选择首选发行版
                              }
                          }
                      

                      当然会。

                      > 甚至不会提示你正在使用错误版本进行构建。

                      没错,“类文件版本错误”这个提示并不会明确指出是JDK版本不对。Gradle本身运行在JDK8环境下,所以即使你用Windows XP安装的JDK也能正常工作。

                      若你对Java的认知停留在二十年前,还认为它未能跟上现代技术发展反而被多数语言超越(Rust的异步实现如何?虚拟线程仍无进展?真棒),那请继续自我安慰吧。但至少出于礼貌,你该把这种观点留给自己,或者至少核实你所言是否准确。

                    • 这始终是我在项目中最为困扰的问题:构建并运行项目究竟能有多简单?理想状态下,仅凭Java和Git能否实现下载、编译、运行?若涉及原生库项目,能否仅安装编译工具直接构建,而无需先构建其他五个项目,更不必费心设置环境变量指向代码位置?

                • > 尝试构建任何复杂项目难道不是第一次就成功吗?

                  我仅用简单的`go build`就成功从源代码构建了Docker守护进程——这是最广泛使用且最复杂的Go项目之一。

                  但我至今没搞懂如何从源代码构建Jenkins。

                  可知晓存在构建流程简单的常用Java项目?或许某个成功案例能改变我的看法。

              • 要构建一个普通的Java软件,你必须安装特定版本的JDK,下载特定的构建系统(Ant、Maven、Gradle、Bazel),并祈祷一切都能一次成功。

                而使用Gradle构建现代Java项目时,你只需在电脑上安装任意JVM。通过Gradle封装器(随代码一同提交)执行任务,它会自动下载并调用固定版本的Gradle,若本地未配置Java工具链(版本、供应商等),Gradle还会自动下载所需组件。

                它就是能用。

                • > 使用Gradle构建现代Java项目[…]它就是能用。

                  关键在于——只有当项目符合“现代”标准、正确使用Gradle时才“能用”。我职业生涯中接触的大多数Java项目都达不到这种质量标准。

                  或许有人辩称简化构建系统是开发者的责任,但C++工具链同样“直接就行”——现代C++项目用CMake仅需两条命令即可构建。

                  优秀的工具能防止项目构建流程演变成未文档化的鲁布·戈德堡装置。

                  • 当遗留代码迁移至据称比Java 8更易升级的新版Java后会怎样?若升级如此简单,长期支持费用该由谁承担?

                    • 遗留系统始终存在,这是软件生命周期的自然组成部分。况且你购买的是任何形式的技术支持,与当前版本无关。所谓支持并非指获取补丁,而是指提交工单时获得的服务等级协议保障。

              • 我不同意此观点。JVM生态的工具链相当出色,至少足够好用。

                相较于Python的env方案或JS生态,Maven基本顺风顺水。Maven已有21年历史。快速检索显示Python拥有/曾拥有:pip、venv、pip-tools、Pipenv、Poetry、PDM、pyenv、pipx、uv、Conda、Mamba、Pixi。

                调试功能完全没问题。现代调试工具一应俱全,支持远程调试(尽管功能有限)、更新并继续执行、评估自定义表达式等。真不知道他们抱怨什么。若使用Clojure,甚至能彻底修改运行中的应用程序。

                监控工具同样出色。轻松收集运行时指标用于性能分析或事件监控,还有Mission Control这类工具可供分析。

              • Python本质上是C语言的前端,如同Perl、PHP和Ruby。其环境配置混乱不堪,尤其当需要为不同Python版本重新编译库时。

                Java编译器、工具链与库之间确实存在历史兼容性问题,但这些问题基本局限于Java语言本身。因此,类似sdkman.io的`nvm`替代方案就足够应对。

              • > Java最迫切缺失的正是Python的`uv`这类工具。

                JBang早已存在(若我没记错)且早于uv。详见 jbang.dev

              • 我们不需要用Rust编写的Java工具。

              • 我完全反对,请永远别给Java添加类似envs的东西。Python正因如此而令人头疼。况且Java生态的强大向后兼容性,通常只需足够新版本的JDK即可。

              • Spring确实有些魔力,但我无法认同你所说的它难以调试。或许当你完全不理解它时会觉得如此,但它的架构并非高深莫测。

          • 认为Oracle邪恶且正因如此才竭力扩大Java市场份额的观点,是我今天读到最滑稽的论调。谢谢你。

        • 我感觉这种风潮正在转变。虽说两个错误不能构成正确,但别忘了微软曾兜售“Linux许可证”,还有SCO与IBM那场闹剧。律师驱动的营收模式已日渐式微,可惜甲骨文起步太晚。他们对自身声誉造成的损害实在惊人。

          • 开源圈之外的人未必都如此在意声誉。

            太阳微系统早期推进Java时,正是甲骨文和IBM在背后助力。

            许多人显然不了解他们在Java发展史中的关键作用。

            甲骨文是首个提供Java驱动程序的RDBMS,将所有GUI工具重写为Java,将JVM集成到数据库中,创建JSF框架,并收购了BEA及其JIT技术。

            还与太阳公司合作开发基于Java操作系统的网络计算机瘦客户端。

      • 同时感谢所有卓越企业的贡献。JEP项目主要由SAP赞助(Datadog与亚马逊提供支持)。

      • 感谢您给予应有的认可,特别是对甲骨文的肯定。

        HN上的舆论风向正在转变。我们终于可以对甲骨文和Facebook给予肯定。

      • [已删除]

        • 唯一付费的部分是技术支持,而OpenJDK团队成员都是甲骨文员工(准确来说,约90%的成员承担着OpenJDK约95%的工作量)。OpenJDK作为甲骨文项目,其性质类似于Chromium之于谷歌。事实上,OpenJDK(更准确地说——OpenJDK JDK)正是甲骨文对Java SE规范实现的官方命名,但我们确实能获得其他公司的贡献,例如JFR的这项重大增强(即便外部贡献也包含甲骨文员工的重要工作)。

          总之,若您不愿向甲骨文或其他销售支持服务的公司购买服务,使用JDK本身是免费的。如今不存在JDK的“企业版”、付费功能或使用限制——这些在Sun管理时期都曾存在。相较二十年前,Java如今显然更自由了——无论是啤酒般的免费,还是言论般的自由。

          • 那么为何每次甲骨文代表来访时,我们总要接受Java使用审计?他们核查设备配置、核心数量等细节,这些内容作为附加条款被纳入甲骨文协议中,而我们每年都必须抗争才能将其删除?他们自己也承认,现在按员工人数收费而非按核心计费……

            虽然你说的每句话听起来都对,但这绝非免费——这是枪口下的胁迫,是赤裸的谎言。若你足够庞大,甲骨文终将追讨订阅费。

            红帽也干同样的勾当。天知道你要是敢在容器里用RHEL运行RHEL,绝对会被宰得血本无归。

            • 贵公司已向甲骨文购买支持服务——无论是针对JDK还是其他产品包——这属于贵公司所购商业服务的条款范畴。对于单纯下载并使用JDK的用户,既不存在审计机制,也不产生任何费用。Oracle不会收集用户姓名或联系方式,下载JDK时无需提供任何信息,且许可证明确允许商业用途(即使选择非开源许可证;非开源版本下载页明确声明[1]:“JDK N二进制文件可免费用于生产环境,且可免费重新分发”)。在Sun时代曾存在诸多使用限制,但Oracle已将其全部取消。

              [1]: [https://www.oracle.com/java/technologies/downloads/](https://www.oracle.com/java/technologies/downloads/)

            • > 天啊,千万别在容器里运行RHEL,否则你会被宰得血本无归。

              订阅版RHEL系统可无限运行RHEL容器。甚至当你在订阅版RHEL系统上运行UBI容器(RHEL内容的可再分发子集)时,系统会自动升级为完整版RHEL。

          • JDK确实存在企业版,名为GraalVM企业版。

            • 这与OpenJDK无关,GraalVM是独立存在的。

              • 你是唯一在里面写“Open”的人。你的父母和祖父母都说的是JDK。

                GraalVM是由无关团队开发的独立产品。其企业版不被视为JDK的企业版本。我认为最接近Oracle企业JDK的是面向12年前Java 8的“企业性能包”,但其中所有特性在近期免费开源版本中均已包含(且后者实际包含更多性能优化)。

                其核心理念在于:对于拥有未积极维护的遗留软件的企业而言,为现代JVM版本的部分性能提升付费,比投入资源升级至现代Java更经济,此举还能为OpenJDK的持续演进提供资金支持。

                • 人们总忘记Java本质上类似C和C++——可选的JDK版本众多,且并非都基于同一代码库。

            • Azul的Zulu可能是其中之一?我认为其企业版搭载了差异化JIT和GC机制。

              • 他们拥有自主研发的Falcon JIT和C4低延迟GC。我很好奇他们的GC与代际ZGC相比表现如何。

        • OpenJDK是规范的实现版本。其大部分开发工作由甲骨文(及其他公司)资助。

          • [已删除]

            • 你认为现阶段可能出现什么跑路行为?OpenJDK是参考规范,完全开源,由多家公司共同维护。即便甲骨文设法将其强制闭源(这可能吗?),其他贡献者也会坚决反对,分叉项目另起炉灶。你认为社区会选择哪个版本的Java?这种操作根本行不通。

            • 为何认为优秀管理方不应拥有经济利益?

        • 不,人们总忘记没人愿意收购Sun——就连击沉它后(本可避免J++式诉讼)的谷歌也放弃了。

          IBM曾考虑过,最终撤回了报价。

          因此反甲骨文阵营本会眼睁睁看着Java在6版枯萎消亡,MaximeVM技术也永远不会以GraalVM之名问世。

        • `sdk install java 21.0.8.fx-librca`

          无需预付核心许可费。

  2. (作者在此)本文属于系列博客文章之一。后续博文如下:[https://mostlynerdless.de/blog/2025/07/30/java-25s-new-cpu-t…](https://mostlynerdless.de/blog/2025/07/30/java-25s-new-cpu-time-profiler-the-implementation-2/) (关于实现) [https://mostlynerdless.de/blog/2025/08/25/java-25s-new-cpu-t…](https://mostlynerdless.de/blog/2025/08/25/java-25s-new-cpu-time-profiler-queue-sizing-3/) (关于队列规模调整)以及第四篇关于性能优化的文章: [https://mostlynerdless.de/blog/2025/09/01/java-25s-new-cpu-t…](https://mostlynerdless.de/blog/2025/09/01/java-25s-new-cpu-time-profiler-removing-redundant-synchronization-4/)

    • 或许可在文章正文中添加这些链接。首篇已提及第二篇内容,可直接将其转化为超链接。或为系列文章建立统一的链接目录列表。

  3. 我只盼着借助新型轻量级线程,从此不必再编写异步响应式代码。这曾是多么低效的错误——绝大多数应用根本不需要这种复杂度,如今我们完全可以断言:没有任何应用需要额外增加这种复杂性。

    • 即便对于需要响应式架构的应用,我仍好奇是否有人真正做过成本分析:相比单纯增加服务器,采用响应式架构所需的额外开发人员和开发时间究竟孰优孰劣。

      • C10k在没有响应式/异步/协程/这类技术的情况下基本难以实现。但借助这些技术,即使在中端消费级硬件上也能轻松达成,具体取决于工作负载特性。

        • 不,传统线程在普通硬件上运行很长时间,轻松支持超过10k个线程是完全可能的。

          c10k概念诞生于上个千禧年,当时全新PC仅配备128MB内存和单核400MHz处理器。而当时实现该目标依赖的是异步IO技术,而非线程。(同期Java开发者开始关注Volanomark——该测试虽采用线程但原理相似,毕竟当时Java尚未支持非阻塞IO)。

          例如2002年Linux系统上运行10万+线程的案例:[https://lkml.iu.edu/hypermail/linux/kernel/0209.2/1153.html](https://lkml.iu.edu/hypermail/linux/kernel/0209.2/1153.html) ..该研究主要关注内存地址空间的节省,因为当时正面临数十年前32位系统4GB的限制。

          (c10k问题同样涉及操作系统TCP堆栈限制,该问题很快得到修复)

        • 如今C10k问题已相当过时,现代机器处理万级线程轻而易举。借助现代负载均衡技术,甚至无需再为此担忧。而这还是Java 25之前的时代。

          • 现代服务器确实如此,即便在技术上可行,在消费级硬件上仍表现不佳。这还忽略了内核内存和线程竞争开销对实际性能的侵蚀。

            其过时不仅源于新硬件,更因我们为新型编程风格获得了更优的人体工程学设计。

        • 这与我讨论的完全不同。拥有数百万虚拟线程并不必然陷入响应地狱。即便没有虚拟线程,增加两倍开发团队成本还是多添几台服务器更昂贵?

          • 关键在于额外服务器被复制多少次。为本地部署产品增配服务器比拔牙还痛苦——这可不是一台服务器,而是成千上万台。

    • 它本是更优的并发推理模型——即“评估两个表达式”,而非虚拟线程的发展方向——‘无需学习任何新知识’,只需“创建两个线程”,继续编写语句序列,假装自90年代以来一切未变。

      [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-…](https://www2.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.pdf)

    • 唉,我也曾这么期待过。问题在于“轻量级”线程其实并不轻量,因为它们需要垃圾回收。理论上,一台机器确实能创建十万个线程,但实际运行中,这些线程会持续消耗处理器资源进行GC循环。

      另一个问题是这些轻量级线程在做什么?如果它们只是占用CPU资源,那还好,你只需承担GC开销即可。但如果它们访问有限资源(数据库、其他HTTP服务等),在实际应用中就会遇到标准问题:你无法向目标系统发送任意数据,外部系统迟早会引发反噬。

      响应式编程的优势在于它不回避上述问题。它强制处理错误和背压机制,因为这些问题不会因切换到绿色线程或轻量级线程而凭空消失。这里没有免费的午餐:网络存在限制,数据库终究需要写入磁盘,诸如此类。

      • > 因此理论上可在单台机器创建十万个线程,但实践中这将持续消耗处理器资源进行垃圾回收。

        对“10万个线程”和GC开销的关注实属转移视线。真正的优势不在于生成海量线程,而在于像Goroutine那样在网络I/O时自动让出执行权。在I/O受限的Web应用中,单个虚拟线程将处理整个请求,这与Goroutine的工作模式完全一致。虚拟线程产生的GC开销,相较于请求中其他操作引发的堆分配而言微不足道。若真存在10万虚拟线程的场景,这些线程绝非短暂存在。

        > 但若它们在实际应用中访问有限资源(数据库、其他HTTP服务等),你将面临标准难题:无法向目标系统发送任意数据

        既然如此,为何还要采用这种方案?这听起来像是架构问题,而非虚拟线程问题。以演员系统为例,你不会让10万个不同演员直接访问数据库。

        > 响应式编程的优势在于它不试图掩盖上述问题。

        此处将完整的高级编程范式(含配套库与框架)与单一的低级并发构造相比较。前者是隐藏复杂性的抽象层,后者则是设计上无法隐藏任何内容的基础构建块。

        它迫使我们处理错误和背压问题,因为这些问题在切换到绿色线程、轻量级线程等时并不会神奇地消失。

        同步代码采用最经得起时间考验且易于理解的方式处理错误。它易于分析和调试。响应式编程需要显式处理背压,因为其异步特性本身就引发了这个问题。在线程数量有限的同步代码中,最简单的“反压”形式就是阻塞操作。若需更复杂的处理,可借助经典工具(阻塞队列、信号量等)或基于这些工具构建的高级库。

        • > 真正的优势不在于生成海量线程,而在于网络I/O时的自动让步

          这恰是常规操作系统线程的行为——当阻塞于I/O时自动挂起。正因如此,10万个操作系统线程执行I/O同样能稳定运行。

          • 没错。我试图阐明的是:如今存在一种轻量级处理单元,既能独立于操作系统调度器实现IO挂起,又无需依赖代码层面的异步/响应式模式。这需要对标准库和运行时进行重大改造。

  4. 我从未想过自己会为Java的新版本感到兴奋,但自从Java 21发布以来,我竟开始享受用这门语言编程。过去几年间,负责该项目的团队确实做得出色,让编写Java变得充满乐趣。

    • 马克·莱因霍尔德自1.x时代起便担任核心技术负责人,至今仍掌舵该项目。他不仅推动了开源转型和快速迭代周期,更关键的是招募并培养了优秀团队。在技术与组织双重挑战下,我难以想象还有比他更持久的领导者。

  5. 越是寻找新语言学习,越想回归Java。满心怀念啊 🙂

  6. CPU时间会过度强调多线程运行的区域,对吧?我发现墙上时间(wall-time)对于发现尚未并行化的串行区域很有用。

    更多细节请见:[https://github.com/dvyukov/perf-load](https://github.com/dvyukov/perf-load)。我们近期实现了无需上下文切换事件的同类方案:[https://github.com/google/highway/blob/master/hwy/profiler.h…](https://github.com/google/highway/blob/master/hwy/profiler.h#L283)

  7. 该方案基于Linux采样API实现,精度更高但本质仍是采样。

    若需消除采样误差的CPU追踪,请使用苹果M4芯片配合最新版Xcode的Instruments工具。

    • (本文作者)

      Xcode无法解析Java内部机制,因此无法识别Java框架,但可辅助原生代码追踪。

  8. 当今Java最大的问题在于开发者普遍能力欠佳。过去十年间,99%的“Java”开发者连堆栈跟踪都读不懂。

    面对如此不堪的开发者群体,实在不明白为何还要改进这门语言。让他们继续用1.8版本吧。生态系统已然进步,开发者水平却每况愈下。

    Java已沦为投机者的温床,他们试图用非Java甚至非编程技能蒙混过关

    • 真的很想知道你为什么这么说。其他语言里也存在这种现象吗?这些年来我合作过许多优秀的Java开发者,自己用了16年多也算还行。当然也有差的,但任何语言都存在这种情况(尤其是JavaScript)。

  9. [已删除]

    • ChatGPT

      • 感谢提醒,我查阅了他们的评论,全是这种风格 🙁 内容虽有实质但明显是AI生成的

      • 该有人写个LLM检测机器人,专门在所有AI垃圾评论下留言

      • 啥?

        • 我觉得他们指的是评论者很像用LLM刷卡玛,到处留下这种评论

          • 在Hacker News这种平台刷卡玛能有什么好处?又不是能拉粉丝什么的。我始终不解这类行为的动机,真想搞清楚他们究竟图什么。

            • 拥有多个高声望账号在造假评论时很有用,因为版主(理所当然地)对资深社区成员比新账号更宽容。

            • 同样的情况在Reddit上也很普遍,通常是为了将特定产品/项目/组织推向聚光灯下。登上Reddit/HN首页能带来海量流量,因此“优化者”们显然发现了这点,开始为未来的投票联盟等活动预热账号,但他们需要在推广间隙混入真实内容,以免账号被封禁。

      • 是的,算是吧。

        我正在做个实验。

        几天前我举报了别人用AI写的文章。它有特定的节奏和典型模式。但在我评论前,很多人似乎都信以为真。这让我很惊讶。

        今天我进一步突破了界限,结果明显触及了底线。

        查看我的评论记录。

        最初我只说“重写得更紧凑些”,最近改为写粗略笔记,说“按此写个HN评论”,然后编辑。

        我一直在用gpt-5。本想测试克劳德十四行诗第4首在模拟人类写作/触发蜘蛛感官方面的表现。

        (全程手动操作。)

        • 我个人认为这种行为完全不可接受。没人来这里是为了和AI讨论。请不要这样做。

  10. 若你追求如此级别的性能,就不该使用Java或其他带垃圾回收机制的语言。

    • 现代追踪移动式垃圾回收器提供了极其高效的内存管理算法,其性能表现往往难以超越。它们的代价不再是性能,而是内存占用空间;而手动内存管理(及引用计数式垃圾回收)则不以性能为优化目标,而是追求占用空间最小化(通常以牺牲性能为代价)。即便这种权衡关系也常被误解,正如近期国际内存管理研讨会这场精彩主题演讲所揭示的:[https://youtu.be/mLNFVNXbw7I](https://youtu.be/mLNFVNXbw7I)

    • 所幸并非所有人都认同这种观点。

      [https://www.ptc.com/en/products/developer-tools/perc](https://www.ptc.com/en/products/developer-tools/perc)

      我们早已受够了那些鼓吹“手动内存管理至高无上”的反GC教派信徒。

      采用GC机制(其子集包含引用计数算法)并不妨碍实现其他特性。

      • Perc有公开的基准测试数据吗?或者能透露些内部运作机制的线索?

      • 还存在非手动、非GC的手动管理机制。

        不过我认同GC确实是可行的内存管理方案。

        • 若指Rust,这种机制并不存在——仿射类型系统需要树形结构,且存在多重作用域问题,这正是借用检查器成为梗的原因。

          我更倾向称其为“编译器辅助”机制,尽管这并非严格准确的术语。

        • 这种机制并不存在。若指Rust语言,仿射类型系统需依赖树形结构,且存在多重作用域问题,这正是借用检查器梗的由来。

          Rust之外存在合理性,其他语言都倾向于将GC与仿射/线性/效果/依赖类型结合使用,而非追求万能解决方案。

          当性能分析器发出指令时,GC结合类型系统实现底层优化的生产力优势显而易见。

          尽管必须承认,Rust成功将ATS和Cyclone的理念引入主流领域。

          此外,随着人工智能驱动语言的兴起,这些优化终将自然实现自动化。

          • 按我所受的GC定义,引用计数也不算真正的GC。

            不过确实,我也考虑过借用检查器的问题。

            未必局限于Rust,其他语言正开始采用类似技术。

            • 或许与你所受的教育不同,且存在争议…但沃森父子早在1987年就将引用计数描述为“适用于并行计算机架构的高效垃圾回收方案”。

              就连《龙书》也将其归类为垃圾回收机制。

              维基百科引用计数条目第二行就明确提及垃圾回收。

              自1960年发明以来争议不断…但引用计数确实属于GC范畴。[0]它或许不是追踪式GC,但本质上仍是GC。

              [0] [https://dl.acm.org/doi/10.1145/367487.367501](https://dl.acm.org/doi/10.1145/367487.367501)

            • 作为垃圾回收的两大核心起点,引用计数与追踪机制堪称其两大支柱(例如[https://web.eecs.umich.edu/~weimerw/2012-4610/reading/bacon-…](https://web.eecs.umich.edu/~weimerw/2012-4610/reading/bacon-garbage.pdf)),我尚未见过任何严肃的内存管理从业者不将引用计数视为垃圾回收机制——尽管某些采用引用计数回收的语言在宣传材料中声称“不存在垃圾回收”。

              通常而言,追踪式垃圾回收侧重吞吐量优化,而引用计数则侧重内存占用优化——尽管两者可通过复杂化设计趋于融合(如我所引论文所示)。

            • 若你对此存在误解,说明学习路径存在偏差。任何正规计算机科学教材都会阐述此概念,例如著名的《垃圾回收手册》:

              [https://gchandbook.org/](https://gchandbook.org/)

              采用此类类型系统的其他语言,同样保留自动资源管理机制以提升开发效率。

              未采用该机制的语言,仍处于寻求实质应用价值的探索阶段。

            • 大多数学术界对垃圾回收的定义都包含引用计数机制,它只是垃圾回收的一种形式。

              [https://gchandbook.org/contents.html](https://gchandbook.org/contents.html)

    • 我认为这种观点已严重过时。

    • 存在缓解此问题的方案,例如飞蛾模式等。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注


京ICP备12002735号