写点什么

java 程序启动后 cpu 高怎么办?

  • 2023-08-18
    北京
  • 本文字数:4453 字

    阅读完需:约 15 分钟

背景知识

分析性能调优和性能问题都需要相关的背景知识。这里的 java 程序刚启动 cpu 很高其实不算是一个问题,是一个 jvm 在必然会出现的现象。


HotSpot VM 是采用 解释执行 + JIT(just-in-time 即时) 编译器编译热点(这就是 HotSpot VM 名字的由来)代码的方式运行。


编译部分是 java 字节码编译成机器本地代码 native code 来运行,这里就和 gcc 的效果一样。而且基于运行时的采集数据去做优化,HotSpot VM 得 JIT 可以做得和 gcc 不相上下,换句话说就是在不同应用场景各有胜负。


在 Client Compiler(简称 C1,轻量级编译器,fast,效果大概和 gcc -o1 类似)下运行,运行次数达到 1000 就会触发编译。


在 Server Compiler(简称 C2,重量级编译器,smart,效果大概和 gcc -o2 类似)下运行,运行次数达到 10000 时触发编译。


在开启多层编译,即-XX:+TieredCompilation,代码首先运行在解释模式,达到 C1 阈值 2000 之后,触发 C1 编译,达到 C2 阈值 15000 后,触发 C2 编译。


编译工作由后台的 Compiler Thread 并发完成,这个过程自然就会抢占 cpu 资源。


后台的编译线程数 由 flag -XX:CICompilerCount 来决定.可以使用 -XX:+PrintFlagsFinal 来观察 jvm 运行时的具体参数值是多少。非多层编译下 CICompilerCount 的默认值是 2。


C:\Program Files\Java\jdk1.6.0_45\bin>java  -XX:+PrintFlagsFinal -version| grep CI     intx CICompilerCount                           = 2               {product}     bool CICompilerCountPerCPU                      = false           {product}     bool CITime                                    = false           {product}     bool ExplicitGCInvokesConcurrent               = false           {product}     bool ExplicitGCInvokesConcurrentAndUnloadsClasses  = false           {product}java version "1.6.0_45"Java(TM) SE Runtime Environment (build 1.6.0_45-b06)Java HotSpot(TM) 64-Bit Server VM (build 20.45-b01, mixed mode)
复制代码


另外一个涉及线程数目运算的标识符是CICompilerCountPerCPU,逻辑是当 CICompilerCountPerCPU == true 时,_compiler_count = max(log2(8)-1,1)。


