踩坑Android 编译线程爆了, gradle 内存 OOM 解决之路

Posted gdutxiaoxu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了踩坑Android 编译线程爆了, gradle 内存 OOM 解决之路相关的知识,希望对你有一定的参考价值。

背景

最近项目在编译,编译多次之后,有挺多人反馈会出现 OOM 的,在项目的根目录下面会出现 hs_err_pid*.log 的错误文件。内容大概如下

这个对我们的开发效率还是有挺大影响的,如果能够解决,对我们的开发效率还是有一定提升的。因此,我们尝试进行解决。

探索原因

从报错的信息来看,‘jar transform Thread’ 有时候的线程数非常多, 很有可能是同时开启的线程数过大,导致内存不足,最终 OOM。

从线程名是 ‘jar transform Thread’ ,根据经验,我们第一时间想到可能是 transform 相关的。

于是,我们找项目当中 transfrom 相关的, 从 buildScan 文件中,找 transfrom 相关的

发现主要有几个

  1. transformClassesWithRealmTransformerForDebug
  2. transformClassesWithCom.xx.xx.gradle.plugin.hilt.HiltContextWrapperRemovePluginForDebug
  3. transformDebugClassesWithAsm

很快我们找到 Hilt, Realm 里面 transform 里面的代码,发现里面的 Thread Name 都不是 jar transform Thread。那应该不是这两个的原因。

讨论之后,我们尝试 dump 编译时 Java 进程的内存信息,看能不能复现?

首先,我们先确定当前进程的 PID

接着我们借助 VisualVM 这个工具,dump 下 JVM 编译时候的进程,编译的时候发现,线程数有时候会越来越多。

于是,我们在想能不能 debug 创建线程的地方,于是,我们在 java.lang.Thread#setName 这里设置条件断点 name.contains("jar transform")

debug gradle assembleDebug 任务,很快我们发现,调用栈关系如下

我们重点关注到了几个跟线程相关的东西

我们跟踪进去,发现这个线程池的核心线程数设置为 2147483647

而上面的线程数不断增多,并且线程名包括 “jar transform”, 那很有可能就是这个线程池了。

我们逐一排查,发现线程池 executor 是在这里传递进来的

跟踪代码,很快我们发现创建改线程池 executor 的地方 DefaultCachedClasspathTransformer#executor

他这里果然没有限制线程的数量。

而我们项目中的 gradle 代码是 6.9.1,于是在想,我们去跟官方最新代码对比一下。

对比官方 gradle 代码

我们首先 clone 官方代码 gradle,找到 DefaultCachedClasspathTransformer,
发现最新代码已经进行了修改,限制了线程的数量。改为跟 CPU 核心数挂钩。

而他是在什么时候进行了修改了,其实很简单,我们可以借助 git 命令,找到他属于哪一个 TAG.

git tag --contains 2a1e74166bc82607e15de78002ef56582b34af0d

很快我们发现了,他在 gradle 7.0 上面对线程池的线程数进行了限制,改为跟 CPU 核心数挂钩。

思考

先来思考一个问题,可能很多人有同样的疑问。

为什么有的机器没出现有的机器会出现,⽐如我同事的mac就没有发现

我么先来看一下 java tranfrom 线程是干什么用的,

我们可以看这里的代码
org.gradle.internal.classpath.DefaultCachedClasspathTransformer.TransformFile#schedule

跟踪下去,你会发现主要是一些 IO 读取操作。

如果说你的机器磁盘性能比较好,那么 IO 读取比较快,线程干完工作之后,就会自动销毁,出现这样现象的可能性会比较低。如果磁盘性能较差,出现的可能性会比较大。

问题解决

既然怀疑问题是因为这里的线程数引起的,于是第一时间我们想到了几种方法

  1. 反射修改线程池的数量
  2. 升级 gradle 版本

于是,我们跟中代码,试试反射能不能修改代码,但很快,我们发现,并没有找到一个好的 hook 点,无法修改。

可能有人会想到 epic,没错,刚开始我也想用 epic。但是 epic 是基于安卓 ART 虚拟机的,而我们编译的时候,是基于 JVM 的,epic 是无法使用的。

接着我们尝试了第二种方法,尝试升级 gradle 版本到 7.0,折腾了一fang之后,发现升级要适配的东西还是蛮多的,一下子无法解决

  1. maven repo 仓库设置 allowInsecureProtocol
  2. grrovy 版本冲突
  3. JavaParser 错误

总之,错误是解决完一个接着一个,还是挺多坑的

柳暗花明又一村

跟汉光爷讨论之后,汉光爷说能不能自己编译一个版本出来。他在官网上找到了编译 gradle 版本的方法

编译完成之后,上传到 CC 的 S3 服务器上面,我们在 gradle-wrapper.properties 下面修改,替换成自己的 gradle 版本

#Thu May 30 18:31:45 CST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
# 解决编译线程数过多,导致 OOM 的问题
distributionUrl=https://xx.cn/static/gradle/gradle-6.9.3-all.zip

再次编译,可以看到 jar transform 相关的线程数,最多变为 8 了,因为我的电脑是 8 核单核心的。

到此,我们对 gradle jar transfrom thread 的线程数进行了限制,合理应该是不会再出现 OOM 了,如果还会出现,可以保留现场,找我或者汉光看看。

总结

可以看到,我们这次的问题解决思路大概是这样的。

  1. 从 error 日志排查发现,很有可能跟 transfrom 相关
  2. 排查项目里面 transfrom 相关的,有没有 jar transform Thread 相关的
  3. dump JVM 内存,看线程相关的,观察 jar transform Thread 是否异常
  4. debug gradle assemble 任务,观察 线程名包括 jar transform Thread Thread 的调用堆栈
  5. 分析 调用堆栈,找到原因
  6. 结合 gradle 官方代码,查看问题是否已经解决

那有没有更快的方法呢?

其实如果一开始能确定是 gradle 问题的话,可以直接在 gradle 里面搜索字符串 jar transforms,然后再一步步反推,其实也是可以的。

以上是关于踩坑Android 编译线程爆了, gradle 内存 OOM 解决之路的主要内容,如果未能解决你的问题,请参考以下文章

踩坑Android 编译线程爆了, gradle 内存 OOM 解决之路

Android踩坑小记:ndk版本与Android Gradle Plugin版本兼容

Android踩坑小记:ndk版本与Android Gradle Plugin版本兼容

Android踩坑小记:ndk版本与Android Gradle Plugin版本兼容

Android踩坑小记:ndk版本与Android Gradle Plugin版本兼容

Android Gradle构建经验总结