什么?Android 编译线程爆了, gradle 内存 OOM 解决之路

2,213 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

本文首发我的微信公众号徐公,收录于 Github·AndroidGuide,这里有 Android 进阶成长知识体系, 希望我们能够一起学习进步,关注公众号徐公,5 年中大厂程序员,一起建立核心竞争力

背景

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

image.png

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

探索原因

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

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

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

image.png

发现主要有几个

  1. transformClassesWithRealmTransformerForDebug
  2. transformClassesWithCom.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 核心数挂钩。

思考

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

为什么在 windows 上面比较容易出现,在 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,折腾了一笔之后,发现升级要适配的东西还是蛮多的,一下子无法解决

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

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

柳暗花明又一村

跟同事讨论之后,说能不能自己编译一个版本出来。于是我们在官网上找到了编译 gradle 版本的方法

编译完成之后,我们在 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
# 解决 编译线程数过多
distributionUrl=https://xx.cn/static/gradle/gradle-6.9.3-all.zip

再次编译,可以看到 jar transform 相关的线程数,最多变为 8 了,跟我们 CPU 数量一样

我们对 gradle jar transfrom thread 的线程数进行了限制,经过

总结

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

  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,然后再一步步反推,其实也是可以的。

推荐阅读

耗时一周,我解决了微信 Matrix 增量编译的 Bug,已提 PR

自定义 Hook,解决 RxJava 异常时堆栈信息显示不全

Android 启动优化(一) - 有向无环图

Android 启动优化(二) - 拓扑排序的原理以及解题思路

Android 启动优化(三)- AnchorTask 开源了

Android 启动优化(四)- AnchorTask 是怎么实现的

Android 启动优化(五)- AnchorTask 1.0.0 版本正式发布了

Android 启动优化(六)- 深入理解布局优化