void NonTieredCompPolicy::initialize() {  // Setup the compiler thread numbers  if (CICompilerCountPerCPU) {    // Example: if CICompilerCountPerCPU is true, then we get    // max(log2(8)-1,1) = 2 compiler threads on an 8-way machine.    // May help big-app startup time.    _compiler_count = MAX2(log2_intptr(os::active_processor_count())-1,1);  } else {    _compiler_count = CICompilerCount;  }}hotspot/src/share/vm/runtime/compilationPolicy.cpp
复制代码


如果在重启之后,卡在解释模式的时间太久导致性能不理想或者出现雪崩现象,可以把-XX:CICompilerCount 这个参数的值适量加大。这样 load 会在重启之后升高,但是会很快降下来,因为更多的线程已经编译完了。


另外一方面,jit 这种典型的 PGO(profile guide optimization)所带来的带来的问题,当下一次行为和之前的完全不同(uncommon trap),那么 native code 就会回滚(deoptimization)到 interpret,然后再次采集数据进行下一次优化。jvm 启动初期会不断的经历 profile-optimization-uncommon trap-deoptimization-profile-optimization-uncommon trap-deoptimization....-optimization.最后达到一个稳定状态。


public void sendMail(User user){    if(user.isVip()){        sendMail();    }}
public void deletlUser(User user){ deleteUser(user); sendMail(user);}
复制代码


经过 inline 和编译会被展开成如下形式


public void sendMail(User user){    if(user.isVip()){        sendMail();    }}
public void deletlUser(User user){ deleteUser(user); if(user.isVip()){ sendMail(); }}
复制代码


再比如前一万次调用的 user 不是 vip,那么就做一次激进优化


public void sendMail(User user){    if(user.isVip()){        sendMail();    }}
public void deletlUser(User user){ deleteUser(user);//sendMail 行为完全被消除。}
复制代码


问题来了,突然来了一次 user.isVip() == true 的调用,那么 jvm 会立马退回到解释执行,然后接着靠收集的数据来做依据做优化。


既然 JIT 每次都需要编译,那么能不能在业务数据变动不大的情况下使用前一次的编译结果来节省编译时间呢。


答案是有的,这种编译器叫 AOT(ahead-of-time)编译器,事先编译器。它可以将上一次的 JIT 结果序列化保存到本地,下一次再使用这些结果。目前主流的 jvm 中,IBM J9 VM 和 Azul zing VM 都有相应的功能,未来发布的 jdk9 也会有 AOT 编译器。IBM J9 VM 就叫 AOT 编译器,J9 的 AOT 会有一些性能问题,使用 AOT 编译器产生的编译结果性能不如 JIT 编译器的性能好;而 Azul zing 的 AOT 编译器就换了个马甲,改了名字叫 Ready now!,是 Zing JVM 的主要竞争力之一(更主要是 C4 垃圾回收器,完全并发整理回收器,stw 的时间在个位数 ms 级别)。 Ready now!的作者也做过一个 jit 的演讲,jvm-mechanics-github,相应的视频在 youtube 和 Azul 都能找到,这里是演讲的文件,看一下 pdf 应该可以了解不少。这里引用其中一幅图来说明一下 jit 的威力和解释执行到底有多慢。


JDK9 带的 AOT 编译器是基于 Graal 编译器来做的。Graal 也是 OpenJDK 项目的一个子项目,目标是取代现在的 C1 和 C2,推特的 jvm 就直接使用了 Graal 而没有使用 C1&C2。JEPS-295这个页面是对 JDK9 中的 AOT 编译器的介绍,具体的性能可以等 JDK9 发布以后看看。


那么现在知道了,解释执行和 jit 编译会倒是 jvm 启动阶段的效率低和 load 高,那么怎么来监控这个行为呢。

监控 jit

几个办法:使用 jvm 参数来监控 jit 的编译;使用 jmx/JFR 来监控 jvm 的编译。还可以使用 perf top 之类的命令来查询占用 cpu 高的线程。

1.打印编译日志

使用 -XX:+PrintCompilation 编译的日志就会打印在 console 里面,如果需要更详细记录到文件的话,就加上 -XX:+LogCompilation -XX:LogFile=<path to file>,这两个 flag 会开启记录 xml 格式的更详细编译日志。


console 的编译日志大概长这个样子


    289    1       3       java.lang.String::hashCode (55 bytes)    290    2       3       java.lang.String::equals (81 bytes)    293    3       3       java.lang.String::indexOf (70 bytes)    294    6     n 0       java.lang.System::arraycopy (native)   (static)    295    4       3       java.lang.String::<init> (82 bytes)    297    7       3       java.lang.Object::<init> (1 bytes)    304   13       4       java.lang.String::charAt (29 bytes)    306   12       4       java.lang.String::length (6 bytes)    307    8       4       java.util.TreeMap::parentOf (13 bytes)    308   10       3       java.lang.CharacterData::of (120 bytes)    310   11       3       java.lang.CharacterDataLatin1::getProperties (11 bytes)    311    9       3       java.lang.Math::min (11 bytes)    316   14       3       java.lang.Character::toUpperCase (6 bytes)    317    5       3       java.util.Arrays::copyOfRange (63 bytes)    320   15       3       java.lang.Character::toLowerCase (9 bytes)    321   16       3       java.lang.CharacterDataLatin1::toLowerCase (39 bytes)    322   17   !   3       java.io.BufferedReader::readLine (304 bytes)    327   18       3       java.lang.AbstractStringBuilder::ensureCapacityInternal (16 bytes)    328   19       3       java.lang.String::getChars (62 bytes)    335   20       3       java.lang.AbstractStringBuilder::append (29 bytes)    336   21       3       java.lang.StringBuilder::append (8 bytes)    339   22       3       java.io.WinNTFileSystem::isSlash (18 bytes)    340   24  s    3       java.lang.StringBuffer::append (13 bytes)    342   25       3       java.lang.StringBuilder::append (8 bytes)    342   23       3       java.lang.String::indexOf (7 bytes)    344   26       3       java.lang.System::getSecurityManager (4 bytes)    345   27       1       java.io.File::getPath (5 bytes)    352   28       3       java.lang.AbstractStringBuilder::<init> (12 bytes)    354   29       3       java.lang.AbstractStringBuilder::append (50 bytes)    356   30       3       java.lang.String::startsWith (72 bytes)    356   32       1       java.lang.Object::<init> (1 bytes)    357    7       3       java.lang.Object::<init> (1 bytes)   made not entrant    359   31       3       java.lang.String::startsWith (7 bytes)    363   33       1       java.util.ArrayList::size (5 bytes)    364   34       3       java.util.HashMap::hash (20 bytes)    369   35       3       java.lang.String::substring (79 bytes)    371   37       3       java.util.HashMap::putVal (300 bytes)    376   38       3       sun.util.locale.LocaleUtils::isUpper (18 bytes)    377   36       3       java.util.HashMap::put (13 bytes)    388   39       3       java.util.jar.Attributes$Name::isValid (32 bytes)    389   40       3       java.util.jar.Attributes$Name::isAlpha (30 bytes)    391   41       4       sun.misc.ASCIICaseInsensitiveComparator::toLower (16 bytes)    391   42       4       sun.misc.ASCIICaseInsensitiveComparator::isUpper (18 bytes)
复制代码


第一行是 jvm 的启动之后的事件第二行是 compilation id 编译任务 id 第三行是一些标识符


b    Blocking compiler (always set for client)  *    Generating a native wrapper  %    On stack replacement  !    Method has exception handlers  s    Synchronized method  
复制代码


第四行是多层编译的日志,在 Tiered Compilation 为 false 时,没有这一行。该列表明是这次编译是第几层的编译(多层编译有 0,1,2,3,4 几层)。第五列就是具体编译的方法以及方法大小了。第五列后面可能会有 made not entrant 以及 made zombie 这样的标识。


另外一个用来事后分析 jit 行为的工具是jitwatch

2.使用 JFR 来观察 jit

JFR 是嵌入到 jmc (Java Mission Control)的一个 jvm 性能采样工具,从 JRockit VM 中移植到 HotSpot VM。 在安装 jdk7 以后版本都应该有这个 jmc 工具。使用 JFR 需要解锁商业特性,换句话就是再生产环境需要给 ora 买授权,但是你测试环境用是没事的,所以测试可以尽情的用。alijvm 实现了自己的 JFR,不得不说 ali 团队的强悍。


JFR 用默认配置就可以采集到编译信息了。如下图所示。


发布于: 17 分钟前阅读数: 7
用户头像

公众号:摸鱼编程 学习事半功倍的编程技巧 2018-05-27 加入

我是小亢,现某大厂架构师,专注提供java、jvm、架构、finops领域知识。

评论

发布
暂无评论
java 程序启动后cpu高怎么办?_JVM_你头顶的那片星空_InfoQ写作社区