欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java生产环境调优(2) 模拟一次内存溢出,以及原因分析

程序员文章站 2022-07-15 14:38:17
...

大学里面的学习都是以广度为主,后来在校招面试的时候,有个面试官问我一个问题,java程序的内存会溢出么?

当时的我一头雾水,我说应该不会吧。当时真的很傻逼,这也暴露了一个问题,大学里面学习知识很广,很少注重深度,很多时候总以为学的多了就是ok的,很多技术词汇信手拈来就很棒,而往往有的时候 适当的深入了解一下还是有点用处的,绝不是为了在面试的时候去装逼。

在工作中就遇到过这样的情况,公司的服务跑在阿里云上面,32G的内存,这个内存可以说是相当的大了,然而在管理上比较的乱,各个团队在上面随便的乱发应用,导致32G 的内存几乎被撑爆了。想上去升级什么的应用程序会突然挂掉,爆内存不够。

怎么办?

首先是分析

什么程序占用的内存比较多了?

找到一条命令

 ps aux|head -1;ps aux|grep -v PID|sort -rn -k +4|head

通过这个命令发现,32G内存中 24G 被2个solr 应用占据,每个12G,

这个真的是太大了,以前用solr 的时候,感觉solr 的速度非常的快,所以时间复杂度小了,空间复杂度大一点也就正常了

如果真是solr出问题了,他不应该占用12G,那也比较难以解决,毕竟solr不是我们做的,所以看下一个高内存应用吧

另外的8个G 其中有个应用占了6个G左右,这个应用是普通的java 应用,所以这个就是我重点观察的对象了。

至于怎么排查了?如何才能知道这个应用究竟是什么原因占用了6个G了?


ok,回到主题,现在来模拟一次这样的情况

代码很简单,

在上代码前,先上一张徐加帅老师画的图

java生产环境调优(2) 模拟一次内存溢出,以及原因分析

这个是jdk8的java的内存结构,其他的版本会有稍许不同

我们主要看这个图的左半部分,也就是堆区

堆区一共分为2部分:yong区+old区
在yong区里面又分为2部分:s区+eden区
在s区中又分为2部分:s0+s1

我们在代码中new出来的对象,都首先是放到堆区中的eden区,当达到一定条件后,对象会到s区,并且在s区的s0和s1中来回换,每换一次岁数加一,当岁数达到一定的值后就会进入old区

eden区的大小和s区的大小默认是8:2 ,为什么是这个数字了,因为很多的对象都是朝生夕死,所以eden区要大一点

我们在java中经常使用对象,可否想过,一个对象占用多大的内存了?

比如下面的

 @Data
class  User {
private String userName;
}

User user=New User();
user.setName("test");

上面代码的user ,有个name是test,这个就算是4字节吧。可能还要保存类的一些信息,就算5个字节吧。实际可能会不止这个数字
如果内存中有成千上万的这个对象,就算他只有5字节,电脑的内存还是会撑爆的。

好了接下来上代码

1.统一环境
springboot 2.1.3.RELEASE,开启web支持

2.上代码

新建一个User对象


@Getter
@Setter
@ToString
public class User {
    private int userId;
    private String userName;
    private String password;
}

新建一个控制层,上代码


@RestController
@Slf4j
public class UserController {

    private List<User> userlist=new ArrayList<>();

    //堆区内存溢出
    @GetMapping("/heapOver")
    public void heapOverTest(){
        while(true){
            userlist.add(new User());
        }
    }
}

这个代码的功能就是不停的向List中添加对象,这个对象肯定是保存在内存里面啊,就算你内存再大,总有加满的那一天,

当内存没有办法再加入对象的时候,也就会报错了。

ok,把代码打包,上传到阿里云 运行看看。

为了更快的达到内存溢出的效果,把堆内存指定的小一点

nohup java -Xmx32M -Xms32M -jar demo-0.0.1-SNAPSHOT.jar &

下面访问一次接口

 curl 127.0.0.1:8080/heapOver

观察报错日志

java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Arrays.java:3181)
        at java.util.ArrayList.grow(ArrayList.java:265)
        at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
        at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
        at java.util.ArrayList.add(ArrayList.java:462)
        at com.example.demo.controller.UserController.heapOverTest(UserController.java:51)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189)
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:123)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)

这个时候报错了,内存溢出了

虽然这个内存溢出是我们手动构造的,我们自己知道内存溢出的原因是User对象太多了,但是如果现实环境中真的发生了这样的错误,又该怎么办了?

这个时候java的内存分析工具就登场了

3.先要把内存溢出的时候,对应的数据搞出来
使用jmat进行堆区数据的导出

jmap -dump:format=b,file=aaa.hprof 20422

20422是java进程的id
java生产环境调优(2) 模拟一次内存溢出,以及原因分析

生成了一个文件 大小是58M

这个是手动导出的,如果想在内存溢出的时候自动导出也是可以的

只需要在启动的时候加上参数就ok了

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./

4.ok到这里我们已经拿到了内存溢出的时候的堆区的存储情况,下面就是对这个堆区内存进行分析了.
另一个工具登场:MAT
这个软件其实和eclipse 的风格是非常的像的
java生产环境调优(2) 模拟一次内存溢出,以及原因分析

打开软件后,选择File–>Open Heap Dump

java生产环境调优(2) 模拟一次内存溢出,以及原因分析

这里插一句,我自己在现实环境中遇到的,像eclipse 这类家族软件,在启动的时候都会去读取配置文件,MAT再启动的时候这个程序最大占用多大内存由配置文件决定
所以当你导出的镜像特别大的时候,MAT程序也会内存爆掉的,需要修改这个程序的ini格式的文件,让这个程序的最大堆内存设置大一点,这样才能完全读取堆内存的镜像

选择第一个Leak Suspects Report,点击finish
过一段时间看下分析的结果

java生产环境调优(2) 模拟一次内存溢出,以及原因分析

可以看到 堆内存主要的大块是3部分 分别是 5.4M 11.4M 和 11.5M

下面黄色的区域给出了可能出问题 一共2个

java生产环境调优(2) 模拟一次内存溢出,以及原因分析

其中第一个的可能性比较大,数据占了堆内存的40.43%

点击detials 查看一下

java生产环境调优(2) 模拟一次内存溢出,以及原因分析

最终可以看到 是

java.util.concurrent.ConcurrentHashMap @ 0xfeda0a78 下面的

java.util.concurrent.ConcurrentHashMap$Node[512] @ 0xfedc6ae8 下面的

java.util.concurrent.ConcurrentHashMap$Node @ 0xffbaf120 下面的

com.example.demo.controller.UserController @ 0xffb0c420 下面 有一个ArraryList对象

java.util.ArrayList @ 0xffb0c438 这个对象下面有很多的User对象

java.lang.Object[360145]

有多少了,继续往下翻

java生产环境调优(2) 模拟一次内存溢出,以及原因分析

360145个 ,差不多36w个 ,占用大小8643480Byte, 大约是8M, 32M的内存分给springboot去运行是比较小的,最后仅仅36w个对象就把应用程序给搞垮了.


上面的情况是比较特殊的,毕竟是人手动构建的一个错误.真实的情况往往比较复杂,也许很久都不会有内存溢出.

或者即使内存溢出了,各种原因也是非常的多,这个就需要平时的积累.去接触,去尝试.

未来的路还很长,只有学习才能让自己变得更加强大.

java生产环境调优(2) 模拟一次内存溢出,以及原因分析