java高手来啊》》怎么用java语句java mongodb查询语句一个HashMap(约20万条)所占的内存啊?

中国领先的IT技术网站
51CTO旗下网站
4.4.4 启示4:用HashMap提高内存查询速度
《Java程序员 上班那点儿事》第4章为大家讲述的是控制内存的功力。本节为启示4:用HashMap提高内存查询速度。
作者:钟声来源:清华大学出版社| 11:03
4.4.4&启示4:用HashMap提高内存查询速度
田富鹏主编的《大学计算机应用基础》中是这样描述内存的:
DRAM:即内存条。常说的内存并不是内部存储器,而是DRAM。
……CPU的运行速度很快,而外部存储器的读取速度相对来说就很慢,如果CPU需要用到的数据总是从外部存储器中读取,由于外部设备很慢,……,CPU可能用到的数据预先读到DRAM中,CPU产生的临时数据也暂时存放在DRAM中,这样的结果是大大的提高了CPU的利用率和计算机运行速度。
这是一个典型计算机基础教材针对内存的描述,也许作为计算机专业的程序员对这段描述并不陌生。但也因为这段描述,而对内存的处理速度有神话的理解,认为内存中的处理速度是非常快的。
以使持有这种观点的程序员遇到一个巨型的内存查询循环的较长时间时,而束手无策了。
请看一下如下程序:
public class MemFor{public static void main (String[] args) {long start=System.currentTimeMillis(); //取得当前时间int len=;&& //设定循环次数int [][] abc=new int[len][2];for (int i=0;i&i++){abc[i][0]=i;abc[i][1]=(i+1);}long get=System.currentTimeMillis();& //取得当前时间//循环将想要的数值取出来,本程序取数组的最后一个值for (int i=0;i&i++){if ((int)abc[i][0]==(-1)){System.out.println("取值结果:"+abc[i][1]);}}long end=System.currentTimeMillis();&& //取得当前时间//输出测试结果System.out.println("赋值循环时间:"+(get-start)+"ms");System.out.println("获取循环时间:"+(end-get)+"ms");System.out.print("Java可控内存:");System.out.println(Runtime.getRuntime().maxMemory()/+"M");System.out.print("已占用内存:");System.out.println(Runtime.getRuntime().totalMemory()/+"M");}}运行这个程序:
java -Xmx1024m MemFor
程序的运行结果如下:
取值结果:3145728
赋值循环时间:2464ms
获取循环时间:70ms
Java可控内存:1016M
已占用内存:128M
我们发现,这个程序循环了3145728次获得想要的结果,循环获取数值的时间用了70毫秒。
你觉得快吗?
是啊,70毫秒虽然小于1秒钟,但是如果你不得不在这个循环外面再套一个循环,即使外层嵌套的循环只有100次,那么,想想看是多少毫秒呢?
回答:70毫秒*100=7000毫秒=7秒
如果,循环1000次呢?
70秒的运行时间对于这个程序来说就是灾难了。&面对这个程序的运行时间很多程序员已经束手无策了,其实,Java给程序员们提供了一个较快的查询方法--哈希表查询。
我们将这个程序用"HashMap"来改造一下,再看看运行结果:
import java.util.*;public class HashMapTest{public static void main (String[] args) {HashMap has=new HashMap();int len=;long start=System.currentTimeMillis();for (int i=0;i&i++){has.put(""+i,""+i);}long end=System.currentTimeMillis();System.out.println("取值结果:"+has.get(""+(-1)));long end2=System.currentTimeMillis();System.out.println("赋值循环时间:"+(end-start)+"ms");System.out.println("获取循环时间:"+(end2-end)+"ms");System.out.print("Java可控内存:");System.out.println(Runtime.getRuntime().maxMemory()/+"M");System.out.print("已占用内存:");System.out.println(Runtime.getRuntime().totalMemory()/+"M");}}运行这个程序:
java -Xmx1024m HashMapTest
程序的运行结果如下:
取之结果:3145727
赋值循环时间:16454ms
获取循环时间:0ms
Java可控内存:1016M
已占用内存:566M
那么现在用HashMap来取值的时间竟然不到1ms,这时我们的程序的效率明显提高了,看来用哈希表进行内存中的数据搜索速度确实很快。
在提高数据搜索速度的同时也要注意到,赋值时间的差异和内存占用的差异。
赋值循环时间:
HashMap:16454ms
普通数组:2464ms
占用内存:
HashMap:566M
普通数组:128M
因此,可以看出HashMap在初始化以及内存占用方面都要高于普通数组,如果仅仅是为了数据存储,用普通数组是比较适合的,但是,如果为了频繁查询的目的,HashMap是必然的选择。
【责任编辑: TEL:(010)】&&&&&&
大家都在看猜你喜欢
热点热点头条头条热点
24H热文一周话题本月最赞
讲师:127470人学习过
讲师:92571人学习过
讲师:12211人学习过
精选博文论坛热帖下载排行
本书以轻松幽默的笔调向读者论述了高质量软件开发方法与C++/C编程规范。它是作者多年从事软件开发工作的经验总结。本书共17章,第1章到第4...
订阅51CTO邮刊java基础(20)
ConcurrentHashMap是Java5中新增加的一个线程安全的Map集合,可以用来替代HashTable。对于ConcurrentHashMap是如何提高其效率的,可能大多人只是知道它使用了多个锁代替HashTable中的单个锁,也就是锁分离技术(Lock Stripping)。实际上,ConcurrentHashMap对提高并发方面的优化,还有一些其它的技巧在里面(比如你是否知道在get操作的时候,它是否也使用了锁来保护?)。
ConcurrentMap & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & &&
提供其他原子 putIfAbsent、remove、replace 方法的 Map。
内存一致性效果:当存在其他并发 collection 时,将对象放入 ConcurrentMap 之前的线程中的操作 happen-before 随后通过另一线程从 ConcurrentMap 中访问或移除该元素的操作。
我们不关心ConcurrentMap中新增的接口,重点理解一下内存一致性效果中的“happens-before”是怎么回事。因为要想从根本上讲明白,这个是无法避开的。这又不得不从Java存储模型来谈起了。
理解JAVA存储模型(JMM)的Happens-Before规则&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
在解释该规则之前,我们先看一段多线程访问数据的代码例子:
public class Test1 {
private int a=1, b=2;
public void foo(){
public int getA(){ // 线程2
public int getB(){ // 线程2
上面的代码,当线程1执行foo方法的时候,线程2访问getA和getB会得到什么样的结果?
A:a=1, b=2
// 都未改变
B:a=3, b=4
// 都改变了
C:a=3, b=2
a改变了,b未改变
D:a=1, b=4
b改变了,a未改变
上面的A,B,C都好理解,但是D可能会出乎一些人的预料。
一些不了解JMM的同学可能会问怎么可能 b=4语句会先于 a=3 执行?
这是一个多线程之间内存可见性(Visibility)顺序不一致的问题。有两种可能会造成上面的D选项。
1) Java编译器的重排序(Reording)操作有可能导致执行顺序和代码顺序不一致。
关于Reording:
Java语言规范规定了JVM要维护内部线程类似顺序化语义(within-thread as-is-serial semantics):只要程序的最终结果等同于它在严格的顺序化环境中执行的结果,那么上述所有的行为都是允许的。
上面的话是《Java并发编程实践》一书中引自Java语言规范的,感觉翻译的不太好。简单的说:假设代码有两条语句,代码顺序是语句1先于语句2执行;那么只要语句2不依赖于语句1的结果,打乱它们的顺序对最终的结果没有影响的话,那么真正交给CPU去执行时,他们的顺序可以是没有限制的。可以允许语句2先于语句1被CPU执行,和代码中的顺序不一致。
重排序(Reordering)是JVM针对现代CPU的一种优化,Reordering后的指令会在性能上有很大提升。(不知道这种优化对于多核CPU是否更加明显,也或许和单核多核没有关系。)
因为我们例子中的两条赋值语句,并没有依赖关系,无论谁先谁后结果都是一样的,所以就可能有Reordering的情况,这种情况下,对于其他线程来说就可能造成了可见性顺序不一致的问题。
2) 从线程工作内存写回主存时顺序无法保证。
下图描述了JVM中主存和线程工作内存之间的交互:
JLS中对线程和主存互操作定义了6个行为,分别为load,save,read,write,assign和use,这些操作行为具有原子性,且相互依赖,有明确的调用先后顺序。这个细节也比较繁琐,我们暂不深入追究。先简单认为线程在修改一个变量时,先拷贝入线程工作内存中,在线程工作内存修改后再写回主存(Main Memery)中。
假设例子中Reording后顺序仍与代码中的顺序一致,那么接下来呢?有意思的事情就发生在线程把Working Copy Memery中的变量写回Main Memery的时刻。线程1把变量写回Main Memery的过程对线程2的可见性顺序也是无法保证的。
上面的列子,a=3; b=4; 这两个语句在 Working Copy Memery中执行后,写回主存的过程对于线程2来说同样可能出现先b=4;后a=3;这样的相反顺序。
正因为上面的那些问题,JMM中一个重要问题就是:如何让多线程之间,对象的状态对于各线程的“可视性”是顺序一致的。它的解决方式就是 Happens-before 规则:
JMM为所有程序内部动作定义了一个偏序关系,叫做happens-before。要想保证执行动作B的线程看到动作A的结果(无论A和B是否发生在同一个线程中),A和B之间就必须满足happens-before关系。
我们现在来看一下“Happens-before”规则都有哪些(摘自《Java并发编程实践》):
① 程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。
② 监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。
③ volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
④ 线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。
⑤ 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。
⑥ 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
⑦ 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
⑧ 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C
我们重点关注的是②,③,这两条也是我们通常编程中常用的。
后续分析ConcurrenHashMap时也会看到使用到锁(ReentrantLock),Volatile,final等手段来保证happens-before规则的。
使用锁方式实现“Happens-before”是最简单,容易理解的。
早期Java中的锁只有最基本的synchronized,它是一种互斥的实现方式。在Java5之后,增加了一些其它锁,比如ReentrantLock,它基本作用和synchronized相似,但提供了更多的操作方式,比如在获取锁时不必像synchronized那样只是傻等,可以设置定时,轮询,或者中断,这些方法使得它在获取多个锁的情况可以避免死锁操作。
而我们需要了解的是ReentrantLock的性能相对synchronized来说有很大的提高。(不过据说Java6后对synchronized进行了优化,两者已经接近了。)在ConcurrentHashMap中,每个hash区间使用的锁正是ReentrantLock。
Volatile可以看做一种轻量级的锁,但又和锁有些不同。
a) 它对于多线程,不是一种互斥(mutex)关系。
b) 用volatile修饰的变量,不能保证该变量状态的改变对于其他线程来说是一种“原子化操作”。
在Java5之前,JMM对Volatile的定义是:保证读写volatile都直接发生在main memory中,线程的working memory不进行缓存。它只承诺了读和写过程的可见性,并没有对Reording做限制,所以旧的Volatile并不太可靠。在Java5之后,JMM对volatile的语义进行了增强。就是我们看到的③ volatile变量法则。
那对于“原子化操作”怎么理解呢?看下面例子:
private static volatile int nextSerialNum = 0;
public static int generateSerialNumber(){
return nextSerialNum++;
上面代码中对nextSerialNum使用了volatile来修饰,根据前面“Happens-Before”法则的第三条Volatile变量法则,看似不同线程都会得到一个新的serialNumber
问题出在了&nextSerialNum++&这条语句上,它不是一个原子化的,实际上是read-modify-write三项操作,这就有可能使得在线程1在write之前,线程2也访问到了nextSerialNum,造成了线程1和线程2得到一样的serialNumber。
所以,在使用Volatile时,需要注意
a)& 需不需要互斥;
b) 对象状态的改变是不是原子化的。
最后也说一下final 关键字。
不变模式(immutable)是多线程安全里最简单的一种保障方式。因为你拿他没有办法,想改变它也没有机会。
不变模式主要通过final关键字来限定的。在JMM中final关键字还有特殊的语义。Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。
3)经过前面的了解,下面我们用Happens-Before规则理解一个经典问题:双重检测锁(DCL)为什么在java中不适用?
public class LazySingleton {
private int someF
private static LazyS
private LazySingleton(){
this.someField = new Random().nextInt(200) + 1; // (1)
public static LazySingleton getInstance() {
if (instance == null) {// (2)
synchronized (LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
return // (6)
public int getSomeField() {
return this.someF
我想简单的用对象创建期间的实际场景来分析一下:(注意,这种场景是我个人的理解,所看的资料也是非官方的,不完全保证正确。如果发现不对请指出。
假设线程1执行完(5)时,线程2正好执行到了(2);
看看 new LazySingleton(); 这个语句的执行过程: 它不是一个原子操作,实际是由多个步骤,我们从我们关注的角度简化一下,简单的认为它主要有2步操作好了:
a) 在内存中分配空间,并将引用指向该内存空间。
b) 执行对象的初始化的逻辑(和操作),完成对象的构建。
此时因为线程1和线程2没有用同步,他们之间不存在“Happens-Before”规则的约束,所以在线程1创建LazySingleton对象的 a),b)这两个步骤对于线程2来说会有可能出现a)可见,b)不可见
造成了线程2获取到了一个未创建完整的lazySingleton对象引用,为后边埋下隐患。
之所以这里举到 DCL这个例子,是因为我们后边分析ConcurrentHashMap时,也会遇到相似的情况。
对于对象的创建,出于乐观考虑,两个线程之间没有用“Happens-Before规则来约束”另一个线程可能会得到一个未创建完整的对象,这种情况必须要检测,后续分析ConcurrentHashMap时再讨论。
ConcurrentHashMap&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
我们关注的操作有:get,put,remove 这3个操作。
对于哈希表,Java中采用链表的方式来解决hash冲突的。一个HashMap的数据结构看起来类似下图:
实现了同步的HashTable也是这样的结构,它的同步使用锁来保证的,并且所有同步操作使用的是同一个锁对象。这样若有n个线程同时在get时,这n个线程要串行的等待来获取锁。
ConcurrentHashMap中对这个数据结构,针对并发稍微做了一点调整。它把区间按照并发级别(concurrentLevel),分成了若干个segment。默认情况下内部按并发级别为16来创建。对于每个segment的容量,默认情况也是16。当然并发级别(concurrentLevel)和每个段(segment)的初始容量都是可以通过构造函数设定的。
创建好默认的ConcurrentHashMap之后,它的结构大致如下图:
看起来只是把以前HashTable的一个hash bucket创建了16份而已。有什么特别的吗?没啥特别的。
继续看每个segment是怎么定义的:
static final class Segment&K,V& extends ReentrantLock implements Serializable
Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。(ReentrantLock前文已经提到,不了解的话就把当做synchronized的替代者吧)这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。
面的这种做法,就称之为“分离锁(lock striping)”。有必要对“分拆锁”和“分离锁”的概念描述一下:
分拆锁(lock spliting)就是若原先的程序中多处逻辑都采用同一个锁,但各个逻辑之间又相互独立,就可以拆(Spliting)为使用多个锁,每个锁守护不同的逻辑。
分拆锁有时候可以被扩展,分成可大可小加锁块的集合,并且它们归属于相互独立的对象,这样的情况就是分离锁(lock striping)。(摘自《Java并发编程实践》)
看上去,单是这样就已经能大大提高多线程并发的性能了。还没完,继续看我们关注的get,put,remove这三个函数怎么保证数据同步的。
先看get方法&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
public V get(Object key) {
int hash = hash(key); // throws NullPointerException if key null
return segmentFor(hash).get(key, hash);
它没有使用同步控制,交给segment去找,再看Segment中的get方法:
V get(Object key, int hash) {
if (count != 0) { // read-volatile // ①
HashEntry&K,V& e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
if (v != null)
// ② 注意这里
return readValueUnderLock(e); // recheck
return null;
它也没有使用锁来同步,只是判断获取的entry的value是否为null,为null时才使用加锁的方式再次去获取。
这个实现很微妙,没有锁同步的话,靠什么保证同步呢?我们一步步分析。
第一步,先判断一下 count != 0;count变量表示segment中存在entry的个数。如果为0就不用找了。
假设这个时候恰好另一个线程put或者remove了这个segment中的一个entry,会不会导致两个线程看到的count值不一致呢?
看一下count变量的定义:&transien
它使用了volatile来修改。我们前文说过,Java5之后,JMM实现了对volatile的保证:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
所以,每次判断count变量的时候,即使恰好其他线程改变了segment也会体现出来。
第二步,获取到要该key所在segment中的索引地址,如果该地址有相同的hash对象,顺着链表一直比较下去找到该entry。当找到entry的时候,先做了一次比较:&if(v != null)&我们用红色注释的地方。
这是为何呢?
考虑一下,如果这个时候,另一个线程恰好新增/删除了entry,或者改变了entry的value,会如何?
先看一下HashEntry类结构。
static final class HashEntry&K,V& {
volatile V
final HashEntry&K,V&
除了 value,其它成员都是final修饰的,也就是说value可以被改变,其它都不可以改变,包括指向下一个HashEntry的next也不能被改变。(那删除一个entry时怎么办?后续会讲到。)
1) 在get代码的①和②之间,另一个线程新增了一个entry
如果另一个线程新增的这个entry又恰好是我们要get的,这事儿就比较微妙了。
下图大致描述了put 一个新的entry的过程。
因为每个HashEntry中的next也是final的,没法对链表最后一个元素增加一个后续entry所以新增一个entry的实现方式只能通过头结点来插入了。
newEntry对象是通过&new HashEntry(K k , V v, HashEntry next)&来创建的。如果另一个线程刚好new 这个对象时,当前线程来get它。因为没有同步,就可能会出现当前线程得到的newEntry对象是一个没有完全构造好的对象引用。
回想一下我们之前讨论的DCL的问题,这里也一样,没有锁同步的话,new 一个对象对于多线程看到这个对象的状态是没有保障的,这里同样有可能一个线程new这个对象的时候还没有执行完构造函数就被另一个线程得到这个对象引用。
所以才需要判断一下:if (v != null)&如果确实是一个不完整的对象,则使用锁的方式再次get一次。
有没有可能会put进一个value为null的entry? 不会的,已经做了检查,这种情况会抛出异常,所以 ②处的判断完全是出于对多线程下访问一个new出来的对象的状态检测。
2) 在get代码的①和②之间,另一个线程修改了一个entry的value
value是用volitale修饰的,可以保证读取时获取到的是修改后的值。
3) 在get代码的①之后,另一个线程删除了一个entry
假设我们的链表元素是:e1-& e2 -& e3 -& e4 我们要删除 e3这个entry,因为HashEntry中next的不可变,所以我们无法直接把e2的next指向e4,而是将要删除的节点之前的节点复制一份,形成新的链表。
它的实现大致如下图所示:
如果我们get的也恰巧是e3,可能我们顺着链表刚找到e1,这时另一个线程就执行了删除e3的操作,而我们线程还会继续沿着旧的链表找到e3返回。这里没有办法实时保证了。
我们第①处就判断了count变量,它保障了在 ①处能看到其他线程修改后的。①之后到②之间,如果再次发生了其他线程再删除了entry节点,就没法保证看到最新的了。
不过这也没什么关系,即使我们返回e3的时候,它被其他线程删除了,暴漏出去的e3也不会对我们新的链表造成影响。
这其实是一种乐观设计,设计者假设 ①之后到②之间 发生被其它线程增、删、改的操作可能性很小,所以不采用同步设计,而是采用了事后(其它线程这期间也来操作,并且可能发生非安全事件)弥补的方式。
而因为其他线程的“改”和“删”对我们的数据都不会造成影响,所以只有对“新增”操作进行了安全检查,就是②处的非null检查,如果确认不安全事件发生,则采用加锁的方式再次get。
这样做减少了使用互斥锁对并发性能的影响。可能有人怀疑remove操作中复制链表的方式是否代价太大,这里我没有深入比较,不过既然Java5中这么实现,我想new一个对象的代价应该已经没有早期认为的那么严重。
我们基本分析完了get操作。对于put和remove操作,是使用锁同步来进行的,不过是用的ReentrantLock而不是synchronized,性能上要更高一些。它们的实现前文都已经提到过,就没什么可分析的了。
我们还需要知道一点,ConcurrentHashMap的迭代器不是Fast-Fail的方式,所以在迭代的过程中别其他线程添加/删除了元素,不会抛出异常,也不能体现出元素的改动。但也没有关系,因为每个entry的成员除了value都是final修饰的,暴漏出去也不会对其他元素造成影响。
加深&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
ConcurrentHashMap&String, Boolean& map = new ...;
Thread a = new Thread {
void run() {
map.put(&first&, true);
map.put(&second&, true);
Thread b = new Thread {
void run() {
map.clear();
a.start();
b.start();
Map(&first& -& true, &second& -& true)
Map(&second& -& true)
Map(&first& -& true)
ConcurrentHashMap&String, Boolean& map = new ...;
List&String& myKeys = new ...;
Thread a = new Thread {
void run() {
map.put(&first&, true);
// more stuff
map.remove(&first&);
map.put(&second&, true);
Thread b = new Thread {
void run() {
Set&String& keys = map.keySet();
for (String key : keys) {
myKeys.add(key);
a.start();
b.start();
List(&first&)
List(&second&)
List(&first&, &second&)
对于这两个现象的解释:ConcurrentHashMap中的clear方法:
public void clear() {
for (int i = 0; i & segments. ++i)
segments[i].clear();
如果线程b先执行了clear,清空了一部分segment的时候,线程a执行了put且正好把“first”放入了“清空过”的segment中,而把“second”放到了还没有清空过的segment中,就会出现上面的情况。
第二段代码,如果线程b执行了迭代遍历到first,而此时线程a还没有remove掉first,那么即使后续删除了first,迭代器里不会反应出来,也不抛出异常,这种迭代器被称为“弱一致性”(weakly consistent)迭代器。
原文链接:/yydcdut/p/3959815.html
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:90853次
积分:1992
积分:1992
排名:千里之外
原创:90篇
转载:17篇
评论:20条
阅读:6247
文章:12篇
阅读:16271
(2)(1)(1)(2)(1)(7)(2)(10)(24)(3)(2)(3)(1)(1)(1)(1)(10)(2)(2)(4)(4)(9)(8)(11)
(window.slotbydup = window.slotbydup || []).push({
id: '4740887',
container: s,
size: '250,250',
display: 'inlay-fix'[JAVA]第二篇(内存管理,HashMap内存泄漏解决办法) - CSDN博客
[JAVA]第二篇(内存管理,HashMap内存泄漏解决办法)
网上看到一个关于内存泄漏处理的例子,原网址:http://www.jb51.net/article/49428.htm,下面笔者将具体分析下这篇文章中的代码,并从中学习JAVA的内存管理。
(Begin:PS:有读者指出:此处并非内存泄露问题,这里表示赞同。此处应该是内存溢出问题。)
Q:在Java中怎么可以产生内存泄露?
A:Java中,造成内存泄露的原因有很多种。典型的例子是一个没有实现hasCode和equals方法的Key类在HashMap中保存的情况。最后会生成很多重复的对象。所有的内存泄露最后都会抛出OutOfMemoryError异常,下面通过一段简短的通过无限循环模拟内存泄露的例子说明一下。
import java.util.HashM
import java.util.M
public class Test {
public static void main(String[] args) {
Map&Key, String& map = new HashMap&Key, String&(1000);
int counter = 0;
while (true) {
// creates duplicate objects due to bad Key class
map.put(new Key(&dummyKey&), &value&);
counter++;
if (counter % 1000 == 0) {
System.out.println(&map size: & + map.size());
System.out.println(&Free memory after count & + counter
+ & is & + getFreeMemory() + &MB&);
sleep(100);
// inner class key without hashcode() or equals() -- bad implementation
static class Key {
public Key(String key) {
this.key =
// delay for a given period in milli seconds
public static void sleep(long sleepFor) {
Thread.sleep(sleepFor);
} catch (InterruptedException e) {
e.printStackTrace();
// get available memory in MB
public static long getFreeMemory() {
return Runtime.getRuntime().freeMemory() / (1024 * 1024);
笔者运行的时候,和那篇文中作者运行结果并不一致,后来配置了下虚拟机的参数,发现虚拟机的运行内存为2的次方,不会设置参数的读者可以参看下网络的资源。我这里给出一种Eclipse的配置方法。右击项目-&Run As-&Run Configuration。配置JVM参数如图。
图1:JVM参数配置
接下来,运行该程序,我们得到这样的结果:
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 23000 is 1MB
map size: 24000
Free memory after count 24000 is 1MB
map size: 25000
Free memory after count 48000 is 0MB
map size: 49000
Free memory after count 49000 is 0MB
Exception in thread &main& java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.resize(Unknown Source)
at java.util.HashMap.putVal(Unknown Source)
at java.util.HashMap.put(Unknown Source)
at test.Test.main(Test.java:13)
引起错误的原因很明显,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 {
public Key(String key) {
this.key =
public boolean equals(Object obj) {
if (obj instanceof Key)
return key.equals(((Key) obj).key);
public int hashCode() {
return key.hashCode();
再次运行,内存泄漏问题得以解决,另外说下,Key是继承自Object。看完该代码觉得自己很有必要回顾并加强下对于内存管理方面的知识总结如下。
(End:PS:有读者指出:上面所述并非内存泄露问题,这里表示赞同。此处应该是内存溢出问题。)
1、JAVA中的垃圾收集器相对于以前的语言的优势是什么?[Source:SAP公司面试题]
过去的语言要求程序员显式的分配内存、回收内存。但是这种手工的回收往往会造成“内存泄漏”(有过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的内存分配情况。下图就是左边程序运行到第六行的示意图。
图1:有向图表示内存示例
我们来看一个例子,观察如下代码,判定对象是否被回收。
Vector v = new Vector();
for (int i = 0; i & 100; i++) {
Object o = new Object();
用有向图的形式可以表示,如图2。
图2:示意图(应该是有100个o,大概示意下,读者明确的就是v到这100个Obj内存空间是可达的)
我们发现,虽然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就支持这一特性。
参考文献:
1、/topic/838030。
2、JAVA程序员面试宝典(第二版)(欧立奇。刘洋,段韬)。
本文已收录于以下专栏:
相关文章推荐
一、Java内存回收机制
不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(H...
先用top命令定位哪些线程占用多:
top - 18:14:46 up 200 days, 23:26, 2 users, load average: 95.13, 88.59, 79.51Tasks...
作者:Maverick blog:http://blog.csdn.net/zhaohuabing  转载请注明出处一、问题的提出  曾经使用JAVA开发大型系统的网友恐怕都有过这种体会:系统运行一段...
map.clear()
只是把map清空了,但是内存没有释放,如果要释放内存不止是要clear()掉,还要和一个空的map来进行swap,将内存释放。
注意map中如果元素不是基本类型,也要进行内...
转自/translate-java-collections-bigdata-mapdb
在一个下雨的夜晚,我在思考Java中内存管理的问题,以及Java...
根据论坛讨论总结,以下代码会出现内存泄漏
import java.util.HashM
import java.util.M
public class HashMapOver {...
虽然jvm可以通过GC自动回收无用的内存,但是代码不好的话仍然存在内存溢出的风险。
最近在网上搜集了一些资料,现整理如下:
一、为什么要了解内存泄露和内存溢出?
java中的Map在提供方便实用的同时,也存在内存浪费巨大的问题。当Map中的Entry数量达到1000万 条以上的时候,需要数G的内存空间 .这里提到的Map使用形式为HashMap,平均每个key...
他的最新文章
讲师:吴岸城
您举报文章:
举报原因:
原文地址:
原因补充:
(最多只允许输入30个字)

我要回帖

更多关于 java sql查询语句 的文章

 

随机推荐