Java单元测试实践-24.Gradle执行test任务卡死问题解决

Java单元测试实践-00.目录(9万多字文档+700多测试示例)
https://blog.csdn.net/a82514921/article/details/107969340

1. Gradle执行test任务卡死问题解决

1.1. test任务卡死问题现象

使用Gradle test任务执行单元测试时,执行一段时间后卡死,通过testLogging参数指定的测试日志查看,执行了几十个测试类后不再继续执行。

1.1.1. 无效的解决方法

1.1.2. 与Gradle版本的关系

使用Gradle 4.x,5.x,6.x版本,均存在以上问题,说明与Gradle版本无关。

1.1.3. 测试结束后关闭数据库连接池

使用jstack命令查看执行测试的GradleWorkerMain进程的线程堆栈,发现存在几十个名为“Druid-ConnectionPool-Destroy”的线程处于TIMED_WAITING状态,几十个名为“Druid-ConnectionPool-Create”的线程处于WAITING状态,如下所示:

"Druid-ConnectionPool-Destroy-1592700401" #72 daemon prio=5 os_prio=0 tid=0x000000005b795000 nid=0xb248 waiting on condition [0x000000006588f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at com.alibaba.druid.pool.DruidDataSource$DestroyConnectionThread.run(DruidDataSource.java:1898)

"Druid-ConnectionPool-Create-1592700401" #71 daemon prio=5 os_prio=0 tid=0x000000005b794000 nid=0xb0d4 waiting on condition [0x000000006e3af000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000000c3064f40> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
    at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:1824)

以上现象说明每个测试类执行完毕后,数据库连接未正常关闭。

在测试基类TestMockBase中通过@TestExecutionListeners指定的TestExecutionListener实现类TestCommonExecutionListener中,在当前类的测试方法均执行完毕时,即afterTestMethod()方法中,判断若当前类在执行最后一个@Test方法时(判断当前类已执行的@Test方法次数是否等于当前类的@Test方法数量),调用DruidDataSource类的close()方法关闭数据源。

经过以上修改后执行test任务,使用jstack命令查看线程堆栈,名为“Druid-ConnectionPool-Destroy”“Druid-ConnectionPool-Create”的线程分别只有一个,说明每个测试类执行完毕后,数据库连接已正常关闭。

若不通过以上方式在每个测试类结束后关闭数据源,还可能导致数据库服务器的连接不释放,可能耗尽服务器连接。

增加以上处理后,test任务执行卡死的问题未解决,说明以上处理能够解决测试结束后数据库连接池未关闭的问题,但无法解决test任务执行卡死的问题。

1.1.4. 修改SoftRefLRUPolicyMSPerMB参数

修改Gradle test任务,增加JVM参数“-XX:SoftRefLRUPolicyMSPerMB=”,调整软引用执行垃圾回收的时间策略,增大或减少后,test任务执行卡死的问题未解决。

SoftRefLRUPolicyMSPerMB参数说明可参考 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 。

1.2. test任务卡死问题解决过程

1.2.1. 查看内存与GC情况

使用“jstat -gccause [PID] [时间间隔] [次数]”命令查看GradleWorkerMain进程的GC情况,例如“jstat -gccause 20600 1s 0”,当test任务卡死时,执行结果如下所示:

S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC   
0.00   0.00 100.00  99.93  98.68  98.69    404    9.326    41   43.136   52.462 Allocation Failure   Ergonomics          
0.00   0.00  78.51  99.86  98.69  98.71    404    9.326    41   44.344   53.670 Allocation Failure   No GC               
0.00   0.00 100.00  99.86  98.69  98.71    404    9.326    42   44.344   53.670 Allocation Failure   Ergonomics          
0.00   0.00  70.04  99.97  98.70  98.70    404    9.326    42   45.605   54.931 Allocation Failure   No GC               
0.00   0.00 100.00  99.97  98.70  98.70    404    9.326    43   45.605   54.931 Allocation Failure   Ergonomics       

可以看到Old使用率(O列)与Metaspace使用率(M列)接近100%,FGC执行很频繁,GC的原因为“Allocation Failure”或“Ergonomics”。

使用“jstat -gc [PID] [时间间隔] [次数]”命令查看GradleWorkerMain进程的内存各分区使用情况,当test任务卡死时,执行结果如下所示:

