@concision 的回应是正确的,对于这种需要,您确实可以依靠jemalloc 来捕获调用 malloc 的位置并将分配可视化为图片或 pdf,感谢jeprof。
当我亲自寻找一种检测本机内存泄漏的方法时,我很快找到了几篇描述这个想法的文章,但我找不到任何描述如何一步一步地做到这一点的文章,所以我会尝试在我的回答中做到这一点。
添加泄漏端点
当您遇到 dropwizard 应用程序的问题时,让我们创建一个泄漏端点作为示例。
让我们在dropwizard-example 中添加我们的泄漏端点。
- 使用
git clone git@github.com:dropwizard/dropwizard.git 克隆存储库
- 使用
git checkout v2.0.13 切换到最后一个标签
- 在
dropwizard-example/src/main/java/com/example/helloworld/api/中添加泄漏类LeakObject
- 在
dropwizard-example/src/main/java/com/example/helloworld/resources/中添加端点类MemoryLeakTestResource
- 在
dropwizard-example/src/main/java/com/example/helloworld/HelloWorldApplication.java中注册端点
班级LeakObject
public class LeakObject {
private static final byte[] ZIP_CONTENT;
static {
try {
ZIP_CONTENT = Files.readAllBytes(
Paths.get("target/dropwizard-example-1.0.0-SNAPSHOT.jar")
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public String buildSaying() throws IOException {
// The native memory leak is caused by the unclosed ZipInputStream
ZipInputStream inputStream = new ZipInputStream(
new ByteArrayInputStream(ZIP_CONTENT)
);
return String.format(
"Hello, I'm a leak in native memory %d !", inputStream.read()
);
}
}
班级MemoryLeakTestResource
@Path("/native-memory-leak")
@Produces(MediaType.TEXT_PLAIN)
public class MemoryLeakTestResource {
private final LeakObject leakObject = new LeakObject();
@GET
public String sayHelloWithLeakNative() throws IOException {
return leakObject.buildSaying();
}
}
班级HelloWorldApplication
public class HelloWorldApplication extends Application<HelloWorldConfiguration> {
...
@Override
public void run(HelloWorldConfiguration configuration, Environment environment) {
...
environment.jersey().register(new MemoryLeakTestResource());
}
}
构建应用程序
在终端中:
- 进入目录
dropwizard-example
- 使用
mvn clean install -DskipTests 构建项目。
这将在target 中创建一个名为dropwizard-example-1.0.0-SNAPSHOT.jar 的胖罐子
安装jemalloc
为了避免与目标环境紧密耦合,让我们使用 docker 来完成这项任务。
要安装jemalloc,我们需要:
- 从 github 下载源代码
- 使用
./configure --enable-prof --enable-debug 配置构建以启用堆分析和泄漏检测功能
- 使用
make 构建它
- 用
make install安装它
对应的Dockerfile
FROM openjdk:11-slim
RUN apt-get update && \
apt-get install -y tcpflow vim htop iotop jq curl gcc make graphviz && \
curl -O -L https://github.com/jemalloc/jemalloc/releases/download/5.2.1/jemalloc-5.2.1.tar.bz2 && \
tar jxf jemalloc-*.tar.bz2 && \
rm jemalloc-*.tar.bz2 && \
cd jemalloc-* && \
./configure --enable-prof --enable-debug && \
make && \
make install && \
cd - && \
rm -rf jemalloc-*
WORKDIR /root
构建你的 docker 镜像:
- 在终端中,进入包含您的
Dockerfile 的目录
- 然后启动构建命令
docker build -t native-memory-leak .
这将创建一个安装了 java 11、jemalloc 和 jeprof 的 docker 映像
使用jemalloc 启动应用程序
为此,您需要:
- 将环境变量
LD_PRELOAD设置为库的位置libjemalloc.so
- 将环境变量
MALLOC_CONF 设置为prof_leak:true,prof_final:true 以启用泄漏报告并使其在应用程序退出时将最终内存使用情况转储到文件(根据<prefix>.<pid>.<seq>.f.heap 命名)
- 启动您的 java 应用程序
这些步骤可以通过以下命令完成:
LD_PRELOAD=/usr/local/lib/libjemalloc.so \
MALLOC_CONF=prof_leak:true,prof_final:true \
java ...
使用jmalloc 识别本机内存泄漏的策略
- 如前所述启动 java 应用程序
- 启动压力测试
- 停止应用程序
- 启动
jeprof
1。启动 java 应用程序
我们将在 docker 容器中启动应用程序。
首先,让我们使用下一条命令启动容器:
docker run -it --rm -v $(pwd):/root \
--name native-memory-leak-test native-memory-leak /bin/bash
此命令将在名为 native-memory-leak-test 的容器中启动一个 bash,该容器基于图像 native-memory-leak 以及可从 /root 获得的当前目录的内容(我们假设为 dropwizard-example)。
从这个 bash 中,您可以使用前面描述的命令启动应用程序,在我们的例子中是这样的:
LD_PRELOAD=/usr/local/lib/libjemalloc.so \
MALLOC_CONF=prof_leak:true,prof_final:true \
java -jar target/dropwizard-example-1.0.0-SNAPSHOT.jar server example.yml
应用程序完全启动后,我们可以进入下一部分
2。启动压力测试
这一步的思路是多次调用导致内存泄漏的端点,以确保泄漏会清楚地出现在最终报告中。
在这种情况下,我们将使用下一条命令从新终端调用端点 2000 次:
docker exec -it native-memory-leak-test \
/bin/bash -c "for i in {1..2000}; do curl -s localhost:8080/native-memory-leak > /dev/null; done"
此命令将使用curl 命令调用端点 2000 次
命令结束后,我们可以进入下一部分
3。停止应用程序
在我们启动应用程序的容器中,我们可以使用 Ctrl+C 将内存使用量转储到类型为 jeprof.<pid>.0.f.heap 的堆文件中来停止它/p>
然后您应该在应用程序的标准输出中看到如下内容:
<jemalloc>: Leak approximation summary: ~<total-bytes> bytes, ~<total-objects> objects, >= 37 contexts
<jemalloc>: Run jeprof on "jeprof.<pid>.0.f.heap" for leak detail
这说明jemalloc已经生成了一个堆文件,名字为jeprof.<pid>.0.f.heap。
4。启动jeprof
从之前生成的堆文件中,我们将使用jeprof 在下一个命令中生成人类可读的输出。
jeprof --show_bytes --gif $(which java) jeprof.<pid>.0.f.heap > result.gif
此命令将在dropwizard-example 中从jeprof.<pid>.0.f.heap 生成一个名为result.gif 的gif 文件,感谢jeprof。
生成的 gif 应该是一棵分配树,其中树干是 "os malloc" 代表 JVM 已分配的内容。您的泄漏(如果有)将与树断开连接,如下例所示:
在这个例子中,我们可以看到我们有一个由“Java_java_util_zip_Inflater_init”引起的泄漏,但我们仍然需要识别在我们的应用程序中调用这个本地方法的 java 代码。
另见Use Case: Leak Checking
找出漏洞
此时,我们知道我们有泄漏,但我们仍然需要找出它在我们的应用程序中的位置。对于这部分,我能找到的最佳方法是使用JProfiler(它是一个商业分析器,但您可以使用试用密钥)。
以下是要做的步骤:
- 在主机上启动 Java 应用程序
- 在主机上启动 JProfiler
- 点击“附加到正在运行的JVM”
- 选择应用对应的JVM,然后点击“开始”
- 选择分析模式“异步采样”
- 编辑模式的设置“异步采样”,勾选“启用原生库采样”,然后点击“确定”
- 在“CPU Views”中,点击记录CPU数据
- 启动压力测试
- 在 “CPU 视图” 中,转到 “调用树”
- 点击菜单项"View" / "Find" 输入要查找的本地方法的FQN(在我们的例子中为“
Java_java_util_zip_Inflater_init ")
使用这种方法,我可以在下一个屏幕截图中看到我的泄漏发生在方法 com.example.helloworld.api.LeakObject.buildSaying 中,正如预期的那样。