Java Application内存泄露

688 阅读3分钟

为了让程序员能更加专注代码本身,Java通过后台GC自动管理内存垃圾。通常情况下,GC程序可以解决大部分内存回收的问题,但是如果代码写的不规范就会存在内存泄露的问题。

垃圾回收机制

Java GC程序通过引用计数和根节点可达性来标记某个内存空间是否需要回收。
引用计数法就是某个对象被引用一次计数加1,不对引用是就减1,当计数为0时就会被回收。这种方式有一个缺点就是可能造成循环引用,导致对象不能被回收。
根节点可达性是指从根对象往下搜索,如果某个对象不能到达,那么就会被回收。
当内存泄露时,很有可能就是某一些对象还在被引用,因为某些代码的漏洞导致空间不能被释放。

问题检查

当遇到内存泄露的问题时,首先需要能看到java application的内存使用情况以及GC情况,这里推荐大家使用VisualVM, 这是Intellij idea上的一个插件,使用非常方便。

VisualVm安装使用

打开idea 的plugins,搜索VisualVM Launcher, 安装即可。 安装完成后可以看到,点击最后两个按钮就可以运行项目并对内存gc进行监控。

测试

运行程序后可以使用JMeter等工具发送大量数据进行测试。可以看到运行的结果: 从这个图上可以看出,在程序运行运行过程中,虽然中间经历了一些GC,但是内存的使用在不断上涨,说明GC并没有释放掉我们想要释放的空间。
我们也可以看一下GC统计: 可以看到GC虽然在执行,但是old区的空间一点没有减少。

代码定位

我们可以在程序运行一段时间后将Heap Dump(尽量不要跑太久,要不然dump会很慢),观察内存的使用情况: 这里我们可以看到com.google.gson包下面的对象占用了很多内存,但是这个是google提供的开源包,它本身不会有问题,应该是自己使用有误。那么我们就发现我们自定义的类LRUCache$Cache有十几万条,并且在这个Cache中我们使用了很多google的类。
在程序中,我们定义了LRUCache的长度,当Cache中数据大于长度,队尾的数据会删除,显然删除队尾的代码有问题。
LRUCache是用LinkedHashMap实现的:

public abstract class LRUCache extends LinkedHashMap<Integer, LRUCache.Cache>
        implements CacheLoader {
    private static final long serialVersionUID = -2387942637668480578L;
    //the cache capacity
    private int capacity;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    public LRUCache(int size) {
        super(size,DEFAULT_LOAD_FACTOR,true);
        this.capacity = size;
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, LRUCache.Cache> eldest) {
        return this.size() > capacity;
    }
    /**
     * put the object into cache with a special key and put it into the head.
     */
    @Override
    public void put(Integer key, JsonObject value) {
        Cache node = new Cache(key, value);
        super.put(key,node);
    }
    /**
     * get the cached object and put it into the head with a special key.
     */
    @Override
    public JsonObject get(Integer key) {
        Cache node = super.get(key);
        if (node != null) {
            return node.value;
        } else {
            return null;
        }
    }
    class Cache {
        JsonObject value;
        // the cache key.
        Integer key;
        public Cache(Integer key, JsonObject value) {
            this.key = key;
            this.value = value;
        }
    }
}

由于LinkedHashMap是线程不安全的,所以当程序需要删除队尾的Cache时,就可能不能正常删除。
所以最终解决办法是加上锁。

public abstract class LRUCache extends LinkedHashMap<Integer, LRUCache.Cache>
        implements CacheLoader {
    private static final long serialVersionUID = -2387942637668480578L;
    //the cache capacity
    private int capacity;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    private final Lock lock = new ReentrantLock();
    public LRUCache(int size) {
        super(size,DEFAULT_LOAD_FACTOR,true);
        this.capacity = size;
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, LRUCache.Cache> eldest) {
        return this.size() > capacity;
    }
    /**
     * put the object into cache with a special key and put it into the head.
     */
    @Override
    public void put(Integer key, JsonObject value) {
        lock.lock();
        try {
            Cache node = new Cache(key, value);
            super.put(key,node);
        } finally {
            lock.unlock();
        }
    }
    /**
     * get the cached object and put it into the head with a special key.
     */
    @Override
    public JsonObject get(Integer key) {
        lock.lock();
        try {
            Cache node = super.get(key);
            if (node != null) {
                return node.value;
            } else {
                return null;
            }
        } finally {
            lock.unlock();
        }
    }
    class Cache {
        JsonObject value;
        Integer key;
        public Cache(Integer key, JsonObject value) {
            this.key = key;
            this.value = value;
        }
    }
}