如何解决Java应用程序中使用的OpenCV JNI库中的内存使用跟踪工具
在Java应用程序中,哪些工具可用于跟踪Opencv JNI库分配的内存。
在我的Java dropwizard服务器中,我使用JNI opencv绑定。服务器启动时,Java堆内存似乎并没有增加到超过GB,而是由GC定期释放。但是,大量(4-5 GB)的内存正在添加到java进程中,不确定该内存来自何处。
如何跟踪在JNI库中分配的内存并确定是否有泄漏。
解决方法
@concision的响应是正确的,对于这种需要,您确实可以依靠jemalloc来捕获调用malloc的位置,并借助jeprof
将分配可视化为图片或pdf。
当我个人在寻找一种检测本机内存泄漏的方法时,我很快找到了几篇描述该思想的文章,但是找不到任何逐步描述该思想的文章,因此我将尝试在我的答案中做到这一点。
添加泄漏的端点
当您遇到一个dropwizard应用程序的问题时,我们以创建一个泄漏端点为例。
让我们在dropwizard-example中添加我们的泄漏端点。
- 使用
git clone git@github.com:dropwizard/dropwizard.git
克隆存储库
- 使用
git checkout v2.0.13
切换到最后一个标签 - 在
LeakObject
中添加泄漏类dropwizard-example/src/main/java/com/example/helloworld/api/
- 在
MemoryLeakTestResource
中添加端点类dropwizard-example/src/main/java/com/example/helloworld/resources/
- 在
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
在名为native-memory-leak
的容器中启动bash,其中包含{{}提供的当前目录(我们假定为dropwizard-example
)的内容。 1}}。
通过此bash,您可以使用前面描述的命令启动应用程序,在本例中将是这样:
/root
应用程序完全启动后,我们可以转到下一部分
2。启动压力测试
在此步骤中,想法是多次调用导致内存泄漏的端点,以确保泄漏将清楚地显示在最终报告中。
在这种情况下,我们将使用下一条命令从新终端调用2000倍的端点:
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
此命令将使用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"
命令调用端点的2000次
命令结束后,我们可以转到下一部分
3。停止应用程序
在启动应用程序的容器中,我们可以使用 Ctrl + C 将其停止使用,以将内存使用情况转储到{{1}类型的堆文件中}
然后您应该在应用程序的标准输出中看到类似以下内容的内容:
curl
这表明jemalloc已生成一个名为jeprof.<pid>.0.f.heap
的堆文件。
4。启动<jemalloc>: Leak approximation summary: ~<total-bytes> bytes,~<total-objects> objects,>= 37 contexts
<jemalloc>: Run jeprof on "jeprof.<pid>.0.f.heap" for leak detail
从先前生成的堆文件中,我们将使用jeprof.<pid>.0.f.heap
和下一条命令来生成易于阅读的输出。
jeprof
由于jeprof
,此命令将从jeprof --show_bytes --gif $(which java) jeprof.<pid>.0.f.heap > result.gif
在result.gif
中生成一个名为dropwizard-example
的gif文件。
生成的gif应该是分配树,其中的主干为“ os malloc” ,代表JVM分配了什么。您的泄漏(如果有的话)将与树断开连接,如下例所示:
在此示例中,我们可以看到由“ jeprof.<pid>.0.f.heap
”引起的泄漏,但是我们仍然需要在应用程序中标识调用此本机方法的Java代码。
发现泄漏代码
在这一点上,我们知道我们有一个泄漏,但是我们仍然需要找出它在应用程序中的位置。对于这一部分,我能找到的最佳方法是使用JProfiler(它是一种商业分析器,但您可以使用试用密钥)。
这是要执行的步骤:
- 在主机上启动Java应用程序
- 在主机上启动JProfiler
- 单击“附加到正在运行的JVM”
- 选择与该应用程序对应的JVM,然后单击“开始”
- 选择配置文件模式“异步采样”
- 编辑模式“异步采样” 的设置以选中“启用本地库采样” ,然后单击“确定”
- 在“ CPU视图” 中,单击以记录CPU数据
- 启动压力测试
- 在“ CPU视图” 中,转到“调用树”
- 点击菜单项“查看” / “查找” 输入要查找的本机方法的FQN(在我们的示例中为“ {{1 }}“)
使用这种方法,我可以在下一个屏幕快照中看到我的泄漏发生在预期的jeprof
方法中。
寻找从JNI库分配的内存泄漏比寻找堆上的内存泄漏要痛苦得多。工具jemalloc
可能会让您感兴趣,因为它可以帮助隔离本机内存分配中大量分配来自何处。然后,您可以使用jeprof
工具生成图形以可视化发生大量分配的堆栈。
看看这些与您的问题非常相似的文章:
- https://technology.blog.gov.uk/2015/12/11/using-jemalloc-to-get-to-the-bottom-of-a-memory-leak/
- https://www.evanjones.ca/java-native-leak-bug.html
他们详细介绍了由JNI库(即,堆上的内存不是 )导致的内存泄漏的恢复过程。