使用HashMap在什么情况会出现内存泄漏?

Q:在Java中怎么可以产生内存泄露?
A:Java中,造成内存泄露的原因有很多种。典型的例子是一个没有实现hasCode和equals方法的Key类在HashMap中保存的情况。最后会生成很多重复的对象。所有的内存泄露最后都会抛出OutOfMemoryError异常,下面通过一段简短的通过无限循环模拟内存泄露的例子说明一下。

package com.test;

import java.util.HashMap;
import java.util.Map;

import static java.lang.Thread.sleep;

/**
 * @author riemann
 * @date 2019/04/21 0:03
 */
public class TestHashMapMemoryLeak {
    public static void main(String[] args) {
        Map<Key, String> map = new HashMap<Key, String>(1000);
        int count = 0;
        while (true) {
            map.put(new Key("dummyKey"), "value");
            count++;
            if (count % 1000 == 0) {
                System.out.println("map size: " + map.size());
                System.out.println("Free memory after count " + count + " is " + getFreeMemory() + "MB");
                sleep(100);
            }
        }
    }

    public static void sleep(long sleepTime) {
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static long getFreeMemory() {
        return Runtime.getRuntime().freeMemory() / (1024 * 1024);
    }

    static class Key {
        private String key;
        public Key(String key) {
            this.key = key;
        }
    }
}

笔者运行的时候,和那篇文中作者运行结果并不一致,后来配置了下虚拟机的参数,发现虚拟机的运行内存为2的次方,不会设置参数的读者可以参看下网络的资源。我这里给出一种IDEA的配置方法。右击项目->Run->Edit Configuration。配置JVM参数如图。

在这里插入图片描述

-Xms3m -Xmx3m -XX:+HeapDumpOnOutOfMemoryError

运行结果:

map size: 1000
Free memory after count 1000 is 2MB
map size: 2000
Free memory after count 2000 is 2MB
map size: 3000
Free memory after count 3000 is 2MB
...
Free memory after count 13000 is 1MB
map size: 14000
Free memory after count 14000 is 1MB
map size: 15000
Free memory after count 15000 is 1MB
map size: 16000
...
Free memory after count 43000 is 0MB
map size: 44000
Free memory after count 44000 is 0MB
map size: 45000
Free memory after count 45000 is 0MB
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to java_pid2732.hprof ...
Exception in thread "main" Heap dump file created [5925166 bytes in 0.118 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
	at com.test.TestHashMapMemoryLeak.main(TestHashMapMemoryLeak.java:17)

引起错误的原因很明显,key的类没有实现hashcode()和 equals(),导致hashmap存储了大量的相同的entry。明明插入了2个Key一样的键值对,可是为什么其中个数暴涨?Fine,我们不用Key这个类来作为Key,而是使用String类来作为Key。具体如下:

Map<String, String> map = new HashMap<String, String>(1000);

再次运行,发现刚刚出现的内存泄漏问题不再出现了,原因很简单,没有覆写hashcode()和equal()方法导致每次都可以成功插入到Map中。覆写hashcode()和equal()来解决这个问题。如下:

static class Key {
        private String key;
        public Key(String key) {
            this.key = key;
        }
        public boolean equals(Object obj) {
            if (obj instanceof Key) {
                return key.equals(((Key) obj).key);
            } else {
                return false;
            }
        }
        public int hashCode() {
            return key.hashCode();
        }
    }

重新执行程序会得到如下结果:

map size: 1
Free memory after count 1000 is 2MB
map size: 1
Free memory after count 2000 is 2MB
map size: 1
Free memory after count 3000 is 2MB
...
map size: 1
Free memory after count 209000 is 2MB
map size: 1
Free memory after count 210000 is 2MB
map size: 1
Free memory after count 211000 is 2MB

再次运行,内存泄漏问题得以解决,另外说下,Key是继承自Object。看完该代码觉得自己很有必要回顾并加强下对于内存管理方面的知识总结如下。

(End:PS:有读者指出:上面所述并非内存泄露问题,这里表示赞同。此处应该是内存溢出问题。)

Q:在实际场景中,你怎么查找内存泄露?
A:通过以下代码获取线程ID

C:\Users\Administrator>jps
2976 RemoteMavenServer
2116 Jps
3548 TestHashMapMemoryLeak
4108 Launcher
4140

通过命令行打开jconsole

jconsole 3548 

实现了hasCode和equals的Key类和没有实现的图表如下所示:

没有内存泄露的:

在这里插入图片描述

造成内存泄露的:

在这里插入图片描述

1、JAVA中的垃圾收集器相对于以前的语言的优势是什么?

过去的语言要求程序员显式的分配内存、回收内存。但是这种手工的回收往往会造成“内存泄漏”(有过C++开发经验的读者应该清楚这个),即由于某种原因使分配的内存始终没有得到释放。如果该任务不断的被重复执行,将会导致大量的内存得不到释放,后果很明显。相比之下,JAVA提供了垃圾收集器来回收内存,避免了很多潜在的危险。JAVA在创建对象时会自动分配内存,并当该对象的引用不存在时释放这块内存。

JAVA中使用被称为垃圾收集器的技术来监视JAVA程序的运行,当对象不再使用时,就自动释放对象所使用的内存。JAVA使用一系列软指针来跟踪对象的各个引用,并用一个对象表将这些软指针映射为对象的引用。之所以称为软指针,是因为这些指针并不直接指向对象,而是指向对象的引用。使用软指针,JAVA的垃圾收集器能够以单独的线程在后台运行,并依次检查每个对象。通过更改对象表项,垃圾收集器可以标记对象、移除对象、移动对象或检查对象。

垃圾收集器是自动运行的,一般情况下,无须显式地请求垃圾收集器。程序运行时,垃圾收集器会不时检查对象的各个引用,并回收无引用对象所占用的内存。调用System类中的静态gc()方法可以运行垃圾收集器,但这些并不能保证立即回收指定对象。

2、JAVA是如何管理内存的?

JAVA的内存管理就是对象的分配和释放问题。在JAVA中,程序员需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在JAVA中,内存的分配是由程序完成的,而内存的释放是由GC完成,这种收支两条线的方法确实简化了一定的工作。但同时,也加重了JVM的工作。这就是JAVA程序运行速度较慢的原因之一。因为GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。

为了更好的理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引用对象。另外,每个线程对象可以作为一个图的起始顶点,例如,大多程序从main进程开始执行,那么该图就是以main进行顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象(连通子图)对象不再被引用,可以被GC回收。

用有向图表示内存管理。对象程序的每一个时刻,都有一个有向图表示JVM的内存分配情况。下图就是左边程序运行到第六行的示意图。

在这里插入图片描述

我们来看一个例子,观察如下代码,判定对象是否被回收。

Vector v = new Vector();
for (int i = 0; i < 100; i++) {
	Object o = new Object();
	v.add(o);
	o = null;
}

我们发现,虽然o=null了,可是依旧没有被回收(原因很明显,该对象依旧被引用),这也正是JAVA中同样存在内存泄漏的原因所在!下面我们来解释下什么是JAVA中的内存泄漏。

3、什么是JAVA中的内存泄漏?

在JAVA中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:(1)对象是可达的,即在有向图中,存在有向路可以与其相连;(2)对象是无用的,即程序以后不会再使用这些对象。

如果对象满足上面的(1)(2)条件,这些对象就可以判定为JAVA中的内存泄漏,这些对象不会被GC所回收,然而他们却占用内存。

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在JAVA中,这些不可达的对象都由GC负责回收,因此,程序员不需要考虑这部分内存泄漏。对于C++,程序员需要自己管理边和顶点,而对于JAVA程序员,只需要管理边就可以了,通过这种方式,JAVA提高了编程效率。

对于程序员而言,GC基本是透明的、不可见的。虽然我们只有几个函数可以访问GC,例如,运行GC的函数System.gc(),但是根据JAVA语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能不同的算法管理GC。通常,GC线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中段式执行GC。但通常来说,我们不需要关心这些,除非在一些特定的场合,GC的执行影响应用程序的性能,例如,对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序的执行而进行垃圾回收,那么需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如,将垃圾回收分解为一系列的小步骤执行。Sun提供的HotSpotJVM就支持这一特性。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
实付 29.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值