Java程序静态全局变量与内存泄漏的关系与分析<一>

1. 所需工具说明

要做这样的测试与分析我们需要以下这些工具:

  1. 集成开发工具(我用的是STS)
  2. jconsole.exe(用来观察堆内存的变化,路径是JDK安装路径bin下,我的是C:\Program Files\Zulu\zulu-8\bin),用法链接
  3. Tomcat(我的是Spring Boot内置有Tomcat),要模拟Tomcat前后端服务

2. 任务

静态全局变量与内存泄漏的关系与分析。
重点考试要考:静态全局变量的生命周期与服务器一致。
按定义格式分两种静态全局变量类型:<1>. private static List<byte[]> byteList = new ArrayList<>();,这是直接声明和new创建;
<2>. private static List<byte[]> byteList;,这是先声明,用的时候再new创建。

3. 测试和分析第一种类型(直接声明和new创建)

  1. 代码
    后端服务代码
package oom;

import java.util.ArrayList;
import java.util.List;

public class OutOfMemery {

    private static List<byte[]> byteList = new ArrayList<>();
    
    public static void clear() {
        if (!byteList.isEmpty()) {
            System.out.println("开始清除byteList数据");
            byteList.clear();
        }        
    }
    
    public static String initial() {
                
        int count = 0;
        while (count < 1000) {
            byteList.add(new byte[4096 * 10]);
            count++;
        }
        
        System.out.println("byteList's HashCode=" + byteList.hashCode() + ", size=" + byteList.size());
        
        return "byteList's HashCode=" + byteList.hashCode() + ", size=" + byteList.size();
    }
    
}

    @RequestMapping("/oom")
    public String oom_test() {
        String value = OutOfMemery.initial();
        return value;
    }
  1. 分析
    测试的类叫OutOfMemery,有一个静态全局变量byteList,里面装的是byte[];initial()方法就是往这个byteList添加1000个byte[4096 * 10],返回的是byteList的hash code和size。controller就是前端发出oom请求就会调用OutOfMemery.initial()。

  2. 测试
    Tomcat服务开启,我们先使用Jconsole查看开启的进程GC次数和内存使用量(如何使用请看上面链接),old, eden survivor依次如下图:

Old
在这里插入图片描述
在这里插入图片描述

我的oom请求url是http://localhost:1010/sprintboot/oom。我们在浏览器输入这个url得到的结果如下图:
在这里插入图片描述
再回头看内存使用量,old, eden survivor依次如下图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
明显的看到3个内存都有增加。PS Scavenge(Young GC)次数是2。

执行第二oom请求看看静态全局变量byteList的变化,注意了这里是重点考试要考,前端结果如下图:
在这里插入图片描述
可以看到静态全局变量byteList的Hash code和之前不一样了,而且size是2000,即第一次请求时有1000,第二次请求又加了1000得到2000。说明了如果使用静态全局变量像List,Map,这些变量的值会在高并发下累加起来。
我们在连接点击10次看看结果:
在这里插入图片描述
size累积到12000。
那么与内存泄漏有什么关系呢?
上面有说到静态全局变量的生命周期与服务器一致,即这个全局变量一直引用List里面的byte[]对象地址,导致所占用的内存在执行GC(包括Full GC)时无法得到回收。那我们来简单验证一下,我们的byteList目前的size是12000,我们在JConsole点击执行GC看看内存是否会回收,点击前后依次如下图:
执行GC前:
在这里插入图片描述

执行GC后:
在这里插入图片描述
分析:
可以看到执行GC后,首先要看的就是GC次数,PS MarkSweep(Full GC)和PSScavenge(Young GC)都加了1;其次看PS Old Gen的堆内存使用量反而增加了,原因看两张图片右下角的堆3个柱的两,执行GC前后面的两个柱(依次是Eden,Survivor)都是有占用的,执行后这两个柱是没有占用了,那之前的占用当然是到了Old Gen了,所以解释了执行GC后Old Gen的堆内存使用量反而增加了。结论就是执行GC也无法回收全局变量byteList所占用的内存

既然有这么个问题,那有什么方法避免呢?

  1. 不使用静态全局变量;
  2. 必定要使用静态全局变量的话,在return结果给前端之前要清除其数据(清除数据即是释放变量里内容byte[]的引用,这样执行GC时就能回收byte[]占用的内存了)。

好,那我们看看清除数据后再return结果给前端会有什么样的变化。
先看代码的变化:clear()方法之前就有只是没有调用,所以变化的在controller:

多了调用OutOfMemery.clear();

    @RequestMapping("/oom")
    public String oom_test() {
        String value = OutOfMemery.initial();
        OutOfMemery.clear();
        return value;
    }

开启Tomcat服务看看,多次请求会有什么不一样。
在这里插入图片描述
无论执行多少请求byteList的size都是1000,因为return结果之前就清除了byteList里面的数据。
说明一下其实byteList在return之前就清空了,但是我们的输出是size=1000,因为没有重新获取。
为了避免误会我们可以修改一下代码,如下:
在OutOfMemery类新增getter方法

public static List<byte[]> getByteList() {
        return byteList;
    }

Controller

  @RequestMapping("/oom")
    public String oom_test() {
        String value = OutOfMemery.initial();
        OutOfMemery.clear();
        String valueAfterRemove = "; 清除数据后,byteList's HashCode=" + OutOfMemery.getByteList().hashCode() + ", size=" + OutOfMemery.getByteList().size();
        return value + valueAfterRemove;
    }

无论我们浏览器刷新多少次oom请求,得到的结果如下图:
在这里插入图片描述
其实跟上面是一样啦,return结果之前就清除了byteList里面的数据。

好了,我们再用JConsole看看内存的占用:
执行GC前:
在这里插入图片描述
可以看到右下角的堆,最左边(Old)几乎是没有占用;中间(Eden)有一点占用;最右边(Survivor)有大量的占用。
执行GC前的次数:PS MarkSweep(Full GC)是1和PSScavenge(Young GC)是8。
我们手动执行GC看看结果:
执行GC后:
在这里插入图片描述
执行GC后,看到右下角的堆,最左边(Old)几乎是没有占用;中间(Eden)几乎是没有占用;最右边(Survivor)几乎是没有占用。
执行GC后的次数:PS MarkSweep(Full GC)是2和PSScavenge(Young GC)是9。

分析:
为什么会这样呢?因为我们后端程序在return前端请求结果之前都会清除静态全局变量byteList里面的内容即是释放变量里内容byte[]的引用,这样执行GC时就能回收byte[]占用的内存了。

经过这样的试验我们也可以发现一点:GC什么时候执行是有自己的规则的,但是我们可以手动的去执行GC(代码层面好像是通过System.gc(),我是没试过)。我简单说一下GC自己的执行规则:当执行new对象操作时,发现没有内存可以装下新的对象就会执行GC(什么对象放什么区域可以自行百度)。

4. 测试和分析第二种类型(先声明,用时再new创建)

这个另起一篇文章(Java程序静态全局变量与内存泄漏的关系与分析<二>)说明。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值