S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
17920.0 18432.0  0.0    0.0   137728.0 137728.0  349696.0   349217.9  1359168.0 1341755.8 169088.0 166882.6    412    9.548  31     28.110   37.658
17920.0 18432.0  0.0    0.0   137728.0 123171.4  349696.0   349370.2  1361472.0 1344177.1 169344.0 167223.2    412    9.548  31     29.112   38.660
17920.0 18432.0  0.0    0.0   137728.0 20468.3   349696.0   349361.2  1363648.0 1346154.5 169728.0 167551.5    412    9.548  32     30.011   39.559

可以看到Metaspace容量(MC列)约为1331.7MB,Metaspace已使用(MU列)约为1314.6MB。

查看操作系统内存,物理内存已接近耗尽。

pic

使用“jmap -heap [PID]”命令,查看GradleWorkerMain进程的内存使用情况,MaxMetaspaceSize为非常大的值,如“MaxMetaspaceSize = 17592186044415 MB”。

也可以使用jconsole工具以图形化方式查看GradleWorkerMain进程的内存使用情况。

当test任务卡死时,Metaspace使用情况如下所示:

pic

Old使用情况如下所示:

pic

1.2.2. 调整Metaspace参数

1.2.2.1. Metaspace相关
  • Metaspace概念

参考 https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html 。

在Java Hotspot VM中,Java类具有内部表示形式,称为类元数据。在Java Hotspot VM的早期版本中,类元数据在永久代中分配。在JDK8中,永久代已删除,且类元数据已分配在本机内存中。默认情况下,可用于类元数据的本地内存量是无限的。使用选项MaxMetaspaceSize对用于类元数据的本机内存量设置上限。

  • Metaspace相关参数

当Metaspace已使用空间达到“高水位线”时,会触发垃圾回收。垃圾回收之后,高水位线可能会升高或降低,具体取决于类元数据释放的空间量。为避免过早引起另一次垃圾回收,高水位线值将会增大。高水位线初始值由MetaspaceSize参数值决定。MetaspaceSize参数的默认值与平台有关,从12MB至约20MB。

MetaspaceSize与MaxMetaspaceSize参数的示例为“-XX:MetaspaceSize= -XX:MaxMetaspaceSize=”。

  • Metaspace空间回收

当类元数据相应的Java类被卸载时,类元数据会被释放。Java类卸载是由于垃圾回收导致的,垃圾回收可以促使类卸载并释放类元数据。当用于类元数据的空间达到一定级别(高水位线)时,会引发垃圾回收。

  • Metaspace内存溢出异常

参考 https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/memleaks002.html 。

当类元数据所需的本机内存量超过MaxMetaSpaceSize参数值时,将引发带有详细信息为“MetaSpace”的java.lang.OutOfMemoryError异常。

1.2.2.2. 限制Metaspace大小

Gradle执行test任务时,默认情况下未指定MaxMetaspaceSize参数,Metaspace的最大大小取决于操作系统可用内存。如前所述,不限制Metaspace大小时,可能导致操作系统内存耗尽。

使用MaxMetaspaceSize参数限制Metaspace大小为256MB,再执行Gradle test任务。

使用“jstat -gccause”命令查看GradleWorkerMain进程的GC情况,当test任务卡死时,执行结果如下所示:

S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC  
0.00   0.00   0.00  19.56  98.57  98.69    199    1.963   120   23.186   25.149 Metadata GC Threshold Last ditch collection
12.50   0.00   0.00  19.56  98.57  98.69    204    2.003   125   24.085   26.088 Metadata GC Threshold Metadata GC Threshold
0.00   0.00   0.00  19.56  98.57  98.69    209    2.038   130   24.982   27.020 Metadata GC Threshold Last ditch collection
0.00   0.00   0.00  19.56  98.57  98.70    215    2.079   136   26.061   28.141 Metadata GC Threshold Last ditch collection
18.75   0.00   0.00  19.56  98.57  98.70    220    2.115   141   26.951   29.066 Metadata GC Threshold Metadata GC Threshold

可以看到Old使用率(O列)较低,Metaspace使用率(M列)接近100%,FGC执行很频繁,GC的原因为“Metadata GC Threshold”或“ditch collection”。

