ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pinpoint profiler-optional 模块深度解析:按 JDK 版本与 JVM 供应商隔离 Agent 编译期与运行期差异

Pinpoint profiler-optional 模块深度解析:按 JDK 版本与 JVM 供应商隔离 Agent 编译期与运行期差异 Pinpoint profiler-optional 模块深度解析按 JDK 版本与 JVM 供应商隔离 Agent 编译期与运行期差异【免费下载链接】pinpointAPM, (Application Performance Management) tool for large-scale distributed systems.项目地址: https://gitcode.com/gh_mirrors/pi/pinpointpinpoint-profiler-optional是 Pinpoint Agent 中负责版本差异隔离的一组 Maven 可选模块凡是需要针对特定 JDK 版本编译的特性如 Lambda 字节码转换、关闭钩子注册、内部 API 访问以及需要针对 Oracle/IBM 等不同 JVM 供应商编译的指标采集代码都被下沉到该模块集中管理。本文以 agent-module/profiler-optional/README.md 为主线结合 profiler-optional-jdk8 与 profiler-optional-jdk9 的真实源码与测试讲清该模块的定位、目录结构、构建要求、内部 API 适配策略与 vendor stub 机制帮助读者理解 Pinpoint 主 profiler 如何做到一份代码基多版本 JVM 兼容。一、模块定位为什么需要可选模块APM Agent 与普通应用不同它必须注入到目标 JVM 内部才能完成字节码插桩与运行时监控。而 Java 7、8、9、11、16 之间的内部 API 变化剧烈JDK 8 之前内部工具类位于sun.misc如sun.misc.SharedSecrets、sun.misc.JavaLangAccessJDK 9 引入模块系统后同类 API 迁移到jdk.internal.miscJDK 12 起又迁移到jdk.internal.access不同供应商Oracle、OpenJDK、IBM J9的OperatingSystemMXBean所在包名与可用方法也不一致。如果在主 profiler 中直接引用这些内部类要么在低版本 JDK 上编译失败要么在运行期抛出NoClassDefFoundError。Pinpoint 的解法正如 README 所述Modules underpinpoint-profiler-optionalcontain optional packages for pinpoint-profiler, containing features and codes that must be compiled against specific versions of JDK.即把必须针对特定 JDK 版本编译的代码拆分为独立模块主 profiler 通过反射或接口在运行期按需加载从而把编译期差异与运行期差异彻底隔离开来。二、模块组织三个子模块的分工聚合 POM 声明了三个子模块并对 jdk8/jdk9 两个模块建立依赖关系modules moduleprofiler-optional-jdk8/module moduleprofiler-optional-jdk9/module moduleprofiler-optional-parent/module /modules模块打包方式职责profiler-optional-parentpom公共父 POM以provided作用域依赖pinpoint-profiler并通过maven-jar-plugin只打包com/navercorp/**/*避免 vendor 桩类混入产物profiler-optional-jdk8jarJDK 8含 7/8 时代专属功能Lambda 元工厂转换、关闭钩子注册、CPU/缓冲区/文件描述符指标profiler-optional-jdk9jarJDK 9 专属功能通过JavaLangAccess/SharedSecrets的模块化内部 API 完成类定义与关闭钩子注册关键设计profiler-optional-jdk8 与 jdk9 的类名刻意保持对应关系如Java7ShutdownHookRegister与Java9ShutdownHookRegister实现同一个接口ShutdownHookRegister主 profiler 只面向接口编程运行期再决定加载哪个实现。这一点在 README 中没有直接写明但从 ShutdownHookRegisterProvider 的反射加载逻辑中可以确认// Java7,8 private static final String JDK7_SHUTDOWN_HOOK_REGISTER com.navercorp.pinpoint.profiler.shutdown.Java7ShutdownHookRegister; // Java9,10,11 private static final String JDK9_SHUTDOWN_HOOK_REGISTER com.navercorp.pinpoint.profiler.shutdown.Java9ShutdownHookRegister;getShutdownHookRegiterClassName()按当前 JVM 版本选择类名JAVA_9及以上走 JDK9 实现JAVA_7及以上走 JDK7 实现否则回退到基于Runtime.addShutdownHook的默认实现。类名本身是字符串常量因此主 profiler 编译时不直接依赖optional 模块实现了松耦合。三、profiler-optional-jdk8面向 JDK 7/8 时代的内部 API该模块的源码目录覆盖四大功能域src/main/java/com/navercorp/pinpoint/profiler/ ├── instrument/lambda/ # Lambda 元工厂字节码转换 ├── monitor/metric/ # CPU / 缓冲区 / 文件描述符指标 │ ├── buffer/ cpu/{ibm,oracle}/ filedescriptor/{ibm,oracle}/ └── shutdown/ # Java7ShutdownHookRegister src/main/java-ibm/ # IBM JVM 供应商桩类3.1 Lambda 元工厂转换让 lambda 表达式也能被插桩Lambda 表达式的字节码由java.lang.invoke.InnerClassLambdaMetafactory在运行期生成Pinpoint 需要截获这一生成过程才能对 lambda 内部方法调用完成插桩。optional-jdk8 中的 LambdaFactoryClassAdaptor 承担字节码重写目标类硬编码为java/lang/invoke/InnerClassLambdaMetafactory根据当前 JVM 版本选择不同版本的LambdaClass描述器JAVA_16用LambdaClassJava16JAVA_15用LambdaClassJava15JAVA_9用LambdaClassJava9否则用LambdaClassJava8见getLambdaClass()通过 ASM 的ClassReader/ClassWriter(COMPUTE_FRAMES)读取字节码交由 MethodInstReplacer 完成指令级替换。MethodInstReplacer是一个 ASMClassVisitor它按MethodInsn描述列表目标类、目标方法、委托类、委托方法把命中点的方法调用指令改写为INVOKESTATIC委托调用并统计transformCount供校验。LambdaFactoryClassAdaptor.transform()会校验类名必须是InnerClassLambdaMetafactory若替换次数异常则记录 warn 日志并保留原始字节码属于失败不阻断的防御性设计。这段转换逻辑的入口位于主 profiler 的 InnerClassLambdaMetafactoryTransformer它以ClassFileTransformer身份挂到 JVM 上当类名命中InnerClassLambdaMetafactory时通过agent 自己的 ClassLoader 反射加载LambdaFactoryClassAdaptor并调用loadTransformedBytecode(byte[])。这正是 README 所述optional 包不参与主 profiler 编译、运行期按需加载模式的典型落地。对应的单元测试 LambdaFactoryClassAdaptorTest 直接调用loadTransformedBytecode(bytes)并用ByteCodeDumper.verify()验证改写后字节码可被系统类加载器正常加载说明该转换链路在测试中被显式验证过。3.2 关闭钩子注册抢占更高优先级Agent 需要在 JVM 关闭时尽早执行清理逻辑如上报心跳、刷新缓冲而普通Runtime.addShutdownHook的优先级不可控。JDK 7/8 时代可以通过内部 API 直接注册高优先级关闭钩子。Java7ShutdownHookRegister 的实现要点JavaLangAccess javaLangAccess SharedSecrets.getJavaLangAccess(); for (int i 3; i 10; i) { try { javaLangAccess.registerShutdownHook(i, true, thread); return; } catch (Throwable ignored) { } } Runtime.getRuntime().addShutdownHook(thread);它通过sun.misc.SharedSecrets.getJavaLangAccess()获取内部访问器尝试用关闭阶段号 39 依次注册阶段号越小越先执行全部失败则回退到Runtime.addShutdownHook。类注释明确警告改名必须同步修改ShutdownHookRegisterProvider中的字符串常量体现了两个模块之间以类名字符串为契约的耦合方式。3.3 系统监控指标按 JVM 供应商拆分的双实现CPU 负载与文件描述符指标的获取强依赖供应商实现Oracle/OpenJDK 系DefaultCpuLoadMetric 强转com.sun.management.OperatingSystemMXBean调用getProcessCpuLoad()/getSystemCpuLoad()IBM J9 系DefaultCpuLoadMetric 强转com.ibm.lang.management.OperatingSystemMXBean方法签名相同但包名不同。两份实现均对getCpuUsage()的调用做NoSuchMethodError捕获捕获后降级为CpuUsageProvider.UNSUPPORTED避免因 JDK 小版本差异导致指标采集整体失败。这正是 README 中vendor-specific stub classes compile against的实践场景——com.ibm.lang.management.OperatingSystemMXBean在 Oracle JDK 上根本不存在因此必须以桩类形式单独提供见下文第五节。此外DefaultBufferMetric 通过ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class)采集 direct/mapped 两类缓冲区池的 count 与 memoryUsed池缺失时以EmptyBufferPoolMXBean返回UNCOLLECTED_VALUE兜底。四、profiler-optional-jdk9面向模块化 JDK 的内部 API 适配JDK 9 引入模块系统后JDK 内部包jdk.internal.*默认对无名模块不可见Pinpoint 必须借助SharedSecrets/JavaLangAccess这类官方预留的逃生通道。该模块目录结构src/main/java/com/navercorp/pinpoint/profiler/ ├── instrument/classloading/ # Java9DefineClass JavaLangAccess 家族 └── shutdown/ # Java9ShutdownHookRegister src/main/java9/ # jdk.internal.misc 桩JDK 9~11 src/main/java11/ # jdk.internal.access 桩JDK 124.1 JavaLangAccessHelper跨 JDK 版本的自动探测JavaLangAccessHelper 是该模块最核心的适配逻辑它用两个常量区分内部包位置JDK 9~11jdk.internal.misc.SharedSecrets/jdk.internal.misc.JavaLangAccessJDK 12jdk.internal.access.SharedSecrets/jdk.internal.access.JavaLangAccess。private static JavaLangAccess newJavaLangAccessor() { try { Class.forName(MISC_JAVA_LANG_ACCESS_CLASS_NAME, ...); // 先试 jdk.internal.misc return new JavaLangAccess9(); } catch (ClassNotFoundException ignored) { } try { // Oracle JDK11 : jdk.internal.access, openJDK11 jdk.internal.misc Class.forName(ACCESS_SHARED_SECRETS_CLASS_NAME, ...); // 再试 jdk.internal.access return new JavaLangAccess11(); } catch (ClassNotFoundException ignored) { } dumpJdkInfo(); throw new IllegalStateException(JavaLangAccess not found); }注意源码注释里记录的坑对应 naver/pinpoint issue #6752同一版本 JDK 11Oracle 与 OpenJDK 的内部包名不一致因此必须用运行时探测Class.forName而非版本号判断。返回的JavaLangAccess接口由模块内定义供Java9DefineClass与Java9ShutdownHookRegister共用。4.2 Java9DefineClass绕过模块封装定义类Java9DefineClass 实现主 profiler 的DefineClass接口通过JavaLangAccessHelper.getJavaLangAccess().defineClass(classLoader, name, bytes, null, null)在任意类加载器下注入类定义从而绕开 JDK 9 模块系统对内部包的强封装。它被主 profiler 的 ASMEngine 注入使用——ASMEngine构造器接收DefineClass实例getDefineClass()返回该实例用于动态生成插桩相关类。4.3 Java9ShutdownHookRegister同构移植Java9ShutdownHookRegister 与 JDK8 版本逻辑完全同构阶段号 39 依次尝试失败回退Runtime.addShutdownHook仅把sun.misc.SharedSecrets换成了JavaLangAccessHelper.getJavaLangAccess()。两份实现保持方法签名一致使 ShutdownHookRegisterProvider 可以按版本透明切换。五、Vendor Stub 机制编译期有类可用运行期交给真实 JDKREADME 的另一个核心设计是vendor-specific stub classesThese classes are not included in the final jar packaging, and the vendor-specific implementations that are compiled against these stubs must be loaded with vendor-supplied implementations.具体落地方式从各模块 POM 可确认桩类与主代码分目录存放jdk8 模块把 IBM 桩类放在src/main/java-ibm如com.ibm.lang.management.OperatingSystemMXBean、UnixOperatingSystemMXBeanjdk9 模块把jdk.internal.misc、jdk.internal.access的桩分别放在src/main/java9与src/main/java11构建期合并源码通过build-helper-maven-plugin的add-sourcegoal 在generate-sources阶段把这些目录加入编译源码路径使com.ibm.../jdk.internal...在编译期有类可用打包期剔除桩类父 POM 的maven-jar-plugin仅打包com/navercorp/**/*桩类的com.ibm.*、jdk.internal.*包路径被自然排除在最终 jar 之外运行期替换真实 JVM 自带供应商实现IBM J9 自带com.ibm.lang.management实现JDK 9 自带jdk.internal.*实现运行时由 JVM 自身提供这些类桩类永远不会出现在运行期类路径上。编译细节上jdk8 模块还通过maven-compiler-plugin的-XDignore.symbol.file参数编译——该参数使 javac 直接使用rt.jar而非受限的 ct.sym 符号文件从而允许编译期引用sun.misc等内部类。这正是针对特定 JDK 版本编译的技术前提。六、构建要求多 JDK 环境变量README 明确列出构建pinpoint-profiler-optional及其子模块的前置条件安装 JDK 7并设置环境变量JAVA_7_HOME指向 JDK 7 主目录安装 JDK 8并设置环境变量JAVA_8_HOME指向 JDK 8 主目录。从当前 POM 可以看到这些变量的实际消费方式!-- profiler-optional-jdk8/pom.xml -- properties jdk.version1.8/jdk.version jdk.home${env.JAVA_8_HOME}/jdk.home /properties !-- profiler-optional-jdk9/pom.xml -- properties jdk.version8/jdk.version jdk.home${env.JAVA_8_HOME}/jdk.home /properties值得注意jdk9 模块当前同样用JAVA_8_HOME作为jdk.home、以 JDK 8 编译器构建POM 中JAVA_9_HOME相关配置被注释。其原理是jdk9 模块源码中直接引用jdk.internal.*的地方均由桩类承担因此可以用 JDK 8 编译器完成编译运行时再加载真实 JDK 9 的jdk.internal.*实现。README 中的JAVA_7_HOME则服务于需要生成 Java 7 兼容字节码的部分如 jdk8 模块面向 7/8 时代的内部 API。构建时机注意请确保这些环境变量已在构建 shell 中导出否则 Maven 构建会因无法解析${env.JAVA_8_HOME}而失败。七、测试与验证如何在普通 JVM 上验证模块化内部 API由于普通测试 JVM 无法直接触达jdk.internal.*jdk9 模块的 POM 提供了一个显式开启的多 JDK 验证 profilejdk9-define-testPOM 注释中有完整说明mvn test -pl agent-module/profiler-optional/profiler-optional-jdk9 \ -Pjdk9-define-test -Dtest.jdk.homejdk 9 home该 profile 会向 surefire fork 的 JVM 追加--add-exports java.base/jdk.internal.miscALL-UNNAMED --add-exports java.base/jdk.internal.accessALL-UNNAMED并设置系统属性pinpoint.jdk9.define.testtrue。注释同时解释了兼容性细节OpenJDK 9~11 使用jdk.internal.misc12 使用jdk.internal.access因此两个包都导出对不存在的包仅产生警告该 profile 只能用于 JDK 9 forkJDK 8 JVM 无法带--add-exports启动。对应测试包括Java9DefineClassTest、GuardCodegenJdk9DefineTest、JavaLangAccessHelperTest、Java9ShutdownHookRegisterTest等位于 profiler-optional-jdk9 测试目录。八、小结pinpoint-profiler-optional用一套简洁而严谨的工程手段解决了 APM Agent 领域最棘手的兼容性问题按 JDK 版本拆模块jdk8 / jdk9 两个 jar 分别封装对应时代的内部 API 用法按供应商拆实现Oracle 与 IBM 的监控指标实现各自独立通过相同接口统一vendor stub 编译模式桩类只服务编译期打包剔除、运行期由真实 JVM 提供运行期反射/接口加载主 profiler 以字符串类名或接口契约按版本加载实现编译期零耦合防御性降级内部 API 不可用时回退到公开 API如Runtime.addShutdownHook、UNSUPPORTED指标提供者保证 Agent 不因版本差异而崩溃。理解了这个模块就理解了 Pinpoint Agent跨版本、跨供应商兼容的底层骨架。如需深入某条链路推荐按以下顺序阅读主 profiler 的 ShutdownHookRegisterProvider → optional-jdk8/jdk9 的ShutdownHookRegister实现 → JavaLangAccessHelper → 各桩类目录与对应测试。【免费下载链接】pinpointAPM, (Application Performance Management) tool for large-scale distributed systems.项目地址: https://gitcode.com/gh_mirrors/pi/pinpoint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表