本文主要是让你充分的认识到什么叫做内存泄露,什么叫做内存溢出,别再傻傻分不清了,别再动不动的升级服务器的内存了。
1.基本概念
1.1.内存泄露
内存泄漏是指程序申请了内存后,不再使用某些内存空间,但未能正确释放,导致这部分内存无法被再次利用。虽然系统可能还有足够的内存供其他操作使用,但长期累积会逐渐耗尽可用内存,最终可能导致内存溢出。
1.2.内存溢出
当应用程序请求的内存超出了 JVM 分配的最大堆内存时,出现OOM异常
1.3.垃圾回收
当应用程序释放占用内存以后,JVM会按照自己的策略对已经释放的内存进行回收。并不是说程序执行结束,立马就进行内存回收,而是会在适当的时机进行回收。这意味着即使方法执行结束,内存的释放也可能会延迟,直到 JVM 认为有必要进行垃圾回收。
1.4.内存泄露-垃圾回收-内存溢出三者的关系关系
当存在内存溢出后,会导致内存逐渐被占用,而此时垃圾回收机制无法对这些占用的内存进行回收,由于内存不会被释放,导致可使用内存越来越少。如果最后内存无法满足接下来或者正在运行的代码所需要的内存,就会发生内存溢出。总结就是下面三条
- 内存泄露:逐步吞噬内存
- 垃圾回收:无法回收内存
- 内存溢出:内存不够分配
2.代码示例
以下代码,并未设置相关JVM参数,全是默认,每个人的JDK,内存都不一样,有人内存8G、16G、32G都有可能,因此可以视情况调整下面的循环次数,这里我是循环3000 * 10000次
2.1.示例1:模拟逐渐占用内存-释放内存
以下代码,就是一个很常见的,需求大概就是创建很多对象,然后把这个对象添加到集合中。为了演示效果,中间加了暂停效果,Thread.sleep。
/**
* @description:内存泄露demo
* @author:hutao
* @throws InterruptedException
* @mail:hutao1@epri.sgcc.com.cn
*/
@GetMapping("/memory/leak")
public String leak() throws InterruptedException {
log.info("开始调用/memory/leak");
List<UserVO> list = new ArrayList<>();
for (int i = 0; i < 3000 * 10000; i++) {
if(i % (300 * 10000) == 0) {
Thread.sleep(500);
}
UserVO temp = new UserVO();
temp.setUserId("ID_"+ i);
temp.setUserName("胡涛_" + i);
temp.setUserAge(i);
list.add(temp);
}
log.info("结束调用/memory/leak");
return "leakTest";
}
启动我们的java程序以后,等待一段时间波动以后,观察到内存的占用率此时呈现一条水平线
接着通过浏览器调用我们上面的接口,在观察内存使用率,可以发现大概使用了5个G的内存,也就是说,该接口在被调用的时候,居然就使用了5个G的内存。
http://127.0.0.1:8080/demo1/memory/leak
然后持续观察一段时间以后,我们不难发现,好像内存没有释放哎?等了好久也没等到内存释放。怎么回事?这个代码是不是存在内存泄露的问题?
这时候别着急,思考一下,如果你没理解咱们上面说的基本概念,你大概会想
第一次调用就占用了5个G,第二次调用不就占用5个G了,第三次调用就内存溢出了。然后当你连续几次调用的时候,你就会发现,咦,咋回事?怎么没有继续占用内存啊,怎么内存呈现波浪形,一会占用,一会释放了。上面代码为啥没有出现我们最终设想内存溢出?到底为啥?
这时候你在看这句话是不是理解了?
1.内存泄露:逐步吞噬内存
2.垃圾回收:无法回收内存
3.内存溢出:内存不够分配
之所以没有出现内存溢出,为啥?因为垃圾回收机制,回收到了内存。然后内存被释放了。所以内存会介于被占用,被释放之间来回跳转。
这里提一句
假设你和我在代码中一样,想要方法结束调用以后,内存直接释放,写了如下这个代码
System.gc();
我的IDE提示我,当然提示语有点侮辱人,不要让我以为我比JVM还聪明,建议我删掉该代码,最主要的是,你发现没暖用,并没有出现程序执行完毕,立马释放内存的情况。
Don't try to be smarter than the JVM, remove this call to run the garbage collector.
2.2.示例2:模拟逐渐占用内存-不释放内存(内存泄露)
上面,我们模拟了会让JVM在他认为该回收垃圾的时候,去回收垃圾,然后释放内存,接下来,我们模拟一个JVM无法释放内存的例子,最红内存溢出OOM错误。
这里改动一下代码。仅需要两行代码,就能阻止垃圾回收机制释放内存,然后导致最后内存溢出。
添加一个cache的属性,并且该属性一直引用我们每次调用接口创建的list
private Map<String, List<UserVO>> cache = new HashMap<>();
cache.put(UUID.randomUUID().toString(), list);
完整代码如下。思路就是把每次接口调用的数据都往cache里面存储。
你可以这样粗鲁的理解:list往cache里面不停的存放,最后导致cache越来越大,最后内存不够
你也可以这样正规的理解下:虽然方法中局部变量list不在使用,但是list被cache引用,而cache是demo1Controller(Spring创建的Controller对象) 对象的属性,demo1Controller一直被Spring引用,Spring一直在整个web引用程序中,因此cache相当于在整个web程序的生命周期都有效.
在 Spring 框架中,Controller 对象默认是单例的。
这意味着每个 Controller 类在 Spring 容器中只会有一个实例
所有的请求都会共享这个实例。
@RestController
@RequestMapping("/demo1")
@Log4j2
public class Demo1Controller {
private Map<String, List<UserVO>> cache = new HashMap<>();
/**
* @description:内存泄露demo
* @author:hutao
* @throws InterruptedException
* @mail:hutao1@epri.sgcc.com.cn
*/
@GetMapping("/memory/leak")
public String leak() throws InterruptedException {
log.info("开始调用/memory/leak");
List<UserVO> list = new ArrayList<>();
for (int i = 0; i < 3000 * 10000; i++) {
if(i % (300 * 10000) == 0) {
Thread.sleep(500);
}
UserVO temp = new UserVO();
temp.setUserId("ID_"+ i);
temp.setUserName("胡涛_" + i);
temp.setUserAge(i);
list.add(temp);
}
cache.put(UUID.randomUUID().toString(), list);
log.info(cache.keySet());
log.info("结束调用/memory/leak");
return "leakTest";
}
}
可以看到,如下所示,虽然没有出现内存一直持续暴涨的情况,但是如我们期待的那样,内存没有被释放,并且出现了OOM:Java heap space错误。如果你和我一样运行了代码,你可能会遇到,此时电脑特别卡,很卡。卡的要死。
2.3.示例3:模拟逐渐占用内存-释放内存(内存溢出)
在示例2中,我们不难发现,当我们第一次调用的时候,程序正常的,而程序在第二次调用的时候,内存并没有有释放,所以内存必然不够,因此第二次请求的时候,内存就不够分配了,因此导致内存溢出。
提示:虽然实际上还有接近5G物理内存,但是并不会把所有内存都分配给JVM
这里的内存不够指,JVM分配到的内存不够
为了方便演示,这里我们开始引入JVM的一下参数说明
-Xms512m -Xmx1g
-Xmx:设置 Java 堆的最大值。默认值通常为物理内存的四分之一。建议根据物理内存大小和其他内存开销来调整此值。
-Xms:设置 Java 堆的初始值。对于服务器端的 JVM,最好将此值与 -Xmx 设置为相同,以避免在运行时频繁调整内存。
在IDE启动中中,添加JVM参数,这里我设置,最大为1g
为了方便观察,这里我们记录一下内存使用情况
/**
* @description:内存溢出demo
* @author:hutao
* @throws InterruptedException
* @mail:hutao1@epri.sgcc.com.cn
*/
@GetMapping("/memory/oom")
public String oom() throws InterruptedException {
log.info("开始调用/memory/oom");
log.info("最大可用内存:{}",Runtime.getRuntime().maxMemory());
List<UserVO> list = new ArrayList<>();
for (int i = 0; i < 3000 * 10000; i++) {
if(i % (300 * 10000) == 0) {
log.info("当前占用内存:{},当前空闲内存:{}", Runtime.getRuntime().totalMemory(),Runtime.getRuntime().freeMemory());
Thread.sleep(500);
}
UserVO temp = new UserVO();
temp.setUserId("ID_"+ i);
temp.setUserName("胡涛_" + i);
temp.setUserAge(i);
list.add(temp);
}
log.info("结束调用/memory/oom");
return "oomTest";
}
通过下面的截图,不难发现几个问题
1通过手动限制最大内存以后,第一次调用就内存溢出
2内存并没有像之前一样,直接占用了5g,而是按照我们分配的占用
3可以看到占用的逐渐增加,最终占满内存,无法给予程序所需要的内存