根据以上现象可知,Gradle执行test任务卡死的原因,是Metaspace内存溢出。

使用“jstat -gc”命令查看GradleWorkerMain进程的内存各分区使用情况,当test任务卡死时,执行结果如下所示:

S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT  
512.0  512.0   0.0    96.0  173568.0   0.0     349696.0   66296.8   262144.0 258427.3 31832.0 31415.2    413    3.393  334    54.315   57.708
512.0  512.0   0.0    0.0   173568.0   0.0     349696.0   66298.0   262144.0 258427.3 31832.0 31415.2    420    3.467  341    55.378   58.844
512.0  512.0   0.0    0.0   173568.0   0.0     349696.0   66300.4   262144.0 258427.3 31832.0 31415.2    426    3.522  347    56.317   59.838

可以看到Metaspace容量(MC列)为256MB,Metaspace已使用(MU列)约为252.4MB。

查看操作系统内存,物理内存还有较大剩余空间。

pic

使用“jmap -heap”命令,查看GradleWorkerMain进程的内存使用情况,MaxMetaspaceSize为256MB。

使用jconsole工具查看,当test任务卡死时,Metaspace使用情况如下所示:

pic

1.2.3. 设置Gradle执行test任务使用新进程

参考 https://docs.gradle.org/current/userguide/java_testing.html#sec:test_execution 、 https://docs.gradle.org/current/dsl/org.gradle.api.tasks.testing.Test.html#org.gradle.api.tasks.testing.Test:forkEvery 、 https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/Test.html#setForkEvery-java.lang.Long- 。

Gradle的test任务的forkEvery参数,可指定每个测试进程执行的测试类的最大数量,当大于该值后会创建新的测试进程。

forkEvery参数的默认值为0,代表所有的测试类重用测试进程。

将forkEvery参数值设置为非0整数后,由于需要不断停止及创建测试进程,对性能会有一定的影响。

由于默认情况下Gradle会一直使用同一个Java进程执行所有的测试类,当出现上述Metaspace内存泄露问题后无法再继续执行。可将forkEvery参数设置为合理的数值,使Gradle执行一定数量的测试类后创建新的测试进程,可解决内存溢出导致测试不再继续的问题。

1.3. 解决Gradle执行test任务卡死方法总结

  • 测试类执行完毕后关闭数据源

可参考示例项目,在测试基类TestMockBase引用的TestCommonExecutionListener的afterTestMethod()方法中,当某个类的测试方法执行完毕时,执行DruidDataSource类的close()方法关闭数据源。

测试类执行完毕后关闭数据源不能解决test任务卡死的问题,可以及时释放测试程序的数据库连接,也能避免数据库服务器连接过多。

  • 调整forkEvery参数

调整Gradle test任务的forkEvery参数值,是解决Gradle执行test任务卡死的最主要方法。

forkEvery参数配置示例如下:

test {
    doFirst {
        forkEvery = 5
    }
}

forkEvery参数应当根据实际情况选择合适的值,若设置过小会由于频繁创建进程影响测试速度;若设置过大可能仍会出现Metaspace内存溢出问题导致测试不继续。

  • 调整测试进程堆大小

Gradle执行test任务时,GradleWorkerMain进程的最大堆大小默认为512MB,可以根据实际情况进行修改。

参考 https://docs.gradle.org/current/dsl/org.gradle.api.tasks.testing.Test.html#org.gradle.api.tasks.testing.Test:minHeapSize 、 https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/Test.html#setMinHeapSize-java.lang.String- 。

通过test任务的minHeapSize、maxHeapSize参数,可以指定GradleWorkerMain进程的最小/最大堆大小,示例如下:

test {
    doFirst {
        minHeapSize = "256m"
        maxHeapSize = "1g"
    }
}
  • 调整测试进程Metaspace大小

可以根据实际情况进行修改GradleWorkerMain进程的MetaspaceSize、MaxMetaspaceSize参数值。

参考 https://docs.gradle.org/current/dsl/org.gradle.api.tasks.testing.Test.html#org.gradle.api.tasks.testing.Test:jvmArgs 、 https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/Test.html#jvmArgs-java.lang.Iterable-。

