-
作者:林军军、彭成寒编者按:笔者在 HBase 业务场景中尝试将 JDK 从 8 升级到 11,使用 G1 GC 作为垃圾回收器,但是性能下降 20%。到底是什么导致了性能衰退?又该如何定位解决?本文介绍如果通过使用 JFR、火焰图等工具确定问题,最后通过版本逐一验证找到了引起性能问题的代码。在毕昇 JDK 中率先修复问题最后将修复推送到上游社区中。希望通过本文的介绍让读者了解到如何解决大版本升级中遇到的性能问题;同时也提醒 Java 开发者要正确地使用参数(使用前要理解参数的含义)。HBase 从 2.3.x 开始正式默认的支持 JDK 11,HBase 对于 JDK 11 的支持指的是 HBase 本身可以通过 JDK 11 的编译、同时相关的测试用例全部通过。由于 HBase 依赖 Hadoop 和 Zookeeper,而目前 Hadoop 和 Zookeeper 尚未支持 JDK 11,所以 HBase 中仍然有一个 jira 来关注 JDK 11 支持的问题:https://issues.apache.org/jira/browse/HBASE-22972。G1 GC 从 JDK 9 以后就成为默认的 GC,而且 HBase 在新的版本中也采用 G1 GC,对于 HBase 是否可以在生产环境中使用 JDK 11?笔者尝试使用 JDK 11 来运行新的 HBase,验证 JDK 11 是否比 JDK 8 有优势。 1 环境介绍验证的方式非常简单,搭建一个 3 节点的 HBase 集群,安装 HBase,采用的版本为 2.3.2,关于 HBase 环境搭建可以参考官网。另外为了验证,使用一个额外的客户端机器,通过 HBase 自带的 PerformanceEvaluation 工具(简称 PE)来验证 HBase 读、写性能。PE 支持随机的读、写、扫描,顺序读、写、扫描等。例如一个简单的随机写命令如下:hbase org.apache.hadoop.hbase.PerformanceEvaluation --rows=10000 --valueSize=8000 randomWrite 5该命令的含义是:创建 5 个客户端,并且执行持续的写入测试。每个客户端每次写入 8000 字节,共写入 10000 行。PE 使用起来非常简单,是 HBase 压测中非常流行的工具,关于 PE 更多的用法可以参考相关手册。本次测试为了验证读写性能,采用如下配置:org.apache.hadoop.hbase.PerformanceEvaluation --writeToWAL=true --nomapred --size=256 --table=Test1 --inmemoryCompaction=BASIC --presplit=50 --compress=SNAPPY sequentialWrite 120JDK 采用 JDK 8u222 和 JDK 11.0.8 分别进行测试,当切换 JDK 时,客户端和 3 台 HBase 服务器统一切换。JDK 的运行参数为:-XX:+PrintGCDetails -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:-ResizePLAB注意:这里禁止 ResizePLAB 是业务根据 HBase 优化资料设置。 2 测试结果:JDK 11性能下降通过 PE 进行测试,运行结束有 TPS 数据,表示性能。在相同的硬件环境、相同的 HBase,仅仅使用不同的 JDK 来运行。同时为了保证结果的准确性,多次运行,取平均值。测试结果如下:从表中可以快速地计算得到吞吐量下降,运行时间增加。结论:使用 G1 GC,JDK 11 相对于 JDK 8 来说性能明显下降。3 原因分析从 JDK 8 到 JDK 11, G1 GC 做了非常多的优化用于提高性能。为什么 JDK 11 对于应用者来说更不友好?简单的总结一下从 JDK 8 到 JDK 11 做的一些比较大的设计变化,如下表所示:优化点描述IHOP 启发式设置IHOP 用于控制并发标记的启动时机,在 JDK 9 中引入该优化,根据应用运行的情况,计算 IHOP 的值,确保在内存耗尽之前启动并发标记。对于性能和运行时间理论上都是正优化,特殊情况下可能会导致性能下降Full GC 的并行话在 JDK10 中将 Full GC 从串行实现优化为并行实现,该优化不会产生负面影响动态线程调整根据 GC 工作线程的负载情况,引入动态的线程数来处理任务。该优化会带来正效果,注意不是 GC 工作线程数目越多 GC 的效果越好(GC 会涉及到多线程的任务窃取和同步机制,过多的线程会导致性能下降)引用集的重构引用集处理优化,设置处理大小、将并行修改为并发等由于从 JDK 8 到 JDK 11 特性变化太多,对于这样的性能下降问题,为了能快速有效的解决,我们做了如下的尝试。3.1 统一 JDK 8 和 JDK 11 的参数,验证效果由于 JDK 11 和 JDK 8 实现变化很多,部分功能完全不同,但是这些变化的功能一般都有参数控制,一种有效的尝试:梳理 JDK 8 和 JDK 11 关于 G1 的参数,将它们设置为相同的值,比如关闭 IHOP 的自适应,关闭线程调整等。这里简单的给出 JDK 8 和 JDK 11 不同参数的比较,如下图所示:将两者参数都设置为和 JDK 8 一样的值,重新验证测试,结果不变,JDK 11 性能仍然下降。3.2 GC日志分析,确定JDK 11性能下降点对于 JDK 8 和 JDK 11 同时配置日志收集功能,重新测试,获得 GC 日志。通过 GC 日志分析,我们发现差异主要在 G1 young gc 的 object copy 阶段(耗时基本在这),JDK 11 的 Young GC 耗时大概 200ms,JDK 8 的 Young GC 耗时大概 100ms,两者设置的目标停顿时间都是 100ms。JDK 11 中 GC 日志片段:JDK 8中 GC 日志片段:我们对整个日志做了统计,有以下发现:并发标记时机不同,混合回收的时机也不同;单次 GC 中对象复制的耗时不同,JDK 11 明显更长;总体 GC 次数 JDK 11 的更多,包括了并发标记的停顿次数;总体 GC 的耗时 JDK 11 更多。针对 Young GC 的性能劣化,我们重点关注测试了和 Young GC 相关的参数,例如:调整 UseDynamicNumberOfGCThreads、G1UseAdaptiveIHOP 、GCTimeRatio 均没有效果。下面我们尝试使用不同的工具来进一步定位到底哪里出了问题。3.3 JFR分析-确认日志分析结果毕昇 JDK 11和毕昇 JDK 8 都引入了 JFR,JFR 作为 JVM 中问题定位的新贵,我们也在该案例进行了尝试,关于JFR的原理和使用,参考本系列的技术文章:Java Flight Recorder - 事件机制详解。3.3.1 JDK 11总体信息JDK 8 中通过 JFR 收集信息。3.3.2 JDK 8总体信息JFR 的结论和我们前面分析的结论一致,JDK 11 中中断比例明显高于 JDK 8。3.3.3 JDK 11中垃圾回收发生的情况3.3.4 JDK 8中垃圾回收发生的情况从图中可以看到在 JDK 11 中应用消耗内存的速度更快(曲线速率更为陡峭),根据垃圾回收的原理,内存的消耗和分配相关。 3.3.5 JDK 11中VM操作3.3.6 JDK 8中VM操作通过 JFR 整体的分析,得到的结论和我们前面的一致,确定了 Young GC 可能存在问题,但是没有更多的信息。3.4 火焰图-发现热点为了进一步的追踪 Young GC 里面到底发生了什么导致对象赋值更为耗时,我们使用Async-perf 进行了热点采集。关于火焰图的使用参考本系列的技术文章:使用 perf 解决 JDK8 小版本升级后性能下降的问题。3.4.1 JDK 11的火焰图3.4.2 JDK 11 GC部分火焰图 3.4.3 JDK 8的火焰图3.4.4 JDK 8 GC部分火焰图通过分析火焰图,并比较 JDK 8 和 JDK 11 的差异,可以得到:在 JDK 11 中,耗时主要在:G1ParEvacuateFollowersClosure::do_void()G1RemSet::scan_rem_set 在 JDK 8 中,耗时主要在:G1ParEvacuateFollowersClosure::do_void()更一步,我们对 JDK 11 里面新出现的 scan_rem_set() 进行更进一步分析,发现该函数仅仅和引用集相关,通过修改 RSet 相关参数(修改 G1ConcRefinementGreenZone ),将 RSet 的处理尽可能地从Young GC的操作中移除。火焰图中参数不再成为热点,但是 JDK 11 仍然性能下降。比较 JDK 8 和 JDK 11 中 G1ParEvacuateFollowersClosure::do_void() 中的不同,除了数组处理外其他的基本没有变化,我们将 JDK 11 此处的代码修改和 JDK 8 完全一样,但是性能仍然下降。结论:虽然 G1ParEvacuateFollowersClosure::do_void() 是性能下降的触发点,但是此处并不是问题的根因,应该是其他的原因造成了该函数调用次数增加或者耗时增加。 3.5 逐个版本验证-最终确定问题我们分析了所有可能的情况,仍然无法快速找到问题的根源,只能使用最笨的办法,逐个版本来验证从哪个版本开始性能下降。在大量的验证中,对于 JDK 9、JDK 10,以及小版本等都重新做了构建(关于 JDK 的构建可以参考官网),我们发现 JDK 9-B74 和 JDK 9-B73 有一个明显的区别。为此我们分析了 JDK 9-B73 合入的代码。发现该代码和 PLAB 的设置相关,为此梳理了所有 PLAB 相关的变动:B66 版本为了解决 PLAB size 获取不对的问题(根据 GC 线程数量动态调整,但是开启 UseDynamicNumberOfGCThreads 后该值有问题,默认是关闭)修复了 bug。具体见 jira:Determining the desired PLAB size adjusts to the the number of threads at the wrong placeB74 发现有问题(desired_plab_sz 可能会有相除截断问题和没有对齐的问题),重新修改,具体见 8079555: REDO - Determining the desired PLAB size adjusts to the the number of threads at the wrong placeB115 中发现 B74 的修改,动态调整 PLAB 大小后,会导致很多情况 PLAB 过小(大概就是不走 PLAB,走了直接分配),频繁的话会导致性能大幅下降,又做了修复 Net PLAB size is clipped to max PLAB size as a whole, not on a per thread basis 重新修改了代码,打印 PLAB 的大小。对比后发现 desired_plab_sz 大小,在性能正常的版本中该值为 1024 或者 4096(分别是 YoungPLAB 和 OLDPLAB),在性能下降的版本中该值为 258。由此确认 desired_plab_sz 不正确的计算导致了性能下降。 3.6 PALB 为什么会引起性能下降?PLAB 是 GC 工作线程在并行复制内存时使用的缓存,用于减少多个并行线程在内存分配时的锁竞争。PLAB 的大小直接影响 GC 工作线程的效率。在 GC 引入动态线程调整的功能时,将原来 PLABSize 的大小作为多个线程的总体 PLAB 的大小,将 PLAB 重新计算,如下面代码片段:其中 desired_plab_sz 主要来自 YoungPLABSize 和 OldPLABSIze 的设置。所以这样的代码修改改变了 YoungPLABSize、OldPLABSize 参数的语义。 另外,在本例中,通过参数显式地禁止了 ResizePLAB 是触发该问题的必要条件,当打开 ResizePLAB 后,PLAB 会根据 GC 工作线程晋升对象的大小和速率来逐步调整 PLAB 的大小。注意,众多资料说明:禁止 ResziePLAB 是为了防止 GC 工作线程的同步,这个说法是不正确的,PLAB 的调整耗时非常的小。PLAB 是 JVM 根据 GC 工作线程使用内存的情况,根据数学模型来调整大小,由于模型的误差,可能导致 PLAB 的大小调整不一定有人工调参效果好。如果你没有对 YoungPLABSize、OldPLABSize 进行调优,并不建议禁止 ResizePLAB。在 HBase 测试中,当打开 ResizePLAB 后 JDK 8 和 JDK 11 性能基本相同,也从侧面说明了该参数的使用情况。 3.7 解决方法&修复方法由于该问题是 JDK 9 引入,在 JDK 9, JDK 10, JDK 11, JDK 12, JDK 13, JDK 14, JDK 15, JDK 16 都会存在性能下降的问题。我们对该问题进行了修正,并提交到社区,具体见Jira: https://bugs.openjdk.java.net/browse/JDK-8257145;代码见:https://github.com/openjdk/jdk/pull/1474;该问题在JDK 17中被修复。 同时该问题在毕昇 JDK 所有版本中第一时间得到解决。 当然对于短时间内无法切换 JDK 的同学,遇到这个问题,该如何解决?难道要等到 JDK 17?一个临时的方法是显式地设置 YoungPLABSize 和 OldPLABSize 的值。YoungPLABSize 设置为 YoungPLABSize* ParallelGCThreads,其中 ParallelGCThreads 为 GC 并行线程数。例如 YoungPLABSize 原来为 1024,ParallelGCThreads 为 8,在 JDK 9~16,将 YoungPLABSize 设置为 8192 即可。其中参数 ParallelGCThreads 的计算方法为:没有设置该参数时,当 CPU 个数小于等于 8, ParallelGCThreads 等于 CPU 个数,当 CPU 个数大于 8,ParallelGCThreads 等于 CPU 个数的 5/8)。 3.8 小结本文分享了针对 JDK 升级后性能下降的解决方法。Java 开发人员如果遇到此类问题,可以按照下面的步骤尝试自行解决:对齐不同 JDK 版本的参数,确保参数相同,看是否可以快速重现;分析 GC 日志,确定是否由 GC 引起。如果是,建议将所有的参数重新验证,包括移除原来的参数。本例中一个最大的失误是,在分析过程中没有将原来业务提供的参数 ResizePLAB 移除重新测试,浪费了很多时间。如果执行该步骤后,定位问题可能可以节约很多时间;使用一些工具,比如 JFR、NMT、火焰图等。本例中尝试使用这些工具,虽然无果,但基本上确认了问题点;最后的最后,如果还是没有解决,请联系毕昇 JDK 社区。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM 和 JDK 等相关编译技术,感兴趣的同学可以添加如下微信小助手入群。原文转载自 openEuler-JDK 从8升级到11,使用 G1 GC,HBase 性能下降近20%。JDK 到底干了什么?
-
作者:宋尧飞编者按:笔者遇到一个非常典型的问题,应用在 X86 正常运行,在 AArch64 上 JVM 就会崩溃。这个典型的 JVM 内部问题。笔者通过分析最终定位到是由于 JVM 中模板解释器代码存在 bug 导致在弱内存模型的平台上 Crash。在分析过程中,涉及到非常多的 JVM 内部知识,比如对象头、GC 复制算法操作、CAS 操作、字节码执行、内存序等,希望对读者有所帮助。本文介绍了一般分析 JVM crash 的方法,并且深入介绍了为什么在 aarch64 平台上引起这样的问题,最后还给出了修改方法并推送到上游社区中。**对于使用非毕昇 JDK 的其他 JDK 只有在 jdk8u292、jdk11.0.9、jdk15以后的版本才得到修复,读者使用时需要注意版本选择避免这类问题发生。背景知识java 程序在发生 crash 时,会生成 hs_err_pid.log 文件,以及 core 文件(需要操作系统开启相关设置),其中 hs_err 文件以文本格式记录了 crash 发生位置的小范围精确现场信息(调用栈、寄存器、线程栈、致命信号、指令上下文等)、jvm 各组件状态信息(java 堆、jit 事件、gc 事件)、系统层面信息(环境变量、入参、内存使用信息、系统版本)等,精简记录了关键信息。而 core 文件是程序崩溃时进程的二进制快照,完整记录了崩溃现场信息,可以使用 gdb 工具来打开 core 文件,恢复出一个崩溃现场,方便分析。约束文中描述的问题适用于 jdk8u292 之前的版本。现象某业务线隔十天半个月总会报过来 crash 问题,crash 位置比较统一,都是在某处执行 young gc 的上下文中,crash 的直接原因是 java 对象的头被写坏了,比如这样:而正常的对象头由 markoop 和 metadata 两部分组成,前者存放该对象的 hash 值、年龄、锁信息等,后者存放该对象所属的 Klass 指针。这里关注的是 markoop,64 位机器上它的具体布局如下:每种布局中每个字段的详细含义可以在 jdk 源码 jdk8u/hotspot/src/share/vm/oops/markOop.hpp 中找到,这里简单给出结论就是 gc 阶段一个正常对象头中的 markoop 不可能是全 0,而是比如这样:此外,crash 时间上也有个特点:基本每次都发生在程序刚启动时的几秒内。分析发生 crash 的 java 对象有个一致的特点,就是总位于 eden 区,我们仔细分析了 crash 位置的 gc 过程逻辑,特别是会在 gc 期间修改对象头的相关源码更是重点关注对象,因为那块代码为了追求性能,使用了无锁编程:补充介绍一下 CAS(Compare And Swap),CAS 的完整意思是比较并替换,并且确保整个操作原子性。CAS 需要 3 个操作数:内存地址 dst,比较值 cmp,要更新的目标值 value。当且仅当内存地址 dst 上的值跟比较值 cmp 相等时,将内存地址 dst 上的值改写为 value,否则就什么都不做,其在 aarch64 上的汇编实现类似如下:然而我们经过反复推敲,这块 gc 逻辑似乎无懈可击,而且位于 eden 区也意味着没有被 gc 搬移过的可能性,这个问题在很长时间里陷入了停滞……直到某一天又收到了一个类似的 crash,这个问题才迎来了转机。在这个 crash 里,也是 java 对象的头被写坏了,但特殊的地方在于,头上的错误值是 0x2000,凭着职业敏感,我们猜测这个特殊的错误值是否来自这个 java 对象本身呢?这个对象的 Java 名字叫 DynamicByteBuffer,来自某个基础组件。反编译得到了问题类 DynamicByteBuffer 的代码:再结合 core 信息中其他正常 DynamicByteBuffer 对象的布局,确定了这个特殊的 0x2000 值原本应该位于 segmentSize 字段上,而且从代码中注意到这个 segmentSize 字段是 final 属性,意味着其值只可能在实例构造函数中被设置,使用 jdk 自带的命令 javap 进行反汇编,得到对应的字节码如下:putfield 这条字节码的作用是给 java 对象的一个字段赋值,在红框中的语义就是给 DynamicByteBuffer 对象的 segmentSize 字段赋值。分析到这里,我们做一下小结,crash 的第一现场并非在 gc 上下文中,而是得往前追溯,发生在这个 java 对象被初始化期间,这期间在初始化它的 segmentSize 字段时,因为某种原因,0x2000 被写到了对象头上。接下来继续分析, JDK 在发生 crash 时会自动生成的 hs_err 日志,其中有记录最近发生的编译事件 “Compilation events (250 events)”,从中没有发现 DynamicByteBuffer 构造函数相关的编译事件,所以可以推断 crash 时 DynamicByteBuffer 这个类的构造函数尚未被编译过(由于 crash 发生在程序启动那几秒,JIT 往往需要预热后才会介入,所以可以假设记录的比较完整),这意味着,它的构造函数只会通过模板解释器去执行,更具体地说,是去执行模板解释器中的 putfield 指令来把 0x2000 写到 segmentSize 字段位置。具体怎么写其实很简单,就是先拿到 segmentSize 字段的偏移量,根据偏移量定位到写的位置,然后写入。然而 JVM 的模板解释器在实现这个 putfield 指令时,额外增加了一条快速实现路径,在 runtime 期间会自动(具体的时间点是 “完整” 执行完第一次 putfield 指令后)从慢速路径切到快速路径上,这个切换操作的实现全程没有加锁,同步完全依赖 barrier,由于整个过程比较复杂,这里首先给一个比较容易理解的并行流程图:注:图中 bcp 指的是 bytecode pointer,就是读字节码。上图表示接近同一时间点前后,两条并行流分别构建一个 DynamicByteBuffer 类型的对象过程中,各自完成 segmentSize 字段赋值的过程,用 Java 代码简单示意如下:其中第一条执行流走的慢速路径,第二条走的快速路径,可以留意到,红色标识的是几次公共内存的访存操作,barrier 就分布在这些位置前后(标在下图中)。接下来再给一个更加精确一点的指令流模型:简单介绍一下这个设计模型:线程从记录了指令的内存地址 bcp(bytecode pointer) 上取出指令,然后跳转到该指令地址上执行,当取出的指令是 bcp1(比如 putfeild 指令的慢速路径)时就是图中左边的指令流;左边的指令流就是计算出字段的 offset 并 str 到指定内存地址,然后插入 barrier,最后将 bcp2 指令(比如 putfeild 指令的快速路径)覆写到步骤 1 中的内存地址 addr 上;后续线程继续执行步骤 1 时,由于取出的指令变成了 bcp2,就改为跳转到图中右边的指令流;右边的指令流就是直接取出步骤 2 中已经存到指定内存地址中的 offset。回顾整个设计模型,左边的指令流通过一个等效于完整 dmb 的 barrier 来保证 str offset 和 str bcp2 这两条 str 指令的执行顺序并且全局可见;而右边的指令流中,ldr bcp 和 ldr offset 这两条 ldr 指令之间没有任何 barrier,设计者可能认为一个无条件跳转指令可以为两条 ldr 指令建立依赖,从而保证执行顺序,然而从实测结果来看是不成立的。这里先来简单补充介绍一下内存顺序模型的概念,现代 CPU 为了提高执行效率,在指令的执行顺序上拥有很大的自主权,对每个独立的 CPU 来说,只要确保语义不变,实际如何执行都有可能,这种方式对于单个 CPU 来说没有问题,当放到多个 CPU 共享数据的时候,这种乱序执行的行为就会引发每个 CPU 看到数据的顺序不一致问题,导致跨 CPU 的程序逻辑乱套了。这就需要对读、写内存指令进行约束,来规范每个 CPU 看到的内存生效行为,由此提出了内存顺序模型的概念:其中 ARM 采用的是一种弱内存模型,这种模型默认对读、写指令没有任何约束,需要由程序员自己通过插入 barrier 来手动保证。再回到这个问题上,测试方式是在 ldr offset 指令后额外加了检测指令:就是检查 offset 值是否为 0,如果为 0 则直接强制 crash(设计上保证了 java 对象的任何实例字段的 offset 不可能是 0)。经过长时间测试,程序果然在这个位置触发了 crash!这说明上面提到的两条 ldr 指令不存在依赖关系,或者说这种依赖关系类似 ARMv8 手册中描述的条件依赖,并不能保证执行顺序。ldr offset 指令先于 ldr bcp 执行,使得读到一个非法的 offset 值 0。更说明了,这才是这个案例的第一案发现场!找到了问题的根因后,解决方法也就顺利出炉了,那就是在两条 ldr 指令之间插入 barrier 来确保这两条 ldr 指令不发生乱序。实测证明,这种修复方案非常有效,这类 crash 现象消失。详细的修复 patch 见 https://hg.openjdk.java.net/jdk/jdk/rev/b9529fcbbd33。目前已经 backport 到 jdk8u292、jdk11.0.9、jdk15。总结Java 虚拟机 (JVM) 为了追求性能,大量使用了无锁编程进行设计,而且这么多年以来 JDK(特别是 JDK8)主要都是面向 X86 平台开发的,如今才慢慢的开始支持 aarch64 平台,所以 aarch64 弱内存序问题是我们面临的一个比较严峻的挑战。后记如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 官网查找相关资源,包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。原文转载自 openEuler-一个 JVM 解释器 bug 在 AArch64 平台导致应用崩溃的问题分析
-
作者:冯世杰编者按:Java Flight Recorder(简称为JFR)曾经是Oracle JDK商业版的附属组件,在JDK 11中被正式开始开源,后又被移植到JDK8中。JFR本身对运行期系统的侵入性很小,同时又能提供相对准确和丰富的运行期信息;合理使用该工具可以极大地提高工作效率。本文介绍JFR剖析的事件机制,希望能帮助大家从原理上理解JFR并正确使用JFR。本篇文章中的源码大部分来自openjdk8u262本文出发点是梳理 JFR 的事件机制, 侧重点在于理解而非应用使用上可以不要太过拘泥细节,可以具体问题具体分析。对于JFR我们有着怎样的预期它是一个辅助分析工具,我们希望借助它,尽可能低开销地收集运行时数据,从而辅助对JVM可能存在的故障、性能瓶颈进行分析。我们结合JFR的Goals来看:提供基于生成和消费数据作为事件的API提供缓存机制和二进制数据格式允许配置和过滤事件为OS、JVM、JDK库提供相应的事件从中,我们能粗略地获取这些信息 :事件以自描述的二进制形式(.jfr)被保存着事件中包含了数据,事件 ≈ 数据.jfr 文件 => read by some Provided API => 重现运行时数据 [ => 可视化]我们想尝试了解JFR的事件驱动机制,具体点就是回答几个问题:一个事件何时产生/启动监控? 经历了怎样的路径? 如何被保存? 保存到哪里? JFR是事件驱动的本节主要是一些前置信息 (假如你有所了解,可以快速浏览或者跳过本节内容): JVM行为基本都是Event,如类加载对应着Class Load Event, 垃圾回收对应GC Event;Event 主要由 timestamp, event name, additional info, data 这几部分组成。Event 收集四类事件的信息: Instant Event , 发生就收集(e.g. Thread Start ...)Duration Event,持续收集一段时间(e.g. GC Event ...)Timed Event , 收集超过指定时间的事件Sample Event,按频率采样以JFR的Class Load Event为例, 看看一个事件的结构。(共计 24 bytes)<memory address> : 98 80 80 00 87 02 95 ae e4 b2 92 03 a2 f7 ae 9a 94 02 02 01 8d 11 00 00Event Size : 98 80 80 00Event ID : 87 02TimeStamp : 95 ae e4 b2 92 03 Duration : a2 f7 ae 9a 94 02Thread ID : 02Stack trace ID : 01PayLoad(记录的数据,fields 取决于各个 Event 类型):加载的类 : 8d 11定义类的 ClassLoader : 00 初始化类的 ClassLoader : 00 多个线程都会产生Event,线程通过无锁(Lock-free)设计记录事件。线程将事件首先写入到 ThreadLocalBuffer(简称TLB), TLB被填满后,将被转存到 Global buffer(circular),对于较旧的数据,可以通过配置,选择丢弃或者写入磁盘,以便连续保存历史记录。示意图如下所示:注意:TLB、Global Buffer和磁盘文件中的事件记录不会相互备份,未及时转存的数据可能发生丢失,本文不会就这点展开阐述。前置内容已经交代清楚,接着回到正轨。一个事件的生命周期以下是枯燥乏味的一堆代码,但是不得不看。首先来看JFR的结构,如下图所示:肉眼可见的一堆钩子,这些hook用于记录对应的触发事件。我们简单地挑一个 Thread Start 的事件,关注一下它的整个被触发到被记录的过程。在线程创建并执行时会调用记录JFR事件,代码如下:可见当一个新的Java线程被创建时,只要开启了JFR, 那么就会执行上述代码; 接着看一下 on_thread_start 干了什么:在此,我们看到了一个事件EventThreadStart,并且在事件中设置信息后被提交。在 JEP 328中有一个更为简单直接例子,如下:无需太过关心其内容。我们只需关注这个事件生成的结构:这里的 EventType 定义于 jfrEventClass.hpp, 该文件是编译时生成的,简单贴一下生成逻辑,可以参考Makefile文件,如下 (同样无需在意太多细节): 回到主旋律,继续来看事件的结构和成员函数,如下:其中最为重要的成员函数是 JfrEvent::commit 方法,用于提交事件,代码如下.在函数中,最后一段代码, 也是核心所在,用于真正记录事件:这下,就可以很容易地和第1节的内容对应上了,特别是其中的事件模型的图片:小结用户是否可以自定义一个JFR事件?注意点有哪些?这里通过JEP 328里的例子(稍微有点改动),来展示如何自定义JFR事件。通过编译后直接执行如下命令:> java -XX:StartFlightRecording,filename=event.jfr Test可以得到如下日志信息:Started recording 1. No limit specified, using maxsize=250MB as default.Use jcmd 57980 JFR.dump name=1 to copy recording data to file.日志可以通过标准的API进行解析,下面通过一个简单代码解析上面生成的事件,代码如下:编译运行> java Viewer | less可以得到如下结果。相信此时你已经对JFR的事件机制有了个不错的感觉。实际上JFR的使用一般配合JMC[1]使用,在JMC中通过页面可以得到统计信息,更有助于判断系统的运行情况。后记如果遇到相关技术问题(包括不限于毕昇JDK),可以在论坛求助,也可以进入官网查找所有相关资源(包括二进制下载、代码仓库、使用教学、安装、学习资料等)。毕昇JDK社区每双周周二举行技术例会,同时有一个技术交流群讨论GCC、LLVM、JDK和V8等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复Compiler入群。原文转载自 openEuler-Java Flight Recorder - 事件机制详解参考[1] https://adoptopenjdk.net/jmc.html
-
作者:宋尧飞编者按:笔者在AArch64中遇到一个G1 GC挂起,CPU利用率高达300%的案例。经过分析发现问题是由JVM的锁机制导致,该问题根因是并发编程中没有正确理解内存序导致。本文着重介绍JVM中Monitor的基本原理,同时演示了在什么情况下会触发该问题。希望通过本文的分析,读者能够了解到内存序对性能、正确性的影响,在并发编程时更加仔细。现象本案例是一个典型的弱内存模型案例,大致的现象就是AArch64平台上,业务挂死,而进程占用CPU持续维持在300%。配合top和gdb,可以看到是3个GC线程在offer_termination处陷入了死循环: 多个并行GC线程在Minor GC结束时调用offer_termination,在offer_termination中自旋等待其他并行GC线程到达该位置,才说明GC任务完成,可以终止。(关于并行任务的中止协议问题,可以参考相关论文,这里不做着重介绍。简单地说,在并行任务执行时,多个任务之间可能存在任务不均衡,所以JVM内部设计了任务均衡机制,同时必须设计任务终止的机制来保证多个任务都能完成,这里的offer_termination就是尝试终止任务)。在该案例中,部分GC线程完成自己的任务,等待其他的GC线程。此时出现挂起,很有可能是因为发生了死锁。所以问题很可能是由于那些尚未完成任务的GC线程上错误地使用锁。所以使用gdb观察了一下其他GC线程,发现其他GC线程全都阻塞在一把JVM的锁上: 而这把Monitor中的情况如下:cxq上积累了大量GC线程OnDeck记录的GC线程已经消失_owner记录的锁持有者为NULL分析在进一步分析前,首先普及一下JVM锁组件Monitor的基本原理,Monitor类主要包含4个核心字段:“Thread * volatile _owner;” 字段指向这把锁的持有线程“SplitWord _LockWord;” 字段被设计为1个机器字长,目的是为了确保操作时天然的原子性,它的最低位被设计为上锁标记位,而高位区域用来存放256字节对齐的竞争队列(cxq)地址“ParkEvent * volatile _EntryList;” 字段指向一个等待队列,跟cxq差别不大,个人理解只是为了缓解cxq的竞争压力而设计“ParkEvent * volatile _OnDeck;” 字段指向这把锁的法定继承人,同时最低位还充当了内部锁的角色接下来通过一组流程图来介绍加解锁的具体流程:上图是加锁的一个整体流程,大致分为3步:1. 首先走快速上锁流程,主要对应锁本身无人持有的最理想情况2. 接着是自旋上锁流程,这是预期将在短时间内获取锁的情况3. 最后是慢速上锁流程,申请者将会加入等待队列(cxq),然后进入睡眠,直到被唤醒后发现自己变成了法定继承者,于是进入自旋,直到完成上锁。 而且,基于性能考虑,整个上锁流程中的每一步几乎都做了“插队”的尝试:如上图代码中所示,“插队”的意思就是不经过排队(cxq),直接尝试置上锁标志位。 上图就是整个解锁流程了,显然真正的解锁操作在第二步中就已经完成了(意味着接下来时刻有“插队”现象发生),剩下的主要就是选出继承者的过程,大致分为以下几步:解锁线程首先需要将内部锁(_OnDeck)标记上锁从竞争队列(cxq)抽取所有等待者放入等待队列(_EntryList)_ EntryList取出头一个元素,写入_OnDeck的同时解除内部锁标记,这代表选出了继承者唤醒继承者当然伴随着整个解锁流程每一步的,还有对“插队”行为的处理。至此,JVM锁组件Monitor的原理就介绍到这里,再回归到问题本身,一个疑问就是_OnDeck上记录的继承者为何消失?作为继承者,既然已经消失在竞争队列和等待队列里,显然意味着它大概率已经持有锁、然后解锁走人了,所以问题很可能跟继承者选取过程有关。基于这种猜测,我们对相关代码着重进行了梳理,就发现了下图两处红框标记位置存在疑点,那就是在选继承者过程第3步中:写_ EntryList和写_OnDeck之间没有barrier来保证执行顺序,这可能出现_OnDeck先于_ EntryList写入的情况,一旦继承人提前持有锁,后果就可能非常糟糕… 这里贴了一张可能的问题场景:线程A处于解锁流程中,由于乱序,先写入了继承者同时解除内部锁线程B处于上锁流程,发现自己就是法定继承者后,立刻完成上锁线程B又迅速进入解锁流程,并从_EntryList中取出头元素(也就是线程B!)作为继承者写入_OnDeck,完成解锁走人线程A此时才更新_EntryList,然后唤醒继承者(也就是线程B!),完成解锁走人_OnDeck上的继承者线程B,实际已经完成加解锁离开,后续等待线程再也无法被唤醒正巧在社区的高版本上找到了一个相关的修复记录,这里贴出2个关键的代码片段: 上面这段代码位于慢速上锁流程,被唤醒后检查继承者是否是自己,修复后的代码在读_OnDeck时加了Load-Acquire的barrier。 上面这段代码位于解锁时选继承者流程,从_ EntryList取出头一个元素,写入_OnDeck的同时解除内部锁标记,修复后的代码在写_OnDeck时加了Store-Release的barrier。显然,围绕_OnDeck添加的这对One-way barrier可以确保:当继承者线程被唤醒时,该线程可以“看”到_EntryList已经被及时更新。总结在AArch64这种弱内存模型的平台上(关于内存序更多的知识在接下来的分享中会详细介绍),一旦涉及多线程对公共内存的每一次访问,必须反复确认是否需要通过barrier来严格保序,而且除非存在有效的依赖关系,否则barrier需要在读写端成对使用。后记如果遇到相关技术问题(包括不限于毕昇JDK),可以在论坛求助(目前毕昇JDK最新的官网http://bishengjdk.openeuler.org已经上线,可以进入官网查找所有相关资源,包括二进制下载、代码仓库、使用教学、安装、学习资料等)。毕昇JDK社区每双周周二举行技术例会,同时有一个技术交流群讨论GCC、LLVM、JDK和V8等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复Compiler入群。原文转载自 openEuler-JVM 锁 bug 导致 G1 GC 挂起问题分析和解决
-
作者:袁含晨编者按:在升级JDK8U的小版本后(从8u74升级到8u202),遇到性能剧烈下降的问题(性能下降13倍)。该应用是一个非常简单的Web应用,且应用在JDK升级前后并无任何发布修改。通常来说JDK小版本升级都是问题修复,不影响功能和性能使用,而应用性能剧烈下降不一定是JVM代码层面的内部bug,有可能是默认选项变化等问题导致。对于这样明确由JDK引起的性能问题,该如何解决?最常见的方法是通过工具分析JVM执行过程,检查函数执行的情况是否发生变化,如果找到变化,则可以深入分析哪些因素引起了变化,并进一步得到根因。笔者使用perf工具分析JVM执行时的热点函数,并对出现问题的函数进行剖析,使用函数插桩来分析函数的执行次数,发现不同版本行为差异的根源,并找到了引起问题的根因。希望读者遇到性能问题时可以参照本文使用perf工具对问题进行定位。 工欲善其事,必先利其器。程序员在定位性能瓶颈的时候,要是有一个趁手的性能调优工具,能一针见血地指出程序的性能问题,可谓事半功倍。Linux中最常用的性能调优工具Perf(Linux系统原生提供的性能分析工具),使用perf先对应用(假设要采样的应用为JavaApp)进行采样,使用record命令,如下:perf record java JavaApp另外perf能按出现的百分比降序打印CPU正在执行的函数名以及调用栈,如命令:perf report -n可打印出:这种结果的输出还是不直观的,Linux性能优化大师Brendan Gregg发明了火焰图(因整个图形看起来像燃烧的火焰而得名),以全局的方式来看各个函数的调用时间分布,以图形化的方式列出调用栈。1 初识火焰图火焰图是基于perf的结果生成的图形,我们先了解一下怎么去看火焰图。以下图为例:X轴表示被抽样到的次数。理解X轴的含义,需先了解采样数据的原理。Perf是在指定时间段内,每隔一段时间采集一次数据,被采集到的次数越多,说明该函数的执行总时间长,可能的原因有:调用次数多,或者单次执行时间长。因此,X轴的宽度不能简单的认为是运行时长。Y轴表示调用栈。如何从火焰图看出性能的瓶颈在哪里?最有理由怀疑的地方,顶层的“平顶”。关于perf和火焰图使用方法可以参考官网http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html。下面是我们利用火焰图来定位问题的一次实战。2 火焰图定位问题的实战2.1 问题场景问题发生的场景是客户端向服务器发起http请求,服务器返回数据给客户端(这是一个非常简单的服务交互)。我们发现使用JDK 8u74的性能要远优于JDK 8u202的性能,下表中统计了20次服务器的响应时长。次数JDK8u74响应时间(单位:秒)JDK8u202响应时间(单位:秒)10.0300.83420.0361.08830.0300.33240.0330.59750.0180.58160.0490.85070.0410.35580.0210.71190.1480.854100.0800.754110.0251.176120.0320.459130.0460.443140.0250.135150.0590.485160.0771.093170.1231.173180.1150.945190.0580.384200.0351.061平均时间0.054050.7155 从响应时间来看,8u202相比8u74性能下降13倍之多,由于应用本身并未做任何修改,所以考虑使用火焰图来定位性能消耗的问题点。在8u74和8u202分别运行应用,并用perf的record抓取数据并生成火焰图。2.2 火焰图定位对比两张火焰图,使用8u74时ClientHandshaker.processMessage占比为1.15%,而在8u202中这个函数占比为23.98%,很明显在ClientHandshaker.processMessage带来了性能差异。 2.3 根因定位两者在这个ClientHandshaker.processMessage上的cpu消耗差异很大,继续分析这个函数找到根因。void processMessage(byte handshakeType, int length) throws IOException { if(this.state >= handshakeType && handshakeType != 0) { //... 异常 } else { label105: switch(handshakeType) { case 0://hello_request this.serverHelloRequest(new HelloRequest(this.input)); break; //... case 2://sever_hello this.serverHello(new ServerHello(this.input, length)); break; case 11:///certificate this.serverCertificate(new CertificateMsg(this.input)); this.serverKey = this.session.getPeerCertificates()[0].getPublicKey(); break; case 12://server_key_exchange 该消息并不是必须的,取决于协商出的key交换算法 //... case 13: //certificate_request 客户端双向验证时需要 //... case 14://server_hello_done this.serverHelloDone(new ServerHelloDone(this.input)); break; case 20://finished this.serverFinished(new Finished(this.protocolVersion, this.input, this.cipherSuite)); } if(this.state < handshakeType) {//握手状态 this.state = handshakeType; } } }processMessage()主要是通过不同的信息类型进行不同的握手消息的处理。而在火焰图中可以看到,JDK8u74图中,主要消耗在函数serverFinished()和serverHello()上,而JDK8u202主要消耗在函数serverHelloDone()和serverKeyExchange()。在介绍火焰图的时候,我们有提到,X轴的长度是映射了被采样到的次数。因此需要进一步确定消耗:函数单次执行耗时过长而成为热点,还是因为频繁调用函数导致函数耗时过长而成为热点。可通过字节码插桩(通过Instrument技术实现对函数的计数,然后编译成agent,执行应用时加载agent,具体使用Instrument的方法可以参考官方文档)查看函数serverHelloDone()的调用次数及执行时间。JDK8u202 数据 Execute count : 253 Execute count : 258 Execute count : 649 Execute count : 661 serverHelloDone execute time [1881195 ns] Execute count : 1223 Execute count : 1234 Execute count : 1843 Execute count : 1852 serverHelloDone execute time [1665012 ns] Execute count : 2446 Execute count : 2456 serverHelloDone execute time [1686206 ns] JDK8u74 数据 Execute count : 56 Execute count : 56 Execute count : 56 Execute count : 56 Execute count : 56 Execute count : 56Execute time是取了每1000次调用的平均值,Execute count每5000ms输出一次总执行次数。很明显使用JDK8u202时在不断调用serverHelloDone,而74在调用56次后没有再调用过这个函数。初始化握手时,serverHelloDone方法中,客户端会根据服务端返回加密套件决定加密方式,构造不同的Client Key Exchange消息;服务器如果允许重用该会话,则通过在Server Hello消息中设置相同的会话ID来应答。这样,客户端和服务器就可以利用原有会话的密钥和加密套件,不必重新协商,也就不再走serverHelloDone方法。从现象来看, JDK8u202没有复用会话,而是建立的新的会话。2.4 水落石出查看JDK8u 161的release notes,添加了TLS会话散列和扩展主密钥扩展支持,找到引入的一个还未修复的issue,对于带有身份验证的TLS的客户端,支持UseExtendedMasterSecret会破坏TLS-Session的恢复,导致不使用现有的TLS-Session,而执行新的Handshake。JDK8u161之后的版本(含JDK8u161),若复用会话时不能成功恢复Session,而是创建新的会话,会造成较大性能消耗,且积压的大量的不可复用的session造成GC压力变大;如果业务场景存在不变更证书密钥,需要复用会话,且对性能有要求,可通过添加参数-Djdk.tls.useExtendedMasterSecret=false来解决这个问题。3 后记如果遇到相关技术问题(包括不限于毕昇JDK),可以通过毕昇JDK社区求助。毕昇JDK社区每双周周二举行技术例会,同时有一个技术交流群讨论GCC、LLVM和JDK等相关编译技术,感兴趣的同学可以添加如下微信小助手入群本文转载自 openEuler-使用 perf 解决 JDK8 小版本升级后性能下降的问题
-
rmic功能说明: rmic 为远程对象生成 stub 和 skeleton。 语法: rmic [ options ] package-qualified-class-name(s)补充说明: rmic 编译器根据编译后的 Java 类(含有远程对象实现)名,为远程对象生成 stub 和 skeleton(远程对象是指实现 java.rmi.Remote 接口的对象)。在 rmic 命令中所给的类必须是经 javac 命令成功编译且是完全包限定的类。 命令选项 -classpath[路径] 指定 rmic 用于查询类的路径。如果设置了该选项,它将覆盖缺省值或 CLASSPATH 环境变量。目录用冒号分隔。 -d[目录] 指定类层次的根目录。此选项可用来指定 stub 和 skeleton 文件的目标目录。 -depend 使编译器考虑重新编译从其它类引用的类。 一般来说,它只重新编译从源代码引用的遗漏或过期的类。 -g 允许生成调试表格。调试表格含有行号和局部变量的有关信息,即 Java 调试工具所使用的信息。缺省情况下,只生成行号。 -J 与 -D 选项联用,它将紧跟其后的选项( -J 与 -D 之间无空格)传给 java 解释器。 -keepgenerated 为 stub 和 skeleton 文件保留所生成的 .java 源文件,并将这些源文件写到与 .class 文件相同的目录中,如果要指定目录,则使用 -d 选项。 -nowarn 关闭警告。如果使用该选项,则编译器不输出任何警告信息。 -show 显示 rmic 编译器的 GUI(图形用户界面)。输入一个或多个包限定类名(以空格分隔),并按回车键或“显示”按钮,创建 stub 和 skeleton。 -vcompat (缺省值)创建与 JDK 1.1 和 1.2 stub 协议版本都兼容的 stub 和 skeleton。 -verbose 使编译器和链接器输出关于正在编译哪些类和正在加载哪些类文件的信息。 -v1.1 创建 JDK 1.1 stub 协议版本的 stub 和 skeleton。 -v1.2 只创建 JDK 1.2 stub 协议版本的 stub。rmid功能说明: rmid 启动激活系统守护进程,以便能够在 Java 虚拟机上注册和激活对象。语法: rmid [-port port] [-log dir]补充说明: rmid 工具启动激活系统守护进程。必须先启动激活系统守护进程,才能向激活系统注册可被激活的对象或在 Java 虚拟机上激活可被激活的对象。命令选项 -C<某些命令行选项> 指定一个选项,在创建每个 rmid 的子守护进程(激活组)时,该选项以命令行参数的形式传给该子守护进程。 -log[目录] 指定目录的名称,激活系统守护进程在该目录中写入其数据库及相关信息。缺省状态下,将在执行 rmid 命令的目录中创建一个 log 目录。 -port[端口] 指定 rmid 的注册服务程序所使用的端口。激活系统守护进程将 ActivationSystem 与该注册服务程序中的名称java.rmi.activation.ActivationSystem 捆绑在一起。 -stop 停止 -port 选项所指定端口上的当前 rmid 调用。若未指定端口,则将停止在端口 1098 上运行的 rmid。rmiregistry功能说明: rmiregistry 命令可在当前主机的指定端口上启动远程对象注册服务程序。语法: rmiregistry [port]补充说明: rmiregistry 命令在当前主机的指定 port 上创建并启动远程对象注册服务程序。如果省略 port,则注册服务程序将在 1099 端口上启动。rmiregistry 命令不产生任何输出而且一般在后台运行。远程对象注册服务程序是自举命名服务。主机上的 RMI 服务器将利用它将远程对象绑定到名字上。客户机即可查询远程对象并进行远程方法调用。注册服务程序一般用于定位应用程序需调用其方法的第一个远程对象。该对象反过来对各应用程序提供相应的支持,用于查找其它对象。
-
-
【功能模块】hadoop apache 移植【操作步骤&问题现象】1、重新编译lz4-1.2.0.jar失败【截图信息】无【日志信息】(可选,上传日志内容或者附件)install-cpptasks:[ivy:cachepath] :: resolving dependencies :: ant-contrib#cpptasks-caller;working[ivy:cachepath] confs: [default][ivy:cachepath] found ant-contrib#cpptasks;1.0b5 in public[ivy:cachepath] found ant#ant;1.6.5 in public[ivy:cachepath] found xerces#xercesImpl;2.8.1 in public[ivy:cachepath] found xml-apis#xml-apis;1.3.03 in public[ivy:cachepath] downloading https://repo1.maven.org/maven2/ant-contrib/cpptasks/1.0b5/cpptasks-1.0b5.jar ...[ivy:cachepath] ........................ (354kB)[ivy:cachepath] .. (0kB)[ivy:cachepath] [SUCCESSFUL ] ant-contrib#cpptasks;1.0b5!cpptasks.jar (751ms)[ivy:cachepath] downloading https://repo1.maven.org/maven2/ant/ant/1.6.5/ant-1.6.5.jar ...[ivy:cachepath] ................................................................... (1009kB)[ivy:cachepath] .. (0kB)[ivy:cachepath] [SUCCESSFUL ] ant#ant;1.6.5!ant.jar (904ms)[ivy:cachepath] downloading https://repo1.maven.org/maven2/xerces/xercesImpl/2.8.1/xercesImpl-2.8.1.jar ...[ivy:cachepath] .............................................................................. (1184kB)[ivy:cachepath] .. (0kB)[ivy:cachepath] [SUCCESSFUL ] xerces#xercesImpl;2.8.1!xercesImpl.jar (939ms)[ivy:cachepath] downloading https://repo1.maven.org/maven2/xml-apis/xml-apis/1.3.03/xml-apis-1.3.03.jar ...[ivy:cachepath] ............. (190kB)[ivy:cachepath] .. (0kB)[ivy:cachepath] [SUCCESSFUL ] xml-apis#xml-apis;1.3.03!xml-apis.jar (709ms)[ivy:cachepath] :: resolution report :: resolve 8935ms :: artifacts dl 3307ms --------------------------------------------------------------------- | | modules || artifacts | | conf | number| search|dwnlded|evicted|| number|dwnlded| --------------------------------------------------------------------- | default | 4 | 4 | 4 | 0 || 4 | 4 | ---------------------------------------------------------------------generate-headers: [mkdir] Created dir: /opt/lz4-java-1.2.0/build/jni-headersBUILD FAILED/opt/lz4-java-1.2.0/build.xml:152: javah does not exist under Java 10 and higher, use the javac task with nativeHeaderDir insteadTotal time: 1 minute 5 seconds
-
使用的是ARM架构的镜像,关于ARM架构安装第三方应用,不知如何安装jdk1.8qwerqverewef 发表于2021-06-07 14:28:47 2021-06-07 14:28:47 最后回复 chuangzhijian@汪汪队 2021-06-07 15:29:19852 1
-
JDK8 中,每个线程对象 Thread 类内部都有一个成员属性 threadLocals(即ThreadLocalMap,它是一个Entry[]数组,而不是 Map 集合哦~),各个线程在调用同一个 ThreadLocal 对象的set(value)方法设置值的时候,就是往各自的 ThreadLocalMap 对象数组中新增值。ThreadLocalMap (Entry[]数组)中存放的是一个个的 Entry节点,它有两个属性字段,虚引用 key(ThreadLocal对象) ,和强引用 value (当前线程变量副本的值)。
-
【功能模块】【操作步骤&问题现象】1、2、【截图信息】【日志信息】(可选,上传日志内容或者附件)
-
官网下载Jmeterhttp://jmeter.apache.org/下载最新版本的 JMeter,解压文件到任意目录 安装JDK,配置Java环境就直接看着篇博客就好啦:https://www.cnblogs.com/poloyy/p/12744072.html注意:应该避免JDK安装路径,Jmeter路径有中文和空格 配置Jmeter环境变量 检查Jmeter配置是否成功cmd敲jmeter或者win+r敲jmeter,能打开jmeter就安装配置成功了 上述只敲jmeter的话,cmd命令窗口会一直存在,很不友好
-
JDK 16 已经发布。与往常一样,新版本 JDK 会带来一系列新功能、功能增强以及 bug 修复。在这个版本中 ZGC 有 46 个功能增强以及 25 个 bug 修复。这里我会介绍一些更有趣的增强功能。摘要通过并行线程栈扫描,ZGC 现在暂停时间是微秒级别,平均暂停时间约为 50 微秒(0.05 毫秒),最大暂停时间约为 500 微秒(0.5 毫秒)。暂停时间不受堆、活动集和根集大小的影响。不再有保留堆区域,ZGC 在需要时进行就地移动。这节约了内存,同时也能保证堆在所有情况下都能成功压缩。转发表现在更有效地进行分配和初始化,这缩短了完成 GC 周期所需的时间,特别是在收集稀疏的大型堆时。 亚毫秒级最大暂停时间(又称并发线程栈处理)当我们开始开发 ZGC 项目时,我们的目标是让 GC 暂停时间永远不超过 10 毫秒。在当时,10 毫秒似乎是一个很有野心的目标。Hotspot 上提供的其他 GC 算法通常会产生比这更糟糕的最大暂停时间,尤其是在使用大堆时。实现这一目标最重要的是并行处理所有繁重的操作,例如移动(relocation)对象、引用处理以及类卸载。那时候 Hotspot 缺乏并行处理它们所需的基础设施,所以需要花费几年的开发时间实现它们。————————————————版权声明:本文为CSDN博主「高可用架构」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/weixin_45583158/article/details/115191620
-
hyper-tuner在openEuler 20.03下用毕昇JDK 11安装问题
上滑加载中
推荐直播
-
全面解析华为云EI-API服务:理论基础与实践应用指南
2024/11/29 周五 18:20-20:20
Alex 华为云学堂技术讲师
本期直播给大家带来的是理论与实践结合的华为云EI-API的服务介绍。从“主要功能,应用场景,实践案例,调用流程”四个维度来深入解析“语音交互API,文字识别API,自然语言处理API,图像识别API及图像搜索API”五大场景下API服务,同时结合实验,来加深开发者对API服务理解。
回顾中 -
企业员工、应届毕业生、在读研究生共探项目实践
2024/12/02 周一 19:00-21:00
姚圣伟 在职软件工程师 昇腾社区优秀开发者 华为云云享专家 HCDG天津地区发起人
大神带你一键了解和掌握LeakyReLU自定义算子在ONNX网络中应用和优化技巧,在线分享如何入门,以及在工作中如何结合实际项目进行学习
即将直播 -
昇腾云服务ModelArts深度解析:理论基础与实践应用指南
2024/12/03 周二 14:30-16:30
Alex 华为云学堂技术讲师
如何快速创建和部署模型,管理全周期AI工作流呢?本期直播聚焦华为昇腾云服务ModelArts一站式AI开发平台功能介绍,同时结合基于ModelArts 的实践性实验,帮助开发者从理论到实验更好地理解和使用ModelArts。
去报名
热门标签