Java反射导致Metaspace OOM分析方式、工具与解决方法

该文章已生成可运行项目,

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”

JDK8JDK9 某个版本开始
非构造函数方法生成的类名前缀sun.reflect.GeneratedMethodAccessorjdk.internal.reflect.GeneratedMethodAccessor
构造函数生成的类名前缀sun.reflect.GeneratedConstructorAccessorjdk.internal.reflect.GeneratedConstructorAccessor
构造函数(序列化)生成的类名前缀sun.reflect.GeneratedSerializationConstructorAccessorjdk.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 101.8.0_144 HotSpot™ 64-Bit3.743.77
Linux CentOS 7.91.8.0_192 HotSpot™ 64-Bit3.773.77
Linux CentOS 7.91.8.0_382 OpenJDK 64-Bit3.763.76
Linux CentOS 7.911.0.18 OpenJDK 64-Bit3.843.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 101.8.0_144 HotSpot™ 64-Bit1176106390.39%
Linux CentOS 7.91.8.0_192 HotSpot™ 64-Bit390391100.26%
Linux CentOS 7.91.8.0_382 OpenJDK 64-Bit347386111.24%
Linux CentOS 7.911.0.18 OpenJDK 64-Bit35934997.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 目录的组件中可能会有一些代码会被扫描到并自动执行,由于依赖环境缺失等原因,可能会执行失败
  • 可能导致部分组件出现版本冲突,导致执行失败
本文章已经生成可运行项目
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值