通过test任务的jvmArgs参数,可以指定GradleWorkerMain进程的用于启动进程的JVM的额外参数(不包括系统属性和最小/最大堆大小)。指定MetaspaceSize、MaxMetaspaceSize参数值的示例如下:

test {
    doFirst {
        jvmArgs "-XX:MetaspaceSize=64m", "-XX:MaxMetaspaceSize=256m"
    }
}

以上参数在gradlew/gradlew.bat/gradle/gradle.bat脚本的DEFAULT_JVM_OPTS选项修改无效。

1.4. Gradle执行test任务内存溢出问题分析

1.4.1. Metaspace内存溢出示例

重复创建不会被垃圾回收的类,可以使程序出现Metaspace内存溢出。

可参考示例工程MetaspaceOOM1、MetaspaceOOM2类,如下所示:

ClassPool classPool = ClassPool.getDefault();
int i = 0;
while (true) {
    classPool.makeClass("test_MetaspaceOOM:" + i++).toClass();
}
ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();
List<Object> list = new ArrayList<>(100000);
while (true) {
    Constructor<?> constructor = reflectionFactory.newConstructorForSerialization(MetaspaceOOM2.class);
    list.add(constructor);
}

在执行时建议使用以下JVM参数:

-Xms256m -Xmx256m -XX:MetaspaceSize=20m -XX:MaxMetaspaceSize=64m -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

若需要在出现Metaspace内存溢出时自动生成heap dump文件,可添加以下参数:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=MetaSpaceOOM.hprof

执行以上代码时,会出现多次GC日志,原因为“Metadata GC Threshold”,最后会出现异常“java.lang.OutOfMemoryError: Metaspace”,并显示内存各分区的使用情况。

1.4.2. Gradle执行test任务Metaspace内存溢出问题原因

使用“jmap -dump:format=b,file=[dump文件路径] [PID]”命令,也可以在需要时手工生成Java进程的heap dump文件,如“jmap -dump:format=b,file=test.hprof 41800”。

可以使用Eclipse Memory Analyzer(MAT)分析heap dump文件,下载地址为 https://www.eclipse.org/mat/downloads.php 。

也可以使用IBM HeapAnalyzer分析heap dump文件,下载地址为 https://www.ibm.com/support/pages/ibm-heapanalyzer 。

使用MAT分析Gradle执行test任务时出现Metasapce内存溢出时GradleWorkerMain进程的heap dump文件。

查看“Leak Suspects”报告,可疑的问题包含javassist.ClassPool与org.powermock.core.classloader.javassist.JavassistMockClassLoader类,如下所示:

pic

pic

查看ClassLoader信息,也可以看到大量org.powermock.core.classloader.javassist.JavassistMockClassLoader实例,如下所示:

pic

JavassistMockClassLoader类继承自MockClassLoader类,MockClassLoader类继承自ClassLoader类。MockClassLoader类API文档为 https://javadoc.io/doc/org.powermock/powermock-core/latest/org/powermock/core/classloader/MockClassLoader.html 。

可以推测Gradle执行test任务Metaspace内存溢出问题原因是测试类执行完毕后,JavassistMockClassLoader未被释放,导致对应的类无法被回收,导致内存泄露。

1.4.3. PowerMockito内存泄露问题

  • PowerMock Classloader内存泄露问题

参考以下内容,可知PowerMock存在Classloader内存泄露问题: https://github.com/powermock/powermock/issues/227 。

  • Classloader内存泄露问题分析方法

为了分析并解决Classloader内存泄露问题,可参考以下内容(及对应系列的其他内容): http://java.jiderhamn.se/2011/12/11/classloader-leaks-i-how-to-find-classloader-leaks-with-eclipse-memory-analyser-mat/ 、 http://java.jiderhamn.se/2012/01/01/classloader-leaks-ii-find-and-work-around-unwanted-references/ 。

  • 尝试根本解决Metaspace内存泄露问题

尝试不通过forkEvery参数,根本解决Metaspace内存泄露问题。尝试了一些方法均无效,例如,在@PowerMockIgnore注解中增加Spring的包,在每个测试类执行完毕时关闭Spring Context。PowerMock Classloader内存泄露问题,可能与被测试代码,及引用依赖包有关,不容易解决,最后还是通过修改forkEvery参数解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值