java生产环境调优(2) 模拟一次内存溢出,以及原因分析
大学里面的学习都是以广度为主,后来在校招面试的时候,有个面试官问我一个问题,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,回到主题,现在来模拟一次这样的情况
代码很简单,
在上代码前,先上一张徐加帅老师画的图
这个是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
生成了一个文件 大小是58M
这个是手动导出的,如果想在内存溢出的时候自动导出也是可以的
只需要在启动的时候加上参数就ok了
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./
4.ok到这里我们已经拿到了内存溢出的时候的堆区的存储情况,下面就是对这个堆区内存进行分析了.
另一个工具登场:MAT
这个软件其实和eclipse 的风格是非常的像的
打开软件后,选择File–>Open Heap Dump
这里插一句,我自己在现实环境中遇到的,像eclipse 这类家族软件,在启动的时候都会去读取配置文件,MAT再启动的时候这个程序最大占用多大内存由配置文件决定
所以当你导出的镜像特别大的时候,MAT程序也会内存爆掉的,需要修改这个程序的ini格式的文件,让这个程序的最大堆内存设置大一点,这样才能完全读取堆内存的镜像
选择第一个Leak Suspects Report,点击finish
过一段时间看下分析的结果
可以看到 堆内存主要的大块是3部分 分别是 5.4M 11.4M 和 11.5M
下面黄色的区域给出了可能出问题 一共2个
其中第一个的可能性比较大,数据占了堆内存的40.43%
点击detials 查看一下
最终可以看到 是
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]
有多少了,继续往下翻
360145个 ,差不多36w个 ,占用大小8643480Byte, 大约是8M, 32M的内存分给springboot去运行是比较小的,最后仅仅36w个对象就把应用程序给搞垮了.
上面的情况是比较特殊的,毕竟是人手动构建的一个错误.真实的情况往往比较复杂,也许很久都不会有内存溢出.
或者即使内存溢出了,各种原因也是非常的多,这个就需要平时的积累.去接触,去尝试.
未来的路还很长,只有学习才能让自己变得更加强大.