1. 前言
Metaspace OOM 是 Java 应用常见的异常,出现 Metaspace OOM 时,大部分情况是因为反射生成的类占用了 Metaspace 太多空间导致的
以下主要包含四个部分的内容:
- Java 反射导致 Metaspace OOM 的原因及分析方式与工具
- Java 反射导致 Metaspace OOM 的解决方式
- 解决 Java 反射导致 Metaspace OOM 问题后是否会出现其他问题
- 怎样提前合理估算应用需要的 Metaspace 大小
重要结论总结如下:
- Java 反射调用某个非构造函数方法达到一定次数后,会通过“膨胀”机制生成对应的 sun.reflect.GeneratedMethodAccessor… 类,用于加快 Java 反射执行速度
- Java 反射调用构造函数时,存在相同的处理,区别在于生成的类为 sun.reflect.GeneratedConstructorAccessor…
- 以上次数由 JVM 参数 sun.reflect.inflationThreshold 指定,默认值为 15
- 每个 GeneratedMethodAccessor…、GeneratedConstructorAccessor… 类占用 Metaspace 空间约 3.7 KB
- 通过反射并行调用某个方法时,膨胀机制可能对一个方法生成多个类,有可能进一步占用 Metaspace 空间
- 指定 JVM 参数 -Dsun.reflect.inflationThreshold=2147483647(Integer.MAX_VALUE)时,可以关闭反射膨胀生成类机制(要求的次数很大)
- 关闭反射膨胀生成类机制后,可以解决 Metaspace OOM 的问题,对功能没有影响;反射调用方法执行速度与开启时相比基本不变
- 对于反射需要执行的类及方法较多的系统,为了避免 Metaspace OOM 问题,可以关闭反射膨胀生成类机制
2. JDK 不同版本反射膨胀机制生成类名区别
JDK 不同版本反射膨胀机制生成类名存在区别,从 JDK9 某个版本开始,包名发生变化,从“sun.reflect”变成“jdk.internal.reflect”
| JDK8 | JDK9 某个版本开始 | |
|---|---|---|
| 非构造函数方法生成的类名前缀 | sun.reflect.GeneratedMethodAccessor | jdk.internal.reflect.GeneratedMethodAccessor |
| 构造函数生成的类名前缀 | sun.reflect.GeneratedConstructorAccessor | jdk.internal.reflect.GeneratedConstructorAccessor |
| 构造函数(序列化)生成的类名前缀 | sun.reflect.GeneratedSerializationConstructorAccessor | jdk.internal.reflect.GeneratedSerializationConstructorAccessor |
3. 验证项目
可以使用以下 Tomcat 项目验证 Metaspace OOM 问题
https://github.com/Adrninistrator/TomcatMetaspaceOOM
https://gitee.com/Adrninistrator/TomcatMetaspaceOOM
以下验证时使用的上下文路径为 /tmso
3.1. 依赖环境
以下使用 JDK8 HotSpot 64-Bit、Tomcat8 环境进行开发验证
3.2. JVM 参数配置
JVM 参数可在 Tomcat 的 setenv.bat/setenv.sh 脚本中通过“export CATALINA_OPTS=“xxx””方式配置
或使用 IDEA 插件 Smart Tomcat 配置
3.2.1. 应用日志目录
log4j2.xml 中通过 log.home 参数指定应用日志目录,以下为使用 D:/logs 目录的示例:
-Dlog.home=D:/logs
3.2.2. 内存大小
设置足够大的内存大小,避免后续验证过程中因为内存不足执行 GC 影响执行耗时
-Xms2g -Xmx2g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m
3.2.3. 打印类加载与卸载日志
将类被引用、被加载、被卸载的日志记录到日志文件
以下为使用 D:/logs/TomcatMetaspaceOOM/class_load.log 日志文件的示例:
-XX:+UnlockDiagnosticVMOptions -XX:-DisplayVMOutput -XX:+LogVMOutput -XX:+TraceClassLoadingPreorder -XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:LogFile=D:/logs/TomcatMetaspaceOOM/class_load.log
3.2.4. 打印 GC 日志
将 GC 详情与发生时间等信息记录到日志文件
以下为使用 D:/logs/TomcatMetaspaceOOM/gc.log 日志文件的示例:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:D:/logs/TomcatMetaspaceOOM/gc.log
3.2.5. javaagent
项目中提供了用于监控类加载次数的组件 tmso_javaagent.jar,需要以 javaagent 形式加载
以下为 tmso_javaagent.jar 保存在 D:/TomcatMetaspaceOOM/lib/目录时的示例:
-javaagent:D:/TomcatMetaspaceOOM/lib/tmso_javaagent.jar
4. 分析是否因为反射膨胀机制生成类导致 Metaspace 不断增加的方式
通过以下方式,若观察到 Metaspace 在不断增加,且反射膨胀机制生成的类的数量在增加,则说明可能是因为反射膨胀机制生成类导致 Metaspace 不断增加
4.1. 查看 Metaspace 大小的方式
4.1.1. jstat -gc
使用 jstat 命令的 -gc 选项,观察 Metaspace 大小是否增长:
jstat -gc {pid} 1s 0
输出示例如下:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
33792.0 35840.0 0.0 0.0 264704.0 119054.1 157696.0 23648.7 35496.0 34592.3 4352.0 4101.7 9 0.196 3 0.254 0.449
33792.0 35840.0 0.0 0.0 264704.0 119054.2 157696.0 23648.7 35496.0 34592.3 4352.0 4101.7 9 0.196 3 0.254 0.449
MC、MU 列分别为 MC: Metaspace 当前已分配可用大小(Metaspace capacity)、Metaspace 已使用大小(Metacspace utilization),单位是 KB
jstat -gc 可能更新较慢,短时间内 Metaspace 增加较多时,可能需要等待一段时间才能反映当前实际值
4.1.2. Java 代码
通过 java.lang.management.ManagementFactory getMemoryPoolMXBeans 方法获取 Metaspace 大小
示例可参考验证项目 com.test.util.JMXUtil getJMXInfo 类
4.1.3. jconsole
jconsole “内存” 标签页会显示 Metaspace 大小
4.1.4. jvisualvm
jvisualvm “监视” 标签页会显示 Metaspace 大小
4.2. jmap
使用 jmap 命令的 -histo 选项,查看反射生成的 sun.reflect.GeneratedMethodAccessor…、sun.reflect.GeneratedConstructorAccessor…、sun.reflect.GeneratedSerializationConstructorAccessor… 类的数量是否变大:
jmap -histo {pid} | grep -E 'sun.reflect.GeneratedMethodAccessor' | wc -l
jmap -histo {pid} | grep -E 'sun.reflect.GeneratedConstructorAccessor' | wc -l
jmap -histo {pid} | grep -E 'sun.reflect.GeneratedSerializationConstructorAccessor' | wc -l
查看以上类及对应实例信息示例如下:
num #instances #bytes class name
----------------------------------------------
3458: 1 16 sun.reflect.GeneratedConstructorAccessor1
3459: 1 16 sun.reflect.GeneratedConstructorAccessor10
3460: 1 16 sun.reflect.GeneratedConstructorAccessor11
3481: 1 16 sun.reflect.GeneratedMethodAccessor1
3482: 1 16 sun.reflect.GeneratedMethodAccessor10
3483: 1 16 sun.reflect.GeneratedMethodAccessor11
4451: 1 16 sun.reflect.GeneratedSerializationConstructorAccessor1
4452: 1 16 sun.reflect.GeneratedSerializationConstructorAccessor10
4453: 1 16 sun.reflect.GeneratedSerializationConstructorAccessor11
4.3. 类加载日志
分析类加载日志中反射生成的以上类的数量是否变大:
grep 'Loading sun.reflect.GeneratedMethodAccessor' class_load.log | wc -l
grep 'Loading sun.reflect.GeneratedConstructorAccessor' class_load.log | wc -l
grep 'Loading sun.reflect.GeneratedSerializationConstructorAccessor' class_load.log | wc -l
以上类加载日志的示例如下:
[Loading sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedMethodAccessor2 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedMethodAccessor2 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedConstructorAccessor1 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedConstructorAccessor1 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedMethodAccessor3 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedMethodAccessor3 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedMethodAccessor4 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedMethodAccessor4 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedSerializationConstructorAccessor1 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedSerializationConstructorAccessor1 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedSerializationConstructorAccessor10 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedSerializationConstructorAccessor10 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedSerializationConstructorAccessor11 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedSerializationConstructorAccessor11 from __JVM_DefineClass__]
5. 分析反射生成的 Native…AccessorImpl、Delegating…AccessorImpl 对象实例数量
通过分析反射相关源码可知,当 JVM 参数 sun.reflect.noInflation 未设置为 true 时(默认为 false):
- 对于某个非构造函数方法,需要通过反射调用一定次数后才会生成 GeneratedMethodAccessor… 类,反射执行过程中 NativeMethodAccessorImpl、DelegatingMethodAccessorImpl 对象的生成是不可避免的
- 对于某个构造函数,需要通过反射调用一定次数后才会生成 GeneratedConstructorAccessor… 或 GeneratedSerializationConstructorAccessor… 类,反射执行过程中 NativeConstructorAccessorImpl、DelegatingConstructorAccessorImpl 对象的生成是不可避免的
NativeMethodAccessorImpl、DelegatingMethodAccessorImpl、NativeConstructorAccessorImpl、DelegatingConstructorAccessorImpl 对象所对应的类分别只有一个,占用 Metaspace 的大小有限,不会无限增大
NativeMethodAccessorImpl、DelegatingMethodAccessorImpl、NativeConstructorAccessorImpl、DelegatingConstructorAccessorImpl 对象实例数量增加时,占用堆内存的空间也会变大,这是不可避免的
通过以下命令可以查看指定 Java 进程中的 Native…AccessorImpl、Delegating…AccessorImpl 对象实例数量:
jmap -histo {pid} | grep -E 'sun.reflect.NativeMethodAccessorImpl|sun.reflect.DelegatingMethodAccessorImpl|sun.reflect.NativeConstructorAccessorImpl|sun.reflect.DelegatingConstructorAccessorImpl'
输出结果示例如下,可以证明通过反射调用方法后 Native…AccessorImpl、Delegating…AccessorImpl 对象实例数量会增加:
num #instances #bytes class name
----------------------------------------------
243: 485 11640 sun.reflect.NativeConstructorAccessorImpl
274: 559 8944 sun.reflect.DelegatingConstructorAccessorImpl
397: 164 3936 sun.reflect.NativeMethodAccessorImpl
410: 227 3632 sun.reflect.DelegatingMethodAccessorImpl
6. 获取反射膨胀机制生成类的原始方法
6.1. 参考项目
参考以下 GitHub 项目
https://github.com/Adrninistrator/java-reflect-generated-analyzer/
https://gitee.com/Adrninistrator/java-reflect-generated-analyzer/
或参考以下文档
https://blog.youkuaiyun.com/a82514921/article/details/144858359
6.2. 类加载日志与原始方法
根据以上类加载日志 class_load.log 中加载的反射生成的类名,结合以上工具获取的反射膨胀机制生成类的原始方法信息,可以获得对应 Java 应用最近反射生成的类对应的原始方法是哪些
例如类加载日志 class_load.log 显示部分反射生成的类如下:
[Loading sun.reflect.GeneratedConstructorAccessor103 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedConstructorAccessor103 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedMethodAccessor48 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedMethodAccessor48 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedMethodAccessor57 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedMethodAccessor57 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedMethodAccessor63 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedMethodAccessor63 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedSerializationConstructorAccessor11 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedSerializationConstructorAccessor11 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedSerializationConstructorAccessor12 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedSerializationConstructorAccessor12 from __JVM_DefineClass__]
[Loading sun.reflect.GeneratedSerializationConstructorAccessor13 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedSerializationConstructorAccessor13 from __JVM_DefineClass__]
使用 java-reflect-generated-analyzer 获取反射膨胀机制生成类的原始方法,以上类对应的原始方法如下:
103 sun.reflect.GeneratedConstructorAccessor103 org.apache.catalina.core.StandardEngine <init>
48 sun.reflect.GeneratedMethodAccessor48 org.apache.catalina.core.StandardEngine setDefaultHost
57 sun.reflect.GeneratedMethodAccessor57 org.apache.catalina.core.StandardEngine getParentClassLoader
63 sun.reflect.GeneratedMethodAccessor63 org.apache.catalina.core.StandardEngine addChild
11 sun.reflect.GeneratedSerializationConstructorAccessor11 java.lang.Object <init>
12 sun.reflect.GeneratedSerializationConstructorAccessor12 java.lang.Object <init>
13 sun.reflect.GeneratedSerializationConstructorAccessor13 java.lang.Object <init>
6.3. 反射生成的类数量
以以上日志出现的 org.apache.catalina.core.StandardEngine 类为例分析
6.3.1. sun.reflect.noInflation=false
当 JVM 参数 sun.reflect.noInflation=false 时,通过反射调用的方法对应类的构造函数会生成一个 sun.reflect.GeneratedConstructorAccessor… 类,如以上 org.apache.catalina.core.StandardEngine 类的构造函数生成了 GeneratedConstructorAccessor103 类
通过反射调用的方法会生成一个 sun.reflect.GeneratedMethodAccessor… 类,如以上 setDefaultHost、getParentClassLoader、addChild 方法分别生成了 GeneratedMethodAccessor48、GeneratedMethodAccessor57、GeneratedMethodAccessor63 类
sun.reflect.GeneratedSerializationConstructorAccessor… 类生成较少
6.3.2. sun.reflect.noInflation=true
当 JVM 参数 sun.reflect.noInflation=true 时,通过反射调用方法膨胀机制生成类时,除了会生成以上 sun.reflect.noInflation=false 时也会生成的类之外,方法生成的 GeneratedMethodAccessor… 类的构造函数还会再生成一个对应的 GeneratedConstructorAccessor… 类`
如通过 GeneratedConstructorAccessor.txt 可以看到 GeneratedMethodAccessor48、GeneratedMethodAccessor57、GeneratedMethodAccessor63 类的构造函数分别生成了 GeneratedConstructorAccessor104、GeneratedConstructorAccessor117、GeneratedConstructorAccessor125 类
104 sun.reflect.GeneratedConstructorAccessor104 sun.reflect.GeneratedMethodAccessor48 <init>
117 sun.reflect.GeneratedConstructorAccessor117 sun.reflect.GeneratedMethodAccessor57 <init>
125 sun.reflect.GeneratedConstructorAccessor125 sun.reflect.GeneratedMethodAccessor63 <init>
通过以下脚本可以验证,当 JVM 参数 sun.reflect.noInflation=true 时,每个 GeneratedMethodAccessor… 类都会生成一个对应的 GeneratedConstructorAccessor… 类:
cat GeneratedMethodAccessor.txt | awk '{print $2}' | sort > method.txt
cat GeneratedConstructorAccessor.txt | grep 'sun.reflect.GeneratedMethodAccessor' | awk '{print $3}' | sort > method_with_constructor.txt
diff -s method.txt method_with_constructor.txt
6.4. 反射生成的类文件大小
通过以下脚本分别查看反射生成的 GeneratedConstructorAccessor…、GeneratedMethodAccessor…、GeneratedSerializationConstructorAccessor… 类文件的大小
find GeneratedConstructorAccessor -type f -name \*.class | xargs du -sb | head
find GeneratedMethodAccessor -type f -name \*.class | xargs du -sb | head
find GeneratedSerializationConstructorAccessor -type f -name \*.class | xargs du -sb | head
结果如下,可以看到反射生成的类文件的大小比较接近,约 1200 至 1400 字节
1289 GeneratedConstructorAccessor/sun/reflect/GeneratedConstructorAccessor1.class
1288 GeneratedConstructorAccessor/sun/reflect/GeneratedConstructorAccessor10.class
1295 GeneratedConstructorAccessor/sun/reflect/GeneratedConstructorAccessor100.class
1295 GeneratedConstructorAccessor/sun/reflect/GeneratedConstructorAccessor101.class
1295 GeneratedConstructorAccessor/sun/reflect/GeneratedConstructorAccessor102.class
1295 GeneratedConstructorAccessor/sun/reflect/GeneratedConstructorAccessor104.class
1295 GeneratedConstructorAccessor/sun/reflect/GeneratedConstructorAccessor105.class
1295 GeneratedConstructorAccessor/sun/reflect/GeneratedConstructorAccessor109.class
1294 GeneratedConstructorAccessor/sun/reflect/GeneratedConstructorAccessor11.class
1295 GeneratedConstructorAccessor/sun/reflect/GeneratedConstructorAccessor110.class
1411 GeneratedMethodAccessor/sun/reflect/GeneratedMethodAccessor10.class
1372 GeneratedMethodAccessor/sun/reflect/GeneratedMethodAccessor100.class
1372 GeneratedMethodAccessor/sun/reflect/GeneratedMethodAccessor101.class
1434 GeneratedMethodAccessor/sun/reflect/GeneratedMethodAccessor102.class
1377 GeneratedMethodAccessor/sun/reflect/GeneratedMethodAccessor103.class
1335 GeneratedMethodAccessor/sun/reflect/GeneratedMethodAccessor104.class
1366 GeneratedMethodAccessor/sun/reflect/GeneratedMethodAccessor105.class
1373 GeneratedMethodAccessor/sun/reflect/GeneratedMethodAccessor106.class
1371 GeneratedMethodAccessor/sun/reflect/GeneratedMethodAccessor107.class
1365 GeneratedMethodAccessor/sun/reflect/GeneratedMethodAccessor108.class
1323 GeneratedSerializationConstructorAccessor/sun/reflect/GeneratedSerializationConstructorAccessor14.class
1322 GeneratedSerializationConstructorAccessor/sun/reflect/GeneratedSerializationConstructorAccessor15.class
1324 GeneratedSerializationConstructorAccessor/sun/reflect/GeneratedSerializationConstructorAccessor16.class
1321 GeneratedSerializationConstructorAccessor/sun/reflect/GeneratedSerializationConstructorAccessor17.class
7. 反射调用方法源码分析
见 https://blog.youkuaiyun.com/a82514921/article/details/144858578
8. 使用反射的常见场景
很多常见的场景会使用反射,特别是涉及数据传递且类型不固定时
8.1. Spring MVC Controller 方法调用
Spring MVC Controller 方法调用对应 Java 应用处理前端请求
通过反射调用 Controller 的对应方法
8.2. JSON 序列化/反序列化
JSON 序列化/反序列化对应不同应用间通过 MQ 或 RPC 等方式进行数据传递
例如使用 jackson、fastjson 等 JSON 组件进行 JSON 序列化/反序列化
进行 JSON 序列化时,通过反射调用对象的 get 方法
进行 JSON 反序列化时,通过反射调用对象的 set 方法
8.3. Bean 拷贝
Bean 拷贝对应应用内不同类之间的数据传递
例如使用 Spring、Apache BeanUtil 等 Bean 拷贝方法
读取 Bean 的字段时,通过反射调用对象的 get 方法
对 Bean 的字段赋值时,通过反射调用对象的 set 方法
8.4. ORM 框架操作
ORM 框架操作对应应用与数据库之间的数据传递
例如使用 MyBatis 进行数据库操作
插入数据库时,通过反射调用插入数据的 get 方法
查询数据库时,通过反射调用返回数据的 set 方法
9. 分析使用反射执行方法的调用堆栈与原始方法
可以使用 Arthas 分析使用反射执行方法的调用堆栈与原始方法
需要先打开 Arthas,attach 到需要分析的 Java 进程,在 Arthas 的命令行中执行以下命令
9.1. 支持对 JDK 中的方法执行 stack、watch 等命令
执行以下命令,以支持对 JDK 中的方法执行 stack、watch 等命令
options unsafe true
执行成功后输出:
NAME BEFORE-VALUE AFTER-VALUE
-----------------------------------
unsafe false true
若不执行该命令,以下 stack、watch 等命令会执行失败,出现以下报错
Affect(class count: 0 , method count: 0) cost in 61 ms, listenerId: 1
No class or method is affected, try:
1. Execute `sm CLASS_NAME METHOD_NAME` to make sure the method you are tracing actually exists (it might be in your parent class).
2. Execute `options unsafe true`, if you want to enhance the classes under the `java.*` package.
3. Execute `reset CLASS_NAME` and try again, your method body might be too large.
4. Match the constructor, use `<init>`, for example: `watch demo.MathGame <init>`
5. Check arthas log: C:\Users\username\logs\arthas\arthas.log
6. Visit https://github.com/alibaba/arthas/issues/47 for more details.
9.2. 分析使用反射执行非构造函数方法
9.2.1. 监控使用反射执行非构造函数方法的调用堆栈
监控 sun.reflect.ReflectionFactory newMethodAccessor 方法被调用时的调用堆栈,当使用反射执行非构造函数方法时会调用该方法
stack sun.reflect.ReflectionFactory newMethodAccessor
Java 通过反射多次调用同一个方法时,(不出现并发生成类的情况下)只会调用一次 sun.reflect.ReflectionFactory newMethodAccessor 方法
- 监控结果示例
以下为监控 jackson 进行 JSON 序列化/反序列化通过反射调用非构造函数方法时的调用堆栈
@sun.reflect.ReflectionFactory.newMethodAccessor()
at java.lang.reflect.Method.acquireMethodAccessor(Method.java:564)
at java.lang.reflect.Method.invoke(Method.java:496)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:689)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
at com.fasterxml.jackson.databind.ObjectMapper._writeValueAndClose(ObjectMapper.java:4624)
at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:3869)
@sun.reflect.ReflectionFactory.newMethodAccessor()
at java.lang.reflect.Method.acquireMethodAccessor(Method.java:564)
at java.lang.reflect.Method.invoke(Method.java:496)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:141)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:314)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4730)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3677)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3645)
9.2.2. 监控使用反射执行的非构造函数方法
监控以上方法被调用时的参数 1,对应通过反射调用的非构造函数方法
watch sun.reflect.ReflectionFactory newMethodAccessor "{params[0]}"
- 监控结果示例
以下为监控到的进行 JSON 序列化/反序列化时,通过反射调用 DTO 的非构造函数方法
method=sun.reflect.ReflectionFactory.newMethodAccessor location=AtExit
ts=2024-12-24 21:20:39.338; [cost=0.012601ms] result=@ArrayList[
@Method[public java.lang.String com.test.dto.TestJsonData1.getData1()],
]
method=sun.reflect.ReflectionFactory.newMethodAccessor location=AtExit
ts=2024-12-24 21:20:39.340; [cost=0.009301ms] result=@ArrayList[
@Method[public void com.test.dto.TestJsonData2.setData1(java.lang.String)],
]
9.3. 分析使用反射执行构造函数
9.3.1. 监控使用反射执行构造函数的调用堆栈
监控 sun.reflect.ReflectionFactory newConstructorAccessor 方法被调用时的调用堆栈,当使用反射执行构造函数时会调用该方法
stack sun.reflect.ReflectionFactory newConstructorAccessor
- 监控结果示例
以下为监控 jackson 进行 JSON 反序列化时,通过反射调用构造函数时的调用堆栈
@sun.reflect.ReflectionFactory.newConstructorAccessor()
at java.lang.reflect.Constructor.acquireConstructorAccessor(Constructor.java:460)
at java.lang.reflect.Constructor.newInstance(Constructor.java:420)
at com.fasterxml.jackson.databind.introspect.AnnotatedConstructor.call(AnnotatedConstructor.java:123)
at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createUsingDefault(StdValueInstantiator.java:278)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:303)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4730)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3677)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3645)
9.3.2. 监控使用反射执行的构造函数
监控以上方法被调用时的参数 1,对应通过反射调用的构造函数
watch sun.reflect.ReflectionFactory newConstructorAccessor "{params[0]}"
- 监控结果示例
以下为监控到的进行 JSON 反序列化时,通过反射调用 DTO 的构造函数
method=sun.reflect.ReflectionFactory.newConstructorAccessor location=AtExit
ts=2024-12-28 22:35:41.893; [cost=0.0555ms] result=@ArrayList[
@Constructor[public com.jar.test.dto.TestData000001()],
]
9.4. sun.reflect.noInflation=true 时 watch 后第一次 HTTP 请求失败
若设置 JVM 参数 sun.reflect.noInflation=true,则第一次执行以上 watch 命令后,访问验证项目的 HTTP 请求时一直没有返回
查看应用日志,出现以下报错:
Caused by: java.lang.NoClassDefFoundError: Could not initialize class com.alibaba.arthas.deps.ch.qos.logback.classic.spi.ThrowableProxy
at com.alibaba.arthas.deps.ch.qos.logback.classic.spi.LoggingEvent.<init>(LoggingEvent.java:119) ~[?:?]
at com.alibaba.arthas.deps.ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:419) ~[?:?]
at com.alibaba.arthas.deps.ch.qos.logback.classic.Logger.filterAndLog_0_Or3Plus(Logger.java:383) ~[?:?]
at com.alibaba.arthas.deps.ch.qos.logback.classic.Logger.error(Logger.java:534) ~[?:?]
at com.taobao.arthas.core.advisor.SpyImpl.atExit(SpyImpl.java:69) ~[?:?]
at java.arthas.SpyAPI.atExit(SpyAPI.java:64) ~[?:4.0.4]
at sun.reflect.ReflectionFactory.newMethodAccessor(ReflectionFactory.java:176) ~[?:1.8.0_144]
at java.lang.reflect.Method.acquireMethodAccessor(Method.java:564) ~[?:1.8.0_144]
at java.lang.reflect.Method.invoke(Method.java:496) ~[?:1.8.0_144]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.39.jar:5.3.39]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.39.jar:5.3.39]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.39.jar:5.3.39]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:903) ~[spring-webmvc-5.3.39.jar:5.3.39]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:809) ~[spring-webmvc-5.3.39.jar:5.3.39]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.39.jar:5.3.39]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1072) ~[spring-webmvc-5.3.39.jar:5.3.39]
... 35 more
“arthas watch 命令导致了方法执行报错 #2743”https://github.com/alibaba/arthas/issues/2743 该 issues 中有提到类似的问题
此时可以在 Arthas 命令行中通过 CTRL+C 结束当前执行的 watch 命令,再次执行相同的 watch 命令,之后 watch 命令能够正常监控到方法调用的参数
10. 分析反射膨胀机制生成类占用 Metaspace 大小
10.1. 反射膨胀机制生成类的大小
通过对 Java 反射相关源码分析,以及将反射膨胀机制生成类 dump 出来后进行比较文件大小,可知反射膨胀机制为不同的方法生成类时,生成的类大小应该很接近,每个类占用 Metaspace 大小也应该接近
因此在验证项目中分析的测试方法通过反射机制生成的类占用的 Metaspace 大小,能够代表任意方法通过反射机制生成的类占用的 Metaspace 大小
10.2. JVM 参数修改
在启动验证项目前,修改以下 JVM 参数
指定 -Dsun.reflect.inflationThreshold=1 ,使对某个方法通过反射调用第 2 次时通过膨胀机制生成对应的类
10.3. 验证项目功能说明
10.3.1. 提前加载公共类
在验证项目中,在初始化以后会调用一些当前应用的 HTTP 请求,提前加载后续需要使用的公共类(如 Spring MVC、Jackson 等相关的类),使验证过程中统计的某操作引起的 Metaspace 大小增长尽量准确
10.3.2. 记录类加载次数
在验证项目中,会通过 javaagent 记录类加载的次数,包括用于进行 JSON 反序列化的类、反射膨胀机制生成的类
10.3.3. /test/serial 服务
在该服务中,会从指定序号(startSeq)开始对指定数量的类(classNum)使用 jackson 进行 JSON 反序列化,执行指定的次数(runTimes),使指定数量的字段(fieldNum)通过 JSON 反序列化进行赋值,以使对应的 set 方法通过反射调用
处理的类名前缀为 com.jar.test.dto.TestData
以上服务用于验证通过 JSON 反序列化通过反射调用 DTO 的字段的 set 方法
10.4. 验证步骤
10.4.1. 加载通过反射调用方法的类
http://127.0.0.1:8080/tmso/test/serial?startSeq=1&classNum=1000&runTimes=1&fieldNum=0
访问以上 URL,从 com.jar.test.dto.TestData000001 类开始,到 com.jar.test.dto.TestData001000 类,对这 1000 个类进行 JSON 反序列化,执行次数为 1,不使字段通过 JSON 反序列化进行赋值,避免 set 方法通过反射调用
本次访问以上 URL 的目的有两个:
- 一是使得用于进行 JSON 反序列化的 com.jar.test.dto.TestData000001 到 com.jar.test.dto.TestData001000 类都被加载到 Metaspace 中,之后不需要再加载,便于计算反射膨胀机制生成类占用 Metaspace 的大小
- 二是使得用于进行 JSON 反序列化的 com.jar.test.dto.TestData000001 到 com.jar.test.dto.TestData001000 类的构造函数都通过反射调用一次,下次再访问时会通过反射膨胀机制生成对应的 GeneratedConstructorAccessor… 类
本次访问以上 URL 后,日志示例如下:
runJsonDeserialize runTimes: 1 classNum: 1000 concurrent: false dyn: false fieldNum: 0 spendTime: 13746
metaspaceUsedKBBefore: 42486.9609 metaspaceUsedKBAfter: 66521.7500 metaspaceUsedKBAdd: 24034.8
ygcTimesBefore: 2 ygcTimesAfter: 3 ygcTimesAdd: 1
com.jar.test.dto.TestData* loadTimesBefore: 0 loadTimesAfter: 1000 loadTimesAdd: 1000
可以看到类名以 com.jar.test.dto.TestData 为前缀的类的加载次数增加了 1000,没有加载以 sun.reflect.GeneratedConstructorAccessor… 为前缀的类
10.4.2. 使反射膨胀机制为被调用方法的类的构造函数生成 GeneratedConstructorAccessor… 类
http://127.0.0.1:8080/tmso/test/serial?startSeq=1&classNum=1000&runTimes=1&fieldNum=0
访问以上 URL,与上一次访问的 URL 相同
本次访问以上 URL 的目的,是使得用于进行 JSON 反序列化的 com.jar.test.dto.TestData000001 到 com.jar.test.dto.TestData001000 通过反射膨胀机制为构造函数生成 GeneratedConstructorAccessor… 类,以计算其占用 Metaspace 的大小
本次访问以上 URL 后,日志示例如下:
runJsonDeserialize runTimes: 1 classNum: 1000 concurrent: false dyn: false fieldNum: 0 spendTime: 425
metaspaceUsedKBBefore: 66564.0859 metaspaceUsedKBAfter: 70305.7500 metaspaceUsedKBAdd: 3741.7
sun.reflect.GeneratedConstructorAccessor* loadTimesBefore: 114 loadTimesAfter: 1114 loadTimesAdd: 1000
可以看到加载了 1000 个类名以 sun.reflect.GeneratedConstructorAccessor… 为前缀的类,Metaspace 大小占用增加了 3741.7 KB
10.4.3. 使方法通过反射调用一次
http://127.0.0.1:8080/tmso/test/serial?startSeq=1&classNum=1000&runTimes=1&fieldNum=1
访问以上 URL,从 com.jar.test.dto.TestData000001 类开始,到 com.jar.test.dto.TestData001000 类,对这 1000 个类进行 JSON 反序列化,执行次数为 1,使 1 个字段的 set 方法通过反射调用
本次访问以上 URL 的目的,是使得用于进行 JSON 反序列化的 com.jar.test.dto.TestData000001 到 com.jar.test.dto.TestData001000 类的第一个字段的 set 方法通过反射调用;下次再访问时会通过反射膨胀机制生成对应的 GeneratedMethodAccessor… 类
本次访问以上 URL 后,日志示例如下:
runJsonDeserialize runTimes: 1 classNum: 1000 concurrent: false dyn: false fieldNum: 1 spendTime: 52
metaspaceUsedKBBefore: 70319.4844 metaspaceUsedKBAfter: 70408.0391 metaspaceUsedKBAdd: 88.6
可以看到没有加载以 sun.reflect.GeneratedMethodAccessor… 为前缀的类
10.4.4. 使反射膨胀机制为被调用方法生成 GeneratedMethodAccessor… 类
http://127.0.0.1:8080/tmso/test/serial?startSeq=1&classNum=1000&runTimes=1&fieldNum=1
访问以上 URL,与上一次访问的 URL 相同
本次访问以上 URL 的目的,是使得用于进行 JSON 反序列化的 com.jar.test.dto.TestData000001 到 com.jar.test.dto.TestData001000 的第 1 个字段的 set 方法通过反射膨胀机制为其生成 GeneratedMethodAccessor… 类,计算其占用 Metaspace 的大小
本次访问以上 URL 后,日志示例如下:
runJsonDeserialize runTimes: 1 classNum: 1000 concurrent: false dyn: false fieldNum: 1 spendTime: 445
metaspaceUsedKBBefore: 70412.1250 metaspaceUsedKBAfter: 74184.1094 metaspaceUsedKBAdd: 3772.0
sun.reflect.GeneratedMethodAccessor* loadTimesBefore: 101 loadTimesAfter: 1101 loadTimesAdd: 1000
可以看到加载了 1000 个类名以 sun.reflect.GeneratedMethodAccessor… 为前缀的类,Metaspace 大小占用增加了 3772.0 KB
10.5. 不同 JDK 版本反射膨胀机制生成类占用 Metaspace 大小
使用以下版本的 JDK 进行了验证,GeneratedConstructorAccessor… 类平均大小为 3.74~3.84 KB,GeneratedMethodAccessor… 类平均大小为 3.76~3.88 KB
| 操作系统版本 | JDK 版本 | 生成 GeneratedConstructorAccessor… 类平均大小 (KB) | 生成 GeneratedMethodAccessor… 类平均大小 (KB) |
|---|---|---|---|
| Windows 10 | 1.8.0_144 HotSpot™ 64-Bit | 3.74 | 3.77 |
| Linux CentOS 7.9 | 1.8.0_192 HotSpot™ 64-Bit | 3.77 | 3.77 |
| Linux CentOS 7.9 | 1.8.0_382 OpenJDK 64-Bit | 3.76 | 3.76 |
| Linux CentOS 7.9 | 11.0.18 OpenJDK 64-Bit | 3.84 | 3.88 |
10.6. 反射膨胀机制生成类对 Metaspace 大小的影响
反射膨胀机制会为被调用方法的类的构造函数生成一个 GeneratedConstructorAccessor… 类,为被调用方法生成一个 GeneratedMethodAccessor… 类
对于 JSON 序列化/反序列化、ORM 框架操作等场景,被使用的 DTO 类的 get/set 方法都会通过反射调用
在以上场景中,每个 DTO 类会生成 1 个 GeneratedConstructorAccessor… 类,每个字段会生成 2 个 GeneratedMethodAccessor… 类
假如应用中共有 500 个 DTO 类,每个类有 40 个字段,则总共会生成的 GeneratedConstructorAccessor...、GeneratedMethodAccessor... 类占用 Metaspace 大小至少为 (500 * 3.7 + 500 * 40 * 2 * 3.7) KB = 146 MB
可以看到,当应用中的需要通过反射调用的方法(字段)数量很多时,反射膨胀机制生成的类占用 Metaspace 是很大的
以上例子中的 164 MB 对于 Metaspace 已经是比较大的值,约为 512 MB 的 28.6%
11. 分析反射膨胀机制对同一个方法重复生成类的情况
分析反射相关源码可知,反射膨胀机制在为方法生成类时,未进行并发控制,有可能对同一个方法生成多个类
11.1. JVM 参数修改
同上
11.2. 验证项目 /test/concurrent 服务
与 /test/serial 服务功能类似,区别在于 /test/serial 服务是串行执行,当前服务是并行执行
runTimes 代表对某个 DTO 类并行执行 JSON 序列化(即反射调用)的并发数
11.3. 验证步骤
11.3.1. 加载通过反射调用方法的类并使方法通过反射调用一次
http://127.0.0.1:8080/tmso/test/concurrent?startSeq=1&classNum=1000&runTimes=1&fieldNum=1
访问以上 URL,说明及目的与上文访问当前 URL 的相同
本次访问以上 URL 后,日志示例如下:
runJsonDeserialize runTimes: 1 classNum: 1000 concurrent: true dyn: false fieldNum: 1 spendTime: 11110
metaspaceUsedKBBefore: 42326.7422 metaspaceUsedKBAfter: 66447.7188 metaspaceUsedKBAdd: 24121.0
ygcTimesBefore: 2 ygcTimesAfter: 3 ygcTimesAdd: 1
com.jar.test.dto.TestData* loadTimesBefore: 0 loadTimesAfter: 1000 loadTimesAdd: 1000
11.3.2. 触发通过反射并发调用方法
http://127.0.0.1:8080/tmso/test/concurrent?startSeq=1&classNum=1000&runTimes={n}&fieldNum=1
访问以上 URL,从 com.jar.test.dto.TestData000001 类开始,到 com.jar.test.dto.TestData001000 类,对这 1000 个类进行 JSON 反序列化,使 1 个字段的 set 方法通过反射调用,对每个类进行 JSON 反序列化,对方法通过反射调用的并发数为参数 n
11.3.2.1. 对方法通过反射调用的并发数为 2
http://127.0.0.1:8080/tmso/test/concurrent?startSeq=1&classNum=1000&runTimes=2&fieldNum=1
对方法通过反射调用的并发数为 2 时,执行日志如下:
runJsonDeserialize runTimes: 2 classNum: 1000 concurrent: true dyn: false fieldNum: 1 spendTime: 888
metaspaceUsedKBBefore: 66469.1719 metaspaceUsedKBAfter: 78824.3828 metaspaceUsedKBAdd: 12355.2
sun.reflect.GeneratedMethodAccessor* loadTimesBefore: 101 loadTimesAfter: 1613 loadTimesAdd: 1512
sun.reflect.GeneratedConstructorAccessor* loadTimesBefore: 115 loadTimesAfter: 1897 loadTimesAdd: 1782
可以看到,反射膨胀机制生成的类为同一个方法生成了多个类
1000 个 DTO 类的构造函数生成的 GeneratedConstructorAccessor… 类数量为 1782
1000 个 set 方法生成的 GeneratedMethodAccessor… 类数量为 1512
11.3.2.2. 对方法通过反射调用的并发数为 20
http://127.0.0.1:8080/tmso/test/concurrent?startSeq=1&classNum=1000&runTimes=20&fieldNum=1
将对方法通过反射调用的并发数提高到 20,执行日志如下:
runJsonDeserialize runTimes: 20 classNum: 1000 concurrent: true dyn: false fieldNum: 1 spendTime: 2620
metaspaceUsedKBBefore: 66470.7266 metaspaceUsedKBAfter: 115580.1250 metaspaceUsedKBAdd: 49109.4
sun.reflect.GeneratedMethodAccessor* loadTimesBefore: 101 loadTimesAfter: 6197 loadTimesAdd: 6096
sun.reflect.GeneratedConstructorAccessor* loadTimesBefore: 115 loadTimesAfter: 7129 loadTimesAdd: 7014
反射膨胀机制生成的类为同一个方法生成多个类的情况明显增加:
1000 个 DTO 类的构造函数生成的 GeneratedConstructorAccessor… 类数量为 7014
1000 个 set 方法生成的 GeneratedMethodAccessor… 类数量为 6096
- 反射膨胀机制生成类的原始方法
通过 java-reflect-generated-analyzer 查看反射膨胀机制生成类的原始方法
GeneratedConstructorAccessor.txt 部分内容如下,可以看到有很多 DTO 类的构造函数生成类的数量为 20 个
20 com.jar.test.dto.TestData000991@<init>
20 com.jar.test.dto.TestData000978@<init>
20 com.jar.test.dto.TestData000976@<init>
20 com.jar.test.dto.TestData000956@<init>
20 com.jar.test.dto.TestData000895@<init>
20 com.jar.test.dto.TestData000889@<init>
20 com.jar.test.dto.TestData000884@<init>
20 com.jar.test.dto.TestData000876@<init>
20 com.jar.test.dto.TestData000862@<init>
20 com.jar.test.dto.TestData000826@<init>
20 com.jar.test.dto.TestData000823@<init>
20 com.jar.test.dto.TestData000743@<init>
20 com.jar.test.dto.TestData000707@<init>
20 com.jar.test.dto.TestData000592@<init>
20 com.jar.test.dto.TestData000538@<init>
GeneratedMethodAccessor.txt 部分内容如下,可以看到有部分 set 方法生成的类的数量接近或达到 20 个
20 com.jar.test.dto.TestData000202@setData0001
19 com.jar.test.dto.TestData000900@setData0001
19 com.jar.test.dto.TestData000884@setData0001
19 com.jar.test.dto.TestData000471@setData0001
19 com.jar.test.dto.TestData000432@setData0001
19 com.jar.test.dto.TestData000395@setData0001
19 com.jar.test.dto.TestData000154@setData0001
18 com.jar.test.dto.TestData000984@setData0001
18 com.jar.test.dto.TestData000896@setData0001
18 com.jar.test.dto.TestData000850@setData0001
18 com.jar.test.dto.TestData000801@setData0001
18 com.jar.test.dto.TestData000693@setData0001
18 com.jar.test.dto.TestData000105@setData0001
17 com.jar.test.dto.TestData000994@setData0001
17 com.jar.test.dto.TestData000871@setData0001
11.4. 反射膨胀机制对同一个方法重复生成类的情况总结
反射膨胀机制对同一个方法重复生成类的情况,出现在为某个类的构造函数生成 GeneratedConstructorAccessor… 类,或为非构造函数方法生成 GeneratedMethodAccessor… 类的阶段,当方法对应的类生成完毕以后,后续再通过反射调用该方法时,会使用已经生成好的类,不会再继续生成类,即不会无限为同一个方法重复生成类
因此,反射膨胀机制对同一个方法重复生成类的情况,一定程度上可能导致占用 Metaspace 空间进一步增大,具体比例与 Java 应用处理请求的并发有关
12. 反射膨胀机制生成类导致 Metaspace OOM 的解决方法
为了避免反射膨胀机制生成类占用 Metaspace,导致 OOM 等问题,可通过以下方式关闭反射膨胀生成类机制解决:
- 保证 JVM 参数中未配置 -Dsun.reflect.noInflation=true,不配置 sun.reflect.noInflation 即可
- 在 JVM 参数中指定 -Dsun.reflect.inflationThreshold=2147483647(某个方法需要通过反射调用 21 亿次才会生成类)
12.1. 关闭反射膨胀生成类机制对功能的影响
默认情况下,通过反射调用某个方法的前 15 次都不会生成类,到第 16 次会生成类,从第 16 次开始会使用生成的类
若关闭反射膨胀生成类机制,相当于每次的操作都和未关闭时的前 15 次相同,执行的代码是原本已经执行过很多次的,不会对功能产生影响
13. 打开/关闭反射膨胀生成类机制的执行速度变化
13.1. 验证项目 /test/reflect 服务
在该服务中,会从指定序号(startSeq)开始对指定数量的类(classNum)的一个方法通过反射调用,每个方法执行指定次数(runTimes)
使用的类名前缀为 com.jar.test.reflect.TestReflect,通过反射调用的方法名为 method4Reflect,该方法中固定返回 int 1
以上方法是静态方法,因此通过反射调用时,只会为方法生成 GeneratedMethodAccessor… 类,不会为类的构造函数生成 GeneratedConstructorAccessor… 类
13.2. 访问的 URL
http://127.0.0.1:8080/tmso/test/reflect?startSeq=1&classNum=1000&runTimes=1
后续每次操作都访问以上 URL,每次访问时,从 com.jar.test.reflect.TestReflect000001 类开始,到 com.jar.test.reflect.TestReflect001000 类,分别通过反射调用一次以上方法
13.3. 打开反射膨胀生成类机制后的执行速度
13.3.1. JVM 参数修改
在启动验证项目前,修改以下 JVM 参数
指定 -Dsun.reflect.inflationThreshold=1 ,使对某个方法通过反射调用第 2 次时通过膨胀机制生成对应的类
13.3.2. 通过反射调用一次方法
本次访问以上 URL 的目的,一是使对应的类被加载,二是使方法通过反射调用一次,下一次调用时能够触发反射膨胀机制生成类,日志示例如下:
runReflect runTimes: 1 classNum: 1000 spendTime: 37
metaspaceUsedKBBefore: 42311.6953 metaspaceUsedKBAfter: 42427.4844 metaspaceUsedKBAdd: 115.8
第一次通过反射调用某方法时,需要生成 NativeMethodAccessorImpl、DelegatingMethodAccessorImpl 类对象,耗时会相对长一点
13.3.3. 使反射膨胀机制生成类
本次访问以上 URL 的目的,是触发反射膨胀机制生成类,日志示例如下:
runReflect runTimes: 1 classNum: 1000 spendTime: 340
metaspaceUsedKBBefore: 42449.1094 metaspaceUsedKBAfter: 46239.0000 metaspaceUsedKBAdd: 3789.9
sun.reflect.GeneratedMethodAccessor* loadTimesBefore: 102 loadTimesAfter: 1102 loadTimesAdd: 1000
由于需要通过反射膨胀机制生成对应的 GeneratedMethodAccessor… 类,因此耗时会明显长一些
13.3.4. 继续通过反射执行方法
通过以下脚本,先清空应用日志,再访问以上 URL 100 次:
> TomcatMetaspaceOOM.log
for i in $(seq 1 100); do
echo $i
curl "http://127.0.0.1:8080/tmso/test/reflect?startSeq=1&classNum=1000&runTimes=1"
done
通过以下脚本,统计应用日志中记录的总耗时:
grep 'runReflect runTimes' TomcatMetaspaceOOM.log | awk -F 'spendTime: ' '{sum+=$2} END {print sum}'
通过日志查看,每次请求执行 1000 次反射调用平均耗时为 10 毫秒,比前两次请求的耗时短
13.4. 关闭反射膨胀生成类机制后的执行速度
13.4.1. JVM 参数修改
在启动验证项目前,修改以下 JVM 参数
指定 -Dsun.reflect.inflationThreshold=2147483647,关闭反射膨胀生成类机制
13.4.2. 通过反射调用一次方法
访问一次以上 URL,说明与目的同上
13.4.3. 继续通过反射执行方法
使用以上相同的脚本操作
13.5. 打开/关闭反射膨胀生成类机制的执行速度变化总结
以下为对 1000 个方法通过反射分别调用 100 次的耗时,在每个环境总共通过反射调用方法十万次
提前通过反射调用了两次方法:
- 开启反射膨胀生成类机制时,使反射膨胀机制提前生成类,避免前面耗时的操作被统计进来
- 关闭反射膨胀生成类机制,使 NativeMethodAccessorImpl、DelegatingMethodAccessorImpl 类提前生成,原因同上
使用不同 JDK 版本的执行情况如下:
| 操作系统版本 | JDK 版本 | 开启反射膨胀生成类机制执行耗时(毫秒) | 关闭反射膨胀生成类机制执行耗时(毫秒) | 关闭/开启反射膨胀生成类机制执行耗时百分比 |
|---|---|---|---|---|
| Windows 10 | 1.8.0_144 HotSpot™ 64-Bit | 1176 | 1063 | 90.39% |
| Linux CentOS 7.9 | 1.8.0_192 HotSpot™ 64-Bit | 390 | 391 | 100.26% |
| Linux CentOS 7.9 | 1.8.0_382 OpenJDK 64-Bit | 347 | 386 | 111.24% |
| Linux CentOS 7.9 | 11.0.18 OpenJDK 64-Bit | 359 | 349 | 97.21% |
由于代码执行速度与服务器实时负载有关,因此以上数据仅供参考。但可以看到,关闭与开启反射膨胀生成类机制时,执行耗时比较接近
14. 分析将指定包下的类加载后占用 Metaspace 大小的方式
14.1. 验证项目 /test/load_packages_classes 服务
验证项目 /test/load_packages_classes 服务用于加载指定包下的类,分析这些类占用 Metaspace 的大小
URL 示例如下:
http://127.0.0.1:8080/tmso/test/load_packages_classes?otherLibPath={otherLibPath}&packages={packages}
- otherLibPath 参数
该参数用于指定保存需要加载的组件(.jar/.war 文件)所在的目录路径,仅处理目录中的组件,不处理子目录中的组件
该参数可为空,为空时加载验证项目的 WEB-INF/classes、WEB-INF/lib 目录中的类
参数示例:
/tmp
D:/tmp
- packages 参数
该参数用于指定需要加载的类所在的包名
若需要指定多个包名,使用半角逗号分隔
参数示例:
a.b
a.b,a.c
14.2. 输出示例
如下所示,代表加载指定包下的全部类后,Metaspace 大小增加了 795.6 KB
load classes in packages: a.b load class num: 370 spendTime: 4080
metaspaceUsedKBBefore: 41581.6406 metaspaceUsedKBAfter: 42377.2344 metaspaceUsedKBAdd: 795.6
14.3. 不将组件复制到 lib 目录的原因
分析将指定包下的类加载后占用 Metaspace 大小时,不将相关组件复制到验证项目的 WEB-INF/lib 目录,原因如下:
- 拷贝到 WEB-INF/lib 目录的组件中可能会有一些代码会被扫描到并自动执行,由于依赖环境缺失等原因,可能会执行失败
- 可能导致部分组件出现版本冲突,导致执行失败

14万+

被折叠的 条评论
为什么被折叠?



